SeaWolf-AI commited on
Commit
8de969c
Β·
verified Β·
1 Parent(s): 6216937

multi-source price ladder + diagnostics

Browse files
Files changed (2) hide show
  1. app/fetch.py +124 -0
  2. app/main.py +27 -10
app/fetch.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """μ‹œμ„Έ μˆ˜μ§‘ β€” μ†ŒμŠ€ ν•˜λ‚˜μ— κΈ°λŒ€μ§€ μ•ŠλŠ”λ‹€.
3
+
4
+ πŸ”΄ HF Spaceμ—μ„œ yfinance κ°€ **빈 데이터**λ₯Ό λŒλ €μ€€λ‹€. μ˜ˆμ™Έλ„ μ•„λ‹ˆκ³  0건이라
5
+ 쑰용히 μ‹€νŒ¨ν•œλ‹€. Yahooκ°€ 데이터센터 IPλ₯Ό 막기 λ•Œλ¬Έμ΄λ‹€.
6
+ ORACLE λ•Œ λ°”μ΄λ‚ΈμŠ€κ°€ 같은 이유둜 λ§‰ν˜”κ³ , κ·Έλ•Œ μž¬μ‹œλ„ 3νšŒμ— 5.2초λ₯Ό 버렸닀.
7
+ μ΄λ²ˆμ—” μ²˜μŒλΆ€ν„° μ‚¬λ‹€λ¦¬λ‘œ λ§Œλ“ λ‹€.
8
+
9
+ μˆœμ„œ
10
+ 1) Yahoo chart API 직접 호좜 β€” yfinance 의 μΏ ν‚€/크럼 절차λ₯Ό κ±΄λ„ˆλ›΄λ‹€
11
+ 2) Stooq CSV β€” λ―Έκ΅­ 주식·ETF. ν‚€κ°€ ν•„μš” μ—†λ‹€
12
+ 3) CoinGecko β€” 코인 μ „μš©
13
+
14
+ πŸ”‘ "성곡"의 νŒμ •μ€ μ˜ˆμ™Έ μ—†μŒμ΄ μ•„λ‹ˆλΌ **λ§‰λŒ€ 개수**λ‹€.
15
+ 0건을 μ„±κ³΅μœΌλ‘œ μ„Έλ©΄ 사닀리가 첫 μΉΈμ—μ„œ λ©ˆμΆ˜λ‹€.
16
+ """
17
+ import io, time
18
+ import pandas as pd
19
+
20
+ UA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
21
+ "(KHTML, like Gecko) Chrome/122.0 Safari/537.36"}
22
+ MIN_BARS = 30 # 이보닀 적으면 μ‹€νŒ¨λ‘œ λ³Έλ‹€
23
+
24
+ # μ’…λͺ©μ½”λ“œ β†’ μ†ŒμŠ€λ³„ 심볼
25
+ MAP = {
26
+ "NVDA": {"yahoo": "NVDA", "yfin": "NVDA", "stooq": "nvda.us"},
27
+ "GOLD": {"yahoo": "GLD", "yfin": "GLD", "stooq": "gld.us"},
28
+ "OIL": {"yahoo": "USO", "yfin": "USO", "stooq": "uso.us"},
29
+ "BTC": {"yahoo": "BTC-USD", "yfin": "BTC-USD", "stooq": "btcusd",
30
+ "gecko": "bitcoin"},
31
+ }
32
+
33
+
34
+ def _yahoo(sym, days):
35
+ import requests
36
+ u = ("https://query1.finance.yahoo.com/v8/finance/chart/%s"
37
+ "?range=%s&interval=1d" % (sym, "10y" if days > 1200 else "5y"))
38
+ r = requests.get(u, headers=UA, timeout=25)
39
+ j = r.json()["chart"]["result"][0]
40
+ ts = j["timestamp"]
41
+ cl = j["indicators"]["quote"][0]["close"]
42
+ # μˆ˜μ •μ£Όκ°€κ°€ 있으면 κ·Έμͺ½μ„ μ“΄λ‹€. 뢄할·배당이 수읡λ₯ μ— μ„žμ΄λ©΄ μ•ˆ λœλ‹€.
43
+ adj = j.get("indicators", {}).get("adjclose")
44
+ if adj and adj[0].get("adjclose"):
45
+ cl = adj[0]["adjclose"]
46
+ s = pd.Series(cl, index=pd.to_datetime(ts, unit="s")).dropna()
47
+ s.index = s.index.normalize().tz_localize(None)
48
+ return s[~s.index.duplicated(keep="last")]
49
+
50
+
51
+ def _yfin(sym, days):
52
+ """yfinance 자체 경둜. μΏ ν‚€Β·ν¬λŸΌμ„ μ–»μ–΄ μ˜€λŠ” μ ˆμ°¨κ°€ λ”°λ‘œ μžˆμ–΄ κ²°κ³Όκ°€ λ‹€λ₯΄λ‹€."""
53
+ import yfinance as yf
54
+ d = yf.download(sym, period="5y", interval="1d", progress=False,
55
+ auto_adjust=True, threads=False)
56
+ if isinstance(d.columns, pd.MultiIndex):
57
+ d.columns = d.columns.droplevel(1)
58
+ s = d["Close"].dropna()
59
+ s.index = pd.to_datetime(s.index).tz_localize(None)
60
+ return s
61
+
62
+
63
+ def _stooq(sym, days):
64
+ import requests
65
+ r = requests.get("https://stooq.com/q/d/l/?s=%s&i=d" % sym,
66
+ headers=UA, timeout=25)
67
+ if not r.text.lstrip().lower().startswith("date"):
68
+ # 봇 차단 νŽ˜μ΄μ§€κ°€ 200으둜 μ˜¨λ‹€. 200을 μ„±κ³΅μœΌλ‘œ μ„Έλ©΄ μ•ˆ λœλ‹€.
69
+ raise ValueError("stooq 차단(HTML 응닡)")
70
+ d = pd.read_csv(io.StringIO(r.text))
71
+ if "Close" not in d.columns:
72
+ raise ValueError("stooq 응닡에 Close μ—†μŒ")
73
+ s = pd.Series(d["Close"].values,
74
+ index=pd.to_datetime(d["Date"])).dropna()
75
+ return s
76
+
77
+
78
+ def _gecko(cid, days):
79
+ import requests
80
+ r = requests.get("https://api.coingecko.com/api/v3/coins/%s/market_chart"
81
+ "?vs_currency=usd&days=%d&interval=daily" % (cid, min(days, 3650)),
82
+ headers=UA, timeout=25)
83
+ j = r.json()
84
+ if "prices" not in j:
85
+ raise ValueError("coingecko: %s" % str(j)[:120])
86
+ p = j["prices"]
87
+ s = pd.Series([v for _, v in p],
88
+ index=pd.to_datetime([t for t, _ in p], unit="ms"))
89
+ s.index = s.index.normalize().tz_localize(None)
90
+ return s[~s.index.duplicated(keep="last")]
91
+
92
+
93
+ LADDER = [("yahoo", _yahoo), ("yfin", _yfin), ("stooq", _stooq), ("gecko", _gecko)]
94
+
95
+
96
+ def daily(asset, days=1800):
97
+ """일봉 μ’…κ°€. μ–΄λŠ μ†ŒμŠ€μ—μ„œ μ™”λŠ”μ§€λ„ ν•¨κ»˜ λŒλ €μ€€λ‹€.
98
+
99
+ λ°˜ν™˜: (μ‹œλ¦¬μ¦ˆ, μ†ŒμŠ€λͺ…, μ‹œλ„κΈ°λ‘)
100
+ """
101
+ m = MAP.get(asset, {})
102
+ trail = []
103
+ for name, fn in LADDER:
104
+ sym = m.get(name)
105
+ if not sym:
106
+ continue
107
+ t0 = time.time()
108
+ try:
109
+ s = fn(sym, days)
110
+ n = len(s)
111
+ trail.append("%s:%d봉/%.1fs" % (name, n, time.time() - t0))
112
+ if n >= MIN_BARS: # 0건은 성곡이 μ•„λ‹ˆλ‹€
113
+ return s.sort_index().iloc[-days:], name, trail
114
+ except Exception as e:
115
+ trail.append("%s:%s" % (name, type(e).__name__))
116
+ return None, None, trail
117
+
118
+
119
+ if __name__ == "__main__":
120
+ for a in MAP:
121
+ s, src, tr = daily(a, 400)
122
+ print("%-5s %-7s %s %s" % (
123
+ a, src or "μ‹€νŒ¨", ("%d봉 λ§ˆμ§€λ§‰ %.2f" % (len(s), s.iloc[-1])) if s is not None else "-",
124
+ " Β· ".join(tr)))
app/main.py CHANGED
@@ -17,6 +17,7 @@ import scoring as SC
17
  import reference as RF
