Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| """FINCHAL ์๋ฒ โ REST + MCP๋ฅผ ํ ํ๋ก์ธ์ค์์ ์๋นํ๋ค. | |
| REST ๋ ์ฌ๋๊ณผ ์น ํ๋ฉด์ด ์ฐ๊ณ , MCP ๋ ์์ด์ ํธ๊ฐ ์ด๋ค. | |
| ๋์ด ๊ฐ์ ์์ฅยท๊ฐ์ ์ฑ์ ๊ธฐ๋ฅผ ๋ณธ๋ค. ๊ฒฝ๋ก๊ฐ ๊ฐ๋ฆฌ๋ฉด ์ฑ์ ์ด ๊ฐ๋ฆฐ๋ค. | |
| """ | |
| import os, sys, json, time, base64, hmac, hashlib, secrets, urllib.parse, urllib.request | |
| import numpy as np, pandas as pd | |
| from fastapi import FastAPI, Request, Header, HTTPException | |
| from fastapi.responses import (JSONResponse, PlainTextResponse, FileResponse, | |
| RedirectResponse) | |
| from fastapi.staticfiles import StaticFiles | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| sys.path[:0] = [HERE, os.path.dirname(HERE)] | |
| import seasons as S | |
| import scoring as SC | |
| import reference as RF | |
| import ledger as L | |
| import mcp as MCP | |
| import fetch as FE | |
| import i18n as I | |
| import baselines as BL | |
| CEIL = json.load(open(os.path.join(os.path.dirname(HERE), "announce_ceiling.json"), | |
| encoding="utf-8"))["ceiling"] | |
| # โโโโโโโโโโโโโโโโโโโโโโโ Hugging Face ๋ก๊ทธ์ธ โโโโโโโโโโโโโโโโโโโโโโโ | |
| # ์ ์์ ์ฐธ๊ฐ์๊ฐ ์์กฐํ ์ ์๋ ๊ณณ์์ ์์ผ ํ๋ค. ์์ ์ ๋ ฅ ID๋ฅผ ๋๋ฉด | |
| # ๋จ์ ์ด๋ฆ์ผ๋ก ์ ์ถํ ์ ์๊ณ , ๊ทธ๋ฌ๋ฉด ์์ํ๊ฐ ์๋ฏธ๋ฅผ ์๋๋ค. | |
| OAUTH_ID = os.environ.get("OAUTH_CLIENT_ID", "") | |
| OAUTH_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "") | |
| OAUTH_ISS = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co") | |
| SPACE_HOST = os.environ.get("SPACE_HOST", "") | |
| COOKIE = "finchal_session" | |
| # ๋ค๋ฅธ ์ถ์ฒ์ iframe ์์์ ๋จ๋ฏ๋ก ์ฐ๋ฆฌ ์ฟ ํค๋ 3์ ์ฟ ํค๋ค. Lax ๋ ๊ฑฐ๊ธฐ์ ๋ฒ๋ ค์ง๊ณ | |
| # None ์ Secure ๋ฅผ ์๊ตฌํ๋ฉฐ Secure ๋ HTTPS ๋ฅผ ์๊ตฌํ๋ค โ Space ์์๋ ์ฐธ, ๋ก์ปฌ์์๋ ๊ฑฐ์ง. | |
| IN_FRAME = bool(SPACE_HOST) | |
| COOKIE_KW = ({"samesite": "none", "secure": True} if IN_FRAME | |
| else {"samesite": "lax", "secure": False}) | |
| # ๐ด ์ฌ์์ํ ๋๋ง๋ค ๋ฐ๋๋ฉด ๋ชจ๋ ์ธ์ ์ด ๋๊ธด๋ค. Space ๋ณ์๋ก ๊ณ ์ ํ ๊ฒ. | |
| SESSION_KEY = os.environ.get("FINCHAL_SESSION_KEY") or secrets.token_hex(16) | |
| app = FastAPI(title="FINCHAL") | |
| S1 = S.SEASONS[1] | |
| # ๊ฐ๋ฐ ์ค ํ๋ฉด ํ์ธ์ฉ. ์์ฆ ๊ฐ๋ง์ผ์๋ ์ฑ์ ํ ๊ตฌ๊ฐ์ด ์์ด ์ฐจํธ์ ์์๊ฐ ๋น๋ฏ๋ก, | |
| # ๊ฐ๋ง ์ ์ ํ๋ฉด์ ๊ฒ์ฆํ๋ ค๋ฉด ์์์ผ์ ์๋น๊ฒจ ๋์ด๋ค. | |
| if os.environ.get("FINCHAL_OPENS"): | |
| S1 = dict(S1, opens=os.environ["FINCHAL_OPENS"]) | |
| ASSETS = S1["assets"] | |
| _PX = {"at": 0.0, "d": {}} | |
| TTL = 900 | |
| _SRC = {} | |
| def prices(asset, interval="1d", bars=800): | |
| """๊ฐ๊ฒฉ ์บ์. ์ด๋ ์์ค๊ฐ ์ด์ ์๋ ์ด ํจ์ ๋ฐ์์๋ ์ ๊ฒฝ ์ฐ์ง ์๋๋ค.""" | |
| k = "%s|%s" % (asset, interval) | |
| if time.time() - _PX["at"] > TTL: | |
| _PX["d"].clear(); _PX["at"] = time.time() | |
| if k not in _PX["d"]: | |
| s, src, trail = FE.daily(asset, 2200) | |
| _SRC[asset] = {"source": src, "trail": trail, | |
| "bars": 0 if s is None else len(s)} | |
| if s is None: | |
| # ๐ด ๋น ์๋ฆฌ์ฆ๋ฅผ ์บ์์ ๋ฃ์ผ๋ฉด TTL ๋์ ๊ณ์ ๋น ๊ฐ์ ์ค๋ค. | |
| # ์์ค๊ฐ ์ ๊น ๋งํ ๊ฒ๊ณผ ์์ ๋งํ ๊ฒ์ ๊ตฌ๋ถํด์ผ ํ๋ค. | |
| return pd.Series(dtype=float) | |
| _PX["d"][k] = s | |
| return _PX["d"][k].iloc[-bars:] | |
| _HX = {"at": 0.0, "d": {}} | |
| def hourly_px(asset): | |
| """์๊ฐ๋ด ์บ์. ์ฑ์ ๊ฒฉ์๊ฐ ์ด๊ฑธ ์ด๋ค.""" | |
| if time.time() - _HX["at"] > 600: | |
| _HX["d"].clear(); _HX["at"] = time.time() | |
| if asset not in _HX["d"]: | |
| _HX["d"][asset] = FE.hourly(asset, 2000) | |
| return _HX["d"][asset] | |
| def season_px(asset): | |
| """์์ฆ ๊ฐ๋ง ์ดํ ๊ตฌ๊ฐ. ์ฑ์ ๊ณผ ๊ธฐ์ค๋ถํฌ๊ฐ ์ด๊ฑธ ๋ณธ๋ค. | |
| ๐ ์๊ฐ๋ด์ด ์์ผ๋ฉด ์๊ฐ๋ด์ผ๋ก ์ฐ๋ค. ์ผ๋ด์ผ๋ก ์ฑ์ ํ๋ฉด ๊ฐ๋ง์ผ์ ํ๋ฃจ๋ฅผ | |
| ํต์งธ๋ก ๊ธฐ๋ค๋ ค์ผ ์ฒซ ์์ต๋ฅ ์ด ๋์ค๋๋ฐ, ๊ท์น์ '๋งค์๊ฐ ๊ฐฑ์ ๊ฐ๋ฅ'์ด๋ผ๊ณ | |
| ๋งํ๊ณ ์๋ค. ์ฌ๋ ๋จ์์ ์ฝ์ํ ๋จ์๊ฐ ๋ค๋ฅด๋ฉด ์ ๋๋ค. | |
| ์๊ฐ๋ด์ด ์์ง ์๋ ์ข ๋ชฉ์ ์ผ๋ด์ผ๋ก ๋ด๋ ค๊ฐ๋ค โ ๋น์ด ์๋ ๊ฒ๋ณด๋ค ๋ซ๋ค. | |
| """ | |
| op = pd.Timestamp(S1["opens"]) | |
| h = hourly_px(asset) | |
| if h is not None and len(h): | |
| sub = h[h.index >= op] | |
| if len(sub) >= 2: | |
| return sub | |
| d = prices(asset, "1d", 2000) | |
| return d[d.index >= op] | |
| def rules_doc(lang="ko"): | |
| T = lambda d: I.t(d, lang) | |
| return { | |
| "lang": lang, | |
| "season": S1["name"], "opens": S1["opens"], "closes": S1["closes"], | |
| "days": (pd.Timestamp(S1["closes"]) - pd.Timestamp(S1["opens"])).days, | |
| "prize": T(I.RULES["prize"]) % (S1["prize_usd_per_asset"], | |
| S1["prize_total_usd"]), | |
| "leverage": T(I.RULES["leverage"]), | |
| "update": T(I.RULES["update"]), | |
| "fees": {k: SC.fee_of(k) for k in ASSETS}, | |
| "fee_note": T(I.RULES["fee_note"]), | |
| "scoring": T(I.RULES["scoring"]), | |
| "participation": T(I.RULES["participation"]), | |
| "assets": {k: {"label": T(I.ASSET[k]["label"]), "why": T(I.ASSET[k]["why"]), | |
| "vol_annual": v["vol"], "hours": v["hours"]} | |
| for k, v in ASSETS.items()}, | |
| "types": {k: I.t(v, lang) for k, v in I.TYPE.items()}, | |
| "warning": T(I.RULES["warning"]), | |
| "manifesto": T(I.MANIFESTO["title"]) + " โ " + T(I.MANIFESTO["sub"]), | |
| # ์ด์ ํ๊ณ์ . ์ค๋ ฅ 0 ์ฐธ๊ฐ์๋ฅผ 20,000ํ ๋๋ ค ๋ธ ์์ 5% ์ง์ ์ด๋ค. | |
| # ์ด ๊ฐ ์๋์ ์์ต์ ์ด์ผ๋ก๋ ์ค๋ช ๋๋ค. | |
| "luck_ceiling": {k: round(v["p95"], 4) for k, v in CEIL.items()}, | |
| "luck_note": T(I.RULES["luck_note"]), | |
| } | |
| def _sign(payload): | |
| raw = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=") | |
| sig = hmac.new(SESSION_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32] | |
| return raw + "." + sig | |
| def _unsign(token): | |
| try: | |
| raw, sig = (token or "").rsplit(".", 1) | |
| except ValueError: | |
| return None | |
| good = hmac.new(SESSION_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32] | |
| if not hmac.compare_digest(sig, good): | |
| return None | |
| pad = "=" * (-len(raw) % 4) | |
| try: | |
| return json.loads(base64.urlsafe_b64decode(raw + pad)) | |
| except Exception: | |
| return None | |
| def current_user(request): | |
| return _unsign(request.cookies.get(COOKIE)) | |
| def _redirect_uri(request): | |
| if SPACE_HOST: | |
| return "https://%s/auth/callback" % SPACE_HOST | |
| return str(request.base_url).rstrip("/") + "/auth/callback" | |
| def lang_of(request, explicit=None): | |
| """๋ธ๋ผ์ฐ์ ์ธ์ด๋ฅผ ์๋ ์๋ณํ๋ค. ์ฌ์ฉ์๊ฐ ๊ณ ๋ฅธ ๊ฐ์ด ์์ผ๋ฉด ๊ทธ๊ฒ ์ด๊ธด๋ค.""" | |
| return I.pick(request.headers.get("accept-language"), | |
| explicit or request.cookies.get("finchal_lang") | |
| or request.query_params.get("lang")) | |
| def me_from(key, lang="ko"): | |
| u = L.user_by_key(key or "") | |
| if not u: | |
| raise PermissionError(I.t(I.HINT["need_key"], lang)) | |
| return u | |
| def score_asset(asset): | |
| """๊ทธ ์ข ๋ชฉ ์ฐธ๊ฐ์ ์ ์์ ์ฑ์ ํ๋ค. ๊ธฐ์ค๋ถํฌ๋ ์์ฆ ์ค์ ๊ฒฝ๋ก๋ก ๋ง๋ ๋ค.""" | |
| px = season_px(asset) | |
| if len(px) < 2: | |
| # ๐ด ๊ฐ๋ง์ผ์๋ ์ฑ์ ํ ๊ตฌ๊ฐ์ด ์๋ค. ๊ทธ๋ ๋ค๊ณ ๋น ํ๋ฅผ ๋ด๋ณด๋ด๋ฉด | |
| # ์ด๋ฏธ ์ ์ํ ์ฐธ๊ฐ์์๊ฒ "์๋ฌด๋ ์๋ค"๊ณ ๋งํ๋ ์ ์ด๊ณ , ๋ฐฉ๋ฌธ์์๊ฒ๋ | |
| # ๊ณ ์ฅ์ผ๋ก ๋ณด์ธ๋ค. ์ ์๋ ์ฌ์ค์ ๊ทธ๋๋ก ๋ณด์ฌ ์ฃผ๊ณ ์์น๋ง ๋น์ ๋๋ค. | |
| users = {u["user"]: u for u in L.all_users()} | |
| seen, out = set(), [] | |
| for r in L.positions(asset): | |
| seen.add(r["u"]) | |
| for uid in seen: | |
| u = users.get(uid, {}) | |
| last = L.latest(asset, uid) | |
| out.append({"user": uid, "strategy": u.get("strategy", "-"), | |
| "type": u.get("type", "agent"), | |
| "joined": u.get("joined", ""), | |
| "ret": None, "score": None, "luck_p95": None, | |
| "turnover": None, "fee_paid": None, "valid": None, | |
| "position": last["p"] if last else None, | |
| "pending": True, | |
| "n_sub": len(L.positions(asset, uid))}) | |
| out.sort(key=lambda x: x["joined"]) | |
| for i, r in enumerate(out, 1): | |
| r["rank"] = i | |
| return out | |
| fee = SC.fee_of(asset) | |
| # ๐ด ํ๋ณธ์ด ์์ผ๋ฉด ์ค์๊ฐ ๊ธฐ์ค๋ถํฌ์ ํญ์ด ๊ฑฐ์ 0์ด๋ผ, ์์ค์ ๋ธ ์ฐธ๊ฐ์์๊ฒ๋ | |
| # '์ด์ ํ๊ณ์ ๋ํ'๊ฐ ๋ถ๋๋ค. ์ค์ ๋ก ๊ธ์์ โ0.033%์ ํ์๊ฐ ๋ถ์๋ค. | |
| # ์ก์์ ์ฑ์ ์ผ๋ก ๋ณด์ฌ์ฃผ๋ ๊ฒ์ด ์ด ๋ํ๊ฐ ์์ ๋ ค๋ ๋ฌธ์ ๊ทธ ์์ฒด์ด๋ฏ๋ก, | |
| # ๊ตฌ๊ฐ์ด ์ถฉ๋ถํ ์์ด๊ธฐ ์ ์๋ ํ์ ์์ฒด๋ฅผ ๋ด๋ฆฌ์ง ์๋๋ค. | |
| span_days = (px.index[-1] - px.index[0]).days | |
| luck_ready = span_days >= 5 and len(px) >= 30 | |
| recs = L.positions(asset) | |
| rows = [] | |
| by_user = {} | |
| for r in recs: | |
| by_user.setdefault(r["u"], {})[pd.Timestamp(r["t"]).tz_localize(None)] = r["p"] | |
| users = {u["user"]: u for u in L.all_users()} | |
| for uid, pos in by_user.items(): | |
| ret = SC.total_return(pos, px, asset) | |
| expo = float(np.abs(pd.Series(pos).reindex( | |
| px.index, method="ffill").fillna(0.0)).mean()) | |
| refv = RF.live(px, fee, expo=max(expo, 0.05), n=3000) | |
| u = users.get(uid, {}) | |
| rows.append({ | |
| "user": uid, "strategy": u.get("strategy", "-"), | |
| "type": u.get("type", "agent"), | |
| "joined": u.get("joined", ""), | |
| "ret": ret, "score": SC.score(ret, refv), | |
| "luck_p95": float(np.percentile(refv, 95)), | |
| "luck_ready": luck_ready, "span_days": span_days, | |
| "turnover": SC.turnover(pos, px), | |
| "fee_paid": SC.fee_paid(pos, px, asset), | |
| "valid": SC.valid_ratio(pos, px), | |
| "n_sub": len(L.positions(asset, uid)), | |
| }) | |
| rows.sort(key=lambda x: -x["ret"]) | |
| for i, r in enumerate(rows, 1): | |
| r["rank"] = i | |
| return rows | |
| _SC = {} | |
| SC_TTL = 300 | |
| def score_cached(asset): | |
| """์ฑ์ ์ ์ฐธ๊ฐ์๋ง๋ค ๊ธฐ์ค๋ถํฌ 3,000๊ฒฝ๋ก๋ฅผ ๋๋ฆฐ๋ค. ๋งค ์์ฒญ๋ง๋ค ํ๋ฉด ํ๋ฉด์ด ์ฃฝ๋๋ค.""" | |
| e = _SC.get(asset) | |
| if e and time.time() - e[0] < SC_TTL: | |
| return e[1] | |
| r = score_asset(asset) | |
| _SC[asset] = (time.time(), r) | |
| return r | |
| def curves(asset, top=12): | |
| """์ฐธ๊ฐ์๋ณ ๋์ ์์ต๋ฅ ๊ณก์ . ํ๋ฉด์ ๊บพ์์ ๊ทธ๋ํ๊ฐ ์ด๊ฑธ ๊ทธ๋ฆฐ๋ค.""" | |
| px = season_px(asset) | |
| if len(px) < 2: | |
| return {"t": [], "series": []} | |
| rows = score_cached(asset) | |
| users = {u["user"]: u for u in L.all_users()} | |
| keep = [r["user"] for r in rows[:top]] | |
| ser = [] | |
| for uid in keep: | |
| pos = {} | |
| for r in L.positions(asset, uid): | |
| pos[pd.Timestamp(r["t"]).tz_localize(None)] = r["p"] | |
| if not pos: | |
| continue | |
| eq, _ = SC.equity(pos, px, asset) | |
| u = users.get(uid, {}) | |
| ser.append({"user": uid, "strategy": u.get("strategy", "-"), | |
| "type": u.get("type", "agent"), | |
| "v": [round(float(x - 1) * 100, 4) for x in eq]}) | |
| # ์์ฅ ์์ฒด๋ ํ ์ค๋ก ๊น์ ์ค๋ค โ ์ฐธ๊ฐ์๊ฐ ๋ฌด์์ ์ด๊ฒจ์ผ ํ๋์ง ๋ณด์ด๊ฒ | |
| mk = (px / px.iloc[0] - 1) * 100 | |
| return {"t": [t.isoformat() for t in px.index], | |
| "market": [round(float(x), 4) for x in mk], | |
| "series": ser} | |
| # โโโโโโโโโโโโโโโโโโโโโโโ REST โโโโโโโโโโโโโโโโโโโโโโโ | |
| def api_rules(request: Request, lang: str = None): | |
| return {"ok": True, **rules_doc(lang_of(request, lang))} | |
| def api_me(request: Request): | |
| u = current_user(request) | |
| out = {"signed_in": bool(u), "user": u, | |
| "oauth_configured": bool(OAUTH_ID and OAUTH_SECRET), | |
| "lang": lang_of(request)} | |
| if u: | |
| # ๋ก๊ทธ์ธํ ์ฌ๋์ ๋ฑ๋ก์ ๋ฐ๋ก ํ ํ์๊ฐ ์๋ค. ํค๋ฅผ ๋ฐ๋ก ์ค๋ค. | |
| out["key"] = L.make_key(u["name"]) | |
| rec = next((x for x in L.all_users() if x["user"] == u["name"]), None) | |
| out["strategy"] = (rec or {}).get("strategy", "") | |
| out["joined"] = (rec or {}).get("joined") | |
| return out | |
| def login(request: Request): | |
| if not (OAUTH_ID and OAUTH_SECRET): | |
| raise HTTPException(503, "OAuth is not configured (needs hf_oauth in README)") | |
| state = secrets.token_urlsafe(16) | |
| q = urllib.parse.urlencode({ | |
| "client_id": OAUTH_ID, "redirect_uri": _redirect_uri(request), | |
| "response_type": "code", "scope": "openid profile", "state": state}) | |
| r = RedirectResponse("%s/oauth/authorize?%s" % (OAUTH_ISS.rstrip("/"), q)) | |
| # ์ ๊ณต์๊ฐ ๋๋๋ ค์ฃผ๋ state ๋ฅผ ์ด ์ฟ ํค์ ๋์กฐํ๋ ๊ฒ์ด, | |
| # ์ 3์๊ฐ ๋จ์ ๋ก๊ทธ์ธ์ ๊ฐ๋ก์ฑ ์๋ฃํ๋ ๊ฒ์ ๋ง๋ ์ ์ผํ ์๋จ์ด๋ค. | |
| r.set_cookie("finchal_state", state, max_age=600, httponly=True, **COOKIE_KW) | |
| return r | |
| def callback(request: Request, code: str = "", state: str = ""): | |
| lang = lang_of(request) | |
| saved = request.cookies.get("finchal_state") | |
| if not code: | |
| raise HTTPException(400, "No authorization code โ please sign in again") | |
| if not saved: | |
| # ์ฟ ํค๊ฐ ์ ์ฅ๋์ง ์์ ๊ฒ์ด๋ค. ์์กฐ๊ฐ ์๋๋ผ ๋ธ๋ผ์ฐ์ ์ ์ฑ ๋ฌธ์ ๋ค. | |
| raise HTTPException(400, ( | |
| "๋ธ๋ผ์ฐ์ ๊ฐ ์ฟ ํค๋ฅผ ์ ์ฅํ์ง ๋ชปํ์ต๋๋ค. ์ด ํ์ด์ง๋ฅผ ์ ํญ์์ ์ง์ ์ด๊ณ " | |
| "(.hf.space ์ฃผ์) ๋ค์ ๋ก๊ทธ์ธํด ์ฃผ์ธ์. 3์ ์ฟ ํค ์ฐจ๋จ์ด ์์ธ์ผ ์ ์์ต๋๋ค." | |
| if lang == "ko" else | |
| "Your browser did not store the cookie. Open this page directly in a new tab " | |
| "(the .hf.space address) and sign in again โ third-party cookie blocking is " | |
| "the usual cause.")) | |
| if state != saved: | |
| raise HTTPException(400, "Sign-in state mismatch โ please try again") | |
| body = urllib.parse.urlencode({ | |
| "client_id": OAUTH_ID, "client_secret": OAUTH_SECRET, | |
| "grant_type": "authorization_code", "code": code, | |
| "redirect_uri": _redirect_uri(request)}).encode() | |
| tr = urllib.request.Request(OAUTH_ISS.rstrip("/") + "/oauth/token", data=body, | |
| headers={"Content-Type": "application/x-www-form-urlencoded"}) | |
| with urllib.request.urlopen(tr, timeout=20) as r: | |
| tok = json.loads(r.read().decode()) | |
| ur = urllib.request.Request(OAUTH_ISS.rstrip("/") + "/oauth/userinfo", | |
| headers={"Authorization": "Bearer " + tok["access_token"]}) | |
| with urllib.request.urlopen(ur, timeout=20) as r: | |
| ui = json.loads(r.read().decode()) | |
| user = {"name": ui.get("preferred_username") or ui.get("name"), | |
| "picture": ui.get("picture"), "at": int(time.time())} | |
| resp = RedirectResponse("/") | |
| resp.set_cookie(COOKIE, _sign(user), max_age=60 * 60 * 24 * 14, | |
| httponly=True, **COOKIE_KW) | |
| resp.delete_cookie("finchal_state") | |
| return resp | |
| def logout(): | |
| r = RedirectResponse("/") | |
| r.delete_cookie(COOKIE) | |
| return r | |
| def set_lang(code: str): | |
| """์ธ์ด ์ ํ์ ๊ธฐ์ตํ๋ค. ์๋ ์๋ณ๋ณด๋ค ์ฌ์ฉ์์ ์ ํ์ด ์ฐ์ ํ๋ค.""" | |
| if code not in I.LANGS: | |
| return JSONResponse({"ok": False, "error": "unknown language"}, 400) | |
| r = JSONResponse({"ok": True, "lang": code}) | |
| r.set_cookie("finchal_lang", code, max_age=60 * 60 * 24 * 365, **COOKIE_KW) | |
| return r | |
| def api_data(asset: str, bars: int = 500, interval: str = "1d"): | |
| if asset not in ASSETS: | |
| return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข ๋ชฉ"}, 400) | |
| if interval == "1h": | |
| h = hourly_px(asset) | |
| if h is None or not len(h): | |
| return JSONResponse({"ok": False, | |
| "error": "์๊ฐ๋ด ์์ / no hourly bars yet"}, 400) | |
| return {"ok": True, "asset": asset, "interval": "1h", | |
| "bars": [{"t": t.isoformat(), "c": float(v)} | |
| for t, v in h.iloc[-min(max(bars, 10), 5000):].items()]} | |
| if interval != "1d": | |
| return JSONResponse({"ok": False, | |
| "error": "1d ๋๋ 1h / 1d or 1h only"}, 400) | |
| s = prices(asset, interval, min(max(bars, 10), 5000)) | |
| return {"ok": True, "asset": asset, "interval": interval, | |
| "bars": [{"t": t.isoformat(), "c": float(v)} for t, v in s.items()]} | |
| # ๐ด ์์จ ์์ด์ ํธ๋ ๋ธ๋ผ์ฐ์ OAuth ๋ฅผ ํต๊ณผํ ์ ์๋ค. ๊ทธ๋ ๋ค๊ณ ์๋ฌด๋ ์ด๋ฆ์ | |
| # ์ง์ด ๋ฑ๋กํ๊ฒ ๋๋ฉด ๋จ์ ์ด๋ฆ์ผ๋ก ์ ์ถํ ์ ์์ด ์์ํ๊ฐ ๋ฌด์๋ฏธํด์ง๋ค. | |
| # ๊ทธ๋์ ๋ณ๋ ๊ด๋ฆฌ ํค๋ฅผ ๊ฐ์ง ์์ฒญ๋ง ์ง์ ์ด๋ฆ์ผ๋ก ๋ฑ๋กํ ์ ์๊ฒ ํ๋ค. | |
| # ์ด ํค๋ Space ์ํฌ๋ฆฟ์ด๊ณ , ๋ฐ๊ธ ๋์์ ์ด์์๊ฐ ์ง์ ํ์ธํ ๊ณ์ ๋ฟ์ด๋ค. | |
| ADMIN_KEY = os.environ.get("FINCHAL_ADMIN_KEY", "") | |
| async def api_agent_enroll(req: Request, x_finchal_admin: str = Header(None)): | |
| """ํค๋๋ฆฌ์ค ์์ด์ ํธ ๋ฑ๋ก. ๊ด๋ฆฌ ํค๊ฐ ์์ด์ผ ํ๋ค.""" | |
| if not ADMIN_KEY or (x_finchal_admin or "").strip() != ADMIN_KEY: | |
| return JSONResponse({"ok": False, "error": "not authorized"}, 403) | |
| b = await req.json() | |
| u = L.register(b["user"], b.get("strategy", "-"), b.get("type", "agent"), | |
| b.get("assets", list(ASSETS))) | |
| return {"ok": True, **u} | |
| async def api_register(req: Request): | |
| b = await req.json() | |
| sess = current_user(req) | |
| if OAUTH_ID and OAUTH_SECRET: | |
| if not (sess and sess.get("name")): | |
| return JSONResponse({"ok": False, "error": | |
| ("Hugging Face ๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค" if lang_of(req) == "ko" | |
| else "Please sign in with Hugging Face")}, 401) | |
| who = sess["name"] # ์ ์์ ์ ์ถ์๊ฐ ์ ํ์ง ์๋๋ค | |
| else: | |
| who = b["user"] | |
| u = L.register(who, b.get("strategy", "-"), | |
| b.get("type", "human" if sess else "agent"), | |
| b.get("assets", list(ASSETS))) | |
| return {"ok": True, **u} | |
| async def api_submit(req: Request, authorization: str = Header(None), | |
| x_finchal_key: str = Header(None)): | |
| b = await req.json() | |
| lang = lang_of(req) | |
| # Authorization ์ ์๋จ ํ๋ก์๊ฐ ์๊ธฐ ์ธ์ฆ์ ์ฐ๊ธฐ๋ ํ๋ค. ๊ทธ๋ ์ฐธ๊ฐํค๊ฐ | |
| # ์ฑ๊น์ง ์ค์ง ์๋๋ค. ์ ์ฉ ํค๋๋ฅผ ํจ๊ป ๋ฐ์ ๊ทธ ์ถฉ๋์ ํผํ๋ค. | |
| key = ((x_finchal_key or "").strip() | |
| or (authorization or "").replace("Bearer ", "").strip() | |
| or b.get("key", "")) | |
| # ๐ ํ๋ฉด์์ ์จ ์์ฒญ์ Hugging Face ๋ก๊ทธ์ธ ์ฟ ํค๊ฐ ๊ณง ์ ์์ด๋ค. | |
| # ์์ ์ ๋ ฅ ID๋ฅผ ํ์ฉํ๋ฉด ๋จ์ ์ด๋ฆ์ผ๋ก ์ ์ถํ ์ ์๊ณ ์์ํ๊ฐ ๋ฌด์๋ฏธํด์ง๋ค. | |
| sess = current_user(req) | |
| if sess and sess.get("name"): | |
| u = L.register(sess["name"], b.get("strategy", "-"), | |
| b.get("type", "human"), list(ASSETS)) | |
| else: | |
| if OAUTH_ID and OAUTH_SECRET and not key: | |
| return JSONResponse({"ok": False, "error": | |
| ("Hugging Face ๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค" if lang == "ko" | |
| else "Please sign in with Hugging Face")}, 401) | |
| try: | |
| u = me_from(key, lang) | |
| except PermissionError as e: | |
| return JSONResponse({"ok": False, "error": str(e)}, 401) | |
| if b["asset"] not in ASSETS: | |
| return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข ๋ชฉ"}, 400) | |
| r = L.submit(u["user"], b["asset"], b["position"], b.get("source", "rest")) | |
| _SC.pop(b["asset"], None) # ๋ฐฉ๊ธ ๋ธ ์ฌ๋์ด ํ์์ ์๊ธฐ๋ฅผ ๋ชป ์ฐพ์ผ๋ฉด ์ ๋๋ค | |
| return {"ok": True, **r} | |
| def api_leaderboard(asset: str): | |
| if asset not in ASSETS: | |
| return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข ๋ชฉ"}, 400) | |
| return {"ok": True, "asset": asset, "rows": score_cached(asset)} | |
| def api_curves(asset: str, top: int = 12): | |
| if asset not in ASSETS: | |
| return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข ๋ชฉ"}, 400) | |
| return {"ok": True, "asset": asset, **curves(asset, top)} | |
| _BLC = {} | |
| def api_baselines(request: Request, asset: str, lang: str = None): | |
| """๊ต๊ณผ์ ๊ท์น๋ค์ ์ต๊ทผ ๊ตฌ๊ฐ์ ์ ์ฉํ ๋๋์๋ณด๊ธฐ. ์ฐธ๊ฐ์ ๊ณก์ ์ ์ฒ๋๋ก ์ด๋ค.""" | |
| if asset not in ASSETS: | |
| return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข ๋ชฉ"}, 400) | |
| g = lang_of(request, lang) | |
| k = "%s|%s" % (asset, g) | |
| e = _BLC.get(k) | |
| if not e or time.time() - e[0] > 1800: | |
| _BLC[k] = (time.time(), BL.curves(prices(asset, "1d", 400), asset, SC, g)) | |
| return {"ok": True, "asset": asset, **_BLC[k][1]} | |
| def api_manifesto(request: Request, lang: str = None): | |
| g = lang_of(request, lang) | |
| T = lambda d: I.t(d, g) | |
| M = I.MANIFESTO | |
| s2 = S.SEASONS.get(2, {}) | |
| return {"ok": True, "lang": g, | |
| "title": T(M["title"]), "sub": T(M["sub"]), | |
| "acts": [[T(a), T(b)] for a, b in M["acts"]], | |
| "why": [[T(a), T(b)] for a, b in M["why"]], | |
| "how": [[T(a), T(b)] for a, b in M["how"]], | |
| "our_stake": T(M["our_stake"]), | |
| "anchors": {k: T(v) for k, v in I.ANCHOR.items()}, | |
| "season2": {"name": ("์์ฆ 2 ยท ์๊ณ " if g == "ko" else "Season 2 ยท Coming"), | |
| "teaser": ("์ ๋ฌผ๊ณผ ์์์ฌ, ๊ทธ๋ฆฌ๊ณ ์ธํ" if g == "ko" | |
| else "Futures, commodities and currencies"), | |
| "note": ("์ข ๋ชฉ์ ์์ฆ 1 ์ข ๋ฃ ํ ํ์ ํฉ๋๋ค." if g == "ko" | |
| else "Assets will be announced after Season 1 closes.")}} | |
| # โโโโโโโโโโโโโโโโโโโโโโโ MCP โโโโโโโโโโโโโโโโโโโโโโโ | |
| def _call(name, args, key=None, lang="ko"): | |
| T = lambda d: I.t(d, lang) | |
| if name == "get_rules": | |
| return rules_doc(lang) | |
| if name == "get_data": | |
| a = args["asset"] | |
| if a not in ASSETS: | |
| return {"error": T(I.HINT["unknown_asset"]) % list(ASSETS)} | |
| iv = args.get("interval", "1d") | |
| n = min(args.get("bars", 500), 5000) | |
| if iv == "1h": | |
| h = hourly_px(a) | |
| if h is None or not len(h): | |
| return {"error": ("์ด ์ข ๋ชฉ์ ์์ง ์๊ฐ๋ด์ด ์์ต๋๋ค. '1d' ๋ฅผ ์ฐ์ญ์์ค." | |
| if lang == "ko" else | |
| "No hourly bars for this asset yet. Use '1d'.")} | |
| return {"asset": a, "interval": "1h", | |
| "bars": [{"t": t.isoformat(sep=" "), "c": round(float(v), 6)} | |
| for t, v in h.iloc[-n:].items()]} | |
| if iv != "1d": | |
| # ์์ฒญ์ ์กฐ์ฉํ ๋ฌด์ํ์ง ์๋๋ค. ๋ชป ์ฃผ๋ ๊ฒ์ ๋ชป ์ค๋ค๊ณ ๋งํ๋ค. | |
| return {"error": ("interval ์ '1d' ๋๋ '1h' ๋ง ๊ฐ๋ฅํฉ๋๋ค." | |
| if lang == "ko" else | |
| "interval must be '1d' or '1h'.")} | |
| s = prices(a, iv, n) | |
| return {"asset": a, "interval": "1d", | |
| "bars": [{"t": t.date().isoformat(), "c": round(float(v), 6)} | |
| for t, v in s.items()]} | |
| if name == "submit_position": | |
| u = me_from(key, lang) | |
| a = args["asset"] | |
| if a not in ASSETS: | |
| return {"error": T(I.HINT["unknown_asset"]) % list(ASSETS)} | |
| r = L.submit(u["user"], a, args["position"], "mcp") | |
| _SC.pop(a, None) | |
| return {"accepted": True, "asset": a, "position": r["p"], "at": r["t"], | |
| "note": T(I.HINT["kept"])} | |
| if name == "check_score": | |
| u = me_from(key, lang) | |
| want = [args["asset"]] if args.get("asset") else u.get("assets", list(ASSETS)) | |
| out = [] | |
| for a in want: | |
| if a not in ASSETS: | |
| continue | |
| rows = score_asset(a) | |
| mine = next((r for r in rows if r["user"] == u["user"]), None) | |
| if not mine: | |
| last = L.latest(a, u["user"]) | |
| out.append({"asset": a, "status": T(I.HINT["pending"]), | |
| "position": last["p"] if last else None, | |
| "hint": T(I.HINT["have_pos"] if last | |
| else I.HINT["no_pos"])}) | |
| elif mine.get("pending") or mine.get("ret") is None: | |
| # ๐ด ํ์๋ ์ฌ๋ผ์ ์์ง๋ง ์์ง ์ด ๊ตฌ๊ฐ์ด ์๋ค. ์ฌ๊ธฐ์ ์์น๋ฅผ | |
| # ๋ฐ์ฌ๋ฆผํ๋ ค ๋ค๋ฉด None ์ ๊ฑธ๋ ค ๋๊ตฌ ํธ์ถ ์์ฒด๊ฐ ์คํจํ๋ค. | |
| out.append({"asset": a, "status": T(I.HINT["pending"]), | |
| "position": mine.get("position"), | |
| "of": len(rows), | |
| "hint": T(I.HINT["have_pos"])}) | |
| else: | |
| out.append({"asset": a, "rank": mine["rank"], "of": len(rows), | |
| "return": round(mine["ret"], 4), | |
| "luck_ceiling": round(mine["luck_p95"], 4), | |
| "score": round(mine["score"], 2), | |
| "fee_paid": round(mine["fee_paid"], 4), | |
| "hint": T(I.HINT["over"] if mine["ret"] > mine["luck_p95"] | |
| else I.HINT["under"])}) | |
| return {"user": u["user"], "strategy": u.get("strategy"), "results": out} | |
| return {"error": "๋ฏธ๊ตฌํ"} | |
| async def mcp_endpoint(req: Request, authorization: str = Header(None), | |
| x_finchal_key: str = Header(None)): | |
| body = await req.json() | |
| lang = lang_of(req) | |
| key = ((x_finchal_key or "").strip() | |
| or (authorization or "").replace("Bearer ", "").strip()) | |
| resp = MCP.handle(body, lambda n, a: _call(n, a, key, lang), lang) | |
| if resp is None: | |
| return JSONResponse({}, 202) | |
| return JSONResponse(resp) | |
| def llms(request: Request, lang: str = None): | |
| """์์ด์ ํธ๊ฐ ๋จผ์ ์ฝ๋ ์์ฝ. ๋๊ตฌ ์ค๋ช ๊ณผ ๊ฐ์ ์ธ์ด์ฌ์ผ ํ๋ค.""" | |
| g = lang_of(request, lang) | |
| r = rules_doc(g) | |
| a = ", ".join("%s (%s)" % (k, v["label"]) for k, v in r["assets"].items()) | |
| N = "\n" | |
| if g == "en": | |
| return (("# %s" + N + N) % r["manifesto"] + | |
| "You take real positions and compete on returns. " | |
| "This is trading, not calling direction." + N + N + | |
| ("Assets: %s" + N) % a + | |
| ("Season: %s to %s" + N) % (r["opens"], r["closes"]) + | |
| ("Prize: %s" + N) % r["prize"] + | |
| ("Position: %s" + N) % r["leverage"] + | |
| ("Updates: %s" + N) % r["update"] + | |
| "Fees: charged on position changes only. " | |
| "Flipping often loses money." + N + | |
| ("Scoring: %s" + N + N) % r["scoring"] + | |
| "Start: get_rules() -> get_data() -> build your own model " | |
| "-> submit_position()" + N) | |
| return (("# %s" + N + N) % r["manifesto"] + | |
| "๋น์ ์ ์ค์ ํฌ์ง์ ์ ์ก์ ์์ต๋ฅ ๋ก ๊ฒจ๋ฃน๋๋ค. ๋ฐฉํฅ ์์ธก์ด ์๋๋ผ ๋งค๋งค์ ๋๋ค." | |
| + N + N + | |
| ("์ข ๋ชฉ: %s" + N) % a + | |
| ("๊ธฐ๊ฐ: %s ~ %s" + N) % (r["opens"], r["closes"]) + | |
| ("์๊ธ: %s" + N) % r["prize"] + | |
| ("ํฌ์ง์ : %s" + N) % r["leverage"] + | |
| ("๊ฐฑ์ : %s" + N) % r["update"] + | |
| "์์๋ฃ: ํฌ์ง์ ๋ณ๊ฒฝ๋ถ์๋ง. ์์ฃผ ๋ค์ง์ผ๋ฉด ์ํด์ ๋๋ค" + N + | |
| ("์ฑ์ : %s" + N + N) % r["scoring"] + | |
| "์์: get_rules() โ get_data() โ ์ง์ ๋ชจ๋ธ๋ง โ submit_position()" + N) | |
| STATIC = os.path.join(HERE, "static") | |
| if os.path.isdir(STATIC): | |
| app.mount("/static", StaticFiles(directory=STATIC), name="static") | |
| def index(): | |
| f = os.path.join(STATIC, "index.html") | |
| if not os.path.exists(f): | |
| return JSONResponse({"ok": True, "note": "ํ๋ฉด ์ค๋น ์ค", **rules_doc()}) | |
| return FileResponse(f, headers={"Cache-Control": "no-cache"}) | |
| # โโโโโโโโโโโโโโโโโโโโโโโ ๊ธฐ์ค์ ์๋ ์ ์ถ โโโโโโโโโโโโโโโโโโโโโโโ | |
| def _anchor_loop(): | |
| """ํ๋ฃจ ํ ๋ฒ ๊ธฐ์ค์ ์ ์ ํฌ์ง์ ์ ๋ธ๋ค. | |
| ๐ ์คํจํด๋ ์๋ฒ๋ฅผ ์ฃฝ์ด๋ฉด ์ ๋๋ค. ๊ธฐ์ค์ ์ ์ฒ๋์ด์ง ์๋น์ค๊ฐ ์๋๋ค. | |
| yfinance๊ฐ ์ ๊น ๋งํ๋ ํ๋ฉด๊ณผ ์ฐธ๊ฐ์ ์ ์ถ์ ๊ณ์ ๋์๊ฐ์ผ ํ๋ค. | |
| """ | |
| import traceback | |
| import anchors as A | |
| while True: | |
| try: | |
| A.run(list(ASSETS), lambda a: prices(a, "1d", 1500), | |
| opens=S1["opens"] + "T00:00:00+00:00") | |
| _SC.clear() | |
| except Exception: | |
| traceback.print_exc() | |
| time.sleep(6 * 3600) | |
| # ๐ ์์ฅ ๋ณต์์ **๋ค๋ฅธ ๋ฌด์๋ณด๋ค ๋จผ์ **๋ค. ๋ณต์ ์ ์ ๋ถํ ํ์์ ์ฐ๋ฉด | |
| # ๊ทธ ํ์์ด ๋ฏธ๋ฌ๋ฅผ ๋ฎ์ด ์์ฅ์ ์ง์ด๋ค. | |
| _RESTORE = L.restore() | |
| _DUR = L.boot_check() | |
| def _flush_loop(): | |
| """์์ฅ์ ์ฃผ๊ธฐ์ ์ผ๋ก ๋ฏธ๋ฌ์ ์ฌ๋ฆฐ๋ค. ์ ์ถ๋ง๋ค ์ปค๋ฐํ๋ฉด ์ ์ฅ์๊ฐ ํฐ์ง๋ค.""" | |
| import traceback | |
| while True: | |
| time.sleep(90) | |
| try: | |
| L.flush() | |
| except Exception: | |
| traceback.print_exc() | |
| def _startup(): | |
| import threading | |
| print("[finchal] ์์ฅ %s ยท ๋ฏธ๋ฌ %s" % (L.ROOT, _RESTORE)) | |
| if L.MIRROR: | |
| threading.Thread(target=_flush_loop, daemon=True).start() | |
| if os.environ.get("FINCHAL_NO_ANCHOR"): | |
| return | |
| threading.Thread(target=_anchor_loop, daemon=True).start() | |
| def api_diag(): | |
| """์ด๋ ์์ค์์ ์์ธ๊ฐ ์ค๋์ง ์ค์ธกํ๋ค. | |
| ๐ ๋ก์ปฌ์์ ๋๋์ง๋ ์๋ฌด ์๋ฏธ๊ฐ ์๋ค. ๋งํ๋ ๊ณณ์ ์๋ฒ๋ค. | |
| """ | |
| out = {} | |
| for a in ASSETS: | |
| s = prices(a, "1d", 5) | |
| out[a] = {**_SRC.get(a, {}), | |
| "last": None if not len(s) else round(float(s.iloc[-1]), 4), | |
| "at": None if not len(s) else s.index[-1].date().isoformat()} | |
| return {"ok": all(v.get("bars") for v in out.values()), "assets": out} | |
| def health(): | |
| return {"ok": True, "season": S1["name"], "assets": list(ASSETS), | |
| "ledger": L.ROOT, "durability": _DUR[0], "boots": _DUR[1], | |
| "mirror": L.mirror_status(), "restore": _RESTORE, | |
| "warn": None if (_DUR[0] == "ํ์ธ๋จ" or L.mirror_status()["restored"]) | |
| else "์์ฅ์ด ์ฌ์์์ ๊ฒฌ๋๋ค๋ ์ฆ๊ฑฐ๊ฐ ์์ต๋๋ค. " | |
| "๋ฏธ๋ฌ(FINCHAL_MIRROR)๋ ์์ ์คํ ๋ฆฌ์ง๋ฅผ ๋ถ์ด์ญ์์ค."} | |