Spaces:
Running
Running
multi-source price ladder + diagnostics
Browse files- app/fetch.py +124 -0
- 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 =
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 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),
|