18
  import ledger as L
19
  import mcp as MCP
 
20
 
21
  CEIL = json.load(open(os.path.join(os.path.dirname(HERE), "announce_ceiling.json"),
22
  encoding="utf-8"))["ceiling"]
@@ -33,21 +34,22 @@ _PX = {"at": 0.0, "d": {}}
33
  TTL = 900
34
 
35
 
 
 
 
36
  def prices(asset, interval="1d", bars=800):
37
- """가격 μΊμ‹œ. μ’…λͺ©λ§ˆλ‹€ μ†ŒμŠ€κ°€ λ‹€λ₯΄μ§€λ§Œ 이 ν•¨μˆ˜ λ°–μ—μ„œλŠ” μ‹ κ²½ μ“°μ§€ μ•ŠλŠ”λ‹€."""
38
  k = "%s|%s" % (asset, interval)
39
  if time.time() - _PX["at"] > TTL:
40
  _PX["d"].clear(); _PX["at"] = time.time()
41
  if k not in _PX["d"]:
42
- src = ASSETS[asset]["src"]
43
- import yfinance as yf
44
- per = "5y" if interval == "1d" else "60d"
45
- d = yf.download(src, period=per, interval=interval,
46
- progress=False, auto_adjust=True, threads=False)
47
- if isinstance(d.columns, pd.MultiIndex):
48
- d.columns = d.columns.droplevel(1)
49
- s = d["Close"].dropna()
50
- s.index = pd.to_datetime(s.index).tz_localize(None)
51
  _PX["d"][k] = s
52
  return _PX["d"][k].iloc[-bars:]
53
 
@@ -371,6 +373,21 @@ def _startup():
371
  threading.Thread(target=_anchor_loop, daemon=True).start()
372
 
373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  @app.get("/health")
375
  def health():
376
  return {"ok": True, "season": S1["name"], "assets": list(ASSETS),
 
17
  import reference as RF
18
  import ledger as L
19
  import mcp as MCP
20
+ import fetch as FE
21
 
22
  CEIL = json.load(open(os.path.join(os.path.dirname(HERE), "announce_ceiling.json"),
23
  encoding="utf-8"))["ceiling"]
 
34
  TTL = 900
35
 
36
 
37
+ _SRC = {}
38
+
39
+
40
  def prices(asset, interval="1d", bars=800):
41
+ """가격 μΊμ‹œ. μ–΄λŠ μ†ŒμŠ€κ°€ μ‚΄μ•„ μžˆλ“  이 ν•¨μˆ˜ λ°–μ—μ„œλŠ” μ‹ κ²½ μ“°μ§€ μ•ŠλŠ”λ‹€."""
42
  k = "%s|%s" % (asset, interval)
43
  if time.time() - _PX["at"] > TTL:
44
  _PX["d"].clear(); _PX["at"] = time.time()
45
  if k not in _PX["d"]:
46
+ s, src, trail = FE.daily(asset, 2200)
47
+ _SRC[asset] = {"source": src, "trail": trail,
48
+ "bars": 0 if s is None else len(s)}
49
+ if s is None:
50
+ # πŸ”΄ 빈 μ‹œλ¦¬μ¦ˆλ₯Ό μΊμ‹œμ— λ„£μœΌλ©΄ TTL λ™μ•ˆ 계속 빈 값을 μ€€λ‹€.
51
+ # μ†ŒμŠ€κ°€ 잠깐 λ§‰νžŒ 것과 영영 λ§‰νžŒ 것을 ꡬ뢄해야 ν•œλ‹€.
52
+ return pd.Series(dtype=float)
 
 
53
  _PX["d"][k] = s
54
  return _PX["d"][k].iloc[-bars:]
55
 
 
373
  threading.Thread(target=_anchor_loop, daemon=True).start()
374
 
375
 
376
+ @app.get("/api/diag")
377
+ def api_diag():
378
+ """μ–΄λŠ μ†ŒμŠ€μ—μ„œ μ‹œμ„Έκ°€ μ˜€λŠ”μ§€ μ‹€μΈ‘ν•œλ‹€.
379
+
380
+ πŸ”‘ λ‘œμ»¬μ—μ„œ λ˜λŠ”μ§€λŠ” 아무 μ˜λ―Έκ°€ μ—†λ‹€. λ§‰νžˆλŠ” 곳은 μ„œλ²„λ‹€.
381
+ """
382
+ out = {}
383
+ for a in ASSETS:
384
+ s = prices(a, "1d", 5)
385
+ out[a] = {**_SRC.get(a, {}),
386
+ "last": None if not len(s) else round(float(s.iloc[-1]), 4),
387
+ "at": None if not len(s) else s.index[-1].date().isoformat()}
388
+ return {"ok": all(v.get("bars") for v in out.values()), "assets": out}
389
+
390
+
391
  @app.get("/health")
392
  def health():
393
  return {"ok": True, "season": S1["name"], "assets": list(ASSETS),