finchal / app /fetch.py
SeaWolf-AI's picture
hourly scoring grid and 13 reference strategies since Jan 1
588034e verified
Raw
History Blame Contribute Delete
7.63 kB
# -*- coding: utf-8 -*-
"""์‹œ์„ธ ์ˆ˜์ง‘ โ€” ์†Œ์Šค ํ•˜๋‚˜์— ๊ธฐ๋Œ€์ง€ ์•Š๋Š”๋‹ค.
๐Ÿ”ด ์‹œ์„ธ ์ œ๊ณต์ž๋“ค์€ ๋ฐ์ดํ„ฐ์„ผํ„ฐ IP๋ฅผ ๋ง‰๋Š”๋‹ค. ์˜ˆ์™ธ๋ฅผ ๋˜์ง€๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋นˆ ์‘๋‹ต์ด๋‚˜
๋ด‡ ์ฐจ๋‹จ ํŽ˜์ด์ง€๋ฅผ 200์œผ๋กœ ๋Œ๋ ค์ฃผ๋ฏ€๋กœ, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋งŒ์œผ๋กœ๋Š” ์žกํžˆ์ง€ ์•Š๋Š”๋‹ค.
๊ทธ๋ž˜์„œ ์†Œ์Šค๋ฅผ ํ•˜๋‚˜ ๋‘์ง€ ์•Š๊ณ  ์‚ฌ๋‹ค๋ฆฌ๋กœ ๋‚ด๋ ค๊ฐ„๋‹ค.
์ˆœ์„œ
1) Yahoo chart API ์ง์ ‘ ํ˜ธ์ถœ โ€” yfinance ์˜ ์ฟ ํ‚ค/ํฌ๋Ÿผ ์ ˆ์ฐจ๋ฅผ ๊ฑด๋„ˆ๋›ด๋‹ค
2) Stooq CSV โ€” ๋ฏธ๊ตญ ์ฃผ์‹ยทETF. ํ‚ค๊ฐ€ ํ•„์š” ์—†๋‹ค
3) CoinGecko โ€” ์ฝ”์ธ ์ „์šฉ
๐Ÿ”‘ "์„ฑ๊ณต"์˜ ํŒ์ •์€ ์˜ˆ์™ธ ์—†์Œ์ด ์•„๋‹ˆ๋ผ **๋ง‰๋Œ€ ๊ฐœ์ˆ˜**๋‹ค.
0๊ฑด์„ ์„ฑ๊ณต์œผ๋กœ ์„ธ๋ฉด ์‚ฌ๋‹ค๋ฆฌ๊ฐ€ ์ฒซ ์นธ์—์„œ ๋ฉˆ์ถ˜๋‹ค.
"""
import io, time
import pandas as pd
UA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/122.0 Safari/537.36"}
MIN_BARS = 30 # ์ด๋ณด๋‹ค ์ ์œผ๋ฉด ์‹คํŒจ๋กœ ๋ณธ๋‹ค
# ์ข…๋ชฉ์ฝ”๋“œ โ†’ ์†Œ์Šค๋ณ„ ์‹ฌ๋ณผ
MAP = {
"NVDA": {"feed": "NVDA", "yahoo": "NVDA", "yfin": "NVDA", "stooq": "nvda.us"},
"GOLD": {"feed": "GOLD", "yahoo": "GLD", "yfin": "GLD", "stooq": "gld.us"},
"OIL": {"feed": "OIL", "yahoo": "USO", "yfin": "USO", "stooq": "uso.us"},
"BTC": {"feed": "BTC", "yahoo": "BTC-USD", "yfin": "BTC-USD",
"stooq": "btcusd", "gecko": "bitcoin"},
}
def _yahoo(sym, days):
import requests
u = ("https://query1.finance.yahoo.com/v8/finance/chart/%s"
"?range=%s&interval=1d" % (sym, "10y" if days > 1200 else "5y"))
r = requests.get(u, headers=UA, timeout=25)
if r.status_code != 200:
raise ValueError("HTTP %d %s" % (r.status_code, r.text[:60]))
j = r.json()["chart"]["result"][0]
ts = j["timestamp"]
cl = j["indicators"]["quote"][0]["close"]
# ์ˆ˜์ •์ฃผ๊ฐ€๊ฐ€ ์žˆ์œผ๋ฉด ๊ทธ์ชฝ์„ ์“ด๋‹ค. ๋ถ„ํ• ยท๋ฐฐ๋‹น์ด ์ˆ˜์ต๋ฅ ์— ์„ž์ด๋ฉด ์•ˆ ๋œ๋‹ค.
adj = j.get("indicators", {}).get("adjclose")
if adj and adj[0].get("adjclose"):
cl = adj[0]["adjclose"]
s = pd.Series(cl, index=pd.to_datetime(ts, unit="s")).dropna()
s.index = s.index.normalize().tz_localize(None)
return s[~s.index.duplicated(keep="last")]
def _yfin(sym, days):
"""yfinance ์ž์ฒด ๊ฒฝ๋กœ. ์ฟ ํ‚คยทํฌ๋Ÿผ์„ ์–ป์–ด ์˜ค๋Š” ์ ˆ์ฐจ๊ฐ€ ๋”ฐ๋กœ ์žˆ์–ด ๊ฒฐ๊ณผ๊ฐ€ ๋‹ค๋ฅด๋‹ค."""
import yfinance as yf
d = yf.download(sym, period="5y", interval="1d", progress=False,
auto_adjust=True, threads=False)
if isinstance(d.columns, pd.MultiIndex):
d.columns = d.columns.droplevel(1)
s = d["Close"].dropna()
s.index = pd.to_datetime(s.index).tz_localize(None)
return s
def _stooq(sym, days):
import requests
r = requests.get("https://stooq.com/q/d/l/?s=%s&i=d" % sym,
headers=UA, timeout=25)
if not r.text.lstrip().lower().startswith("date"):
# ๋ด‡ ์ฐจ๋‹จ ํŽ˜์ด์ง€๊ฐ€ 200์œผ๋กœ ์˜จ๋‹ค. 200์„ ์„ฑ๊ณต์œผ๋กœ ์„ธ๋ฉด ์•ˆ ๋œ๋‹ค.
raise ValueError("stooq ์ฐจ๋‹จ(HTML ์‘๋‹ต)")
d = pd.read_csv(io.StringIO(r.text))
if "Close" not in d.columns:
raise ValueError("stooq ์‘๋‹ต์— Close ์—†์Œ")
s = pd.Series(d["Close"].values,
index=pd.to_datetime(d["Date"])).dropna()
return s
def _gecko(cid, days):
import requests
r = requests.get("https://api.coingecko.com/api/v3/coins/%s/market_chart"
"?vs_currency=usd&days=%d&interval=daily" % (cid, min(days, 3650)),
headers=UA, timeout=25)
j = r.json()
if "prices" not in j:
raise ValueError("coingecko: %s" % str(j)[:120])
p = j["prices"]
s = pd.Series([v for _, v in p],
index=pd.to_datetime([t for t, _ in p], unit="ms"))
s.index = s.index.normalize().tz_localize(None)
return s[~s.index.duplicated(keep="last")]
def _feed_h(sym, bars):
"""์‹œ๊ฐ„๋ด‰ ํ”ผ๋“œ. ์ฑ„์  ๊ฒฉ์ž๊ฐ€ ์ด๊ฑธ ์“ด๋‹ค.
๐Ÿ”‘ ์ผ๋ด‰์œผ๋กœ ์ฑ„์ ํ•˜๋ฉด ๊ฐœ๋ง‰์ผ์— ํ•˜๋ฃจ๋ฅผ ํ†ต์งธ๋กœ ๊ธฐ๋‹ค๋ ค์•ผ ์ฒซ ์ˆ˜์ต๋ฅ ์ด ๋‚˜์˜จ๋‹ค.
๊ทœ์น™์—๋Š” '๋งค์‹œ๊ฐ„ ๊ฐฑ์‹  ๊ฐ€๋Šฅ'์ด๋ผ๊ณ  ์จ ๋†“๊ณ  ์ฑ„์ ๋งŒ ์ผ ๋‹จ์œ„์ด๋ฉด ์–ด๊ธ‹๋‚œ๋‹ค.
"""
import os
from huggingface_hub import hf_hub_download
repo = os.environ.get("FINCHAL_FEED", "FINAL-Bench/finchal-prices")
tok = os.environ.get("HF_TOKEN") or os.environ.get("FINCHAL_HF_TOKEN")
f = hf_hub_download(repo, "prices_1h/%s.csv" % sym, repo_type="dataset", token=tok)
d = pd.read_csv(f)
s = pd.Series(d["close"].values, index=pd.to_datetime(d["ts"])).dropna()
s = s[~s.index.duplicated(keep="last")].sort_index()
return s.iloc[-bars:]
def hourly(asset, bars=2000):
"""์‹œ๊ฐ„๋ด‰. ํ”ผ๋“œ์—๋งŒ ์žˆ๋‹ค. ์—†์œผ๋ฉด None ์„ ๋Œ๋ ค์ฃผ๊ณ  ํ˜ธ์ถœ๋ถ€๊ฐ€ ์ผ๋ด‰์œผ๋กœ ๋‚ด๋ ค๊ฐ„๋‹ค."""
sym = MAP.get(asset, {}).get("feed")
if not sym:
return None
try:
s = _feed_h(sym, bars)
return s if len(s) >= 24 else None
except Exception:
return None
def _feed(sym, days):
"""์šฐ๋ฆฌ๊ฐ€ ๋ฐœํ–‰ํ•œ ์‹œ์„ธ ํ”ผ๋“œ. ๊ณต๊ฐœ ์†Œ์Šค๊ฐ€ ์ „๋ถ€ ๋ง‰ํžŒ ์„œ๋ฒ„์—์„œ๋Š” ์ด๊ฒŒ ์œ ์ผํ•œ ๊ธธ์ด๋‹ค.
๐Ÿ”ด ์‹ค์ธก: HF Space์—์„œ Yahoo 429 ยท Stooq ๋ด‡์ฐจ๋‹จ ยท CoinGecko ์ฐจ๋‹จ.
๋ฐ”๊นฅ ์ธํ„ฐ๋„ท์€ ๋˜๋Š”๋ฐ ์‹œ์„ธ ์ œ๊ณต์ž๋“ค์ด ๋ฐ์ดํ„ฐ์„ผํ„ฐ IP๋ฅผ ๋ง‰๋Š”๋‹ค.
๊ทธ๋ž˜์„œ ์‹œ์„ธ๋ฅผ '๊ฐ€์ ธ์˜ค๋Š”' ๋Œ€์‹  '๋ฐ›๋Š”' ๊ตฌ์กฐ๋กœ ๋ฐ”๊พผ๋‹ค โ€”
๋„คํŠธ์›Œํฌ๊ฐ€ ๋˜๋Š” ๊ณณ์—์„œ publish_prices.py ๊ฐ€ ๋ฐ์ดํ„ฐ์…‹์— ์˜ฌ๋ฆฌ๊ณ ,
์„œ๋ฒ„๋Š” ๊ทธ ๋ฐ์ดํ„ฐ์…‹์„ ์ฝ๋Š”๋‹ค.
"""
import os
from huggingface_hub import hf_hub_download
repo = os.environ.get("FINCHAL_FEED", "FINAL-Bench/finchal-prices")
tok = os.environ.get("HF_TOKEN") or os.environ.get("FINCHAL_HF_TOKEN")
f = hf_hub_download(repo, "prices/%s.csv" % sym, repo_type="dataset", token=tok)
d = pd.read_csv(f)
s = pd.Series(d["close"].values, index=pd.to_datetime(d["date"])).dropna()
return s[~s.index.duplicated(keep="last")]
# ํ”ผ๋“œ๋ฅผ ๋งจ ์•ž์— ๋‘”๋‹ค. ์„œ๋ฒ„์—์„œ ์œ ์ผํ•˜๊ฒŒ ํ†ตํ•˜๋Š” ๊ฒฝ๋กœ์ด๊ณ ,
# ๋กœ์ปฌ์—์„œ๋Š” ํ”ผ๋“œ๊ฐ€ ์—†์œผ๋ฉด ๋ฐ”๋กœ ๋‹ค์Œ ์นธ์œผ๋กœ ๋‚ด๋ ค๊ฐ„๋‹ค.
LADDER = [("feed", _feed), ("yahoo", _yahoo), ("yfin", _yfin),
("stooq", _stooq), ("gecko", _gecko)]
def daily(asset, days=1800):
"""์ผ๋ด‰ ์ข…๊ฐ€. ์–ด๋А ์†Œ์Šค์—์„œ ์™”๋Š”์ง€๋„ ํ•จ๊ป˜ ๋Œ๋ ค์ค€๋‹ค.
๋ฐ˜ํ™˜: (์‹œ๋ฆฌ์ฆˆ, ์†Œ์Šค๋ช…, ์‹œ๋„๊ธฐ๋ก)
"""
m = MAP.get(asset, {})
trail = []
for name, fn in LADDER:
sym = m.get(name)
if not sym:
continue
t0 = time.time()
try:
s = fn(sym, days)
n = len(s)
trail.append("%s:%d๋ด‰/%.1fs" % (name, n, time.time() - t0))
if n >= MIN_BARS: # 0๊ฑด์€ ์„ฑ๊ณต์ด ์•„๋‹ˆ๋‹ค
return s.sort_index().iloc[-days:], name, trail
except Exception as e:
# ๐Ÿ”ด ์˜ˆ์™ธ '์ข…๋ฅ˜'๋งŒ ๋‚จ๊ธฐ๋ฉด ์™œ ๋ง‰ํ˜”๋Š”์ง€ ๋ชจ๋ฅธ๋‹ค. ์ง„๋‹จ์ด ์•ˆ ๋˜๋Š” ๋กœ๊ทธ๋Š”
# ์—†๋Š” ๋กœ๊ทธ์™€ ๊ฐ™๋‹ค. ๋ฉ”์‹œ์ง€๊นŒ์ง€ ๋‚จ๊ธด๋‹ค.
trail.append("%s:%s %s" % (name, type(e).__name__, str(e)[:90]))
return None, None, trail
if __name__ == "__main__":
for a in MAP:
s, src, tr = daily(a, 400)
print("%-5s %-7s %s %s" % (
a, src or "์‹คํŒจ", ("%d๋ด‰ ๋งˆ์ง€๋ง‰ %.2f" % (len(s), s.iloc[-1])) if s is not None else "-",
" ยท ".join(tr)))