SeaWolf-AI commited on
Commit
48097ce
ยท
verified ยท
1 Parent(s): 64c4d74

FINCHAL season 1 server

Browse files
Files changed (15) hide show
  1. .claude_launch +0 -0
  2. Dockerfile +12 -0
  3. README.md +34 -5
  4. announce_ceiling.json +38 -0
  5. app/anchors.py +84 -0
  6. app/ledger.py +129 -0
  7. app/main.py +349 -0
  8. app/mcp.py +133 -0
  9. app/static/index.html +436 -0
  10. luck_ceiling.json +30 -0
  11. reference.py +124 -0
  12. reference_dist.json +0 -0
  13. requirements.txt +5 -0
  14. scoring.py +185 -0
  15. 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: Finchal
3
- emoji: ๐Ÿจ
4
- colorFrom: gray
5
  colorTo: green
6
  sdk: docker
7
- pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }