Spaces:
Running
Running
FINCHAL season 1 server
Browse files- .claude_launch +0 -0
- Dockerfile +12 -0
- README.md +34 -5
- announce_ceiling.json +38 -0
- app/anchors.py +84 -0
- app/ledger.py +129 -0
- app/main.py +349 -0
- app/mcp.py +133 -0
- app/static/index.html +436 -0
- luck_ceiling.json +30 -0
- reference.py +124 -0
- reference_dist.json +0 -0
- requirements.txt +5 -0
- scoring.py +185 -0
- seasons.py +213 -0
.claude_launch
ADDED
|
File without changes
|
Dockerfile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
RUN pip install --no-cache-dir --upgrade pip
|
| 4 |
+
COPY requirements.txt .
|
| 5 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 6 |
+
COPY . .
|
| 7 |
+
# ๐ด HF Space ์ปจํ
์ด๋๋ ์ฌ์์ํ๋ฉด ํ์ผ์ด ์ฌ๋ผ์ง๋ค.
|
| 8 |
+
# ์์ฅ์ด ์ฌ๋ผ์ง๋ฉด ์์ฆ์ด ํต์งธ๋ก ์์ด์ง๋ค. ์์ ์คํ ๋ฆฌ์ง๋ฅผ ๋ฐ๋์ ์ฐ๊ฒฐํ๋ค.
|
| 9 |
+
ENV FINCHAL_DATA=/data/finchal
|
| 10 |
+
ENV PYTHONUNBUFFERED=1
|
| 11 |
+
EXPOSE 7860
|
| 12 |
+
CMD ["python","-m","uvicorn","app.main:app","--host","0.0.0.0","--port","7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,39 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: FINCHAL ํ์ด๋ธ์
์์ธก ์ฑ๋ฆฐ์ง
|
| 3 |
+
emoji: ๐
|
| 4 |
+
colorFrom: blue
|
| 5 |
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
license: apache-2.0
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# FINCHAL โ AI๋ ์์ฅ์ ์ด๊ธธ ์ ์๋๊ฐ
|
| 13 |
+
|
| 14 |
+
122์ผ๊ฐ์ ๊ณต๊ฐ ์คํ. ๋ค ๊ฐ์ ์์ฅ, ์ด ์๊ธ $2,000.
|
| 15 |
+
|
| 16 |
+
ํฌ์ง์
์ ์ ์ถํ๋ฉด ์ค์ ์์ธ๋ก ๋ชจ์ ๋งค๋งค๋์ด ์์ต๋ฅ ์ด ์์์ ๋ฐ์๋ฉ๋๋ค.
|
| 17 |
+
์ ๋ต์ ๋ฏธ๋์ ์์ผ๋ฏ๋ก ๊ณผ๊ฑฐ๋ฅผ ๋ง์ถ๋ ๋ฐฉ์์ผ๋ก๋ ์ด๊ธธ ์ ์์ต๋๋ค.
|
| 18 |
+
|
| 19 |
+
## ์ฐธ๊ฐ
|
| 20 |
+
|
| 21 |
+
**์ฌ๋** โ ํ๋ฉด์ ์ฌ๋ผ์ด๋๋ก ํฌ์ง์
์ ๋ด๋ฉด ๋ฉ๋๋ค.
|
| 22 |
+
|
| 23 |
+
**์์ด์ ํธ** โ MCP ์๋ฒ๋ฅผ ๋ถ์ด๋ฉด ๋๊ตฌ ๋ค ๊ฐ๊ฐ ์๊น๋๋ค.
|
| 24 |
+
|
| 25 |
+
```
|
| 26 |
+
claude mcp add --transport http finchal https://<space>/mcp \
|
| 27 |
+
--header "Authorization: Bearer <์ฐธ๊ฐํค>"
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
`get_rules` ยท `get_data` ยท `submit_position` ยท `check_score`
|
| 31 |
+
|
| 32 |
+
## ๊ท์น
|
| 33 |
+
|
| 34 |
+
- ํฌ์ง์
โ1.0 ~ +1.0, ๋ ๋ฒ๋ฆฌ์ง 1 ๊ณ ์
|
| 35 |
+
- ๋งค์๊ฐ ๊ฐฑ์ ๊ฐ๋ฅ. ๋ค์ ๋ผ ๋๊น์ง ์ง์ ํฌ์ง์
์ด ์ ์ง๋จ
|
| 36 |
+
- ํฌ์ง์
๋ณ๊ฒฝ๋ถ์ ์ค๊ฑฐ๋ ์์๋ฃ ๋ถ๊ณผ
|
| 37 |
+
- ์ข
๋ชฉ๋ณ ์ต์ข
์์ต๋ฅ 1์์๊ฒ ๊ฐ $500
|
| 38 |
+
|
| 39 |
+
`/llms.txt` ์ ์์ด์ ํธ์ฉ ์์ฝ์ด ์์ต๋๋ค.
|
announce_ceiling.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"season": 1,
|
| 3 |
+
"opens": "2026-08-24",
|
| 4 |
+
"closes": "2026-12-24",
|
| 5 |
+
"days": 122,
|
| 6 |
+
"method": "๋ธ๋ก๋ถํธ์คํธ๋ฉ 5์ผ ยท ๋ฌด์์ ํฌ์ง์
ยท ๋ ๋ฒ๋ฆฌ์ง1 ยท ์์๋ฃ ๋ฐ์",
|
| 7 |
+
"note": "๊ฐ๋ง ๊ณต์ง์ฉ ์ถ์ ์น. ์ค์ ์ฑ์ ์ ์์ฆ ์ค์ ๊ฒฝ๋ก๋ก ๋งค์ผ ์ฌ๊ณ์ฐํ๋ค.",
|
| 8 |
+
"ceiling": {
|
| 9 |
+
"NVDA": {
|
| 10 |
+
"p50": -0.06322198717446942,
|
| 11 |
+
"p95": 0.5170854949790162,
|
| 12 |
+
"p99": 0.86208529355278,
|
| 13 |
+
"fee": 0.0002,
|
| 14 |
+
"days": 122
|
| 15 |
+
},
|
| 16 |
+
"BTC": {
|
| 17 |
+
"p50": -0.16992914478643856,
|
| 18 |
+
"p95": 0.8661647545448895,
|
| 19 |
+
"p99": 1.6673872469103215,
|
| 20 |
+
"fee": 0.0006,
|
| 21 |
+
"days": 122
|
| 22 |
+
},
|
| 23 |
+
"GOLD": {
|
| 24 |
+
"p50": -0.024204711467122608,
|
| 25 |
+
"p95": 0.09196673224668211,
|
| 26 |
+
"p99": 0.1406877930906722,
|
| 27 |
+
"fee": 0.0002,
|
| 28 |
+
"days": 122
|
| 29 |
+
},
|
| 30 |
+
"OIL": {
|
| 31 |
+
"p50": -0.045836532750657744,
|
| 32 |
+
"p95": 0.2685994543369719,
|
| 33 |
+
"p99": 0.4344880361598838,
|
| 34 |
+
"fee": 0.0003,
|
| 35 |
+
"days": 122
|
| 36 |
+
}
|
| 37 |
+
}
|
| 38 |
+
}
|
app/anchors.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""๊ธฐ์ค์ ๋ด โ ์ฐธ๊ฐ์์ ๊ฐ์ ํ์ ๋์ผ ์ฒ๋.
|
| 3 |
+
|
| 4 |
+
ODC๊ฐ ์น์ธ์ฝ๊ณผ ๋ถํ์ฑ ํํฉ๋ฌผ์ ๊ฐ์ ํ์ ๋ฃ์ด ์ฒ๋๋ฅผ ๋ณด์ฌ์ฃผ๋ฏ,
|
| 5 |
+
๋ฆฌ๋๋ณด๋์ ๊ธฐ์ค ์ ๋ต์ ํจ๊ป ์ฌ๋ฆฐ๋ค. ์ฒ๋๊ฐ ์๋ ์์ํ๋ ์ฝ์ ์ ์๋ค.
|
| 6 |
+
|
| 7 |
+
๐ด ์ฃผ์ต์ธก ๋ชจ๋ธ์ ๋ฃ์ง ์๋๋ค.
|
| 8 |
+
์ด ์ฑ๋ฆฐ์ง์ ๋ช
๋ถ์ "๋๊ฐ ๋ ๋์ ์์ธก ๋ชจ๋ธ์ ๋ง๋๋๊ฐ"์ด์ง
|
| 9 |
+
"์ฃผ์ต์๊ฐ ์ ์ผ ์ํ๋ค"๊ฐ ์๋๋ค. ์ฃผ์ต์๊ฐ ์๊ธฐ ๋ชจ๋ธ์ ์ฐธ๊ฐ์ ํ์ ์ฌ๋ฆฌ๋ฉด
|
| 10 |
+
๊ทธ ๋ช
๋ถ์ด ํ๋ ค์ง๋ค. ๊ธฐ์ค์ ์ ์ฒ๋์ด์ง ๊ฒฝ์์๊ฐ ์๋๋ค.
|
| 11 |
+
|
| 12 |
+
๊ธฐ์ค์ ์
โ ์ ๋ถ ๊ณต๊ฐ๋ ๊ต๊ณผ์ ๊ธฐ๋ฒ์ด๋ค
|
| 13 |
+
BUYHOLD ํญ์ +1. ์ด๊ฑธ ๋ชป ์ด๊ธฐ๋ฉด ์๋ฌด ์๋ฏธ ์๋ค
|
| 14 |
+
VOLTARGET ์ง์๊ฐ์ค ๋ณ๋์ฑ์ผ๋ก ํฌ๊ธฐ๋ง ์กฐ์ . ๋ฐฉํฅ์ ๋กฑ ๊ณ ์
|
| 15 |
+
RANDOM ๋งค์ผ -1/0/+1 ๋ฌด์์. ๋ฐ๋ฅ
|
| 16 |
+
|
| 17 |
+
๐ VOLTARGET ์ ๋ฐฉํฅ์ ์์ธกํ์ง ์๋๋ค.
|
| 18 |
+
๋ฐฉํฅ ์์ธก์ 60๊ฐ ๋ณ์๋ก ๋ค์ฏ ๋จ๊ณ์ ๊ฑธ์ณ ์ํํ๊ณ ์ ๋ถ ์์ธก๋ ฅ์ด ์์๋ค.
|
| 19 |
+
๊ธฐ์ค์ ์ด ๋ฐฉํฅ์ ๋งํ๋ ์ฒํ๋ฉด, ๊ธฐ๊ฐํ ๊ฒ์ ๊ณต๊ฐ๋ฉด์ ์ฌ๋ฆฌ๋ ์
์ด ๋๋ค.
|
| 20 |
+
๊ทธ๋์ ์ด ๊ธฐ์ค์ ์ '์ผ๋ง๋ ํฌ๊ฒ ๋ค ๊ฒ์ธ๊ฐ'๋ง ์ ํ๋ค.
|
| 21 |
+
"""
|
| 22 |
+
import os, sys
|
| 23 |
+
import numpy as np, pandas as pd
|
| 24 |
+
|
| 25 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 26 |
+
sys.path[:0] = [HERE, os.path.dirname(HERE)]
|
| 27 |
+
import ledger as L
|
| 28 |
+
|
| 29 |
+
ANCHORS = {
|
| 30 |
+
"BUYHOLD": "๋จ์ ๋ณด์ ",
|
| 31 |
+
"VOLTARGET": "๋ณ๋์ฑ ํ๊ฒํ
",
|
| 32 |
+
"RANDOM": "๋ฌด์์",
|
| 33 |
+
}
|
| 34 |
+
SPAN = 60
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def position(kind, px, seed=0):
|
| 38 |
+
"""์ค๋ ์ก์ ํฌ์ง์
. -1 ~ +1."""
|
| 39 |
+
if kind == "BUYHOLD":
|
| 40 |
+
return 1.0
|
| 41 |
+
|
| 42 |
+
if kind == "RANDOM":
|
| 43 |
+
# ๋ ์ง๋ฅผ ์๋์ ๋ฃ์ด ๊ฐ์ ๋ ๊ฐ์ ๊ฐ โ ์ฑ์ ์ด ์ฌํ ๊ฐ๋ฅํด์ผ ํ๋ค
|
| 44 |
+
g = np.random.default_rng(
|
| 45 |
+
int(pd.Timestamp.utcnow().strftime("%Y%m%d")) * 97 + seed)
|
| 46 |
+
return float(g.choice([-1.0, 0.0, 1.0]))
|
| 47 |
+
|
| 48 |
+
s = np.log(px).diff().ewm(span=SPAN).std().dropna()
|
| 49 |
+
if len(s) < SPAN * 2:
|
| 50 |
+
return 0.0
|
| 51 |
+
# ๋ชฉํ ๋ณ๋์ฑ = ๊ทธ ์ข
๋ชฉ ๊ณผ๊ฑฐ ์ค์๊ฐ. ๋ณ๋์ฑ์ด ๋ฎ์ผ๋ฉด ํฌ๊ฒ, ๋์ผ๋ฉด ์๊ฒ ๋ ๋ค.
|
| 52 |
+
target = float(s.iloc[:len(s) // 2].median())
|
| 53 |
+
return float(max(0.0, min(1.0, target / float(s.iloc[-1]))))
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def run(assets, prices_fn, dry=False):
|
| 57 |
+
"""๊ธฐ์ค์ ์
์ ์ค๋ ํฌ์ง์
์ ๊ณ์ฐํด ์์ฅ์ ๋ฃ๋๋ค."""
|
| 58 |
+
if not dry:
|
| 59 |
+
for name in ANCHORS:
|
| 60 |
+
L.register(name, ANCHORS[name], "anchor", list(assets))
|
| 61 |
+
out = []
|
| 62 |
+
for i, a in enumerate(assets):
|
| 63 |
+
px = prices_fn(a)
|
| 64 |
+
if px is None or len(px) < SPAN * 2:
|
| 65 |
+
continue
|
| 66 |
+
for name in ANCHORS:
|
| 67 |
+
p = position(name, px, seed=i)
|
| 68 |
+
if not dry:
|
| 69 |
+
L.submit(name, a, p, "anchor")
|
| 70 |
+
out.append({"anchor": name, "asset": a, "position": round(p, 4)})
|
| 71 |
+
return out
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
if __name__ == "__main__":
|
| 75 |
+
import main as M
|
| 76 |
+
dry = "--dry" in sys.argv
|
| 77 |
+
rows = run(list(M.ASSETS), lambda a: M.prices(a, "1d", 1500), dry=dry)
|
| 78 |
+
print("๊ธฐ์ค์ ํฌ์ง์
%s\n" % ("(๋ชจ์)" if dry else "์ ์ถ ์๋ฃ"))
|
| 79 |
+
by = {}
|
| 80 |
+
for r in rows:
|
| 81 |
+
by.setdefault(r["asset"], {})[r["anchor"]] = r["position"]
|
| 82 |
+
print(" %-6s" % "์ข
๋ชฉ" + "".join("%12s" % k for k in ANCHORS))
|
| 83 |
+
for a, v in by.items():
|
| 84 |
+
print(" %-6s" % a + "".join("%12.3f" % v.get(k, 0) for k in ANCHORS))
|
app/ledger.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""์์ฅ โ ์ฐธ๊ฐ์ยทํฌ์ง์
ยท์ ์๋ฅผ ๋ด๋๋ค.
|
| 3 |
+
|
| 4 |
+
ํ์ผ ๊ธฐ๋ฐ์ด๋ค. HF Space ์ฌ์์ ์ ์ฌ๋ผ์ง๋ฏ๋ก ์ด์์์๋ ์ธ๋ถ ์ ์ฅ์๋ก ์ฎ๊ธด๋ค.
|
| 5 |
+
์ง๊ธ์ ๊ตฌ์กฐ๋ฅผ ํ์ ํ๋ ๊ฒ ๋ชฉ์ ์ด๊ณ , ์ ์ฅ ์์น๋ง ๋ฐ๊พธ๋ฉด ๋๋๋ก ์ธํฐํ์ด์ค๋ฅผ ์ข๊ฒ ๋๋ค.
|
| 6 |
+
|
| 7 |
+
๐ด ํฌ์ง์
์ ์ ๋ ๋ฎ์ด์ฐ์ง ์๋๋ค. ๋งค ์ ์ถ์ append ํ๋ค.
|
| 8 |
+
๋ฎ์ด์ฐ๋ฉด "์ธ์ ๋ฌด์์ ๋๋๊ฐ"๊ฐ ์ฌ๋ผ์ง๊ณ , ๊ทธ๊ฒ ์ฌ๋ผ์ง๋ฉด ์ฑ์ ์ ์ฌํํ ์ ์๋ค.
|
| 9 |
+
"""
|
| 10 |
+
import os, json, time, hashlib, threading
|
| 11 |
+
from datetime import datetime, timezone
|
| 12 |
+
|
| 13 |
+
ROOT = os.environ.get("FINCHAL_DATA", os.path.join(os.path.dirname(__file__), "_data"))
|
| 14 |
+
LOCK = threading.Lock()
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _p(*a):
|
| 18 |
+
p = os.path.join(ROOT, *a)
|
| 19 |
+
os.makedirs(os.path.dirname(p), exist_ok=True)
|
| 20 |
+
return p
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def now():
|
| 24 |
+
return datetime.now(timezone.utc).replace(microsecond=0)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _read(path, default):
|
| 28 |
+
try:
|
| 29 |
+
with open(path, encoding="utf-8") as f:
|
| 30 |
+
return json.load(f)
|
| 31 |
+
except Exception:
|
| 32 |
+
return default
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _write(path, obj):
|
| 36 |
+
tmp = path + ".tmp"
|
| 37 |
+
with open(tmp, "w", encoding="utf-8") as f:
|
| 38 |
+
json.dump(obj, f, ensure_ascii=False)
|
| 39 |
+
os.replace(tmp, path) # ์์์ ๊ต์ฒด โ ์ค๊ฐ์ ์ฃฝ์ด๋ ๋ฐ์ชฝ ํ์ผ์ด ์ ๋จ๋๋ค
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ์ฐธ๊ฐ์ โโโโโโโโโโโโโโโโโโโโโโโ
|
| 43 |
+
def make_key(user_id):
|
| 44 |
+
raw = "%s|%s|finchal" % (user_id, os.environ.get("FINCHAL_SALT", "s1"))
|
| 45 |
+
return "fc_" + hashlib.sha256(raw.encode()).hexdigest()[:24]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def register(user_id, strategy, ptype, assets):
|
| 49 |
+
"""์ฐธ๊ฐ ๋ฑ๋ก. ์ด๋ฏธ ์์ผ๋ฉด ์ ๋ต๋ช
ยท์ ํ๋ง ๊ฐฑ์ ํ๊ณ ํค๋ ์ ์งํ๋ค."""
|
| 50 |
+
with LOCK:
|
| 51 |
+
u = _read(_p("users.json"), {})
|
| 52 |
+
key = make_key(user_id)
|
| 53 |
+
prev = u.get(user_id, {})
|
| 54 |
+
u[user_id] = {
|
| 55 |
+
"user": user_id, "key": key, "strategy": strategy, "type": ptype,
|
| 56 |
+
"assets": sorted(set(assets)),
|
| 57 |
+
"joined": prev.get("joined", now().isoformat()),
|
| 58 |
+
"visits": prev.get("visits", []),
|
| 59 |
+
}
|
| 60 |
+
_write(_p("users.json"), u)
|
| 61 |
+
return u[user_id]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def user_by_key(key):
|
| 65 |
+
for v in _read(_p("users.json"), {}).values():
|
| 66 |
+
if v.get("key") == key:
|
| 67 |
+
return v
|
| 68 |
+
return None
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def touch(user_id):
|
| 72 |
+
"""์ ์ ๊ธฐ๋ก โ ์ฌ๋ ๋ถ๋ฌธ์ '์ฃผ 1ํ ์ ์' ์๊ฑด ํ์ ์ ์ด๋ค."""
|
| 73 |
+
with LOCK:
|
| 74 |
+
u = _read(_p("users.json"), {})
|
| 75 |
+
if user_id in u:
|
| 76 |
+
v = u[user_id].setdefault("visits", [])
|
| 77 |
+
d = now().date().isoformat()
|
| 78 |
+
if not v or v[-1] != d:
|
| 79 |
+
v.append(d)
|
| 80 |
+
v[:] = v[-400:]
|
| 81 |
+
_write(_p("users.json"), u)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def all_users():
|
| 85 |
+
return list(_read(_p("users.json"), {}).values())
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ํฌ์ง์
โโโโโโโโโโโโโโโโโโโโโโโ
|
| 89 |
+
def submit(user_id, asset, position, source="api"):
|
| 90 |
+
"""ํฌ์ง์
์ ์ถ. ๋ฎ์ด์ฐ์ง ์๊ณ ์ค ๋จ์๋ก ์๋๋ค."""
|
| 91 |
+
pos = max(-1.0, min(1.0, float(position)))
|
| 92 |
+
rec = {"t": now().isoformat(), "u": user_id, "a": asset,
|
| 93 |
+
"p": pos, "src": source}
|
| 94 |
+
with LOCK:
|
| 95 |
+
with open(_p("positions", "%s.jsonl" % asset), "a", encoding="utf-8") as f:
|
| 96 |
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
| 97 |
+
touch(user_id)
|
| 98 |
+
return rec
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def positions(asset, user_id=None):
|
| 102 |
+
out = []
|
| 103 |
+
p = _p("positions", "%s.jsonl" % asset)
|
| 104 |
+
if not os.path.exists(p):
|
| 105 |
+
return out
|
| 106 |
+
with open(p, encoding="utf-8") as f:
|
| 107 |
+
for line in f:
|
| 108 |
+
try:
|
| 109 |
+
r = json.loads(line)
|
| 110 |
+
except Exception:
|
| 111 |
+
continue
|
| 112 |
+
if user_id is None or r.get("u") == user_id:
|
| 113 |
+
out.append(r)
|
| 114 |
+
return out
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def latest(asset, user_id):
|
| 118 |
+
ps = positions(asset, user_id)
|
| 119 |
+
return ps[-1] if ps else None
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ์ ์ ์ค๋
์ท โโโโโโโโโโโโโโโโโโโโโโโ
|
| 123 |
+
def save_scores(asset, rows):
|
| 124 |
+
_write(_p("scores", "%s.json" % asset),
|
| 125 |
+
{"at": now().isoformat(), "rows": rows})
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def load_scores(asset):
|
| 129 |
+
return _read(_p("scores", "%s.json" % asset), {"at": None, "rows": []})
|
app/main.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""FINCHAL ์๋ฒ โ REST + MCP๋ฅผ ํ ํ๋ก์ธ์ค์์ ์๋นํ๋ค.
|
| 3 |
+
|
| 4 |
+
REST ๋ ์ฌ๋๊ณผ ์น ํ๋ฉด์ด ์ฐ๊ณ , MCP ๋ ์์ด์ ํธ๊ฐ ์ด๋ค.
|
| 5 |
+
๋์ด ๊ฐ์ ์์ฅยท๊ฐ์ ์ฑ์ ๊ธฐ๋ฅผ ๋ณธ๋ค. ๊ฒฝ๋ก๊ฐ ๊ฐ๋ฆฌ๋ฉด ์ฑ์ ์ด ๊ฐ๋ฆฐ๋ค.
|
| 6 |
+
"""
|
| 7 |
+
import os, sys, json, time
|
| 8 |
+
import numpy as np, pandas as pd
|
| 9 |
+
from fastapi import FastAPI, Request, Header
|
| 10 |
+
from fastapi.responses import JSONResponse, PlainTextResponse, FileResponse
|
| 11 |
+
from fastapi.staticfiles import StaticFiles
|
| 12 |
+
|
| 13 |
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
+
sys.path[:0] = [HERE, os.path.dirname(HERE)]
|
| 15 |
+
import seasons as S
|
| 16 |
+
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"]
|
| 23 |
+
|
| 24 |
+
app = FastAPI(title="FINCHAL")
|
| 25 |
+
S1 = S.SEASONS[1]
|
| 26 |
+
# ๋ก์ปฌ์์ ํ๋ฉด์ ํ์ธํ ๋๋ง ์ด๋ค. ์์ฆ์ด ์ค๋ ์ด๋ฆฌ๋ฉด ์ฑ์ ํ ๊ตฌ๊ฐ์ด ์์ด
|
| 27 |
+
# ์ฐจํธ์ ์์๊ฐ ๋น์ด ๋ณด์ด๋๋ฐ, ๊ทธ๊ฒ ๊ฒฐํจ์ธ์ง ์ ์์ธ์ง ๊ตฌ๋ถ์ด ์ ๋๋ค.
|
| 28 |
+
if os.environ.get("FINCHAL_OPENS"):
|
| 29 |
+
S1 = dict(S1, opens=os.environ["FINCHAL_OPENS"])
|
| 30 |
+
ASSETS = S1["assets"]
|
| 31 |
+
|
| 32 |
+
_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 |
+
|
| 54 |
+
|
| 55 |
+
def season_px(asset):
|
| 56 |
+
"""์์ฆ ๊ฐ๋ง ์ดํ ๊ตฌ๊ฐ๋ง. ์ฑ์ ๊ณผ ๊ธฐ์ค๋ถํฌ๊ฐ ์ด๊ฑธ ๋ณธ๋ค."""
|
| 57 |
+
s = prices(asset, "1d", 2000)
|
| 58 |
+
return s[s.index >= pd.Timestamp(S1["opens"])]
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def rules_doc():
|
| 62 |
+
return {
|
| 63 |
+
"season": S1["name"], "opens": S1["opens"], "closes": S1["closes"],
|
| 64 |
+
"days": (pd.Timestamp(S1["closes"]) - pd.Timestamp(S1["opens"])).days,
|
| 65 |
+
"prize": "์ข
๋ชฉ๋ณ ์ต์ข
์์ต๋ฅ 1์ %d๋ฌ๋ฌ ยท ์ด %d๋ฌ๋ฌ"
|
| 66 |
+
% (S1["prize_usd_per_asset"], S1["prize_total_usd"]),
|
| 67 |
+
"leverage": "ํฌ์ง์
-1.0 ~ +1.0. ๋ ๋ฒ๋ฆฌ์ง 1 ๊ณ ์ . ๋ฒ์ด๋๋ฉด ์๋ฆผ",
|
| 68 |
+
"update": "๋งค์๊ฐ ๊ฐฑ์ ๊ฐ๋ฅ. ๋ค์ ๋ผ ๋๊น์ง ์ง์ ํฌ์ง์
์ด ์ ์ง๋จ",
|
| 69 |
+
"fees": {k: SC.fee_of(k) for k in ASSETS},
|
| 70 |
+
"fee_note": "ํฌ์ง์
๋ณ๊ฒฝ๋ถ์๋ง ๋ถ๊ณผ. ์์ฃผ ๋ค์ง์ผ๋ฉด ์์๋ฃ๋ก ์๋๋ค",
|
| 71 |
+
"scoring": "์ต์ข
์์ต๋ฅ ๋ก ์ข
๋ชฉ๋ณ ์์. ์์ '์ด์ผ๋ก ์ด๋ด ํ๋ฅ '์ ํจ๊ป ํ์",
|
| 72 |
+
"participation": "ํด๋น ์ข
๋ชฉ ๊ฐ์ค~๋ง๊ฐ ๊ธฐ๊ฐ์ 70%% ์ด์ ํฌ์ง์
์ด ์ ํจํด์ผ ์๊ธ ๋์",
|
| 73 |
+
"assets": {k: {"label": v["label"], "why": v["why"],
|
| 74 |
+
"vol_annual": v["vol"], "hours": v["hours"]}
|
| 75 |
+
for k, v in ASSETS.items()},
|
| 76 |
+
"types": {k: v["label"] for k, v in S.TYPES.items()},
|
| 77 |
+
"warning": ("์๋น๋์ ์ค์ ์ด 2026-08-26์ด๋ค. ์ค์ ์ผ ๋ณ๋์ ํ์์ผ์ 2.7๋ฐฐ์ด๊ณ "
|
| 78 |
+
"87%๊ฐ ์๊ฐ ๊ฐญ์ด๋ผ ์ฅ์ค ๋์์ด ์ ๋๋ค. ์์ฆ ์ค 1ํ๋ค."),
|
| 79 |
+
"manifesto": S.MANIFESTO["title"] + " โ " + S.MANIFESTO["sub"],
|
| 80 |
+
# ์ด์ ํ๊ณ์ . ์ค๋ ฅ 0 ์ฐธ๊ฐ์๋ฅผ 20,000ํ ๋๋ ค ๋ธ ์์ 5% ์ง์ ์ด๋ค.
|
| 81 |
+
# ์ด ๊ฐ ์๋์ ์์ต์ ์ด์ผ๋ก๋ ์ค๋ช
๋๋ค.
|
| 82 |
+
"luck_ceiling": {k: round(v["p95"], 4) for k, v in CEIL.items()},
|
| 83 |
+
"luck_note": ("์ค๋ ฅ 0์ธ ์ฐธ๊ฐ์๋ฅผ ์์ฆ ๊ธธ์ด๋งํผ 20,000ํ ๋๋ ค ๋ธ ์์ 5% ์ง์ . "
|
| 84 |
+
"์์๋ฃ๋ฅผ ๋๊ฐ์ด ๋ฌผ๋ ธ์ต๋๋ค. ์ด ์ ์ ๋์ด์ผ ์ค๋ ฅ์ ์ฆ๊ฑฐ๊ฐ ๋ฉ๋๋ค."),
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def me_from(key):
|
| 89 |
+
u = L.user_by_key(key or "")
|
| 90 |
+
if not u:
|
| 91 |
+
raise PermissionError(
|
| 92 |
+
"์ฐธ๊ฐ ํค๊ฐ ํ์ํฉ๋๋ค. ํ๋ฉด์์ Hugging Face๋ก ๋ก๊ทธ์ธํ๋ฉด fc_ ๋ก ์์ํ๋ "
|
| 93 |
+
"ํค๊ฐ ๋์ต๋๋ค. MCP ์ค์ ์ Authorization ํค๋์ ๋ฃ์ด ์ฃผ์ธ์.")
|
| 94 |
+
return u
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def score_asset(asset):
|
| 98 |
+
"""๊ทธ ์ข
๋ชฉ ์ฐธ๊ฐ์ ์ ์์ ์ฑ์ ํ๋ค. ๊ธฐ์ค๋ถํฌ๋ ์์ฆ ์ค์ ๊ฒฝ๋ก๋ก ๋ง๋ ๋ค."""
|
| 99 |
+
px = season_px(asset)
|
| 100 |
+
if len(px) < 2:
|
| 101 |
+
return [] # ์์ฆ ์ฒซ๋ ์ ๊ณ์ฐํ ๊ตฌ๊ฐ์ด ์๋ค
|
| 102 |
+
fee = SC.fee_of(asset)
|
| 103 |
+
recs = L.positions(asset)
|
| 104 |
+
rows = []
|
| 105 |
+
by_user = {}
|
| 106 |
+
for r in recs:
|
| 107 |
+
by_user.setdefault(r["u"], {})[pd.Timestamp(r["t"]).tz_localize(None)] = r["p"]
|
| 108 |
+
users = {u["user"]: u for u in L.all_users()}
|
| 109 |
+
for uid, pos in by_user.items():
|
| 110 |
+
ret = SC.total_return(pos, px, asset)
|
| 111 |
+
expo = float(np.abs(pd.Series(pos).reindex(
|
| 112 |
+
px.index, method="ffill").fillna(0.0)).mean())
|
| 113 |
+
refv = RF.live(px, fee, expo=max(expo, 0.05), n=3000)
|
| 114 |
+
u = users.get(uid, {})
|
| 115 |
+
rows.append({
|
| 116 |
+
"user": uid, "strategy": u.get("strategy", "-"),
|
| 117 |
+
"type": u.get("type", "agent"),
|
| 118 |
+
"joined": u.get("joined", ""),
|
| 119 |
+
"ret": ret, "score": SC.score(ret, refv),
|
| 120 |
+
"luck_p95": float(np.percentile(refv, 95)),
|
| 121 |
+
"turnover": SC.turnover(pos, px),
|
| 122 |
+
"fee_paid": SC.fee_paid(pos, px, asset),
|
| 123 |
+
"valid": SC.valid_ratio(pos, px),
|
| 124 |
+
"n_sub": len(L.positions(asset, uid)),
|
| 125 |
+
})
|
| 126 |
+
rows.sort(key=lambda x: -x["ret"])
|
| 127 |
+
for i, r in enumerate(rows, 1):
|
| 128 |
+
r["rank"] = i
|
| 129 |
+
return rows
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
_SC = {}
|
| 133 |
+
SC_TTL = 300
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def score_cached(asset):
|
| 137 |
+
"""์ฑ์ ์ ์ฐธ๊ฐ์๋ง๋ค ๊ธฐ์ค๋ถํฌ 3,000๊ฒฝ๋ก๋ฅผ ๋๋ฆฐ๋ค. ๋งค ์์ฒญ๋ง๋ค ํ๋ฉด ํ๋ฉด์ด ์ฃฝ๋๋ค."""
|
| 138 |
+
e = _SC.get(asset)
|
| 139 |
+
if e and time.time() - e[0] < SC_TTL:
|
| 140 |
+
return e[1]
|
| 141 |
+
r = score_asset(asset)
|
| 142 |
+
_SC[asset] = (time.time(), r)
|
| 143 |
+
return r
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def curves(asset, top=12):
|
| 147 |
+
"""์ฐธ๊ฐ์๋ณ ๋์ ์์ต๋ฅ ๊ณก์ . ํ๋ฉด์ ๊บพ์์ ๊ทธ๋ํ๊ฐ ์ด๊ฑธ ๊ทธ๋ฆฐ๋ค."""
|
| 148 |
+
px = season_px(asset)
|
| 149 |
+
if len(px) < 2:
|
| 150 |
+
return {"t": [], "series": []}
|
| 151 |
+
rows = score_cached(asset)
|
| 152 |
+
users = {u["user"]: u for u in L.all_users()}
|
| 153 |
+
keep = [r["user"] for r in rows[:top]]
|
| 154 |
+
ser = []
|
| 155 |
+
for uid in keep:
|
| 156 |
+
pos = {}
|
| 157 |
+
for r in L.positions(asset, uid):
|
| 158 |
+
pos[pd.Timestamp(r["t"]).tz_localize(None)] = r["p"]
|
| 159 |
+
if not pos:
|
| 160 |
+
continue
|
| 161 |
+
eq, _ = SC.equity(pos, px, asset)
|
| 162 |
+
u = users.get(uid, {})
|
| 163 |
+
ser.append({"user": uid, "strategy": u.get("strategy", "-"),
|
| 164 |
+
"type": u.get("type", "agent"),
|
| 165 |
+
"v": [round(float(x - 1) * 100, 4) for x in eq]})
|
| 166 |
+
# ์์ฅ ์์ฒด๋ ํ ์ค๋ก ๊น์ ์ค๋ค โ ์ฐธ๊ฐ์๊ฐ ๋ฌด์์ ์ด๊ฒจ์ผ ํ๋์ง ๋ณด์ด๊ฒ
|
| 167 |
+
mk = (px / px.iloc[0] - 1) * 100
|
| 168 |
+
return {"t": [t.isoformat() for t in px.index],
|
| 169 |
+
"market": [round(float(x), 4) for x in mk],
|
| 170 |
+
"series": ser}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ REST โโโโโโโโโโโโโโโโโโโโโโโ
|
| 174 |
+
@app.get("/api/rules")
|
| 175 |
+
def api_rules():
|
| 176 |
+
return {"ok": True, **rules_doc()}
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
@app.get("/api/data")
|
| 180 |
+
def api_data(asset: str, bars: int = 500, interval: str = "1d"):
|
| 181 |
+
if asset not in ASSETS:
|
| 182 |
+
return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข
๋ชฉ"}, 400)
|
| 183 |
+
s = prices(asset, interval, min(max(bars, 10), 5000))
|
| 184 |
+
return {"ok": True, "asset": asset, "interval": interval,
|
| 185 |
+
"bars": [{"t": t.isoformat(), "c": float(v)} for t, v in s.items()]}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.post("/api/register")
|
| 189 |
+
async def api_register(req: Request):
|
| 190 |
+
b = await req.json()
|
| 191 |
+
u = L.register(b["user"], b.get("strategy", "-"),
|
| 192 |
+
b.get("type", "agent"), b.get("assets", list(ASSETS)))
|
| 193 |
+
return {"ok": True, **u}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
@app.post("/api/submit")
|
| 197 |
+
async def api_submit(req: Request, authorization: str = Header(None)):
|
| 198 |
+
b = await req.json()
|
| 199 |
+
key = (authorization or "").replace("Bearer ", "").strip() or b.get("key", "")
|
| 200 |
+
try:
|
| 201 |
+
u = me_from(key)
|
| 202 |
+
except PermissionError as e:
|
| 203 |
+
return JSONResponse({"ok": False, "error": str(e)}, 401)
|
| 204 |
+
if b["asset"] not in ASSETS:
|
| 205 |
+
return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข
๋ชฉ"}, 400)
|
| 206 |
+
r = L.submit(u["user"], b["asset"], b["position"], b.get("source", "rest"))
|
| 207 |
+
_SC.pop(b["asset"], None) # ๋ฐฉ๊ธ ๋ธ ์ฌ๋์ด ํ์์ ์๊ธฐ๋ฅผ ๋ชป ์ฐพ์ผ๋ฉด ์ ๋๋ค
|
| 208 |
+
return {"ok": True, **r}
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
@app.get("/api/leaderboard")
|
| 212 |
+
def api_leaderboard(asset: str):
|
| 213 |
+
if asset not in ASSETS:
|
| 214 |
+
return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข
๋ชฉ"}, 400)
|
| 215 |
+
return {"ok": True, "asset": asset, "rows": score_cached(asset)}
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
@app.get("/api/curves")
|
| 219 |
+
def api_curves(asset: str, top: int = 12):
|
| 220 |
+
if asset not in ASSETS:
|
| 221 |
+
return JSONResponse({"ok": False, "error": "์ ์ ์๋ ์ข
๋ชฉ"}, 400)
|
| 222 |
+
return {"ok": True, "asset": asset, **curves(asset, top)}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
@app.get("/api/manifesto")
|
| 226 |
+
def api_manifesto():
|
| 227 |
+
return {"ok": True, **S.MANIFESTO, "anchors": S.ANCHORS,
|
| 228 |
+
"types": {k: v for k, v in S.TYPES.items()},
|
| 229 |
+
"season2": {k: v for k, v in S.SEASONS.get(2, {}).items()
|
| 230 |
+
if k != "assets"}}
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ MCP โโโโโโโโโโโโโโโโโโโโโโโ
|
| 234 |
+
def _call(name, args, key=None):
|
| 235 |
+
if name == "get_rules":
|
| 236 |
+
return rules_doc()
|
| 237 |
+
if name == "get_data":
|
| 238 |
+
a = args["asset"]
|
| 239 |
+
if a not in ASSETS:
|
| 240 |
+
return {"error": "์ ์ ์๋ ์ข
๋ชฉ. ๊ฐ๋ฅ: %s" % list(ASSETS)}
|
| 241 |
+
s = prices(a, args.get("interval", "1d"), min(args.get("bars", 500), 5000))
|
| 242 |
+
return {"asset": a, "interval": args.get("interval", "1d"),
|
| 243 |
+
"bars": [{"t": t.date().isoformat(), "c": round(float(v), 6)}
|
| 244 |
+
for t, v in s.items()]}
|
| 245 |
+
if name == "submit_position":
|
| 246 |
+
u = me_from(key)
|
| 247 |
+
a = args["asset"]
|
| 248 |
+
if a not in ASSETS:
|
| 249 |
+
return {"error": "์ ์ ์๋ ์ข
๋ชฉ. ๊ฐ๋ฅ: %s" % list(ASSETS)}
|
| 250 |
+
r = L.submit(u["user"], a, args["position"], "mcp")
|
| 251 |
+
_SC.pop(a, None)
|
| 252 |
+
return {"accepted": True, "asset": a, "position": r["p"], "at": r["t"],
|
| 253 |
+
"note": "๋ค์ ์ ์ถํ ๋๊น์ง ์ด ํฌ์ง์
์ด ์ ์ง๋ฉ๋๋ค"}
|
| 254 |
+
if name == "check_score":
|
| 255 |
+
u = me_from(key)
|
| 256 |
+
want = [args["asset"]] if args.get("asset") else u.get("assets", list(ASSETS))
|
| 257 |
+
out = []
|
| 258 |
+
for a in want:
|
| 259 |
+
if a not in ASSETS:
|
| 260 |
+
continue
|
| 261 |
+
rows = score_asset(a)
|
| 262 |
+
mine = next((r for r in rows if r["user"] == u["user"]), None)
|
| 263 |
+
if not mine:
|
| 264 |
+
last = L.latest(a, u["user"])
|
| 265 |
+
out.append({"asset": a, "status": "์ง๊ณ ์ ",
|
| 266 |
+
"position": last["p"] if last else None,
|
| 267 |
+
"hint": ("ํฌ์ง์
์ ์ ์๋์ต๋๋ค. ์์ฆ์ด ํ๋ฃจ ์ด์ ์งํ๋๋ฉด "
|
| 268 |
+
"์์ต๋ฅ ์ด ๊ณ์ฐ๋ฉ๋๋ค." if last else
|
| 269 |
+
"์์ง ํฌ์ง์
์ ๋ด์ง ์์์ต๋๋ค.")})
|
| 270 |
+
else:
|
| 271 |
+
out.append({"asset": a, "rank": mine["rank"], "of": len(rows),
|
| 272 |
+
"return": round(mine["ret"], 4),
|
| 273 |
+
"luck_ceiling": round(mine["luck_p95"], 4),
|
| 274 |
+
"score": round(mine["score"], 2),
|
| 275 |
+
"fee_paid": round(mine["fee_paid"], 4),
|
| 276 |
+
"hint": ("์ด์ ํ๊ณ์ ์ ๋์์ต๋๋ค" if mine["ret"] > mine["luck_p95"]
|
| 277 |
+
else "์์ง ์ด์ผ๋ก๋ ๋์ฌ ์ ์๋ ๋ฒ์์
๋๋ค")})
|
| 278 |
+
return {"user": u["user"], "strategy": u.get("strategy"), "results": out}
|
| 279 |
+
return {"error": "๋ฏธ๊ตฌํ"}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
@app.post("/mcp")
|
| 283 |
+
async def mcp_endpoint(req: Request, authorization: str = Header(None)):
|
| 284 |
+
body = await req.json()
|
| 285 |
+
key = (authorization or "").replace("Bearer ", "").strip()
|
| 286 |
+
resp = MCP.handle(body, lambda n, a: _call(n, a, key))
|
| 287 |
+
if resp is None:
|
| 288 |
+
return JSONResponse({}, 202)
|
| 289 |
+
return JSONResponse(resp)
|
| 290 |
+
|
| 291 |
+
|
| 292 |
+
@app.get("/llms.txt", response_class=PlainTextResponse)
|
| 293 |
+
def llms():
|
| 294 |
+
r = rules_doc()
|
| 295 |
+
return (
|
| 296 |
+
"# %s\n\n" % r["manifesto"] +
|
| 297 |
+
"๋น์ ์ ์ค์ ํฌ์ง์
์ ์ก์ ์์ต๋ฅ ๋ก ๊ฒจ๋ฃน๋๋ค. ๋ฐฉํฅ ์์ธก์ด ์๋๋ผ ๋งค๋งค์
๋๋ค.\n\n"
|
| 298 |
+
"์ข
๋ชฉ: %s\n" % ", ".join("%s(%s)" % (k, v["label"]) for k, v in ASSETS.items()) +
|
| 299 |
+
"๊ธฐ๊ฐ: %s ~ %s\n" % (r["opens"], r["closes"]) +
|
| 300 |
+
"์๊ธ: %s\n" % r["prize"] +
|
| 301 |
+
"ํฌ์ง์
: %s\n" % r["leverage"] +
|
| 302 |
+
"๊ฐฑ์ : %s\n" % r["update"] +
|
| 303 |
+
"์์๋ฃ: ํฌ์ง์
๋ณ๊ฒฝ๋ถ์๋ง. ์์ฃผ ๋ค์ง์ผ๋ฉด ์ํด์
๋๋ค\n"
|
| 304 |
+
"์ฑ์ : ์ข
๋ชฉ๋ณ ์ต์ข
์์ต๋ฅ ์์\n\n"
|
| 305 |
+
"์์: get_rules() โ get_data() โ ์ง์ ๋ชจ๋ธ๋ง โ submit_position()\n")
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
STATIC = os.path.join(HERE, "static")
|
| 309 |
+
if os.path.isdir(STATIC):
|
| 310 |
+
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
@app.get("/")
|
| 314 |
+
def index():
|
| 315 |
+
f = os.path.join(STATIC, "index.html")
|
| 316 |
+
if not os.path.exists(f):
|
| 317 |
+
return JSONResponse({"ok": True, "note": "ํ๋ฉด ์ค๋น ์ค", **rules_doc()})
|
| 318 |
+
return FileResponse(f, headers={"Cache-Control": "no-cache"})
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ๊ธฐ์ค์ ์๋ ์ ์ถ โโโโโโโโโโโโโโโโโโโโโโโ
|
| 322 |
+
def _anchor_loop():
|
| 323 |
+
"""ํ๋ฃจ ํ ๋ฒ ๊ธฐ์ค์ ์
์ ํฌ์ง์
์ ๋ธ๋ค.
|
| 324 |
+
|
| 325 |
+
๐ ์คํจํด๋ ์๋ฒ๋ฅผ ์ฃฝ์ด๋ฉด ์ ๋๋ค. ๊ธฐ์ค์ ์ ์ฒ๋์ด์ง ์๋น์ค๊ฐ ์๋๋ค.
|
| 326 |
+
yfinance๊ฐ ์ ๊น ๋งํ๋ ํ๋ฉด๊ณผ ์ฐธ๊ฐ์ ์ ์ถ์ ๊ณ์ ๋์๊ฐ์ผ ํ๋ค.
|
| 327 |
+
"""
|
| 328 |
+
import traceback
|
| 329 |
+
import anchors as A
|
| 330 |
+
while True:
|
| 331 |
+
try:
|
| 332 |
+
A.run(list(ASSETS), lambda a: prices(a, "1d", 1500))
|
| 333 |
+
_SC.clear()
|
| 334 |
+
except Exception:
|
| 335 |
+
traceback.print_exc()
|
| 336 |
+
time.sleep(6 * 3600)
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
@app.on_event("startup")
|
| 340 |
+
def _startup():
|
| 341 |
+
if os.environ.get("FINCHAL_NO_ANCHOR"):
|
| 342 |
+
return
|
| 343 |
+
import threading
|
| 344 |
+
threading.Thread(target=_anchor_loop, daemon=True).start()
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
@app.get("/health")
|
| 348 |
+
def health():
|
| 349 |
+
return {"ok": True, "season": S1["name"], "assets": list(ASSETS)}
|
app/mcp.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""MCP ์๋ฒ โ ์์ด์ ํธ๊ฐ ๋๊ตฌ๋ก ์ง์ ์ฐธ๊ฐํ๋ค.
|
| 3 |
+
|
| 4 |
+
์ด๊ฒ ์ด ์ฑ๋ฆฐ์ง์ ์ ์ฒด์ฑ์ด๋ค. Numerai๋ M6๋ ์ฌ๋์ด CSV๋ฅผ ์ฌ๋ฆฐ๋ค.
|
| 5 |
+
์ฐธ๊ฐ์๋ ์ด ํ ์ค๋ง ์น๋ฉด ๋๋ค:
|
| 6 |
+
|
| 7 |
+
claude mcp add finchal https://<space>/mcp --header "Authorization: Bearer fc_xxx"
|
| 8 |
+
|
| 9 |
+
๊ทธ๋ฌ๋ฉด ์์ด์ ํธ์๊ฒ ๋๊ตฌ ๋ค ๊ฐ๊ฐ ์๊ธฐ๊ณ , ์ฌ๋์ "์ฑ๋ฆฐ์ง ์ฐธ๊ฐํด์ค" ํ ๋ง๋๋ง ํ๋ฉด ๋๋ค.
|
| 10 |
+
|
| 11 |
+
ํ๋กํ ์ฝ์ JSON-RPC 2.0 ์์ Streamable HTTP. ์ธ ๋ฉ์๋๋ง ๊ตฌํํ๋ฉด ๋ถ๋๋ค:
|
| 12 |
+
initialize / tools/list / tools/call
|
| 13 |
+
notifications/* ๋ ์๋ต ์์ด 202 ๋ก ๋ฐ๋๋ค.
|
| 14 |
+
"""
|
| 15 |
+
import json
|
| 16 |
+
from typing import Any, Dict
|
| 17 |
+
|
| 18 |
+
PROTOCOL = "2025-06-18"
|
| 19 |
+
SERVER = {"name": "finchal", "version": "1.0.0"}
|
| 20 |
+
|
| 21 |
+
TOOLS = [
|
| 22 |
+
{
|
| 23 |
+
"name": "get_rules",
|
| 24 |
+
"description": (
|
| 25 |
+
"์ฑ๋ฆฐ์ง ๊ท์น๊ณผ ํ์ฌ ์์ฆ ์ ๋ณด๋ฅผ ์ฝ๋๋ค. ์ฐธ๊ฐ ์ ์ ๋ฐ๋์ ๋จผ์ ํธ์ถํ๋ค. "
|
| 26 |
+
"์ข
๋ชฉ ๋ชฉ๋กยท๋ง๊ฐ์ผยท๋ ๋ฒ๋ฆฌ์ง ์ํยท์์๋ฃยท์ฑ์ ๋ฐฉ์์ด ๋ค์ด ์๋ค."),
|
| 27 |
+
"inputSchema": {"type": "object", "properties": {}},
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"name": "get_data",
|
| 31 |
+
"description": (
|
| 32 |
+
"์ข
๋ชฉ์ ๊ณผ๊ฑฐ ์์ธ๋ฅผ ๋ฐ๋๋ค. ์ด ๋ฐ์ดํฐ๋ก ์ง์ ๋ชจ๋ธ์ ๋ง๋ค์ด ์์ธกํ๋ผ. "
|
| 33 |
+
"๋ฏธ๋ ๋ฐ์ดํฐ๋ ์ฃผ์ง ์๋๋ค โ ์ ๋ต์ ์์ง ์ธ์์ ์๋ค."),
|
| 34 |
+
"inputSchema": {
|
| 35 |
+
"type": "object",
|
| 36 |
+
"properties": {
|
| 37 |
+
"asset": {"type": "string",
|
| 38 |
+
"description": "์ข
๋ชฉ ์ฝ๋. NVDA / BTC / GOLD / OIL"},
|
| 39 |
+
"bars": {"type": "integer", "default": 500,
|
| 40 |
+
"description": "๋ฐ์ ๋ด ๊ฐ์ (์ต๋ 5000)"},
|
| 41 |
+
"interval": {"type": "string", "enum": ["1d", "1h"], "default": "1d"},
|
| 42 |
+
},
|
| 43 |
+
"required": ["asset"],
|
| 44 |
+
},
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"name": "submit_position",
|
| 48 |
+
"description": (
|
| 49 |
+
"ํฌ์ง์
์ ์ ์ถํ๋ค. -1.0(์ ๋ ์) ~ +1.0(์ ๋ ๋กฑ). ๋ ๋ฒ๋ฆฌ์ง๋ 1๋ก ๊ณ ์ ์ด๋ฉฐ "
|
| 50 |
+
"๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด ์๋ฆฐ๋ค. ๋ค์ ์ ์ถํ ๋๊น์ง ์ด ํฌ์ง์
์ด ์ ์ง๋๋ค. "
|
| 51 |
+
"ํฌ์ง์
์ ๋ฐ๊ฟ ๋๋ง๋ค ์์๋ฃ๊ฐ ๋๊ฐ๋ฏ๋ก ์์ฃผ ๋ค์ง์ผ๋ฉด ์ํด๋ค."),
|
| 52 |
+
"inputSchema": {
|
| 53 |
+
"type": "object",
|
| 54 |
+
"properties": {
|
| 55 |
+
"asset": {"type": "string"},
|
| 56 |
+
"position": {"type": "number", "minimum": -1, "maximum": 1},
|
| 57 |
+
"note": {"type": "string", "description": "์ ํ. ํ๋จ ๊ทผ๊ฑฐ ๋ฉ๋ชจ"},
|
| 58 |
+
},
|
| 59 |
+
"required": ["asset", "position"],
|
| 60 |
+
},
|
| 61 |
+
},
|
| 62 |
+
{
|
| 63 |
+
"name": "check_score",
|
| 64 |
+
"description": (
|
| 65 |
+
"๋ด ์ฑ์ ๊ณผ ์์๋ฅผ ํ์ธํ๋ค. ์์ต๋ฅ ยท์์ยท์ด์ ํ๊ณ์ ๋๋น ์์น๋ฅผ ๋๋ ค์ค๋ค."),
|
| 66 |
+
"inputSchema": {
|
| 67 |
+
"type": "object",
|
| 68 |
+
"properties": {"asset": {"type": "string",
|
| 69 |
+
"description": "์๋ตํ๋ฉด ์ฐธ๊ฐ ์ค์ธ ์ ์ข
๋ชฉ"}},
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _ok(rid, result):
|
| 76 |
+
return {"jsonrpc": "2.0", "id": rid, "result": result}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def _err(rid, code, msg):
|
| 80 |
+
return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": msg}}
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _text(obj):
|
| 84 |
+
"""๋๊ตฌ ๊ฒฐ๊ณผ๋ ํ
์คํธ ์ฝํ
์ธ ๋ก ๋๋ ค์ค๋ค. ์์ด์ ํธ๊ฐ JSON์ ์ ์ฝ์ผ๋ฏ๋ก
|
| 85 |
+
๊ตฌ์กฐ๋ฅผ ๊ทธ๋๋ก ์ง๋ ฌํํ๋, ์ฌ๋์ด ๋ก๊ทธ๋ฅผ ๋ณผ ๋๋ ์ฝํ๊ฒ ๋ค์ฌ์ด๋ค."""
|
| 86 |
+
return {"content": [{"type": "text",
|
| 87 |
+
"text": json.dumps(obj, ensure_ascii=False, indent=1)}]}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def handle(body: Dict[str, Any], call) -> Any:
|
| 91 |
+
"""call(name, args) -> dict ๋ฅผ ๋ฐ์ ๋๊ตฌ๋ฅผ ์ค์ ๋ก ์คํํ๋ค.
|
| 92 |
+
|
| 93 |
+
๋ฐํ์ด None ์ด๋ฉด ์๋ต ์์(notification)์ด๋ผ๋ ๋ป์ด๋ค.
|
| 94 |
+
"""
|
| 95 |
+
method = body.get("method")
|
| 96 |
+
rid = body.get("id")
|
| 97 |
+
|
| 98 |
+
if method == "initialize":
|
| 99 |
+
return _ok(rid, {
|
| 100 |
+
"protocolVersion": PROTOCOL,
|
| 101 |
+
"capabilities": {"tools": {"listChanged": False}},
|
| 102 |
+
"serverInfo": SERVER,
|
| 103 |
+
"instructions": (
|
| 104 |
+
"๊ธ์ต ์์ธก ์ฑ๋ฆฐ์ง๋ค. ๋ฐฉํฅ์ ๋งํ๋ ๊ฒ์ด ์๋๋ผ ์ค์ ํฌ์ง์
์ ์ก์ "
|
| 105 |
+
"์์ต๋ฅ ๋ก ๊ฒจ๋ฃฌ๋ค. get_rules ๋ก ์์ํ๋ผ."),
|
| 106 |
+
})
|
| 107 |
+
|
| 108 |
+
if method and method.startswith("notifications/"):
|
| 109 |
+
return None # ์๋ฆผ์ ์๋ตํ์ง ์๋๋ค
|
| 110 |
+
|
| 111 |
+
if method == "tools/list":
|
| 112 |
+
return _ok(rid, {"tools": TOOLS})
|
| 113 |
+
|
| 114 |
+
if method == "tools/call":
|
| 115 |
+
p = body.get("params") or {}
|
| 116 |
+
name = p.get("name")
|
| 117 |
+
args = p.get("arguments") or {}
|
| 118 |
+
if not any(t["name"] == name for t in TOOLS):
|
| 119 |
+
return _err(rid, -32602, "์ ์ ์๋ ๋๊ตฌ: %s" % name)
|
| 120 |
+
try:
|
| 121 |
+
return _ok(rid, _text(call(name, args)))
|
| 122 |
+
except PermissionError as e:
|
| 123 |
+
return _ok(rid, {"content": [{"type": "text", "text": str(e)}],
|
| 124 |
+
"isError": True})
|
| 125 |
+
except Exception as e:
|
| 126 |
+
return _ok(rid, {"content": [
|
| 127 |
+
{"type": "text", "text": "%s: %s" % (type(e).__name__, e)}],
|
| 128 |
+
"isError": True})
|
| 129 |
+
|
| 130 |
+
if method == "ping":
|
| 131 |
+
return _ok(rid, {})
|
| 132 |
+
|
| 133 |
+
return _err(rid, -32601, "์ง์ํ์ง ์๋ ๋ฉ์๋: %s" % method)
|
app/static/index.html
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="ko">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
| 6 |
+
<title>FINCHAL ยท ํ์ด๋ธ์
์์ธก ์ฑ๋ฆฐ์ง</title>
|
| 7 |
+
<script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
|
| 8 |
+
<style>
|
| 9 |
+
/* ORACLE ํ๋ฉด๊ณผ ๊ฐ์ ๋ฐ์ ํ๋ ํธ. ์ด๋์ด ๋ฐฐ๊ฒฝ์์ ์์ต๋ฅ ์์ด ์ ์ฝํ๋ค. */
|
| 10 |
+
:root{
|
| 11 |
+
--bg:#f5f6f8; --panel:#ffffff; --panel2:#f4f6f9; --line:#e4e8ee;
|
| 12 |
+
--tx:#0f1729; --dim:#5f6b7f; --faint:#6b7684;
|
| 13 |
+
--up:#07804e; --up-bg:#e8f6ef; --dn:#c62828; --dn-bg:#fdecea;
|
| 14 |
+
--acc:#1f5fd6; --acc-bg:#eaf1fd; --gold:#9a6b00; --gold-bg:#fdf5e3;
|
| 15 |
+
--sh:0 1px 2px rgba(16,24,40,.05),0 1px 3px rgba(16,24,40,.06);
|
| 16 |
+
--sh2:0 4px 12px rgba(16,24,40,.07),0 1px 3px rgba(16,24,40,.05);
|
| 17 |
+
}
|
| 18 |
+
*{box-sizing:border-box}
|
| 19 |
+
body{margin:0;background:var(--bg);color:var(--tx);
|
| 20 |
+
font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","Pretendard","Noto Sans KR",sans-serif;
|
| 21 |
+
-webkit-font-smoothing:antialiased}
|
| 22 |
+
a{color:var(--acc);text-decoration:none}
|
| 23 |
+
.wrap{max-width:1240px;margin:0 auto;padding:0 20px 72px}
|
| 24 |
+
.card{background:var(--panel);border:1px solid var(--line);border-radius:14px;
|
| 25 |
+
padding:20px 22px;box-shadow:var(--sh)}
|
| 26 |
+
h2{font-size:17px;margin:0 0 14px;letter-spacing:-.01em}
|
| 27 |
+
h3{font-size:14px;margin:0 0 8px;color:var(--dim);font-weight:600}
|
| 28 |
+
.mono{font-variant-numeric:tabular-nums;
|
| 29 |
+
font-family:"SF Mono",ui-monospace,"Cascadia Mono",Consolas,monospace}
|
| 30 |
+
.up{color:var(--up)} .dn{color:var(--dn)} .dim{color:var(--dim)} .faint{color:var(--faint)}
|
| 31 |
+
|
| 32 |
+
/* โโโโโ ํ์ด๋ก โโโโโ */
|
| 33 |
+
.hero{padding:52px 0 28px;text-align:center}
|
| 34 |
+
.kicker{display:inline-block;background:var(--acc-bg);color:var(--acc);font-weight:700;
|
| 35 |
+
font-size:12px;letter-spacing:.06em;padding:6px 14px;border-radius:999px}
|
| 36 |
+
.hero h1{font-size:clamp(30px,5vw,50px);margin:16px 0 6px;letter-spacing:-.03em;line-height:1.14}
|
| 37 |
+
.hero .sub{color:var(--dim);font-size:16px}
|
| 38 |
+
.prize{display:inline-flex;align-items:baseline;gap:10px;margin-top:22px;
|
| 39 |
+
background:var(--gold-bg);border:1px solid #f0dfae;border-radius:14px;padding:14px 26px}
|
| 40 |
+
.prize b{font-size:34px;letter-spacing:-.02em;color:var(--gold)}
|
| 41 |
+
.hero .meta{margin-top:14px;color:var(--faint);font-size:13px}
|
| 42 |
+
.ctas{display:flex;gap:10px;justify-content:center;margin-top:24px;flex-wrap:wrap}
|
| 43 |
+
.btn{border:1px solid var(--line);background:var(--panel);color:var(--tx);
|
| 44 |
+
padding:11px 20px;border-radius:10px;font-weight:600;font-size:14px;cursor:pointer;
|
| 45 |
+
box-shadow:var(--sh);transition:.12s}
|
| 46 |
+
.btn:hover{box-shadow:var(--sh2);transform:translateY(-1px)}
|
| 47 |
+
.btn.pri{background:var(--acc);border-color:var(--acc);color:#fff}
|
| 48 |
+
.btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
|
| 49 |
+
|
| 50 |
+
/* โโโโโ ์ข
๋ชฉ ํญ โโโโโ */
|
| 51 |
+
.tabs{display:flex;gap:8px;margin:26px 0 16px;flex-wrap:wrap}
|
| 52 |
+
.tab{border:1px solid var(--line);background:var(--panel);border-radius:11px;
|
| 53 |
+
padding:11px 18px;cursor:pointer;font-weight:600;box-shadow:var(--sh);transition:.12s;
|
| 54 |
+
display:flex;align-items:center;gap:9px}
|
| 55 |
+
.tab:hover{box-shadow:var(--sh2)}
|
| 56 |
+
.tab.on{border-color:var(--acc);background:var(--acc-bg);color:var(--acc)}
|
| 57 |
+
.tab .p{font-size:12px;font-weight:500;color:var(--faint)}
|
| 58 |
+
.tab.on .p{color:var(--acc)}
|
| 59 |
+
|
| 60 |
+
.grid{display:grid;grid-template-columns:1fr 336px;gap:16px;align-items:start}
|
| 61 |
+
.grid>*{min-width:0}
|
| 62 |
+
@media(max-width:940px){.grid{grid-template-columns:1fr}}
|
| 63 |
+
.row3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px}
|
| 64 |
+
@media(max-width:940px){.row3{grid-template-columns:1fr}}
|
| 65 |
+
|
| 66 |
+
/* โโโโโ ์งํ โโโโโ */
|
| 67 |
+
.kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--line);
|
| 68 |
+
border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:14px}
|
| 69 |
+
.kpi{background:var(--panel);padding:14px 16px}
|
| 70 |
+
.kpi .l{font-size:11px;color:var(--faint);letter-spacing:.02em}
|
| 71 |
+
.kpi .v{font-size:21px;font-weight:700;letter-spacing:-.02em;margin-top:3px}
|
| 72 |
+
@media(max-width:700px){.kpis{grid-template-columns:repeat(2,1fr)}}
|
| 73 |
+
|
| 74 |
+
#chart{height:clamp(380px,calc(100vh - 340px),620px)}
|
| 75 |
+
.legend{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:12px;font-size:12px}
|
| 76 |
+
.legend i{width:16px;height:3px;border-radius:2px;display:inline-block;
|
| 77 |
+
vertical-align:middle;margin-right:6px}
|
| 78 |
+
|
| 79 |
+
/* โโโโโ ํ โโโโโ */
|
| 80 |
+
.tw{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
| 81 |
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
| 82 |
+
/* ์ข์ ํ๋ฉด์์ 8์ด์ด ์ง๋๋ฆฌ๋ฉด ์ซ์๋ฅผ ๋ชป ์ฝ๋๋ค. ์ค์ด์ง ๋ง๊ณ ๋ฐ์ด์ ๋ณด๊ฒ ํ๋ค. */
|
| 83 |
+
.tw table{min-width:660px}
|
| 84 |
+
th{text-align:left;font-size:11px;color:var(--faint);font-weight:600;
|
| 85 |
+
padding:0 10px 9px;letter-spacing:.03em;border-bottom:1px solid var(--line)}
|
| 86 |
+
td{padding:11px 10px;border-bottom:1px solid var(--line)}
|
| 87 |
+
tr:last-child td{border-bottom:0}
|
| 88 |
+
tbody tr:hover{background:var(--panel2)}
|
| 89 |
+
.num{text-align:right}
|
| 90 |
+
.rk{width:34px;font-weight:700;color:var(--faint)}
|
| 91 |
+
.rk.g{color:var(--gold)}
|
| 92 |
+
.who b{font-weight:600}
|
| 93 |
+
.who small{display:block;color:var(--faint);font-size:11px;margin-top:1px}
|
| 94 |
+
.pill{display:inline-block;font-size:10px;padding:2px 7px;border-radius:6px;
|
| 95 |
+
background:var(--panel2);color:var(--dim);border:1px solid var(--line);font-weight:600}
|
| 96 |
+
.pill.base{background:#f2f0fb;color:#5b4bb5;border-color:#e3ddf6}
|
| 97 |
+
.star{color:var(--gold);font-weight:700}
|
| 98 |
+
|
| 99 |
+
/* โโโโโ ์ ์ถ ํจ๋ โโโโโ */
|
| 100 |
+
.sub label{display:block;font-size:12px;color:var(--dim);margin:14px 0 5px;font-weight:600}
|
| 101 |
+
.sub input[type=text]{width:100%;padding:9px 11px;border:1px solid var(--line);
|
| 102 |
+
border-radius:9px;background:var(--panel2);color:var(--tx);font-size:13px}
|
| 103 |
+
.sub input[type=range]{width:100%;accent-color:var(--acc)}
|
| 104 |
+
.posv{font-size:30px;font-weight:700;text-align:center;letter-spacing:-.02em;margin:6px 0}
|
| 105 |
+
.posl{text-align:center;font-size:12px;color:var(--faint);margin-bottom:6px}
|
| 106 |
+
.marks{display:flex;justify-content:space-between;font-size:10px;color:var(--faint)}
|
| 107 |
+
.note{background:var(--panel2);border-radius:9px;padding:10px 12px;font-size:12px;
|
| 108 |
+
color:var(--dim);margin-top:12px;line-height:1.55}
|
| 109 |
+
.key{background:var(--up-bg);border:1px solid #bfe5d3;border-radius:9px;padding:10px 12px;
|
| 110 |
+
font-size:12px;margin-top:12px;word-break:break-all}
|
| 111 |
+
|
| 112 |
+
/* โโโโโ ๊ท์นยทํ๋กฌํํธ โโโโโ */
|
| 113 |
+
pre{background:#0f1729;color:#e6edf7;border-radius:11px;padding:14px 16px;overflow:auto;
|
| 114 |
+
font-size:12px;line-height:1.65;margin:0;
|
| 115 |
+
font-family:"SF Mono",ui-monospace,Consolas,monospace}
|
| 116 |
+
.copy{float:right;font-size:11px;padding:5px 11px;border-radius:7px;border:1px solid var(--line);
|
| 117 |
+
background:var(--panel);cursor:pointer;font-weight:600}
|
| 118 |
+
.why{display:grid;gap:12px}
|
| 119 |
+
.why div{background:var(--panel2);border-radius:10px;padding:12px 14px}
|
| 120 |
+
.why b{display:block;margin-bottom:3px}
|
| 121 |
+
.why span{font-size:12.5px;color:var(--dim)}
|
| 122 |
+
.foot{margin-top:34px;padding-top:18px;border-top:1px solid var(--line);
|
| 123 |
+
color:var(--faint);font-size:12px;text-align:center}
|
| 124 |
+
.load{padding:40px;text-align:center;color:var(--faint)}
|
| 125 |
+
</style>
|
| 126 |
+
</head>
|
| 127 |
+
<body>
|
| 128 |
+
<div class="wrap">
|
| 129 |
+
|
| 130 |
+
<!-- โ ์ทจ์ง -->
|
| 131 |
+
<div class="hero">
|
| 132 |
+
<span class="kicker">FINANCIAL FORECAST CHALLENGE ยท SEASON 1</span>
|
| 133 |
+
<h1 id="m-title">AI๋ ์์ฅ์ ์ด๊ธธ ์ ์๋๊ฐ</h1>
|
| 134 |
+
<div class="sub" id="m-sub">122์ผ๊ฐ์ ๊ณต๊ฐ ์คํ</div>
|
| 135 |
+
<div class="prize"><span class="dim">์ด ์๊ธ</span><b>$2,000</b>
|
| 136 |
+
<span class="dim" id="prize-rule">์ข
๋ชฉ๋ณ 1์ $500 ร 4์ข
๋ชฉ</span></div>
|
| 137 |
+
<div class="meta" id="m-meta">โ</div>
|
| 138 |
+
<div class="ctas">
|
| 139 |
+
<button class="btn pri" onclick="go('#submit')">์ง์ ์ฐธ๊ฐํ๊ธฐ</button>
|
| 140 |
+
<button class="btn" onclick="go('#guide')">์์ด์ ํธ๋ก ์ฐธ๊ฐํ๊ธฐ</button>
|
| 141 |
+
</div>
|
| 142 |
+
</div>
|
| 143 |
+
|
| 144 |
+
<!-- ์ข
๋ชฉ ํญ -->
|
| 145 |
+
<div class="tabs" id="tabs"></div>
|
| 146 |
+
|
| 147 |
+
<!-- โก ์ฐจํธ + โฃ ์ ์ถ -->
|
| 148 |
+
<div class="grid">
|
| 149 |
+
<div class="card">
|
| 150 |
+
<div class="kpis" id="kpis"></div>
|
| 151 |
+
<div id="chart"></div>
|
| 152 |
+
<div class="legend" id="legend"></div>
|
| 153 |
+
</div>
|
| 154 |
+
|
| 155 |
+
<div>
|
| 156 |
+
<div class="card sub" id="submit">
|
| 157 |
+
<h2>ํฌ์ง์
์ ์ถ</h2>
|
| 158 |
+
<div class="posv mono" id="pv">+1.00</div>
|
| 159 |
+
<div class="posl" id="pl">์ ๋ ๋กฑ</div>
|
| 160 |
+
<input type="range" id="pos" min="-100" max="100" value="100" step="5"
|
| 161 |
+
oninput="onPos()">
|
| 162 |
+
<div class="marks"><span>โ1.0 ์</span><span>0 ๊ด๋ง</span><span>+1.0 ๋กฑ</span></div>
|
| 163 |
+
|
| 164 |
+
<label>์ฐธ๊ฐ ID</label>
|
| 165 |
+
<input type="text" id="uid" placeholder="huggingface ์์ด๋">
|
| 166 |
+
<label>์ ๋ต๋ช
</label>
|
| 167 |
+
<input type="text" id="strat" placeholder="์: 20์ผ ์ถ์ธ์ถ์ข
">
|
| 168 |
+
<button class="btn pri" style="width:100%;margin-top:14px" id="go"
|
| 169 |
+
onclick="submit()">์ ์ถ</button>
|
| 170 |
+
<div id="msg"></div>
|
| 171 |
+
<div class="note">
|
| 172 |
+
์ ์ถํ๋ฉด <b>๋ค์ ๋ผ ๋๊น์ง</b> ์ด ํฌ์ง์
์ด ์ ์ง๋ฉ๋๋ค.
|
| 173 |
+
๋ฐ๊ฟ ๋๋ง๋ค ์์๋ฃ๊ฐ ๋๊ฐ๋ฏ๋ก ์์ฃผ ๋ค์ง์ผ๋ฉด ์ํด์
๋๋ค.
|
| 174 |
+
</div>
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<div class="card" style="margin-top:16px">
|
| 178 |
+
<h2>์ด์ ํ๊ณ์ </h2>
|
| 179 |
+
<div id="luck"></div>
|
| 180 |
+
<div class="note" id="luck-note">โ</div>
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
|
| 185 |
+
<!-- โข ๋ญํน -->
|
| 186 |
+
<div class="card" style="margin-top:16px">
|
| 187 |
+
<h2>์์ <span class="faint" style="font-weight:400;font-size:13px"
|
| 188 |
+
id="rk-sub"></span></h2>
|
| 189 |
+
<div class="tw"><table>
|
| 190 |
+
<thead><tr>
|
| 191 |
+
<th class="rk">#</th><th>์ฐธ๊ฐ์ / ์ ๋ต</th><th>์ ํ</th>
|
| 192 |
+
<th class="num">์์ต๋ฅ </th><th class="num">์ด ๋๋น</th>
|
| 193 |
+
<th class="num">ํ์ </th><th class="num">์์๋ฃ</th><th class="num">์ฐธ๊ฐ์ผ</th>
|
| 194 |
+
</tr></thead>
|
| 195 |
+
<tbody id="rank"><tr><td colspan="8" class="load">๋ถ๋ฌ์ค๋ ์คโฆ</td></tr></tbody>
|
| 196 |
+
</table></div>
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
<!-- ๊ท์น ยท ๊ฐ์ด๋ -->
|
| 200 |
+
<div class="row3" id="guide">
|
| 201 |
+
<div class="card">
|
| 202 |
+
<h2>์ ํ๋๊ฐ</h2>
|
| 203 |
+
<div class="why" id="why"></div>
|
| 204 |
+
</div>
|
| 205 |
+
<div class="card">
|
| 206 |
+
<h2>์ด๋ป๊ฒ ๋ค๋ฅธ๊ฐ</h2>
|
| 207 |
+
<div class="why" id="how"></div>
|
| 208 |
+
</div>
|
| 209 |
+
<div class="card">
|
| 210 |
+
<h2>๊ท์น</h2>
|
| 211 |
+
<div class="why" id="rules"></div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<div class="card" style="margin-top:16px">
|
| 216 |
+
<h2>์์ด์ ํธ๋ก ์ฐธ๊ฐํ๊ธฐ
|
| 217 |
+
<button class="copy" onclick="copyP()">๋ณต์ฌ</button></h2>
|
| 218 |
+
<p class="dim" style="margin:0 0 12px;font-size:13px">
|
| 219 |
+
Claude CodeยทCodex ๋ฑ์ MCP ์๋ฒ๋ฅผ ๋ถ์ด๋ฉด ๋๊ตฌ ๋ค ๊ฐ๊ฐ ์๊น๋๋ค.
|
| 220 |
+
์๋๋ฅผ ๊ทธ๋๋ก ๋ถ์ฌ๋ฃ์ผ์ญ์์ค.</p>
|
| 221 |
+
<pre id="prompt">โ</pre>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<div class="foot">
|
| 225 |
+
<span id="stake">โ</span><br>
|
| 226 |
+
<span id="s2">โ</span>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<script>
|
| 231 |
+
/* ์ฐจํธ ์ค์ ์๋ CSS ๋ณ์๋ฅผ ๋ฃ์ ์ ์๋ค. ๋ฌธ์์ด ๊ทธ๋๋ก ๋ค์ด๊ฐ ๋ฌด์์ด ๋๋ค. */
|
| 232 |
+
const C = {
|
| 233 |
+
up:'#07804e', dn:'#c62828', line:'#e4e8ee', tx:'#0f1729', dim:'#6b7684',
|
| 234 |
+
pal:['#1f5fd6','#07804e','#c62828','#9a6b00','#7b4bb5','#0d8a9e',
|
| 235 |
+
'#d1640a','#4a5568','#b5297a','#2f7d32','#5b6ab5','#8a6d3b']
|
| 236 |
+
};
|
| 237 |
+
let R=null, ASSET=null, chart=null, lines=[];
|
| 238 |
+
|
| 239 |
+
const fmt = v => (v>=0?'+':'') + v.toFixed(2) + '%';
|
| 240 |
+
const go = h => document.querySelector(h).scrollIntoView({behavior:'smooth',block:'center'});
|
| 241 |
+
const cls = v => v>0?'up':(v<0?'dn':'dim');
|
| 242 |
+
|
| 243 |
+
async function boot(){
|
| 244 |
+
R = await (await fetch('/api/rules')).json();
|
| 245 |
+
document.getElementById('m-meta').textContent =
|
| 246 |
+
`${R.opens} ~ ${R.closes} ยท ${R.days}์ผ ยท ๋ ๋ฒ๋ฆฌ์ง 1 ๊ณ ์ ยท ๋งค์๊ฐ ๊ฐฑ์ `;
|
| 247 |
+
document.getElementById('prize-rule').textContent =
|
| 248 |
+
`์ข
๋ชฉ๋ณ 1์ $500 ร ${Object.keys(R.assets).length}์ข
๋ชฉ`;
|
| 249 |
+
document.getElementById('luck-note').textContent = R.luck_note;
|
| 250 |
+
|
| 251 |
+
document.getElementById('tabs').innerHTML = Object.entries(R.assets).map(([k,v]) =>
|
| 252 |
+
`<div class="tab" data-a="${k}" onclick="pick('${k}')">
|
| 253 |
+
<span>${v.label}</span><span class="p">${k}</span></div>`).join('');
|
| 254 |
+
|
| 255 |
+
document.getElementById('luck').innerHTML = Object.entries(R.luck_ceiling)
|
| 256 |
+
.sort((a,b)=>b[1]-a[1]).map(([k,v]) =>
|
| 257 |
+
`<div style="display:flex;justify-content:space-between;padding:7px 0;
|
| 258 |
+
border-bottom:1px solid var(--line)">
|
| 259 |
+
<span>${R.assets[k].label}</span>
|
| 260 |
+
<b class="mono up">+${(v*100).toFixed(1)}%</b></div>`).join('');
|
| 261 |
+
|
| 262 |
+
const man = await (await fetch('/api/manifesto')).json();
|
| 263 |
+
document.getElementById('m-title').textContent = man.title;
|
| 264 |
+
document.getElementById('m-sub').textContent = man.sub;
|
| 265 |
+
const box = (a) => a.map(([t,d]) => `<div><b>${t}</b><span>${d}</span></div>`).join('');
|
| 266 |
+
document.getElementById('why').innerHTML = box(man.why);
|
| 267 |
+
document.getElementById('how').innerHTML = box(man.how);
|
| 268 |
+
document.getElementById('stake').textContent = man.our_stake;
|
| 269 |
+
document.getElementById('s2').textContent =
|
| 270 |
+
`${man.season2.name} โ ${man.season2.teaser}. ์ข
๋ชฉ์ ์์ฆ 1 ์ข
๋ฃ ํ ํ์ ํฉ๋๋ค.`;
|
| 271 |
+
|
| 272 |
+
document.getElementById('rules').innerHTML = box([
|
| 273 |
+
['ํฌ์ง์
', R.leverage],
|
| 274 |
+
['๊ฐฑ์ ', R.update],
|
| 275 |
+
['์์๋ฃ', Object.entries(R.fees).map(([k,v])=>`${k} ${(v*100).toFixed(2)}%`).join(' ยท ')
|
| 276 |
+
+ ' โ ' + R.fee_note],
|
| 277 |
+
['์์', R.scoring],
|
| 278 |
+
['์ฐธ์ฌ ์๊ฑด', R.participation],
|
| 279 |
+
]);
|
| 280 |
+
|
| 281 |
+
pick(Object.keys(R.assets)[0]);
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
function prompt_(a){
|
| 285 |
+
document.getElementById('prompt').textContent =
|
| 286 |
+
`claude mcp add --transport http finchal ${location.origin}/mcp \\
|
| 287 |
+
--header "Authorization: Bearer <๋ด ์ฐธ๊ฐํค>"
|
| 288 |
+
|
| 289 |
+
# ๊ทธ ๋ค์ ์์ด์ ํธ์๊ฒ ์ด๋ ๊ฒ ๋งํ๋ฉด ๋ฉ๋๋ค
|
| 290 |
+
"finchal ์ฑ๋ฆฐ์ง์ ์ฐธ๊ฐํด์ค. get_rules๋ก ๊ท์น์ ์ฝ๊ณ ,
|
| 291 |
+
get_data๋ก ${a} ์์ธ๋ฅผ ๋ฐ์์ ์ง์ ๋ชจ๋ธ์ ๋ง๋ ๋ค์
|
| 292 |
+
submit_position์ผ๋ก ํฌ์ง์
์ ๋ด์ค. ๋งค์ผ check_score๋ก ์ฑ์ ์ ํ์ธํ๊ณ
|
| 293 |
+
ํ์ํ๋ฉด ํฌ์ง์
์ ์กฐ์ ํด."
|
| 294 |
+
|
| 295 |
+
# ๋๊ตฌ ๋ท
|
| 296 |
+
get_rules() ๊ท์นยท์ข
๋ชฉยท์์๋ฃยท๋ง๊ฐ
|
| 297 |
+
get_data(asset,bars) ๊ณผ๊ฑฐ ์์ธ (๋ฏธ๋๋ ์ฃผ์ง ์์ต๋๋ค)
|
| 298 |
+
submit_position(asset, position) -1.0 ~ +1.0
|
| 299 |
+
check_score(asset) ๋ด ์์์ ์ด์ ํ๊ณ์ ๋๋น ์์น`;
|
| 300 |
+
}
|
| 301 |
+
function copyP(){
|
| 302 |
+
navigator.clipboard.writeText(document.getElementById('prompt').textContent);
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
async function pick(a){
|
| 306 |
+
ASSET = a;
|
| 307 |
+
document.querySelectorAll('.tab').forEach(t =>
|
| 308 |
+
t.classList.toggle('on', t.dataset.a === a));
|
| 309 |
+
prompt_(a);
|
| 310 |
+
document.getElementById('rk-sub').textContent =
|
| 311 |
+
`${R.assets[a].label} ยท 1์ $500 ยท ${R.assets[a].why}`;
|
| 312 |
+
await Promise.all([drawChart(a), drawRank(a)]);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
async function drawChart(a){
|
| 316 |
+
const d = await (await fetch(`/api/curves?asset=${a}&top=12`)).json();
|
| 317 |
+
const el = document.getElementById('chart');
|
| 318 |
+
if(chart){ chart.remove(); chart = null; }
|
| 319 |
+
chart = LightweightCharts.createChart(el, {
|
| 320 |
+
layout:{background:{color:'#ffffff'}, textColor:C.dim, fontSize:11},
|
| 321 |
+
grid:{vertLines:{color:C.line}, horzLines:{color:C.line}},
|
| 322 |
+
rightPriceScale:{borderColor:C.line},
|
| 323 |
+
timeScale:{borderColor:C.line, timeVisible:false},
|
| 324 |
+
crosshair:{mode:1}, height:el.clientHeight,
|
| 325 |
+
});
|
| 326 |
+
const t = d.t.map(x => x.slice(0,10));
|
| 327 |
+
const mk = (name, vals, color, w, dash) => {
|
| 328 |
+
const s = chart.addLineSeries({color, lineWidth:w, lineStyle:dash?2:0,
|
| 329 |
+
priceLineVisible:false, lastValueVisible:false});
|
| 330 |
+
s.setData(t.map((x,i)=>({time:x, value:vals[i]})));
|
| 331 |
+
return s;
|
| 332 |
+
};
|
| 333 |
+
const leg = [];
|
| 334 |
+
if(d.market && d.market.length){
|
| 335 |
+
mk('์์ฅ', d.market, C.dim, 2, true);
|
| 336 |
+
leg.push([C.dim, `${R.assets[a].label} ์์ฅ (๋จ์ ๋ณด์ )`,
|
| 337 |
+
d.market[d.market.length-1]]);
|
| 338 |
+
}
|
| 339 |
+
(d.series||[]).forEach((s,i) => {
|
| 340 |
+
const col = C.pal[i % C.pal.length];
|
| 341 |
+
mk(s.user, s.v, col, 2, false);
|
| 342 |
+
leg.push([col, `${s.user} ยท ${s.strategy}`, s.v[s.v.length-1]]);
|
| 343 |
+
});
|
| 344 |
+
chart.timeScale().fitContent();
|
| 345 |
+
document.getElementById('legend').innerHTML = leg.length
|
| 346 |
+
? leg.map(([c,n,v]) => `<span><i style="background:${c}"></i>${n}
|
| 347 |
+
<b class="mono ${cls(v)}">${fmt(v)}</b></span>`).join('')
|
| 348 |
+
: '<span class="faint">์์ง ๊ณก์ ์ด ์์ต๋๋ค. ์์ฆ์ด ํ๋ฃจ ์ด์ ์งํ๋๋ฉด ๊ทธ๋ ค์ง๋๋ค.</span>';
|
| 349 |
+
|
| 350 |
+
const rows = (d.series||[]);
|
| 351 |
+
const best = rows.length ? Math.max(...rows.map(s=>s.v[s.v.length-1])) : 0;
|
| 352 |
+
const mkt = d.market && d.market.length ? d.market[d.market.length-1] : 0;
|
| 353 |
+
const K = [
|
| 354 |
+
['์ฐธ๊ฐ์', rows.length + '๋ช
', ''],
|
| 355 |
+
['ํ์ฌ 1์', rows.length?fmt(best):'โ', cls(best)],
|
| 356 |
+
[`${R.assets[a].label} ์์ฅ`, d.market&&d.market.length?fmt(mkt):'โ', cls(mkt)],
|
| 357 |
+
['์ด์ ํ๊ณ์ ', '+' + (R.luck_ceiling[a]*100).toFixed(1) + '%', 'dim'],
|
| 358 |
+
];
|
| 359 |
+
document.getElementById('kpis').innerHTML = K.map(([l,v,c]) =>
|
| 360 |
+
`<div class="kpi"><div class="l">${l}</div>
|
| 361 |
+
<div class="v mono ${c}">${v}</div></div>`).join('');
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
async function drawRank(a){
|
| 365 |
+
const d = await (await fetch(`/api/leaderboard?asset=${a}`)).json();
|
| 366 |
+
const tb = document.getElementById('rank');
|
| 367 |
+
if(!d.rows || !d.rows.length){
|
| 368 |
+
tb.innerHTML = `<tr><td colspan="8" class="load">
|
| 369 |
+
์์ง ์ฐธ๊ฐ์๊ฐ ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ๊ฐ ๋์ญ์์ค.</td></tr>`;
|
| 370 |
+
return;
|
| 371 |
+
}
|
| 372 |
+
const ceil = R.luck_ceiling[a];
|
| 373 |
+
const B = {human:'๐ง ์ฌ๋', expert:'๐ ์ ๋ฌธ๊ฐ', agent:'๐ค ์์ด์ ํธ', anchor:'๊ธฐ์ค์ '};
|
| 374 |
+
tb.innerHTML = d.rows.map(r => {
|
| 375 |
+
const over = r.ret > r.luck_p95;
|
| 376 |
+
return `<tr>
|
| 377 |
+
<td class="rk ${r.rank===1?'g':''}">${r.rank}</td>
|
| 378 |
+
<td class="who"><b>${r.user}</b><small>${r.strategy}</small></td>
|
| 379 |
+
<td><span class="pill ${r.type==='anchor'?'base':''}">${B[r.type]||r.type}</span></td>
|
| 380 |
+
<td class="num mono ${cls(r.ret*100)}"><b>${fmt(r.ret*100)}</b></td>
|
| 381 |
+
<td class="num mono">${over?'<span class="star">โ
๋์</span>'
|
| 382 |
+
:`<span class="faint">${(r.ret/Math.max(ceil,1e-9)*100).toFixed(0)}%</span>`}</td>
|
| 383 |
+
<td class="num mono faint">${r.turnover.toFixed(1)}</td>
|
| 384 |
+
<td class="num mono ${r.fee_paid>0.02?'dn':'faint'}">
|
| 385 |
+
โ${(r.fee_paid*100).toFixed(2)}%</td>
|
| 386 |
+
<td class="num faint">${(r.joined||'').slice(5,10)}</td>
|
| 387 |
+
</tr>`;
|
| 388 |
+
}).join('');
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
function onPos(){
|
| 392 |
+
const v = +document.getElementById('pos').value / 100;
|
| 393 |
+
document.getElementById('pv').textContent = (v>=0?'+':'') + v.toFixed(2);
|
| 394 |
+
document.getElementById('pv').className = 'posv mono ' + cls(v);
|
| 395 |
+
document.getElementById('pl').textContent =
|
| 396 |
+
v === 0 ? '๊ด๋ง โ ์์ฅ์ ๋
ธ์ถ๋์ง ์์ต๋๋ค'
|
| 397 |
+
: v > 0 ? `๋กฑ ${(v*100).toFixed(0)}% โ ์ค๋ฅด๋ฉด ๋ฒ๋๋ค`
|
| 398 |
+
: `์ ${(-v*100).toFixed(0)}% โ ๋ด๋ฆฌ๋ฉด ๋ฒ๋๋ค`;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
async function submit(){
|
| 402 |
+
const uid = document.getElementById('uid').value.trim();
|
| 403 |
+
const st = document.getElementById('strat').value.trim() || '-';
|
| 404 |
+
const msg = document.getElementById('msg');
|
| 405 |
+
if(!uid){ msg.innerHTML = '<div class="note dn">์ฐธ๊ฐ ID๋ฅผ ์
๋ ฅํ์ญ์์ค.</div>'; return; }
|
| 406 |
+
const b = document.getElementById('go');
|
| 407 |
+
b.disabled = true; b.textContent = '์ ์ถ ์คโฆ';
|
| 408 |
+
try{
|
| 409 |
+
const reg = await (await fetch('/api/register', {method:'POST',
|
| 410 |
+
headers:{'Content-Type':'application/json'},
|
| 411 |
+
body:JSON.stringify({user:uid, strategy:st, type:'human',
|
| 412 |
+
assets:Object.keys(R.assets)})})).json();
|
| 413 |
+
const pos = +document.getElementById('pos').value / 100;
|
| 414 |
+
const r = await (await fetch('/api/submit', {method:'POST',
|
| 415 |
+
headers:{'Content-Type':'application/json',
|
| 416 |
+
'Authorization':'Bearer ' + reg.key},
|
| 417 |
+
body:JSON.stringify({asset:ASSET, position:pos, source:'web'})})).json();
|
| 418 |
+
if(!r.ok) throw new Error(r.error || '์คํจ');
|
| 419 |
+
msg.innerHTML = `<div class="key"><b>์ ์๋์ต๋๋ค.</b>
|
| 420 |
+
${R.assets[ASSET].label} ํฌ์ง์
${(pos>=0?'+':'')+pos.toFixed(2)}<br>
|
| 421 |
+
<span class="dim">์ฐธ๊ฐํค (์์ด์ ํธ ์ฐ๊ฒฐ์ ์๋๋ค) โ</span><br>
|
| 422 |
+
<b class="mono">${reg.key}</b></div>`;
|
| 423 |
+
drawRank(ASSET);
|
| 424 |
+
}catch(e){
|
| 425 |
+
msg.innerHTML = `<div class="note dn">${e.message}</div>`;
|
| 426 |
+
}
|
| 427 |
+
b.disabled = false; b.textContent = '์ ์ถ';
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
addEventListener('resize', () => {
|
| 431 |
+
if(chart) chart.applyOptions({height:document.getElementById('chart').clientHeight});
|
| 432 |
+
});
|
| 433 |
+
onPos(); boot();
|
| 434 |
+
</script>
|
| 435 |
+
</body>
|
| 436 |
+
</html>
|
luck_ceiling.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"season": 1,
|
| 3 |
+
"start": "2026-08-25",
|
| 4 |
+
"end": "2026-12-24",
|
| 5 |
+
"days": 121,
|
| 6 |
+
"n_sim": 20000,
|
| 7 |
+
"block": 5,
|
| 8 |
+
"seed": 20260825,
|
| 9 |
+
"method": "๋ธ๋ก ๋ถํธ์คํธ๋ฉ ยท ๋ฌด์์ ํฌ์ง์
-1/0/+1 ยท ๋ ๋ฒ๋ฆฌ์ง 1 ยท 95๋ถ์",
|
| 10 |
+
"ceiling": {
|
| 11 |
+
"SOL": 0.887,
|
| 12 |
+
"ETH": 0.6227,
|
| 13 |
+
"BTC": 0.5821,
|
| 14 |
+
"TSLA": 0.5264,
|
| 15 |
+
"NVDA": 0.4399,
|
| 16 |
+
"OIL": 0.3546,
|
| 17 |
+
"AAPL": 0.2271,
|
| 18 |
+
"GOLD": 0.1497
|
| 19 |
+
},
|
| 20 |
+
"vol": {
|
| 21 |
+
"SOL": 0.7955,
|
| 22 |
+
"ETH": 0.5821,
|
| 23 |
+
"BTC": 0.5575,
|
| 24 |
+
"TSLA": 0.4945,
|
| 25 |
+
"NVDA": 0.4295,
|
| 26 |
+
"OIL": 0.3407,
|
| 27 |
+
"AAPL": 0.2324,
|
| 28 |
+
"GOLD": 0.1558
|
| 29 |
+
}
|
| 30 |
+
}
|
reference.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""๊ธฐ์ค๋ถํฌ โ '์ค๋ ฅ 0์ด์๋ค๋ฉด ์ด๋๊น์ง ๊ฐ์๊น'๋ฅผ ๊ณ์ฐํ๋ค.
|
| 3 |
+
|
| 4 |
+
๋ ๊ฐ์ง๋ฅผ ๊ตฌ๋ถํ๋ค.
|
| 5 |
+
|
| 6 |
+
๊ฐ๋ง ์ ์ถ์ ์น (announce)
|
| 7 |
+
์ฐธ๊ฐ์์๊ฒ "์ด ์ข
๋ชฉ์ ์ด์ผ๋ก ์ด๋งํผ๊น์ง ๋์จ๋ค"๊ณ ๋ฏธ๋ฆฌ ์๋ ค์ฃผ๋ ๊ฐ.
|
| 8 |
+
๊ณผ๊ฑฐ 5๋
์ ๋ธ๋ก ๋ถํธ์คํธ๋ฉํด์ ๋ธ๋ค. ํ๋ฉด์ '์ด์ ํ๊ณ์ '์ ์ด๋ค.
|
| 9 |
+
|
| 10 |
+
์ฑ์ ์ฉ ๊ธฐ์ค (live)
|
| 11 |
+
์ค์ ์์๋ฅผ ์ ํ๋ ๊ฐ. ์์ฆ์ด ์งํ๋ ๋งํผ์ **์ค์ ๊ฒฝ๋ก** ์์์
|
| 12 |
+
์ค๋ ฅ 0 ์ฐธ๊ฐ์๋ฅผ ๋๋ ค ๋ง๋ ๋ค. ๋งค์ผ ๋ค์ ๊ณ์ฐํ๋ค.
|
| 13 |
+
|
| 14 |
+
์ ๋๋๋๊ฐ
|
| 15 |
+
๊ธฐ์ค์ ๊ณผ๊ฑฐ ๋ถํธ์คํธ๋ฉ์ผ๋ก ๊ณ ์ ํ๋ฉด, ์์ฆ ์ค ๊ทธ ์ข
๋ชฉ์ด ํฌ๊ฒ ์ค๋ฅผ ๋
|
| 16 |
+
์ฐธ๊ฐ์ ์ ์์ด ๋ถํ๋ ค์ง๋ค. ์ค์ธก์์ GOLD ์ฐ์น๋ฅ 99.6%๊ฐ ๋์๋ค.
|
| 17 |
+
์ฌ๋ฐ๋ฅธ ๋ฐ์ฌ์ค์ '์์ฅ์ด ์ค์ ๋ก ๊ทธ๋ ๊ฒ ์์ง์์ ๋ ์ด์ผ๋ก ๋ผ ์ ์์๋ ์ฑ์ '์ด๋ค.
|
| 18 |
+
|
| 19 |
+
๐ ๊ฐ๋ง ์ ์ ๊ณ ์ ยท๊ณต๊ฐํ๋ ๊ฒ์ ์ซ์๊ฐ ์๋๋ผ **๋ฐฉ๋ฒ**์ด๋ค.
|
| 20 |
+
์ซ์๋ฅผ ๊ณ ์ ํ๋ฉด ์์ฅ์ด ์์๊ณผ ๋ค๋ฅด๊ฒ ์์ง์์ ๋ ์ฑ์ ์ด ํต์งธ๋ก ์ด๊ธ๋๋ค.
|
| 21 |
+
|
| 22 |
+
๐ ๊ธฐ์ค ์ฐธ๊ฐ์๋ ์ฐธ๊ฐ์์ ๋๊ฐ์ด ์์๋ฃ๋ฅผ ๋ธ๋ค.
|
| 23 |
+
์ ๋ฌผ๋ฆฌ๋ฉด ์ฐธ๊ฐ์๋ง ์ํด๋ฅผ ๋ณด๊ณ ์์ํ๋ค. BTC์์ ๊ทธ ์ฐจ์ด๊ฐ 77.2%p์๋ค.
|
| 24 |
+
"""
|
| 25 |
+
import os, json, warnings
|
| 26 |
+
import numpy as np, pandas as pd
|
| 27 |
+
warnings.filterwarnings("ignore")
|
| 28 |
+
|
| 29 |
+
ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 30 |
+
BLOCK = 5
|
| 31 |
+
N_LIVE = 4000
|
| 32 |
+
N_ANNOUNCE = 20000
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def _sim(returns, n, expo, fee, seed, block=None):
|
| 36 |
+
"""์ค๋ ฅ 0 ์ฐธ๊ฐ์ n๋ช
์ ์ต์ข
์์ต๋ฅ . expo ๋งํผ์ ์๊ฐ์ ํฌ์ง์
์ ๋
ธ์ถํ๋ค."""
|
| 37 |
+
g = np.random.default_rng(seed)
|
| 38 |
+
r = np.asarray(returns, dtype=float)
|
| 39 |
+
L = len(r)
|
| 40 |
+
out = np.empty(n)
|
| 41 |
+
for i in range(n):
|
| 42 |
+
if block: # ๋ถํธ์คํธ๋ฉ (๊ฐ๋ง ์ ์ถ์ )
|
| 43 |
+
nb = int(np.ceil(L / block))
|
| 44 |
+
st = g.integers(0, max(len(r) - block, 1), size=nb)
|
| 45 |
+
path = np.concatenate([r[j:j+block] for j in st])[:L]
|
| 46 |
+
else: # ์ค์ ๊ฒฝ๋ก (์ฑ์ )
|
| 47 |
+
path = r
|
| 48 |
+
act = g.random(L) < expo
|
| 49 |
+
pos = np.where(act, g.choice([-1.0, 1.0], size=L), 0.0)
|
| 50 |
+
cost = np.abs(np.diff(np.r_[0.0, pos])) * fee
|
| 51 |
+
out[i] = np.prod(1 + np.r_[0.0, pos[:-1]] * path - cost) - 1
|
| 52 |
+
return np.sort(out)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def live(prices, fee, expo=2/3, n=N_LIVE, seed=None):
|
| 56 |
+
"""์ฑ์ ์ฉ โ ์์ฆ ๊ฒฝ๊ณผ๋ถ์ ์ค์ ๊ฒฝ๋ก ์์์ ๋ง๋ ๋ค. ๋งค์ผ ๋ค์ ๋ถ๋ฅธ๋ค.
|
| 57 |
+
|
| 58 |
+
seed ๋ฅผ ๋ ์ง๋ก ์ฃผ๋ฉด ๊ฐ์ ๋ ๊ฐ์ ๊ฐ์ด ๋์ ์ฌํ์ด ๋๋ค.
|
| 59 |
+
"""
|
| 60 |
+
r = pd.Series(prices, dtype=float).sort_index().pct_change().fillna(0.0).values
|
| 61 |
+
s = seed if seed is not None else int(pd.Timestamp.utcnow().strftime("%Y%m%d"))
|
| 62 |
+
return _sim(r, n, expo, fee, s)
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def announce(returns, fee, days, expo=2/3, n=N_ANNOUNCE, seed=20260824):
|
| 66 |
+
"""๊ฐ๋ง ์ ์ถ์ โ ๊ณผ๊ฑฐ๋ฅผ ๋ธ๋ก ๋ถํธ์คํธ๋ฉํด์ '์ด๋งํผ๊น์ง ๋์จ๋ค'๋ฅผ ๋ธ๋ค."""
|
| 67 |
+
r = np.asarray(returns, dtype=float)
|
| 68 |
+
# ์์ฆ ๊ธธ์ด๋งํผ๋ง ์ฐ๋๋ก ์๋ผ ๋ถ์ธ๋ค
|
| 69 |
+
reps = int(np.ceil(days / len(r))) + 1
|
| 70 |
+
pool = np.tile(r, reps)[:max(len(r), days * 3)]
|
| 71 |
+
return _sim(pool[:days] if len(pool) >= days else pool, n, expo, fee, seed, block=BLOCK)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def pct_of(ret, refv):
|
| 75 |
+
import bisect
|
| 76 |
+
return bisect.bisect_left(refv, ret) / len(refv)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def score_of(ret, refv):
|
| 80 |
+
import math
|
| 81 |
+
p = min(pct_of(ret, refv), 1 - 1e-6)
|
| 82 |
+
return -math.log10(max(1.0 - p, 1e-6))
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ๊ฐ๋ง ๊ณต์ง๊ฐ ์์ฑ โโโโโโโโโโโโโโโโโโโโโโโ
|
| 86 |
+
if __name__ == "__main__":
|
| 87 |
+
import seasons as S
|
| 88 |
+
import scoring as SC
|
| 89 |
+
import yfinance as yf
|
| 90 |
+
|
| 91 |
+
s1 = S.SEASONS[1]
|
| 92 |
+
days = (pd.Timestamp(s1["closes"]) - pd.Timestamp(s1["opens"])).days
|
| 93 |
+
print("์์ฆ 1 ๊ฐ๋ง ๊ณต์ง๊ฐ โ %d์ผ ยท ์์๋ฃ ๋ฐ์\n" % days)
|
| 94 |
+
print(" %-6s %-8s %8s %11s %11s %11s"
|
| 95 |
+
% ("์ข
๋ชฉ", "๋ฐ์ดํฐ", "์์๋ฃ", "์ด ์ค์", "์ด ์์5%", "์ด ์์1%"))
|
| 96 |
+
out = {}
|
| 97 |
+
for code, a in s1["assets"].items():
|
| 98 |
+
src = a["src"]
|
| 99 |
+
if code == "BTC":
|
| 100 |
+
p = os.path.join(os.path.dirname(ROOT), "btc-oracle", "data", "hourly.parquet")
|
| 101 |
+
H = pd.read_parquet(p).sort_index()
|
| 102 |
+
if "close" not in H.columns:
|
| 103 |
+
H = H.rename(columns={"c": "close"})
|
| 104 |
+
px = H.close.groupby(H.index.normalize()).last()
|
| 105 |
+
else:
|
| 106 |
+
d = yf.download(src, period="5y", interval="1d",
|
| 107 |
+
progress=False, auto_adjust=True)["Close"]
|
| 108 |
+
px = (d.iloc[:, 0] if isinstance(d, pd.DataFrame) else d).dropna()
|
| 109 |
+
r = px.pct_change().dropna().values
|
| 110 |
+
fee = SC.fee_of(code)
|
| 111 |
+
c = announce(r, fee, days)
|
| 112 |
+
out[code] = {"p50": float(np.median(c)), "p95": float(np.percentile(c, 95)),
|
| 113 |
+
"p99": float(np.percentile(c, 99)), "fee": fee, "days": days}
|
| 114 |
+
print(" %-6s %-8s %7.2f%% %10.1f%% %10.1f%% %10.1f%%"
|
| 115 |
+
% (code, src, fee * 100, np.median(c) * 100,
|
| 116 |
+
np.percentile(c, 95) * 100, np.percentile(c, 99) * 100))
|
| 117 |
+
json.dump({"season": 1, "opens": s1["opens"], "closes": s1["closes"], "days": days,
|
| 118 |
+
"method": "๋ธ๋ก๋ถํธ์คํธ๋ฉ 5์ผ ยท ๋ฌด์์ ํฌ์ง์
ยท ๋ ๋ฒ๋ฆฌ์ง1 ยท ์์๋ฃ ๋ฐ์",
|
| 119 |
+
"note": "๊ฐ๋ง ๊ณต์ง์ฉ ์ถ์ ์น. ์ค์ ์ฑ์ ์ ์์ฆ ์ค์ ๊ฒฝ๋ก๋ก ๋งค์ผ ์ฌ๊ณ์ฐํ๋ค.",
|
| 120 |
+
"ceiling": out},
|
| 121 |
+
open(os.path.join(ROOT, "announce_ceiling.json"), "w"),
|
| 122 |
+
ensure_ascii=False, indent=1)
|
| 123 |
+
print("\n ํ๋ฉด์ '์ด์ ํ๊ณ์ '์๋ ์์5%๋ฅผ ์ด๋ค.")
|
| 124 |
+
print(" ์ ์ฅ: finchal/announce_ceiling.json")
|
reference_dist.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
| 3 |
+
pandas==2.2.3
|
| 4 |
+
numpy==2.2.1
|
| 5 |
+
yfinance==0.2.51
|
scoring.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""์ฑ์ ์์ง โ ํฌ์ง์
๊ธฐ๋ก์์ ์์ต๋ฅ ๊ณผ ์์ ์ ์๋ฅผ ๋ธ๋ค.
|
| 3 |
+
|
| 4 |
+
๊ท์น (์ฌ์ ๋ฑ๋ก)
|
| 5 |
+
ํฌ์ง์
์ -1.0 ~ +1.0. ๋ฒ์๋ฅผ ๋ฒ์ด๋๋ฉด ์๋ผ๋ธ๋ค (๋ ๋ฒ๋ฆฌ์ง 1 ๊ณ ์ )
|
| 6 |
+
ํฌ์ง์
๊ฐฑ์ ์ ๋งค์๊ฐ ๊ฐ๋ฅ. ๋ฏธ๊ฐฑ์ ๊ตฌ๊ฐ์ ์ง์ ํฌ์ง์
์ด ์ ์ง๋๋ค
|
| 7 |
+
์์ต๋ฅ = ๊ฐ ๊ตฌ๊ฐ ํฌ์ง์
x ๊ทธ ๊ตฌ๊ฐ ์์ต๋ฅ ์ ๋ณต๋ฆฌ
|
| 8 |
+
๊ฑฐ๋๋น์ฉ = ํฌ์ง์
๋ณ๊ฒฝ๋ถ์ ์๋ณต 0.02% (๋ฏธ๊ตญ์ฃผ์ ๋ฌด๋ฃ + ์คํ๋ ๋ ์ถ์ )
|
| 9 |
+
์์ ์ ์ = -log10(1 - ๋ฐฑ๋ถ์) ๋ฐฑ๋ถ์๋ ๊ทธ ์ข
๋ชฉ์ '์ค๋ ฅ0 ๋ถํฌ' ๊ธฐ์ค
|
| 10 |
+
|
| 11 |
+
๐ด ์ฑ์ ๊ธฐ๋ฅผ ๋ชจ๋ธ๋ณด๋ค ๋จผ์ ๊ฒ์ฆํ๋ค. ์ด ํ์ผ์ ๋ฐ๊พธ๋ฉด selftest()๋ฅผ ๋ฐ๋์ ๋ค์ ๋๋ฆฐ๋ค.
|
| 12 |
+
์ด๋ฒ ํ๋ก์ ํธ์์๋ง ์ฑ์ ยท์๋ฎฌ ๊ฒฐํจ์ 8๊ฑด ๊ฒช์๋ค. ๋๋ถ๋ถ '๊ทธ๋ด๋ฏํ๊ฒ ํ๋ฆฐ' ๊ฐ์ด์๋ค.
|
| 13 |
+
"""
|
| 14 |
+
import os, json, math, bisect
|
| 15 |
+
import numpy as np
|
| 16 |
+
import pandas as pd
|
| 17 |
+
|
| 18 |
+
ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 19 |
+
# ์ข
๋ชฉ๋ณ ์ค๊ฑฐ๋ ๋น์ฉ. ํฌ์ง์
๋ณ๊ฒฝ๋ถ(|ฮw|)์ ๊ณฑํ๋ค.
|
| 20 |
+
# ๐ ์ด๊ฑธ ๋ฌผ๋ฆฌ์ง ์์ผ๋ฉด ํฌ์ง์
์ ์ด๋น ๋ค์ง๋ ๋ฌด์ฐจ๋ณ ์ ์ถ์ด ์ด๋์ ๋ณธ๋ค.
|
| 21 |
+
# ๋งค์๊ฐ ยฑ1์ ๋ค์ง์ผ๋ฉด 121์ผ์ |ฮw| ํฉ๊ณ๊ฐ 5,808์ด ๋๋๋ฐ, ์ฝ์ธ 0.06%๋ฉด
|
| 22 |
+
# ์์๋ฃ๋ง ์๋ณธ์ 348%๋ค. ๊ท์น์ด ์๋๋ผ ๋น์ฉ์ด ๊ทธ๋ฐ ํ๋์ ๋ง๋๋ค.
|
| 23 |
+
FEES = {
|
| 24 |
+
# ์ฝ์ธ โ ๋ฐ์ด๋ธ์ค ํ
์ด์ปค 0.05% + ์ฌ๋ฆฌํผ์ง 0.01%
|
| 25 |
+
"BTC": 0.0006, "ETH": 0.0006, "SOL": 0.0008,
|
| 26 |
+
# ๋ฏธ๊ตญ ์ฃผ์ โ ์์๋ฃ ๋ฌด๋ฃ์ง๋ง ์คํ๋ ๋ยท์ฌ๋ฆฌํผ์ง๊ฐ ๋จ๋๋ค
|
| 27 |
+
"NVDA": 0.0002, "TSLA": 0.0002, "AAPL": 0.0002,
|
| 28 |
+
# ์ ๋ฌผ โ ๊ณ์ฝ๋น ์์๋ฃ + ํธ๊ฐํญ
|
| 29 |
+
"GOLD": 0.0002, "OIL": 0.0003,
|
| 30 |
+
}
|
| 31 |
+
FEE_DEFAULT = 0.0006
|
| 32 |
+
FEE = FEE_DEFAULT # selftest์์ ์์๋ก ๋ฐ๊ฟ ์ฐ๋ ์ ์ญ๊ฐ
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def fee_of(asset=None):
|
| 36 |
+
if asset is None:
|
| 37 |
+
return FEE
|
| 38 |
+
return FEES.get(asset, FEE_DEFAULT)
|
| 39 |
+
POS_MIN, POS_MAX = -1.0, 1.0
|
| 40 |
+
|
| 41 |
+
def clip(p):
|
| 42 |
+
"""๋ ๋ฒ๋ฆฌ์ง 1 ์ํ. ๋๊ฒจ ์ ์ถํด๋ ์๋ผ๋ธ๋ค โ ๊ฑฐ๋ถํ์ง ์๋ ์ด์ ๋
|
| 43 |
+
์์ด์ ํธ๊ฐ ๋ฐ์ฌ๋ฆผ ์ค์ฐจ๋ก 1.0000001์ ๋ณด๋ด๋ ์ผ์ด ํํ๊ธฐ ๋๋ฌธ์ด๋ค."""
|
| 44 |
+
return max(POS_MIN, min(POS_MAX, float(p)))
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def equity(positions, prices, asset=None):
|
| 48 |
+
"""ํฌ์ง์
๊ธฐ๋ก + ๊ฐ๊ฒฉ์์ ์์ฐ ๊ณก์ ์ ๋ง๋ ๋ค.
|
| 49 |
+
|
| 50 |
+
positions: ์๊ฐโํฌ์ง์
(์ ์ถ๋ ์์ ๋ง). ์ฌ์ด ๊ตฌ๊ฐ์ ์ง์ ๊ฐ์ด ์ ์ง๋๋ค
|
| 51 |
+
prices: ์๊ฐโ๊ฐ๊ฒฉ (์ฑ์ ๊ฒฉ์. ๋ณดํต 1์๊ฐ ๊ฐ๊ฒฉ)
|
| 52 |
+
"""
|
| 53 |
+
pos = pd.Series(positions, dtype=float).sort_index().map(clip)
|
| 54 |
+
px = pd.Series(prices, dtype=float).sort_index()
|
| 55 |
+
w = pos.reindex(px.index, method="ffill").fillna(0.0)
|
| 56 |
+
r = px.pct_change().fillna(0.0)
|
| 57 |
+
cost = w.diff().abs().fillna(w.abs()) * fee_of(asset)
|
| 58 |
+
g = w.shift(1).fillna(0.0) * r - cost # ๐ ํฌ์ง์
์ ๋ค์ ๊ตฌ๊ฐ์ ์ ์ฉ๋๋ค
|
| 59 |
+
return (1 + g).cumprod(), g
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def total_return(positions, prices, asset=None):
|
| 63 |
+
eq, _ = equity(positions, prices, asset)
|
| 64 |
+
return float(eq.iloc[-1] - 1.0)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def turnover(positions, prices):
|
| 68 |
+
"""ํฌ์ง์
๋ณ๊ฒฝ ์ด๋. ๋ฌด์ฐจ๋ณ ์ ์ถ ํ์ง์ ํ๋ฉด ํ์์ ์ด๋ค."""
|
| 69 |
+
pos = pd.Series(positions, dtype=float).sort_index().map(clip)
|
| 70 |
+
px = pd.Series(prices, dtype=float).sort_index()
|
| 71 |
+
w = pos.reindex(px.index, method="ffill").fillna(0.0)
|
| 72 |
+
return float(w.diff().abs().fillna(w.abs()).sum())
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def fee_paid(positions, prices, asset=None):
|
| 76 |
+
return turnover(positions, prices) * fee_of(asset)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def percentile(ret, refv):
|
| 80 |
+
"""๊ทธ ์ข
๋ชฉ์ '์ค๋ ฅ0 ๋ถํฌ'์์ ๋ด ์์ต๋ฅ ์ ๋ฐฑ๋ถ์.
|
| 81 |
+
|
| 82 |
+
refv ๋ reference.live(๊ฐ๊ฒฉ, ์์๋ฃ, ๋
ธ์ถ) ์ด ๋ง๋ ์ ๋ ฌ๋ ๋ฐฐ์ด์ด๋ค.
|
| 83 |
+
์์ฆ ์ค์ ๊ฒฝ๋ก๋ก ๋งค์ผ ๋ค์ ๋ง๋ ๋ค โ ๊ณผ๊ฑฐ ๋ถํธ์คํธ๋ฉ์ผ๋ก ๊ณ ์ ํ๋ฉด
|
| 84 |
+
์์ฆ ์ค ๊ทธ ์ข
๋ชฉ์ด ์ค๋ฅผ ๋ ์ฐธ๊ฐ์ ์ ์์ด ๋ถํ๋ ค์ง๋ค.
|
| 85 |
+
"""
|
| 86 |
+
return bisect.bisect_left(refv, ret) / len(refv)
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def score(ret, refv):
|
| 90 |
+
"""์ด์ผ๋ก ์ด ์ฑ์ ์ด ๋์ฌ ํ๋ฅ ์ ์ญ์ ์ง์. 2.0์ด๋ฉด 1/100, 3.0์ด๋ฉด 1/1,000.
|
| 91 |
+
|
| 92 |
+
๐ ์์๋ **์ข
๋ชฉ ์์์๋ง** ๋งค๊ธด๋ค. ์๊ธ์ด ์ข
๋ชฉ๋ณ์ด๋ผ ์ข
๋ชฉ ๊ฐ ๋น๊ต๋ฅผ ํ์ง ์๋๋ค.
|
| 93 |
+
์ข
๋ชฉ ๊ฐ ๋น๊ต๋ ์ฌ์ฏ ๋ฒ ์๋ํด๋ ํธ์ค์ด ์กํ์ง ์์๋ค
|
| 94 |
+
(4.7 -> 99.6 -> 3.7 -> 6.0 -> 4.5๋ฐฐ). ๋ถํฌ ๊ผฌ๋ฆฌ ๋ชจ์์ด ์ข
๋ชฉ๋ง๋ค ๋ค๋ฅด๊ณ ,
|
| 95 |
+
์ฐ์น์๋ ์ต๋๊ฐ์ด๋ผ ๊ผฌ๋ฆฌ๊ฐ ๊ฒฐ์ ํ๋ค. ์ข
๋ชฉ๋ณ ์์์ด ๊ทธ ๋น๊ต๋ฅผ ์์ค๋ค.
|
| 96 |
+
|
| 97 |
+
์ข
๋ชฉ ์์์๋ ์ ๊ต์ ๋๋ค โ ์ค๋ ฅ0 ์ฐธ๊ฐ์์ ๋ฐฑ๋ถ์ ์ค์์ด 0.47~0.54์๋ค.
|
| 98 |
+
"""
|
| 99 |
+
p = min(percentile(ret, refv), 1 - 1e-6)
|
| 100 |
+
return -math.log10(max(1.0 - p, 1e-6))
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def valid_ratio(positions, prices):
|
| 104 |
+
"""ํฌ์ง์
์ด ์ ํจํ๋ ๊ตฌ๊ฐ ๋น์จ. ์ฐธ์ฌ ์๊ฑด ํ์ ์ ์ด๋ค."""
|
| 105 |
+
pos = pd.Series(positions, dtype=float).sort_index()
|
| 106 |
+
if not len(pos):
|
| 107 |
+
return 0.0
|
| 108 |
+
px = pd.Series(prices, dtype=float).sort_index()
|
| 109 |
+
w = pos.reindex(px.index, method="ffill")
|
| 110 |
+
return float(w.notna().mean())
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# โโโโโโโโโโโโโโโโโโโโโโโ ์ฑ์ ๊ธฐ ์์ฒด ๊ฒ์ฆ โโโโโโโโโโโโโโโโโโโโโโโ
|
| 114 |
+
def selftest(verbose=True):
|
| 115 |
+
"""๋ซํ ํํ๋ก ๋ต์ ์๋ ๊ฒฝ์ฐ๋ค. ํ๋๋ผ๋ ํ๋ฆฌ๋ฉด ์ฑ์ ์ ์ ๋ขฐํ ์ ์๋ค."""
|
| 116 |
+
out = []
|
| 117 |
+
idx = pd.date_range("2026-01-01", periods=101, freq="h")
|
| 118 |
+
|
| 119 |
+
def chk(name, got, want, tol=1e-9):
|
| 120 |
+
ok = abs(got - want) <= tol
|
| 121 |
+
out.append((name, got, want, ok))
|
| 122 |
+
return ok
|
| 123 |
+
|
| 124 |
+
# โ ํฌ์ง์
0์ด๋ฉด ์์ต๋ 0 โ ๊ฐ๊ฒฉ์ด ์ด๋ป๊ฒ ์์ง์ด๋
|
| 125 |
+
px = pd.Series(100 * np.cumprod(1 + np.random.default_rng(0).normal(0, .01, 101)), index=idx)
|
| 126 |
+
chk("ํฌ์ง์
0 โ ์์ต 0", total_return({idx[0]: 0.0}, px), 0.0)
|
| 127 |
+
|
| 128 |
+
# โก ํฌ์ง์
+1 ยท ๋ฌด๋น์ฉ์ด๋ฉด ๋จ์๋ณด์ ์ ์ ํํ ๊ฐ์์ผ
|
| 129 |
+
global FEE
|
| 130 |
+
keep = FEE
|
| 131 |
+
FEE = 0.0
|
| 132 |
+
chk("ํฌ์ง์
+1 = ๋จ์๋ณด์ ", total_return({idx[0]: 1.0}, px),
|
| 133 |
+
float(px.iloc[-1] / px.iloc[0] - 1), 1e-9)
|
| 134 |
+
# โข ํฌ์ง์
-1 ์ ๋ถํธ๊ฐ ๋ค์งํ ๋ณต๋ฆฌ (๋จ์ -x ๊ฐ ์๋๋ค)
|
| 135 |
+
w = -1.0
|
| 136 |
+
want = float(np.prod(1 + w * px.pct_change().fillna(0).values[1:]) - 1)
|
| 137 |
+
chk("ํฌ์ง์
-1 = ์ญ๋ฐฉํฅ ๋ณต๋ฆฌ", total_return({idx[0]: -1.0}, px), want, 1e-9)
|
| 138 |
+
FEE = keep
|
| 139 |
+
|
| 140 |
+
# โฃ ๋ ๋ฒ๋ฆฌ์ง ์ํ โ 2.0์ ๋ด๋ 1.0์ผ๋ก ์๋ ค์ผ
|
| 141 |
+
FEE = 0.0
|
| 142 |
+
chk("ํฌ์ง์
2.0 โ 1.0์ผ๋ก ์ ๋จ",
|
| 143 |
+
total_return({idx[0]: 2.0}, px), total_return({idx[0]: 1.0}, px), 1e-12)
|
| 144 |
+
FEE = keep
|
| 145 |
+
|
| 146 |
+
# โค ๋ฏธ๋๋ฅผ ๋ณด์ง ์๋๊ฐ โ ๊ฐ๊ฒฉ์ด ์ค๋ฅด๊ธฐ ์ง์ ์ ํฌ์ง์
์ ๋ฃ์ด๋
|
| 147 |
+
# ๊ทธ ๊ตฌ๊ฐ ์์ต์ ๋ชป ๋จน๋๋ค (ํฌ์ง์
์ ๋ค์ ๊ตฌ๊ฐ๋ถํฐ ์ ์ฉ)
|
| 148 |
+
p2 = pd.Series([100.0] * 50 + [200.0] * 51, index=idx)
|
| 149 |
+
FEE = 0.0
|
| 150 |
+
late = total_return({idx[50]: 1.0}, p2) # ์ ํ๊ฐ ์ผ์ด๋ ๋ด์ ์ง์
|
| 151 |
+
early = total_return({idx[49]: 1.0}, p2) # ํ ๋ด ๋จผ์ ์ง์
|
| 152 |
+
ok = abs(late) < 1e-9 and early > 0.9
|
| 153 |
+
out.append(("๋ฏธ๋ ์ฐจ๋จ (๋ฆ๊ฒ ๋ค์ด๊ฐ๋ฉด ๋ชป ๋จน์)", late, 0.0, ok))
|
| 154 |
+
FEE = keep
|
| 155 |
+
|
| 156 |
+
# โฅ ๋น์ฉ์ด ์ค์ ๋ก ๋น ์ง๋๊ฐ
|
| 157 |
+
a = total_return({idx[0]: 1.0}, px)
|
| 158 |
+
FEE = 0.01
|
| 159 |
+
b = total_return({idx[0]: 1.0}, px)
|
| 160 |
+
FEE = keep
|
| 161 |
+
out.append(("๋น์ฉ์ด ์์ต์ ๋ฎ์ถค", b, a, b < a))
|
| 162 |
+
|
| 163 |
+
# โฆ ๋ฐฑ๋ถ์ยท์ ์๊ฐ ๋จ์กฐ์ธ๊ฐ โ ์ธ๊ณต ๊ธฐ์ค๋ถํฌ๋ก ํ์ธ
|
| 164 |
+
fake = np.sort(np.random.default_rng(1).normal(0, .3, 4000))
|
| 165 |
+
out.append(("์ ์ ๋จ์กฐ ์ฆ๊ฐ", score(0.6, fake), score(0.1, fake),
|
| 166 |
+
score(0.6, fake) > score(0.1, fake)))
|
| 167 |
+
med = float(np.median(fake))
|
| 168 |
+
out.append(("์ค์๊ฐ ๊ทผ์ฒ ์ ์ ~0.3", score(med, fake), 0.3,
|
| 169 |
+
abs(score(med, fake) - 0.30) < 0.05))
|
| 170 |
+
|
| 171 |
+
if verbose:
|
| 172 |
+
print("=" * 72)
|
| 173 |
+
print("์ฑ์ ๊ธฐ ์์ฒด ๊ฒ์ฆ")
|
| 174 |
+
print("=" * 72)
|
| 175 |
+
for n, g, w, ok in out:
|
| 176 |
+
print(" %-32s %14.8f %s" % (n, g, "โ
" if ok else "๐ด ์คํจ (๊ธฐ๋ %.8f)" % w))
|
| 177 |
+
bad = [x for x in out if not x[3]]
|
| 178 |
+
print("\n %d/%d ํต๊ณผ%s" % (len(out) - len(bad), len(out),
|
| 179 |
+
"" if not bad else " ๐ด ์คํจ %d๊ฑด โ ์ฑ์ ์ค๋จ" % len(bad)))
|
| 180 |
+
return all(x[3] for x in out)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
if __name__ == "__main__":
|
| 184 |
+
ok = selftest()
|
| 185 |
+
raise SystemExit(0 if ok else 1)
|
seasons.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""์์ฆ ๋ฑ๋ก๋ถ. ์์ฆ๋ง๋ค ๋ฌ๋ผ์ง๋ ๊ฒ์ ์ ๋ถ ์ฌ๊ธฐ ์๊ณ ๋ค๋ฅธ ๊ณณ์๋ ์๋ค.
|
| 3 |
+
|
| 4 |
+
ODC์ ๊ฐ์ ์์น์ ๋ฐ๋ฅธ๋ค. ์์ฆ๋ณ ๊ท์น์ ๊ณต์ ํ์ง ์๊ณ ์์ฆ๋ง๋ค ๋ํ์ดํด ์ ๋๋ค.
|
| 5 |
+
์์ฆ 2์๋ ์์ฆ 1์์ ๋ฐ๊ฒฌํ ๊ฒฐํจ์ ๊ต์ ์ด ๋ค์ด๊ฐ๋๋ฐ, ๊ทธ ๊ต์ ์ด ์์ฆ 1์
|
| 6 |
+
๋ฟ์์๋ ์ ๋๊ธฐ ๋๋ฌธ์ด๋ค. ์์ฆ 1 ์ฐธ๊ฐ์๋ ์ด๋ฏธ ๊ณต๊ฐ๋ ๊ท์น ์๋ ์ฑ์ ๋๋ค.
|
| 7 |
+
|
| 8 |
+
์์ฆ 1๊ณผ 2๋ ๊ฒน์ณ ๋์๊ฐ๋ค. ๊ทธ๋์ '์ง๊ธ ์์ฆ'์ ์์๊ฐ ์๋๋ผ ์กฐํ๋ค.
|
| 9 |
+
|
| 10 |
+
์ ๊ทํ ๊ณ์(์ด์ ํ๊ณ์ )์ ๊ทผ๊ฑฐ ์์น๋ calibrate.py๊ฐ ์ฐ์ถํด luck_ceiling.json์
|
| 11 |
+
๋ฃ๋๋ค. ๊ฐ๋ง ์ ๋ฐ์ดํฐ๋ก๋ง ๊ณ์ฐํ๊ณ ์์ฆ ์ค์๋ ์ฌ๊ณ์ฐํ์ง ์๋๋ค.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
SEASONS = {
|
| 15 |
+
1: {
|
| 16 |
+
"name": "์์ฆ 1 ยท ๋ค ๊ฐ์ ์์ฅ",
|
| 17 |
+
"opens": "2026-08-24",
|
| 18 |
+
"closes": "2026-12-24",
|
| 19 |
+
# ์ฑ๊ฒฉ์ด ๊ฐ๋ฆฌ๋ ๋ค ์์ฅ. ์๋ก ๊ฑฐ์ ๋ฌด์๊ด์ด๋ผ(-0.04 ~ 0.19) ์ฐธ๊ฐ์๊ฐ ํฉ์ด์ง๋ค.
|
| 20 |
+
# ๋์ง์ฝ์ธ์ ๋นํธ์ฝ์ธ๊ณผ ์๊ด 0.763, ๋ฐ๋์ฒดETF๋ ์๋น๋์์ 0.797์ด๋ผ ๋บ๋ค.
|
| 21 |
+
# ๋ ์ข
๋ชฉ์ด ์ฌ์ค์ ํ๋๊ฐ ๋๋ฉด 4์ข
๋ชฉ์ด 3์ข
๋ชฉ์ด ๋๋ค.
|
| 22 |
+
#
|
| 23 |
+
# AI๊ฐ ๋ง๋ ๊ฒ ์๋น๋์ โ ์ธ๊ฐ์ด ์ค๋ ๋ฏฟ์ ๊ฒ ๊ธ
|
| 24 |
+
# AI๊ฐ ํ๋ ๊ฒ ๋นํธ์ฝ์ธ โ ์ธ์์ ๊ตด๋ ค์จ ๊ฒ ์ ๊ฐ
|
| 25 |
+
#
|
| 26 |
+
# ๐ด ์๋น๋์ ์ค์ ์ด ๊ฐ๋ง ์ดํ ๋ค(8/26)๋ค. ์ค์ ์ผ ๋ณ๋์ ํ์์ผ์ 2.7๋ฐฐ์ด๊ณ
|
| 27 |
+
# 87%๊ฐ ์๊ฐ ๊ฐญ์ด๋ผ ์ฅ์ค ๋์์ด ์ ๋๋ค. ๋ชจ๋์๊ฒ ๊ฐ์ ์กฐ๊ฑด์ด์ง๋ง
|
| 28 |
+
# ์ด๋ฐ ์์์ ์ด์ด ํฌ๊ฒ ์์ธ๋ค๋ ์ ์ ์ฐธ๊ฐ ์๋ด์ ๋ช
์ํ๋ค.
|
| 29 |
+
"assets": {
|
| 30 |
+
"NVDA": {"label": "์๋น๋์", "cat": "stock", "vol": 0.514,
|
| 31 |
+
"why": "AI ๊ทธ ์์ฒด. ์ด ์์ฅ์ ์์ธกํ๋ ๊ฒ์ด ์ด ๋ํ์ ์์ง",
|
| 32 |
+
"src": "NVDA", "hours": "rth",
|
| 33 |
+
"anchor": {1: 0.781, 5: 0.772, 21: 0.780}},
|
| 34 |
+
"BTC": {"label": "๋นํธ์ฝ์ธ", "cat": "coin", "vol": 0.392,
|
| 35 |
+
"why": "AI๊ฐ ํ๋ ์ ํํ. 24์๊ฐ ๊ฑฐ๋ยทํด์ฅ ์์",
|
| 36 |
+
"src": "BTC-USD", "hours": "24h",
|
| 37 |
+
"anchor": {1: 0.807, 7: 0.801, 30: 0.757}},
|
| 38 |
+
"GOLD": {"label": "๊ธ", "cat": "commodity", "vol": 0.212,
|
| 39 |
+
"why": "์ธ๋ฅ๊ฐ 5,000๋
๋ฏฟ์ด์จ ๊ฐ์น. ๋ฐฉํฅ์ด ์์ด ์ด์ผ๋ก ๋ฒ๊ธฐ ๊ฐ์ฅ ์ด๋ ต๋ค",
|
| 40 |
+
# ๐ด ์ ๋ฌผ(GC=F)์ด ์๋๋ผ ETF๋ฅผ ์ด๋ค. GC=F๋ ์๋ง ์ด์์ ํ๊ฐ
|
| 41 |
+
# ์ ์์ 2.4๋ฐฐ๋ก, ๋กค ์์ ๊ฐ๊ฒฉ์ฐจ๊ฐ ์์ต๋ฅ ์ ์์ฌ ๋ ๋ค.
|
| 42 |
+
"src": "GLD", "hours": "rth",
|
| 43 |
+
"anchor": {1: 0.748, 5: 0.762, 21: 0.744}},
|
| 44 |
+
"OIL": {"label": "์ ๊ฐ", "cat": "commodity", "vol": 0.369,
|
| 45 |
+
"why": "์ธ์์ ๊ตด๋ ค์จ ์ฐ์
์ ์ฌ์ฅ. AI๋ ์ ์์ฅ๋ ์์ธกํ๋๊ฐ",
|
| 46 |
+
# ๐ด CL=F๋ 3๋
์์ต์ด +10%์ธ๋ฐ USO๋ +89%๋ค. 79%p ๊ดด๋ฆฌ๋
|
| 47 |
+
# ๊ทผ์๋ฌผ์ ์ด์ด๋ถ์ผ ๋ ์๊ธฐ๋ ๋กค ์๊ณก์ด๋ค. ์ฐธ๊ฐ์๊ฐ ์ค์ ๋ก
|
| 48 |
+
# ๋ผ ์ ์๋ ์์ต์ด๋ผ ETF๋ฅผ ์ด๋ค.
|
| 49 |
+
"src": "USO", "hours": "rth",
|
| 50 |
+
"anchor": {1: 0.782, 5: 0.780, 21: 0.804}},
|
| 51 |
+
},
|
| 52 |
+
"prize_usd_per_asset": 500,
|
| 53 |
+
"prize_total_usd": 2000,
|
| 54 |
+
"prize_rule": "์ข
๋ชฉ๋ณ ์ต์ข
์์ต๋ฅ 1์์๊ฒ ๊ฐ 500๋ฌ๋ฌ. ์ด 2,000๋ฌ๋ฌ",
|
| 55 |
+
# ์ข
๋ชฉ ๊ฐ ์์ต๋ฅ ์ ์ง์ ๋น๊ตํ๋ ๋ฐฉ์์ ํ๊ธฐํ๋ค. ๋ค ๋ฒ ๊ณ ์ณ๋ ํธ์ค์ด
|
| 56 |
+
# ์กํ์ง ์์๋ค(4.7๋ฐฐ -> 99.6๋ฐฐ -> 3.7๋ฐฐ -> 6.0๋ฐฐ). ๋ถํฌ ๋ชจ์์ด ์ข
๋ชฉ๋ง๋ค
|
| 57 |
+
# ๊ทผ๋ณธ์ ์ผ๋ก ๋ฌ๋ผ, ์ด๋ ํ ํต๊ณ๋์ ๋ง์ถ๋ฉด ๋ค๋ฅธ ์ง์ ์ด ์ด๊ธ๋๋ค.
|
| 58 |
+
# ์ข
๋ชฉ๋ณ ์์์ ๊ทธ ๋น๊ต ์์ฒด๋ฅผ ์์ค๋ค. ํตํฉ ์์๋ ์ฐธ๊ณ ํ์๋ก๋ง ๋๋ค.
|
| 59 |
+
"leverage": 1.0,
|
| 60 |
+
"min_participation": 0.70,
|
| 61 |
+
"update": "hourly",
|
| 62 |
+
"ledger": "s1/",
|
| 63 |
+
},
|
| 64 |
+
2: {
|
| 65 |
+
# ์๊ณ ๋ง ํ๋ค. ์ข
๋ชฉ์ ์์ฆ 1์ด ๋๋ ๋ค ํ์ ํ๋ค.
|
| 66 |
+
# ์์ฆ 1์์ ์ด๋ค ์ฑ๊ฒฉ์ ์์ฅ์ ์ฐธ๊ฐ์๊ฐ ๋ชจ์ด๋์ง ๋ณด๊ณ ์ ํ๋ ํธ์ด ๋ซ๋ค.
|
| 67 |
+
"name": "์์ฆ 2 ยท ์๊ณ ",
|
| 68 |
+
"opens": None,
|
| 69 |
+
"closes": None,
|
| 70 |
+
"assets": {},
|
| 71 |
+
"status": "announced",
|
| 72 |
+
"teaser": "์ ๋ฌผ๊ณผ ์์์ฌ, ๊ทธ๋ฆฌ๊ณ ์ธํ",
|
| 73 |
+
# ์ค์ธกํด๋ ํ๋ณด๋ค. ํ์ ์๋.
|
| 74 |
+
"candidates": {
|
| 75 |
+
"COPPER": {"label": "๊ตฌ๋ฆฌ", "vol": 0.291,
|
| 76 |
+
"note": "AI ๋ฐ์ดํฐ์ผํฐ ์์ฌ๋ ์์ผ๋ ๋ฐ์ดํฐ๋ก๋ ์ฝํ๋ค. "
|
| 77 |
+
"๋ฐ๋์ฒด ์๊ด 0.28~0.36 < ๊ธ ์๊ด 0.40. "
|
| 78 |
+
"์์ฌ๋ก ๋ด์ธ์ฐ๋ฉด ์ฐธ๊ฐ์๊ฐ ์๊ดํ๋ก ๋ฐ๋ฐํ ์ ์๋ค"},
|
| 79 |
+
"OIL": {"label": "์ ๊ฐ", "vol": 0.405, "note": "๋กค์ค๋ฒ ์ฒ๋ฆฌ ํ์"},
|
| 80 |
+
"USDKRW": {"label": "๋ฌ๋ฌ/์", "vol": 0.097, "note": "์ ๋ณ๋"},
|
| 81 |
+
"USDJPY": {"label": "๋ฌ๋ฌ/์", "vol": 0.093, "note": "์ ๋ณ๋"},
|
| 82 |
+
"DXY": {"label": "๋ฌ๋ฌ์ง์", "vol": 0.064,
|
| 83 |
+
"note": "๋ณ๋์ฑ์ด ๊ธ์ 1/3. ์์๊ฐ ์์์ ์์ ๊ฐ๋ฆด ์ํ"},
|
| 84 |
+
},
|
| 85 |
+
"prize_usd_per_asset": 500,
|
| 86 |
+
"leverage": 1.0,
|
| 87 |
+
"min_participation": 0.70,
|
| 88 |
+
"update": "hourly",
|
| 89 |
+
"ledger": "s2/",
|
| 90 |
+
# ๐ด ์ ๋ฌผยท์ธํ์ ๋กค์ค๋ฒ๊ฐ ์๋ค. ๊ทผ์๋ฌผ ๋ง๊ธฐ ์ ์ฐจ์๋ฌผ๋ก ๊ฐ์ํ๋ฉฐ ๊ฐ๊ฒฉ์ด
|
| 91 |
+
# ์ ํํ๋๋ฐ, ์ด๊ฑธ ์์ต๋ฅ ์ ๊ทธ๋๋ก ๋ฃ์ผ๋ฉด ์๋ ์์ต์ด ์๊ธด๋ค.
|
| 92 |
+
# ์ฐ๊ฒฐ์ ๋ฌผ ๊ตฌ์ฑ ๋ฐฉ์์ ํ์ ํ๊ธฐ ์ ์๋ ์์ฆ 2๋ฅผ ์ด์ง ์๋๋ค.
|
| 93 |
+
"rollover": None,
|
| 94 |
+
},
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# ์ฐธ๊ณ ์ฉ โ ํ๋ฉด์ '์ด์ ํ๊ณ์ '์ ๊ทธ์ ๋ ์ด๋ค. ์์ ๊ณ์ฐ์๋ ์ฐ์ง ์๋๋ค.
|
| 98 |
+
LUCK_CEILING = {
|
| 99 |
+
"SOL": 0.8870, "ETH": 0.6227, "BTC": 0.5821, "TSLA": 0.5264,
|
| 100 |
+
"NVDA": 0.4399, "OIL": 0.3546, "AAPL": 0.2271, "GOLD": 0.1497,
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
# ๋ฆฌ๋๋ณด๋์ ์ฐธ๊ฐ์์ ํจ๊ป ๋๋ ๊ธฐ์ค์ . ODC๊ฐ ์น์ธ์ฝ์ ๊ฐ์ ํ์ ๋ฃ๋ ๊ฒ๊ณผ ๊ฐ๋ค.
|
| 104 |
+
# ์ฒ๋๊ฐ ์๋ ๋ฆฌ๋๋ณด๋๋ ์์๋ฅผ ์ฝ์ ์ ์๋ค.
|
| 105 |
+
#
|
| 106 |
+
# ๐ด ์ฃผ์ต์ธก ๋ชจ๋ธ์ ๋ฃ์ง ์๋๋ค. ์
๋ค ๊ณต๊ฐ๋ ๊ต๊ณผ์ ๊ธฐ๋ฒ์ด๋ค.
|
| 107 |
+
# ์ฃผ์ต์๊ฐ ์๊ธฐ ๋ชจ๋ธ์ ์ฐธ๊ฐ์ ํ์ ์ฌ๋ฆฌ๋ฉด "๋๊ฐ ๋ ๋์ ๋ชจ๋ธ์ ๋ง๋๋๊ฐ"๋ผ๋
|
| 108 |
+
# ๋ช
๋ถ์ด ํ๋ ค์ง๋ค. ๊ธฐ์ค์ ์ ์ฒ๋์ด์ง ๊ฒฝ์์๊ฐ ์๋๋ค.
|
| 109 |
+
ANCHORS = {
|
| 110 |
+
"BUYHOLD": "๋จ์ ๋ณด์ โ ํฌ์ง์
ํญ์ +1. ์ด๊ฑธ ๋ชป ์ด๊ธฐ๋ฉด ์๋ฏธ๊ฐ ์๋ค",
|
| 111 |
+
"VOLTARGET": "๋ณ๋์ฑ ํ๊ฒํ
โ ์ง์๊ฐ์ค ๋ณ๋์ฑ์ผ๋ก ํฌ๊ธฐ๋ง ์กฐ์ . ๋ฐฉํฅ์ ๋กฑ ๊ณ ์ ",
|
| 112 |
+
"RANDOM": "๋ฌด์์ โ ๋งค์ผ -1/0/+1 ์์ ์ ํ. ๋ฐ๋ฅ",
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def season_of(asset):
|
| 117 |
+
for sid, s in SEASONS.items():
|
| 118 |
+
if asset in s.get("assets", {}):
|
| 119 |
+
return sid
|
| 120 |
+
return None
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# ๋ฐฑ๋ถ์ ์ฑ์ ๊ธฐ์คํ โ calibrate.py ์ฐ์ถ. ์ข
๋ชฉ๋ณ '์ค๋ ฅ0 ์ฐธ๊ฐ์' ์์ต๋ฅ ๋ถํฌ.
|
| 124 |
+
_REF = None
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def _ref():
|
| 128 |
+
global _REF
|
| 129 |
+
if _REF is None:
|
| 130 |
+
import json, os
|
| 131 |
+
_REF = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
| 132 |
+
"reference_dist.json"), encoding="utf-8"))
|
| 133 |
+
return _REF
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def percentile(ret, asset):
|
| 137 |
+
"""๊ทธ ์ข
๋ชฉ์ ์ค๋ ฅ0 ๋ถํฌ์์ ๋ด ์์ต๋ฅ ์ด ์ด๋ ๋ฐฑ๋ถ์์ธ๊ฐ."""
|
| 138 |
+
import bisect
|
| 139 |
+
R = _ref()
|
| 140 |
+
tbl = R["ref"].get(asset)
|
| 141 |
+
if not tbl:
|
| 142 |
+
return None
|
| 143 |
+
return bisect.bisect_left(tbl, ret) / len(tbl)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def score(ret, asset):
|
| 147 |
+
"""์์ ์ ์ = -log10(1 - ๋ฐฑ๋ถ์) = '์ด์ผ๋ก ์ด ์ฑ์ ์ด ๋์ฌ ํ๋ฅ '์ ์ญ์ ์ง์.
|
| 148 |
+
|
| 149 |
+
2.0 ์ด๋ฉด ์ด์ผ๋ก ์ด๋ด ํ๋ฅ 1/100, 3.0 ์ด๋ฉด 1/1,000.
|
| 150 |
+
|
| 151 |
+
๐ด ์์ต๋ฅ ์ ์ข
๋ชฉ๋ณ ๊ณ์๋ก ๋๋๋ ๋ฐฉ์(= 95๋ถ์ ๋น์จ)์ ์ฐ์ง ์๋๋ค.
|
| 152 |
+
๊ทธ ์ง์ ๋ง ๋ง๊ณ ๊ผฌ๋ฆฌ๋ ์ ๋ง์, ์ฐ์น ํ๋ฅ ์ด SOL 22.3% vs GOLD 4.8%๋ก
|
| 153 |
+
4.7๋ฐฐ ํธ์ค๋๋ค. ์ฐ์น์๋ ์ต๋๊ฐ์ด๊ณ ์ต๋๊ฐ์ ๊ผฌ๋ฆฌ ๋ชจ์์ด ์ ํ๋ค.
|
| 154 |
+
๋ถํฌ ์ ์ฒด๋ฅผ ๋ง์ถ๋ ๋ฐฑ๋ถ์๋ก ๋ฐ๊พธ๋ ํธ์ค์ด 1.6๋ฐฐ(๋
ธ์ด์ฆ ์์ค)๋ก ๋ด๋ ค๊ฐ๋ค.
|
| 155 |
+
|
| 156 |
+
ํ๋ฉด์๋ ์์ต๋ฅ ์ ๊ทธ๋๋ก ๋ณด์ฌ์ค๋ค. ์์๋ง ์ด ์ ์๋ก ๋งค๊ธด๋ค.
|
| 157 |
+
"""
|
| 158 |
+
import math
|
| 159 |
+
p = percentile(ret, asset)
|
| 160 |
+
if p is None:
|
| 161 |
+
return None
|
| 162 |
+
return -math.log10(max(1.0 - min(p, 1 - 1e-6), 1e-6))
|
| 163 |
+
|
| 164 |
+
# โโ ์ฐธ๊ฐ ์ ํ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 165 |
+
# ์๊ธฐ ์ ๊ณ ๋ค. ๊ฒ์ฆํ ๋ฐฉ๋ฒ์ด ์๋ค. ๊ทธ๋์ ์๊ธ์ ํตํฉ ์์์๋ง ๊ฑธ๊ณ
|
| 166 |
+
# ์ ํ๋ณ ์์๋ ๋ช
์๋ก๋ง ๋๋ค. ์์ด๋ ์ฌ๋์ด ๋์๋ ๋ํ๊ฐ ๋ฌด๋์ง์ง ์๊ฒ.
|
| 167 |
+
# ๋ฑ๋ก์ ๊น๋ค๋กญ๊ฒ ๋ง๋ค๋ฉด ์ฌ๋์ด ์์ ์ค์ง ์๋๋ฐ, ๊ทธ๊ฒ ๋ ํฐ ์์ค์ด๋ค.
|
| 168 |
+
TYPES = {
|
| 169 |
+
"human": {"badge": "๐ง", "label": "์ฌ๋", "desc": "์ง์ ํ๋จํด ์ ์ถ"},
|
| 170 |
+
"expert": {"badge": "๐", "label": "์ ๋ฌธ๊ฐยท๊ธฐ๊ด", "desc": "์์ฒด ์๊ณ ๋ฆฌ์ฆ"},
|
| 171 |
+
"agent": {"badge": "๐ค", "label": "AI ์์ด์ ํธ", "desc": "์๋ ์ ์ถ"},
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
# ์ ํ๋ณ ์ฐธ์ฌ ์๊ฑด. ์ฌ๋์ ๋งค์๊ฐ ๊ฐฑ์ ํ ์ ์์ผ๋ ์ ์ ๋น๋๋ก ์ํํ๋ค.
|
| 175 |
+
# ํฌ์ง์
์ ๊ทธ๋๋ก ๋๋ ๊ฒ๋ ์ ๋ต์ด๋ค โ NVDA์์ ๋จ์๋ณด์ ๊ฐ ๋ณ๋์ฑ ํ๊ฒํ
์ ์ด๊ฒผ๋ค.
|
| 176 |
+
PARTICIPATION = {
|
| 177 |
+
"human": {"valid_ratio": 0.70, "min_visits_per_week": 1},
|
| 178 |
+
"expert": {"valid_ratio": 0.70, "min_visits_per_week": 0},
|
| 179 |
+
"agent": {"valid_ratio": 0.70, "min_visits_per_week": 0},
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# ์๊ธ์ ํตํฉ 1๋ช
. ์ ํ๋ณ 1์๋ ๋ช
์ ๋ถ๋ฌธ์ด๋ค.
|
| 183 |
+
PRIZE_SCOPE = "overall"
|
| 184 |
+
HONOR_SCOPES = ["human", "expert", "agent"]
|
| 185 |
+
|
| 186 |
+
MANIFESTO = {
|
| 187 |
+
"title": "AI๋ ์์ฅ์ ์ด๊ธธ ์ ์๋๊ฐ",
|
| 188 |
+
"sub": "122์ผ๊ฐ์ ๊ณต๊ฐ ์คํ",
|
| 189 |
+
"cta_agent": "๋น์ ์ ์์ด์ ํธ๏ฟฝ๏ฟฝ๏ฟฝ ๋ง ์์ ์ฌ๋ฆฌ์ญ์์ค",
|
| 190 |
+
"cta_human": "์ง์ ์ฐธ๊ฐํ๊ธฐ",
|
| 191 |
+
"why": [
|
| 192 |
+
("์ฑ๊ณต๋ง ๋ณด๋๋ฉ๋๋ค",
|
| 193 |
+
"์์ต ๋ธ ์ฌ๋๋ง ์ธํฐ๋ทฐํฉ๋๋ค. ์ง ์ฌ๋ 999๋ช
์ ๊ธฐ๋ก๋ ๋จ์ง ์์ต๋๋ค."),
|
| 194 |
+
("๋ฐฑํ
์คํธ๋ ์กฐ์๋ฉ๋๋ค",
|
| 195 |
+
"๊ณผ๊ฑฐ ๋ฐ์ดํฐ๋ก ์ฐ 200%๋ ๋๊ตฌ๋ ๋ง๋ญ๋๋ค. ๋ฏธ๋๋ก๋ ์ ๋ฉ๋๋ค."),
|
| 196 |
+
("์ด๊ณผ ์ค๋ ฅ์ด ์ ๋๋ฉ๋๋ค",
|
| 197 |
+
"ํ ๋ฌ์ 30% ๋ธ ์ฌ๋์ด ์ค๋ ฅ์ธ์ง ์ด์ธ์ง ํ๋ณํ ๋ฐฉ๋ฒ์ด ์์์ต๋๋ค."),
|
| 198 |
+
],
|
| 199 |
+
"how": [
|
| 200 |
+
("์ด์ ํฌ๊ธฐ๋ฅผ ์ซ์๋ก ์๋๋ค",
|
| 201 |
+
"์ค๋ ฅ 0์ธ ์ฐธ๊ฐ์๊ฐ 122์ผ์ ์ด๋๊น์ง ๊ฐ๋์ง 20,000ํ ์๋ฎฌ๋ ์ด์
์ผ๋ก ์์ต๋๋ค. "
|
| 202 |
+
"์์๋ฃ๊น์ง ๋ฌผ๋ฆฐ ๊ฐ์ผ๋ก ๋นํธ์ฝ์ธ +86.6%, ๊ธ +9.2%. "
|
| 203 |
+
"์ด ์ ์ ๋์ง ๋ชปํ ์์ต์ ์ค๋ ฅ์ ์ฆ๊ฑฐ๊ฐ ๋์ง ์์ต๋๋ค."),
|
| 204 |
+
("์กฐ์์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค",
|
| 205 |
+
"์ ๋ต์ด ๋ฏธ๋์ ์์ต๋๋ค. ์ ์ถํ ๋ค์ ์ธ์์ด ์ ๋ต์ ๋ง๋ญ๋๋ค."),
|
| 206 |
+
("๊ฐ์ ์กฐ๊ฑด์
๋๋ค",
|
| 207 |
+
"๋ ๋ฒ๋ฆฌ์ง 1 ๊ณ ์ , ๊ฐ์ ๋ฐ์ดํฐ, ๊ฐ์ ์๊ฐ. ํฌ๊ฒ ๊ฑธ์ด ์ด๊ธฐ๋ ๊ธธ์ ๋ง์์ต๋๋ค."),
|
| 208 |
+
],
|
| 209 |
+
# ์ฃผ์ต์ธก์ ์์์ ๋ค์ด๊ฐ์ง ์๋๋ค. ๋์ ๊ธฐ์ค์ ์
์ ๊ฐ์ ํ์ ์ฌ๋ ค
|
| 210 |
+
# ์ฐธ๊ฐ์๊ฐ ์๊ธฐ ์ฑ์ ์ ๋ฌด์์ ๊ฒฌ์ค์ผ ํ๋์ง ๋ณผ ์ ์๊ฒ ํ๋ค.
|
| 211 |
+
"our_stake": "์ฃผ์ต์ธก์ ์์์ ์ฐธ๊ฐํ์ง ์์ต๋๋ค. ๋์ ๋จ์ ๋ณด์ ยท๋ณ๋์ฑ ํ๊ฒํ
ยท๋ฌด์์ "
|
| 212 |
+
"์ธ ๊ธฐ์ค์ ์ ๊ฐ์ ํ์ ์ฌ๋ ค, ์ฑ์ ์ ๋ฌด์์ ๊ฒฌ์ค์ง ๋ณด์ด๊ฒ ํฉ๋๋ค.",
|
| 213 |
+
}
|