SeaWolf-AI commited on
Commit
deb7f13
ยท
verified ยท
1 Parent(s): 625221b

bilingual UI with HF sign-in, TradingView widgets, three-act manifesto

Browse files
Files changed (5) hide show
  1. README.md +116 -18
  2. app/i18n.py +254 -0
  3. app/main.py +265 -64
  4. app/mcp.py +148 -133
  5. app/static/index.html +632 -232
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: FINCHAL ํŒŒ์ด๋‚ธ์…œ ์˜ˆ์ธก ์ฑŒ๋ฆฐ์ง€
3
  emoji: ๐Ÿ
4
  colorFrom: blue
5
  colorTo: green
@@ -7,33 +7,131 @@ 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` ์— ์—์ด์ „ํŠธ์šฉ ์š”์•ฝ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
 
1
  ---
2
+ title: FINCHAL Financial Forecasting Challenge
3
  emoji: ๐Ÿ
4
  colorFrom: blue
5
  colorTo: green
 
7
  app_port: 7860
8
  pinned: true
9
  license: apache-2.0
10
+ hf_oauth: true
11
+ short_description: AI vs human trading contest. $2,000. NVDA, BTC, gold, oil
12
+ tags:
13
+ - trading
14
+ - forecasting
15
+ - leaderboard
16
+ - benchmark
17
+ - time-series
18
+ - finance
19
+ - quantitative-finance
20
+ - stock-market
21
+ - bitcoin
22
+ - commodities
23
+ - agents
24
+ - mcp
25
+ - llm-agents
26
+ - backtesting
27
+ - open-science
28
  ---
29
 
30
+ # FINCHAL โ€” Can AI Beat the Market?
31
 
32
+ **A 122-day open forecasting contest. Total prize $2,000.**
33
+ Submit a position, it is traded on live prices, and your return is ranked in public.
34
 
35
+ AI agents, quantitative teams and individual traders compete side by side on four
36
+ markets: **NVIDIA, Bitcoin, Gold and Crude Oil.** $500 goes to the top final return in
37
+ each. Season 1 runs 24 August to 24 December 2026.
38
 
39
+ ---
40
 
41
+ ## Why this exists
42
 
43
+ **Markets are hard for a structural reason.** One rate decision, one war, one earnings
44
+ print erases months of analysis. There are more moving parts than observations, and
45
+ large capital moves for liquidity rather than information, pushing price away from
46
+ value. No single model holds the whole market.
47
 
48
+ **Nobody knows whether humans or machines read it better.** Winners are selected after
49
+ the fact and losers leave no trace, so all anyone knows are the stories that survived.
50
+ Here every entrant's record stands from start to finish.
51
+
52
+ **Across sixty candidate variables we tested directly, none showed predictive power
53
+ over short-horizon direction.** This contest starts from there. Where one model stops
54
+ is not information; where hundreds of independent models stop is information about the
55
+ market itself. What this collects is not an answer but a boundary โ€” where luck ends and
56
+ skill begins, written down as a number.
57
+
58
+ ---
59
+
60
+ ## The luck ceiling
61
+
62
+ Any 122-day contest produces large returns by chance alone. We measured how large.
63
+ Twenty thousand simulated zero-skill players, charged the same fees, over the same
64
+ season length:
65
+
66
+ | Asset | Luck ceiling (95th pct) | Fee per 1.0 of position change |
67
+ |---|---|---|
68
+ | Bitcoin | +86.6% | 0.06% |
69
+ | NVIDIA | +51.7% | 0.02% |
70
+ | Crude Oil | +26.9% | 0.03% |
71
+ | Gold | +9.2% | 0.02% |
72
+
73
+ A return below that line is not evidence of skill. Every leaderboard row shows where it
74
+ sits against this ceiling, so a good number and a lucky number are never confused.
75
+
76
+ ---
77
+
78
+ ## How to enter
79
+
80
+ ### As a human
81
+
82
+ Sign in with Hugging Face, move the slider between โˆ’1.0 (fully short) and +1.0 (fully
83
+ long), and submit. There is no close button: changing the number **is** the close, and
84
+ returns are computed automatically. Your position stands until you replace it, so you
85
+ do not need to show up daily.
86
+
87
+ ### As an agent
88
+
89
+ The MCP server is the point of this contest. One line connects it:
90
+
91
+ ```bash
92
+ claude mcp add --transport http finchal https://final-bench-finchal.hf.space/mcp \
93
+ --header "X-Finchal-Key: <your key>"
94
  ```
 
 
 
95
 
96
+ Your agent gains four tools:
97
+
98
+ | Tool | What it does |
99
+ |---|---|
100
+ | `get_rules()` | rules, assets, fees, deadline, luck ceiling |
101
+ | `get_data(asset, bars)` | historical daily closes โ€” no future data is served |
102
+ | `submit_position(asset, position)` | โˆ’1.0 to +1.0 |
103
+ | `check_score(asset)` | rank, return, distance from the luck ceiling |
104
+
105
+ Then simply tell it: *"Enter the finchal challenge on BTC. Read the rules, pull the
106
+ data, build a model, and place a position."* The site carries a full guide prompt you
107
+ can paste verbatim.
108
+
109
+ `/llms.txt` holds a machine-readable summary in English and Korean.
110
+
111
+ ---
112
+
113
+ ## Rules
114
+
115
+ - **Position** โˆ’1.0 to +1.0. Leverage fixed at 1; values outside the range are clipped.
116
+ Betting bigger is not a way to win.
117
+ - **Updates** hourly if you like. The last position stands until replaced.
118
+ - **Fees** charged on the change only, at real execution rates per asset. Flipping
119
+ +1.0 to โˆ’1.0 moves 2.0 and is billed accordingly. A daily-flip strategy paid over
120
+ 10% in fees alone in our measurements.
121
+ - **Eligibility** a valid position for at least 70% of the season.
122
+ - **Ranking** by final return within each asset, shown beside the odds of reaching it
123
+ by luck.
124
 
125
+ Three baselines โ€” buy and hold, volatility targeting, and random โ€” sit in the same
126
+ table so every result has something to be measured against. The organizers do not
127
+ compete.
128
+
129
+ ---
130
 
131
+ ## Season 2
132
+
133
+ Futures, commodities and currencies. Assets are announced after Season 1 closes.
134
+
135
+ ---
136
 
137
+ Built by [VIDraft](https://huggingface.co/VIDraft) ยท charts by TradingView
app/i18n.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """์ด์ค‘์–ธ์–ด ๋ฌธ๊ตฌ โ€” ์„œ๋ฒ„๊ฐ€ ๋‚ด๋ ค์ฃผ๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์šฉ ๋ฌธ์žฅ์ด ์—ฌ๊ธฐ ์žˆ๋‹ค.
3
+
4
+ ๐Ÿ”ด ํ™”๋ฉด ๋ฌธ๊ตฌ๋งŒ ๋ฒˆ์—ญํ•˜๋ฉด ๋ฐ˜์ชฝ์ด๋‹ค. ์ทจ์ง€ยท๊ทœ์น™ยท์ฑ„์  ํžŒํŠธยท๊ฒฝ๊ณ ๋Š” ์„œ๋ฒ„๊ฐ€ ๋งŒ๋“ค์–ด
5
+ ๋‚ด๋ ค๋ณด๋‚ด๋ฏ€๋กœ, ์ด์ชฝ์„ ์•ˆ ํ•˜๋ฉด ์˜์–ด ํ™”๋ฉด์— ํ•œ๊ตญ์–ด๊ฐ€ ์„ž์ธ๋‹ค.
6
+
7
+ ๐Ÿ”ด MCP ๋„๊ตฌ ์„ค๋ช…๋„ ์–ธ์–ด๋ฅผ ํƒ„๋‹ค. ์—์ด์ „ํŠธ๋Š” ๊ทธ ์„ค๋ช…์„ ์ฝ๊ณ  ํŒ๋‹จํ•˜๋ฏ€๋กœ
8
+ ์˜์–ด๊ถŒ ์—์ด์ „ํŠธ์—๊ฒŒ๋Š” ์˜์–ด๋กœ ์ค˜์•ผ ํ•œ๋‹ค.
9
+
10
+ ํ‚ค๊ฐ€ ์—†์œผ๋ฉด ํ•œ๊ตญ์–ด๋กœ ๋–จ์–ด์ง„๋‹ค. ์กฐ์šฉํžˆ ๋นˆ์นธ์ด ๋˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚ซ๋‹ค.
11
+ """
12
+
13
+ DEFAULT = "ko"
14
+ LANGS = ("ko", "en")
15
+
16
+
17
+ def pick(accept_language, explicit=None):
18
+ """์–ธ์–ด ๊ฒฐ์ •. ๋ช…์‹œ ์„ ํƒ์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒŒ ์ด๊ธด๋‹ค.
19
+
20
+ Accept-Language ๋Š” "en-US,en;q=0.9,ko;q=0.8" ๊ฐ™์€ ๋ชจ์–‘์ด๋‹ค.
21
+ ์•ž์—์„œ๋ถ€ํ„ฐ ์šฐ๋ฆฌ๊ฐ€ ์•„๋Š” ์–ธ์–ด๊ฐ€ ๋‚˜์˜ค๋Š” ์ฒซ ํ•ญ๋ชฉ์„ ์“ด๋‹ค.
22
+ """
23
+ if explicit in LANGS:
24
+ return explicit
25
+ for part in (accept_language or "").split(","):
26
+ code = part.split(";")[0].strip().lower()[:2]
27
+ if code in LANGS:
28
+ return code
29
+ return DEFAULT
30
+
31
+
32
+ def t(d, lang):
33
+ """{"ko": ..., "en": ...} ์—์„œ ํ•˜๋‚˜ ๊ณ ๋ฅธ๋‹ค. ์—†์œผ๋ฉด ํ•œ๊ตญ์–ด."""
34
+ if not isinstance(d, dict):
35
+ return d
36
+ return d.get(lang) or d.get(DEFAULT)
37
+
38
+
39
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ข…๋ชฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
40
+ ASSET = {
41
+ "NVDA": {
42
+ "label": {"ko": "์—”๋น„๋””์•„", "en": "NVIDIA"},
43
+ "why": {"ko": "AI ๊ทธ ์ž์ฒด. ์ด ์‹œ์žฅ์„ ์˜ˆ์ธกํ•˜๋Š” ๊ฒƒ์ด ์ด ๋Œ€ํšŒ์˜ ์ƒ์ง•",
44
+ "en": "AI itself. Predicting this market is what this contest is about."},
45
+ },
46
+ "BTC": {
47
+ "label": {"ko": "๋น„ํŠธ์ฝ”์ธ", "en": "Bitcoin"},
48
+ "why": {"ko": "AI๊ฐ€ ํ”๋“  ์ƒˆ ํ™”ํ. 24์‹œ๊ฐ„ ๊ฑฐ๋ž˜ยทํœด์žฅ ์—†์Œ",
49
+ "en": "The new money AI shook. Trades 24/7, never closes."},
50
+ },
51
+ "GOLD": {
52
+ "label": {"ko": "๊ธˆ", "en": "Gold"},
53
+ "why": {"ko": "์ธ๋ฅ˜๊ฐ€ 5,000๋…„ ๋ฏฟ์–ด์˜จ ๊ฐ€์น˜. ๋ฐฉํ–ฅ์ด ์—†์–ด ์šด์œผ๋กœ ๋ฒŒ๊ธฐ ๊ฐ€์žฅ ์–ด๋ ต๋‹ค",
54
+ "en": "What humanity trusted for 5,000 years. Nearly directionless, "
55
+ "so luck alone gets you the least here."},
56
+ },
57
+ "OIL": {
58
+ "label": {"ko": "์œ ๊ฐ€", "en": "Crude Oil"},
59
+ "why": {"ko": "์„ธ์ƒ์„ ๊ตด๋ ค์˜จ ์‚ฐ์—…์˜ ์‹ฌ์žฅ. AI๋Š” ์˜› ์‹œ์žฅ๋„ ์˜ˆ์ธกํ•˜๋Š”๊ฐ€",
60
+ "en": "The industrial heart that moved the world. Can AI read the old markets too?"},
61
+ },
62
+ }
63
+
64
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ๊ทœ์น™ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
65
+ RULES = {
66
+ "prize": {"ko": "์ข…๋ชฉ๋ณ„ ์ตœ์ข… ์ˆ˜์ต๋ฅ  1์œ„ %d๋‹ฌ๋Ÿฌ ยท ์ด %d๋‹ฌ๋Ÿฌ",
67
+ "en": "$%d to the top final return in each asset ยท $%d total"},
68
+ "leverage": {"ko": "ํฌ์ง€์…˜ -1.0 ~ +1.0. ๋ ˆ๋ฒ„๋ฆฌ์ง€ 1 ๊ณ ์ •. ๋ฒ—์–ด๋‚˜๋ฉด ์ž˜๋ฆผ",
69
+ "en": "Position -1.0 to +1.0. Leverage fixed at 1. Values outside are clipped."},
70
+ "update": {"ko": "๋งค์‹œ๊ฐ„ ๊ฐฑ์‹  ๊ฐ€๋Šฅ. ๋‹ค์‹œ ๋‚ผ ๋•Œ๊นŒ์ง€ ์ง์ „ ํฌ์ง€์…˜์ด ์œ ์ง€๋จ",
71
+ "en": "Update hourly if you like. Your last position stands until you replace it."},
72
+ "fee_note": {"ko": "ํฌ์ง€์…˜ ๋ณ€๊ฒฝ๋ถ„์—๋งŒ ๋ถ€๊ณผ. ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์ˆ˜์ˆ˜๋ฃŒ๋กœ ์žƒ๋Š”๋‹ค",
73
+ "en": "Charged on position changes only. Flip often and the fees eat you."},
74
+ "scoring": {"ko": "์ข…๋ชฉ๋ณ„ ์ตœ์ข… ์ˆ˜์ต๋ฅ ๋กœ ์ˆœ์œ„. ์˜†์— '์šด์œผ๋กœ ์ด๋Ÿด ํ™•๋ฅ '์„ ํ•จ๊ป˜ ํ‘œ์‹œ",
75
+ "en": "Ranked by final return within each asset, shown next to the odds "
76
+ "of getting there by luck."},
77
+ "participation": {"ko": "ํ•ด๋‹น ์ข…๋ชฉ ๊ฐœ์„ค~๋งˆ๊ฐ ๊ธฐ๊ฐ„์˜ 70% ์ด์ƒ ํฌ์ง€์…˜์ด ์œ ํšจํ•ด์•ผ ์ƒ๊ธˆ ๋Œ€์ƒ",
78
+ "en": "A valid position for at least 70% of the asset's season "
79
+ "to be eligible for prizes."},
80
+ "luck_note": {"ko": "์‹ค๋ ฅ 0์ธ ์ฐธ๊ฐ€์ž๋ฅผ ์‹œ์ฆŒ ๊ธธ์ด๋งŒํผ 20,000ํšŒ ๋Œ๋ ค ๋‚ธ ์ƒ์œ„ 5% ์ง€์ . "
81
+ "์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋˜‘๊ฐ™์ด ๋ฌผ๋ ธ์Šต๋‹ˆ๋‹ค. ์ด ์„ ์„ ๋„˜์–ด์•ผ ์‹ค๋ ฅ์˜ ์ฆ๊ฑฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.",
82
+ "en": "The 95th percentile of 20,000 zero-skill players run over the "
83
+ "same season length, charged the same fees. Clear this line "
84
+ "before calling a result skill."},
85
+ "warning": {"ko": "์—”๋น„๋””์•„ ์‹ค์ ์ด 2026-08-26์ด๋‹ค. ์‹ค์ ์ผ ๋ณ€๋™์€ ํ‰์ƒ์ผ์˜ 2.7๋ฐฐ์ด๊ณ  "
86
+ "87%๊ฐ€ ์‹œ๊ฐ€ ๊ฐญ์ด๋ผ ์žฅ์ค‘ ๋Œ€์‘์ด ์•ˆ ๋œ๋‹ค. ์‹œ์ฆŒ ์ค‘ 1ํšŒ๋‹ค.",
87
+ "en": "NVIDIA reports on 2026-08-26. Earnings-day moves run 2.7x a normal "
88
+ "day and 87% of that lands in the opening gap, so you cannot react "
89
+ "intraday. It happens once this season."},
90
+ "durability_warn": {"ko": "์›์žฅ์ด ์žฌ์‹œ์ž‘์„ ๊ฒฌ๋”˜๋‹ค๋Š” ์ฆ๊ฑฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. "
91
+ "๋ฏธ๋Ÿฌ๋‚˜ ์˜์† ์Šคํ† ๋ฆฌ์ง€๋ฅผ ๋ถ™์ด์‹ญ์‹œ์˜ค.",
92
+ "en": "No evidence the ledger survives a restart. "
93
+ "Attach a mirror or persistent storage."},
94
+ }
95
+
96
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ฑ„์  ํžŒํŠธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
97
+ HINT = {
98
+ "pending": {"ko": "์ง‘๊ณ„ ์ „", "en": "not scored yet"},
99
+ "have_pos": {"ko": "ํฌ์ง€์…˜์€ ์ ‘์ˆ˜๋์Šต๋‹ˆ๋‹ค. ์‹œ์ฆŒ์ด ํ•˜๋ฃจ ์ด์ƒ ์ง„ํ–‰๋˜๋ฉด ์ˆ˜์ต๋ฅ ์ด ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค.",
100
+ "en": "Your position is recorded. Returns appear once the season has "
101
+ "run for at least a day."},
102
+ "no_pos": {"ko": "์•„์ง ํฌ์ง€์…˜์„ ๋‚ด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.",
103
+ "en": "You have not submitted a position yet."},
104
+ "over": {"ko": "์šด์˜ ํ•œ๊ณ„์„ ์„ ๋„˜์—ˆ์Šต๋‹ˆ๋‹ค",
105
+ "en": "You are past the luck ceiling"},
106
+ "under": {"ko": "์•„์ง ์šด์œผ๋กœ๋„ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฒ”์œ„์ž…๋‹ˆ๋‹ค",
107
+ "en": "Still inside what luck alone could produce"},
108
+ "unknown_asset": {"ko": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ. ๊ฐ€๋Šฅ: %s",
109
+ "en": "Unknown asset. Available: %s"},
110
+ "need_key": {"ko": "์ฐธ๊ฐ€ ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์—์„œ ๋กœ๊ทธ์ธํ•˜๋ฉด fc_ ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ‚ค๊ฐ€ "
111
+ "๋‚˜์˜ต๋‹ˆ๋‹ค. MCP ์„ค์ •์˜ Authorization ํ—ค๋”์— ๋„ฃ์–ด ์ฃผ์„ธ์š”.",
112
+ "en": "A participant key is required. Sign in on the site to get a key "
113
+ "starting with fc_, then put it in the Authorization header of "
114
+ "your MCP config."},
115
+ "kept": {"ko": "๋‹ค์‹œ ์ œ์ถœํ•  ๋•Œ๊นŒ์ง€ ์ด ํฌ์ง€์…˜์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค",
116
+ "en": "This position stands until you submit again"},
117
+ }
118
+
119
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ์ทจ์ง€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
120
+ MANIFESTO = {
121
+ "title": {"ko": "AI๋Š” ์‹œ์žฅ์„ ์ด๊ธธ ์ˆ˜ ์žˆ๋Š”๊ฐ€",
122
+ "en": "Can AI Beat the Market?"},
123
+ "sub": {"ko": "122์ผ๊ฐ„์˜ ๊ณต๊ฐœ ์‹คํ—˜", "en": "A 122-day open experiment"},
124
+
125
+ # ๋Œ€ํšŒ์˜ ๋ช…๋ถ„. 3๋ง‰์œผ๋กœ ๊ฐ„๋‹ค.
126
+ #
127
+ # ๐Ÿ”ด 3๋ง‰์—์„œ "์ง‘๋‹จ์ง€์„ฑ์ด ์‹œ์žฅ์„ ํ‘ผ๋‹ค"๊ณ  ์“ฐ์ง€ ์•Š๋Š”๋‹ค.
128
+ # ์šฐ๋ฆฌ ์ž์‹ ์˜ ๋ฐฉํ–ฅ ์˜ˆ์ธก ์—ฐ๊ตฌ๊ฐ€ 60๊ฐœ ๋ณ€์ˆ˜๋กœ ์ „๋ถ€ ์‹คํŒจํ–ˆ๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ๋ชป ํ•œ ๊ฒƒ์„
129
+ # ์ฐธ๊ฐ€์ž๊ฐ€ ํ•ด๋‚ผ ๊ฒƒ์ด๋ผ๊ณ  ๋ฏธ๋ฆฌ ๋งํ•  ์ˆ˜ ์—†๋‹ค. ๋Œ€์‹  ์ด ๋Œ€ํšŒ๊ฐ€ ์‹ค์ œ๋กœ ๋งŒ๋“ค์–ด๋‚ด๋Š”
130
+ # ๊ฒƒ โ€” '๊ฒฝ๊ณ„์˜ ์ธก์ •' โ€” ์„ ๋งํ•œ๋‹ค. ๊ทธ๊ฑด ๊ทผ๊ฑฐ๊ฐ€ ์žˆ๊ณ , ์•ฝํ•˜์ง€๋„ ์•Š๋‹ค.
131
+ #
132
+ # ๐Ÿ”ด "์„ธ๋ ฅ์ด ์‹œ์žฅ์„ ์™œ๊ณกํ•œ๋‹ค"๊ณ  ์“ฐ์ง€ ์•Š๋Š”๋‹ค. ์šฐ๋ฆฌ๊ฐ€ ์ธก์ •ํ•œ ์  ์—†๋Š” ์ฃผ์žฅ์ด๊ณ 
133
+ # ์Œ๋ชจ๋ก ์œผ๋กœ ์ฝํžŒ๋‹ค. ๋Œ€ํ˜• ์ž๊ธˆ์˜ ์œ ๋™์„ฑ ์ˆ˜์š”๋ผ๋Š”, ๋ฌธํ—Œ์— ๊ทผ๊ฑฐ๊ฐ€ ์žˆ๋Š”
134
+ # ํ‘œํ˜„์œผ๋กœ ๊ฐ™์€ ์‚ฌ์‹ค์„ ๋งํ•œ๋‹ค.
135
+ "acts": [
136
+ ({"ko": "์‹œ์žฅ์€ ์™œ ์–ด๋ ค์šด๊ฐ€",
137
+ "en": "Why markets are hard"},
138
+ {"ko": "๊ธˆ๋ฆฌ ๊ฒฐ์ • ํ•˜๋‚˜, ์ „์Ÿ ํ•˜๋‚˜, ์‹ค์  ๋ฐœํ‘œ ํ•˜๋‚˜๊ฐ€ ๋ช‡ ๋‹ฌ์น˜ ๋ถ„์„์„ ์ง€์›๋‹ˆ๋‹ค. "
139
+ "๋ณ€์ˆ˜์˜ ์ˆ˜๊ฐ€ ๊ด€์ธก์น˜๋ณด๋‹ค ๋งŽ๊ณ , ํฐ ์ž๊ธˆ์€ ์ •๋ณด๊ฐ€ ์•„๋‹ˆ๋ผ ์œ ๋™์„ฑ ๋•Œ๋ฌธ์— "
140
+ "์›€์ง์—ฌ ๊ฐ€๊ฒฉ์„ ์‹ค์ œ ๊ฐ€์น˜์—์„œ ๋ฐ€์–ด๋ƒ…๋‹ˆ๋‹ค. "
141
+ "์–ด๋–ค ๋‹จ์ผ ๋ชจ๋ธ๋„ ์‹œ์žฅ ์ „์ฒด๋ฅผ ๋‹ด์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ์€ ๋ชจ๋ธ์ด ๋ถ€์กฑํ•ด์„œ๊ฐ€ ์•„๋‹ˆ๋ผ "
142
+ "๋Œ€์ƒ์ด ๊ทธ๋Ÿฐ ์„ฑ์งˆ์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.",
143
+ "en": "One rate decision, one war, one earnings print erases months of "
144
+ "analysis. There are more moving parts than observations, and large "
145
+ "capital moves for liquidity rather than information, pushing price "
146
+ "away from value. No single model holds the whole market โ€” not because "
147
+ "our models are weak, but because that is what the thing is."}),
148
+
149
+ ({"ko": "์ธ๊ฐ„๊ณผ AI, ๋ˆ„๊ฐ€ ๋‚˜์€์ง€ ์•„๋ฌด๋„ ๋ชจ๋ฅธ๋‹ค",
150
+ "en": "Nobody knows whether humans or AI read it better"},
151
+ {"ko": "์ธ๊ฐ„์€ ๋งฅ๋ฝ๊ณผ ์„œ์‚ฌ๋ฅผ ์ฝ๊ณ , AI๋Š” ํŒจํ„ด๊ณผ ๊ทœ๋ชจ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. "
152
+ "๋‘˜์„ ๊ฐ™์€ ์กฐ๊ฑด์—์„œ ๊ฐ™์€ ๊ธฐ๊ฐ„ ๊ฒจ๋ฃจ๊ฒŒ ํ•œ ๊ณต๊ฐœ ๊ธฐ๋ก์ด ์—†์Šต๋‹ˆ๋‹ค. "
153
+ "์„ฑ๊ณต์€ ์‚ฌํ›„์— ์„ ๋ณ„๋˜์–ด ๋ณด๋„๋˜๊ณ  ์‹คํŒจ๋Š” ๊ธฐ๋ก์กฐ์ฐจ ๋‚จ์ง€ ์•Š์œผ๋‹ˆ, "
154
+ "์šฐ๋ฆฌ๊ฐ€ ์•„๋Š” ๊ฒƒ์€ ์‚ด์•„๋‚จ์€ ์ด์•ผ๊ธฐ๋ฟ์ž…๋‹ˆ๋‹ค. "
155
+ "122์ผ ๋™์•ˆ ๋ชจ๋“  ์ฐธ๊ฐ€์ž์˜ ์„ฑ์ ์„ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€ ๋‚จ๊น๋‹ˆ๋‹ค.",
156
+ "en": "Humans read context and narrative; machines read patterns and scale. "
157
+ "No public record exists of the two competing under identical "
158
+ "conditions over the same window. Winners are selected after the fact "
159
+ "and losers leave no trace, so all we know are the stories that "
160
+ "survived. For 122 days every entrant's record stands, start to finish."}),
161
+
162
+ ({"ko": "ํ•จ๊ป˜ ์žฌ๋ฉด ๋ฌด์—‡์„ ์•Œ ์ˆ˜ ์žˆ๋‚˜",
163
+ "en": "What we actually learn by measuring together"},
164
+ # ๐Ÿ”ด ๊ฐ™์€ ์‚ฌ์‹ค์„ '์šฐ๋ฆฌ๊ฐ€ ์‹คํŒจํ–ˆ๋‹ค'๋กœ ์“ฐ๋ฉด ์žํ•ด์ด๊ณ  '์ธก์ •ํ–ˆ๋‹ค'๋กœ ์“ฐ๋ฉด ๊ถŒ์œ„๋‹ค.
165
+ # ๊ณต๊ฐœ๋ฉด์—๋Š” ํ›„์ž๋งŒ ์˜ฌ๋ฆฐ๋‹ค.
166
+ {"ko": "๋‹จ๊ธฐ ๋ฐฉํ–ฅ์€ ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ์‹œํ—˜ํ•œ 60๊ฐœ ํ›„๋ณด ๋ณ€์ˆ˜ ์–ด๋””์—์„œ๋„ ์˜ˆ์ธก๋ ฅ์ด "
167
+ "ํ™•์ธ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด ๋Œ€ํšŒ๋Š” ๊ทธ ์ง€์ ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. "
168
+ "ํ•œ ๋ชจ๋ธ์ด ๋ฉˆ์ถ˜ ์ž๋ฆฌ๋Š” ์ •๋ณด๊ฐ€ ์•„๋‹ˆ์ง€๋งŒ, ์ˆ˜๋ฐฑ ๊ฐœ์˜ ๋…๋ฆฝ๋œ ๋ชจ๋ธ์ด ๊ฐ™์€ "
169
+ "์ž๋ฆฌ์—์„œ ๋ฉˆ์ถ”๋ฉด ๊ทธ๊ฒƒ์€ ์‹œ์žฅ์˜ ์„ฑ์งˆ์— ๋Œ€ํ•œ ์ •๋ณด์ž…๋‹ˆ๋‹ค. "
170
+ "๊ทธ๋ž˜์„œ ์ด ๋Œ€ํšŒ๊ฐ€ ๋ชจ์œผ๋Š” ๊ฒƒ์€ ๋‹ต์ด ์•„๋‹ˆ๋ผ ๊ฒฝ๊ณ„์ž…๋‹ˆ๋‹ค. "
171
+ "์–ด๋””๊นŒ์ง€๊ฐ€ ์šด์ด๊ณ  ์–ด๋””๋ถ€ํ„ฐ๊ฐ€ ์‹ค๋ ฅ์ธ์ง€, ๊ทธ ์„ ์„ ์ˆซ์ž๋กœ ๋‚จ๊น๋‹ˆ๋‹ค.",
172
+ "en": "Across sixty candidate variables we tested directly, none showed "
173
+ "predictive power over short-horizon direction. This contest starts "
174
+ "from there. Where one model stops is not information; where hundreds "
175
+ "of independent models stop is information about the market itself. "
176
+ "So what this collects is not an answer but a boundary โ€” where luck "
177
+ "ends and skill begins, written down as a number."}),
178
+ ],
179
+ "why": [
180
+ ({"ko": "์„ฑ๊ณต๋งŒ ๋ณด๋„๋ฉ๋‹ˆ๋‹ค", "en": "Only the winners get written up"},
181
+ {"ko": "์ˆ˜์ต ๋‚ธ ์‚ฌ๋žŒ๋งŒ ์ธํ„ฐ๋ทฐํ•ฉ๋‹ˆ๋‹ค. ์ง„ ์‚ฌ๋žŒ 999๋ช…์€ ๊ธฐ๋ก๋„ ๋‚จ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
182
+ "en": "The one who made money gets interviewed. The 999 who lost leave no record."}),
183
+ ({"ko": "๋ฐฑํ…Œ์ŠคํŠธ๋Š” ์กฐ์ž‘๋ฉ๋‹ˆ๋‹ค", "en": "Backtests can be bent"},
184
+ {"ko": "๊ณผ๊ฑฐ ๋ฐ์ดํ„ฐ๋กœ ์—ฐ 200%๋Š” ๋ˆ„๊ตฌ๋‚˜ ๋งŒ๋“ญ๋‹ˆ๋‹ค. ๋ฏธ๋ž˜๋กœ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค.",
185
+ "en": "Anyone can produce 200% a year on past data. Nobody can on future data."}),
186
+ ({"ko": "์šด๊ณผ ์‹ค๋ ฅ์ด ์•ˆ ๋‚˜๋‰ฉ๋‹ˆ๋‹ค", "en": "Luck and skill look identical"},
187
+ {"ko": "ํ•œ ๋‹ฌ์— 30% ๋‚ธ ์‚ฌ๋žŒ์ด ์‹ค๋ ฅ์ธ์ง€ ์šด์ธ์ง€ ํŒ๋ณ„ํ•  ๋ฐฉ๋ฒ•์ด ์—†์—ˆ์Šต๋‹ˆ๋‹ค.",
188
+ "en": "There has been no way to tell whether +30% in a month was skill or luck."}),
189
+ ],
190
+ "how": [
191
+ ({"ko": "์šด์˜ ํฌ๊ธฐ๋ฅผ ์ˆซ์ž๋กœ ์••๋‹ˆ๋‹ค", "en": "We measured how far luck alone gets you"},
192
+ {"ko": "์‹ค๋ ฅ 0์ธ ์ฐธ๊ฐ€์ž๊ฐ€ 122์ผ์— ์–ด๋””๊นŒ์ง€ ๊ฐ€๋Š”์ง€ 20,000ํšŒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์œผ๋กœ ์Ÿ€์Šต๋‹ˆ๋‹ค. "
193
+ "์ˆ˜์ˆ˜๋ฃŒ๊นŒ์ง€ ๋ฌผ๋ฆฐ ๊ฐ’์œผ๋กœ ๋น„ํŠธ์ฝ”์ธ +86.6%, ๊ธˆ +9.2%. "
194
+ "์ด ์„ ์„ ๋„˜์ง€ ๋ชปํ•œ ์ˆ˜์ต์€ ์‹ค๋ ฅ์˜ ์ฆ๊ฑฐ๊ฐ€ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.",
195
+ "en": "20,000 simulations of a zero-skill player over 122 days, charged the same "
196
+ "fees: Bitcoin +86.6%, Gold +9.2%. A return below that line is not "
197
+ "evidence of skill."}),
198
+ ({"ko": "์กฐ์ž‘์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค", "en": "It cannot be gamed"},
199
+ {"ko": "์ •๋‹ต์ด ๋ฏธ๋ž˜์— ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ์ถœํ•œ ๋’ค์— ์„ธ์ƒ์ด ์ •๋‹ต์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.",
200
+ "en": "The answer lives in the future. The world writes it after you submit."}),
201
+ ({"ko": "๊ฐ™์€ ์กฐ๊ฑด์ž…๋‹ˆ๋‹ค", "en": "Everyone gets the same conditions"},
202
+ {"ko": "๋ ˆ๋ฒ„๋ฆฌ์ง€ 1 ๊ณ ์ •, ๊ฐ™์€ ๋ฐ์ดํ„ฐ, ๊ฐ™์€ ์‹œ๊ฐ. ํฌ๊ฒŒ ๊ฑธ์–ด ์ด๊ธฐ๋Š” ๊ธธ์„ ๋ง‰์•˜์Šต๋‹ˆ๋‹ค.",
203
+ "en": "Leverage fixed at 1, same data, same clock. Betting bigger is not a way to win."}),
204
+ ],
205
+ "our_stake": {
206
+ "ko": "์ฃผ์ตœ์ธก์€ ์ˆœ์œ„์— ์ฐธ๊ฐ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ๋‹จ์ˆœ ๋ณด์œ ยท๋ณ€๋™์„ฑ ํƒ€๊ฒŸํŒ…ยท๋ฌด์ž‘์œ„ "
207
+ "์„ธ ๊ธฐ์ค€์„ ์„ ๊ฐ™์€ ํ‘œ์— ์˜ฌ๋ ค, ์„ฑ์ ์„ ๋ฌด์—‡์— ๊ฒฌ์ค„์ง€ ๋ณด์ด๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.",
208
+ "en": "The organizers do not compete. Instead three baselines โ€” buy and hold, "
209
+ "volatility targeting, and random โ€” sit in the same table so you can see "
210
+ "what your result should be measured against."},
211
+ }
212
+
213
+ ANCHOR = {
214
+ "BUYHOLD": {"ko": "๋‹จ์ˆœ ๋ณด์œ ", "en": "Buy and Hold"},
215
+ "VOLTARGET": {"ko": "๋ณ€๋™์„ฑ ํƒ€๊ฒŸํŒ…", "en": "Volatility Targeting"},
216
+ "RANDOM": {"ko": "๋ฌด์ž‘์œ„", "en": "Random"},
217
+ }
218
+
219
+ TYPE = {
220
+ "human": {"ko": "์‚ฌ๋žŒ", "en": "Human"},
221
+ "expert": {"ko": "์ „๋ฌธ๊ฐ€ยท๊ธฐ๊ด€", "en": "Quant / Institution"},
222
+ "agent": {"ko": "AI ์—์ด์ „ํŠธ", "en": "AI Agent"},
223
+ "anchor": {"ko": "๊ธฐ์ค€์„ ", "en": "Baseline"},
224
+ }
225
+
226
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ MCP ๋„๊ตฌ ์„ค๋ช… โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
227
+ TOOLDESC = {
228
+ "get_rules": {
229
+ "ko": "์ฑŒ๋ฆฐ์ง€ ๊ทœ์น™๊ณผ ํ˜„์žฌ ์‹œ์ฆŒ ์ •๋ณด๋ฅผ ์ฝ๋Š”๋‹ค. ์ฐธ๊ฐ€ ์ „์— ๋ฐ˜๋“œ์‹œ ๋จผ์ € ํ˜ธ์ถœํ•œ๋‹ค. "
230
+ "์ข…๋ชฉ ๋ชฉ๋กยท๋งˆ๊ฐ์ผยท๋ ˆ๋ฒ„๋ฆฌ์ง€ ์ƒํ•œยท์ˆ˜์ˆ˜๋ฃŒยท์ฑ„์  ๋ฐฉ์‹์ด ๋“ค์–ด ์žˆ๋‹ค.",
231
+ "en": "Read the challenge rules and current season. Always call this first. "
232
+ "Covers the asset list, deadline, leverage cap, fees, and scoring."},
233
+ "get_data": {
234
+ "ko": "์ข…๋ชฉ์˜ ๊ณผ๊ฑฐ ์‹œ์„ธ๋ฅผ ๋ฐ›๏ฟฝ๏ฟฝ๋‹ค. ์ด ๋ฐ์ดํ„ฐ๋กœ ์ง์ ‘ ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด ์˜ˆ์ธกํ•˜๋ผ. "
235
+ "๋ฏธ๋ž˜ ๋ฐ์ดํ„ฐ๋Š” ์ฃผ์ง€ ์•Š๋Š”๋‹ค โ€” ์ •๋‹ต์€ ์•„์ง ์„ธ์ƒ์— ์—†๋‹ค.",
236
+ "en": "Fetch historical prices for an asset. Build your own model on it. "
237
+ "No future data is served โ€” the answer does not exist yet."},
238
+ "submit_position": {
239
+ "ko": "ํฌ์ง€์…˜์„ ์ œ์ถœํ•œ๋‹ค. -1.0(์ „๋Ÿ‰ ์ˆ) ~ +1.0(์ „๋Ÿ‰ ๋กฑ). ๋ ˆ๋ฒ„๋ฆฌ์ง€๋Š” 1๋กœ ๊ณ ์ •์ด๋ฉฐ "
240
+ "๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž˜๋ฆฐ๋‹ค. ๋‹ค์‹œ ์ œ์ถœํ•  ๋•Œ๊นŒ์ง€ ์ด ํฌ์ง€์…˜์ด ์œ ์ง€๋œ๋‹ค. "
241
+ "ํฌ์ง€์…˜์„ ๋ฐ”๊ฟ€ ๋•Œ๋งˆ๋‹ค ์ˆ˜์ˆ˜๋ฃŒ๊ฐ€ ๋‚˜๊ฐ€๋ฏ€๋กœ ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์†ํ•ด๋‹ค.",
242
+ "en": "Submit a position from -1.0 (fully short) to +1.0 (fully long). Leverage is "
243
+ "fixed at 1 and values outside the range are clipped. The position stands "
244
+ "until you submit again. Every change costs a fee, so flipping often loses money."},
245
+ "check_score": {
246
+ "ko": "๋‚ด ์„ฑ์ ๊ณผ ์ˆœ์œ„๋ฅผ ํ™•์ธํ•œ๋‹ค. ์ˆ˜์ต๋ฅ ยท์ˆœ์œ„ยท์šด์˜ ํ•œ๊ณ„์„  ๋Œ€๋น„ ์œ„์น˜๋ฅผ ๋Œ๋ ค์ค€๋‹ค.",
247
+ "en": "Check your standing: return, rank, and where you sit against the luck ceiling."},
248
+ }
249
+
250
+ MCP_INSTRUCTIONS = {
251
+ "ko": "๊ธˆ์œต ์˜ˆ์ธก ์ฑŒ๋ฆฐ์ง€๋‹ค. ๋ฐฉํ–ฅ์„ ๋งžํžˆ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ์‹ค์ œ ํฌ์ง€์…˜์„ ์žก์•„ "
252
+ "์ˆ˜์ต๋ฅ ๋กœ ๊ฒจ๋ฃฌ๋‹ค. get_rules ๋กœ ์‹œ์ž‘ํ•˜๋ผ.",
253
+ "en": "This is a financial forecasting challenge. You do not call direction โ€” you take "
254
+ "real positions and compete on returns. Start with get_rules."}
app/main.py CHANGED
@@ -4,10 +4,11 @@
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__))
@@ -18,10 +19,27 @@ import reference as RF
18
  import ledger as L
19
  import mcp as MCP
20
  import fetch as FE
 
21
 
22
  CEIL = json.load(open(os.path.join(os.path.dirname(HERE), "announce_ceiling.json"),
23
  encoding="utf-8"))["ceiling"]
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  app = FastAPI(title="FINCHAL")
26
  S1 = S.SEASONS[1]
27
  # ๋กœ์ปฌ์—์„œ ํ™”๋ฉด์„ ํ™•์ธํ•  ๋•Œ๋งŒ ์“ด๋‹ค. ์‹œ์ฆŒ์ด ์˜ค๋Š˜ ์—ด๋ฆฌ๋ฉด ์ฑ„์ ํ•  ๊ตฌ๊ฐ„์ด ์—†์–ด
@@ -60,39 +78,75 @@ def season_px(asset):
60
  return s[s.index >= pd.Timestamp(S1["opens"])]
61
 
62
 
63
- def rules_doc():
 
64
  return {
 
65
  "season": S1["name"], "opens": S1["opens"], "closes": S1["closes"],
66
  "days": (pd.Timestamp(S1["closes"]) - pd.Timestamp(S1["opens"])).days,
67
- "prize": "์ข…๋ชฉ๋ณ„ ์ตœ์ข… ์ˆ˜์ต๋ฅ  1์œ„ %d๋‹ฌ๋Ÿฌ ยท ์ด %d๋‹ฌ๋Ÿฌ"
68
- % (S1["prize_usd_per_asset"], S1["prize_total_usd"]),
69
- "leverage": "ํฌ์ง€์…˜ -1.0 ~ +1.0. ๋ ˆ๋ฒ„๋ฆฌ์ง€ 1 ๊ณ ์ •. ๋ฒ—์–ด๋‚˜๋ฉด ์ž˜๋ฆผ",
70
- "update": "๋งค์‹œ๊ฐ„ ๊ฐฑ์‹  ๊ฐ€๋Šฅ. ๋‹ค์‹œ ๋‚ผ ๋•Œ๊นŒ์ง€ ์ง์ „ ํฌ์ง€์…˜์ด ์œ ์ง€๋จ",
71
  "fees": {k: SC.fee_of(k) for k in ASSETS},
72
- "fee_note": "ํฌ์ง€์…˜ ๋ณ€๊ฒฝ๋ถ„์—๋งŒ ๋ถ€๊ณผ. ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์ˆ˜์ˆ˜๋ฃŒ๋กœ ์žƒ๋Š”๋‹ค",
73
- "scoring": "์ตœ์ข… ์ˆ˜์ต๋ฅ ๋กœ ์ข…๋ชฉ๋ณ„ ์ˆœ์œ„. ์˜†์— '์šด์œผ๋กœ ์ด๋Ÿด ํ™•๋ฅ '์„ ํ•จ๊ป˜ ํ‘œ์‹œ",
74
- "participation": "ํ•ด๋‹น ์ข…๋ชฉ ๊ฐœ์„ค~๋งˆ๊ฐ ๊ธฐ๊ฐ„์˜ 70%% ์ด์ƒ ํฌ์ง€์…˜์ด ์œ ํšจํ•ด์•ผ ์ƒ๊ธˆ ๋Œ€์ƒ",
75
- "assets": {k: {"label": v["label"], "why": v["why"],
76
  "vol_annual": v["vol"], "hours": v["hours"]}
77
  for k, v in ASSETS.items()},
78
- "types": {k: v["label"] for k, v in S.TYPES.items()},
79
- "warning": ("์—”๋น„๋””์•„ ์‹ค์ ์ด 2026-08-26์ด๋‹ค. ์‹ค์ ์ผ ๋ณ€๋™์€ ํ‰์ƒ์ผ์˜ 2.7๋ฐฐ์ด๊ณ  "
80
- "87%๊ฐ€ ์‹œ๊ฐ€ ๊ฐญ์ด๋ผ ์žฅ์ค‘ ๋Œ€์‘์ด ์•ˆ ๋œ๋‹ค. ์‹œ์ฆŒ ์ค‘ 1ํšŒ๋‹ค."),
81
- "manifesto": S.MANIFESTO["title"] + " โ€” " + S.MANIFESTO["sub"],
82
  # ์šด์˜ ํ•œ๊ณ„์„ . ์‹ค๋ ฅ 0 ์ฐธ๊ฐ€์ž๋ฅผ 20,000ํšŒ ๋Œ๋ ค ๋‚ธ ์ƒ์œ„ 5% ์ง€์ ์ด๋‹ค.
83
  # ์ด ๊ฐ’ ์•„๋ž˜์˜ ์ˆ˜์ต์€ ์šด์œผ๋กœ๋„ ์„ค๋ช…๋œ๋‹ค.
84
  "luck_ceiling": {k: round(v["p95"], 4) for k, v in CEIL.items()},
85
- "luck_note": ("์‹ค๋ ฅ 0์ธ ์ฐธ๊ฐ€์ž๋ฅผ ์‹œ์ฆŒ ๊ธธ์ด๋งŒํผ 20,000ํšŒ ๋Œ๋ ค ๋‚ธ ์ƒ์œ„ 5% ์ง€์ . "
86
- "์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋˜‘๊ฐ™์ด ๋ฌผ๋ ธ์Šต๋‹ˆ๋‹ค. ์ด ์„ ์„ ๋„˜์–ด์•ผ ์‹ค๋ ฅ์˜ ์ฆ๊ฑฐ๊ฐ€ ๋ฉ๋‹ˆ๋‹ค."),
87
  }
88
 
89
 
90
- def me_from(key):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  u = L.user_by_key(key or "")
92
  if not u:
93
- raise PermissionError(
94
- "์ฐธ๊ฐ€ ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ํ™”๋ฉด์—์„œ Hugging Face๋กœ ๋กœ๊ทธ์ธํ•˜๋ฉด fc_ ๋กœ ์‹œ์ž‘ํ•˜๋Š” "
95
- "ํ‚ค๊ฐ€ ๋‚˜์˜ต๋‹ˆ๋‹ค. MCP ์„ค์ •์˜ Authorization ํ—ค๋”์— ๋„ฃ์–ด ์ฃผ์„ธ์š”.")
96
  return u
97
 
98
 
@@ -174,14 +228,102 @@ def curves(asset, top=12):
174
 
175
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ REST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
176
  @app.get("/api/rules")
177
- def api_rules():
178
- return {"ok": True, **rules_doc()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
 
181
  @app.get("/api/data")
182
  def api_data(asset: str, bars: int = 500, interval: str = "1d"):
183
  if asset not in ASSETS:
184
  return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
 
 
 
185
  s = prices(asset, interval, min(max(bars, 10), 5000))
186
  return {"ok": True, "asset": asset, "interval": interval,
187
  "bars": [{"t": t.isoformat(), "c": float(v)} for t, v in s.items()]}
@@ -190,8 +332,18 @@ def api_data(asset: str, bars: int = 500, interval: str = "1d"):
190
  @app.post("/api/register")
191
  async def api_register(req: Request):
192
  b = await req.json()
193
- u = L.register(b["user"], b.get("strategy", "-"),
194
- b.get("type", "agent"), b.get("assets", list(ASSETS)))
 
 
 
 
 
 
 
 
 
 
195
  return {"ok": True, **u}
196
 
197
 
@@ -199,15 +351,27 @@ async def api_register(req: Request):
199
  async def api_submit(req: Request, authorization: str = Header(None),
200
  x_finchal_key: str = Header(None)):
201
  b = await req.json()
 
202
  # Authorization ์€ ์•ž๋‹จ ํ”„๋ก์‹œ๊ฐ€ ์ž๊ธฐ ์ธ์ฆ์— ์“ฐ๊ธฐ๋„ ํ•œ๋‹ค. ๊ทธ๋•Œ ์ฐธ๊ฐ€ํ‚ค๊ฐ€
203
  # ์•ฑ๊นŒ์ง€ ์˜ค์ง€ ์•Š๋Š”๋‹ค. ์ „์šฉ ํ—ค๋”๋ฅผ ํ•จ๊ป˜ ๋ฐ›์•„ ๊ทธ ์ถฉ๋Œ์„ ํ”ผํ•œ๋‹ค.
204
  key = ((x_finchal_key or "").strip()
205
  or (authorization or "").replace("Bearer ", "").strip()
206
  or b.get("key", ""))
207
- try:
208
- u = me_from(key)
209
- except PermissionError as e:
210
- return JSONResponse({"ok": False, "error": str(e)}, 401)
 
 
 
 
 
 
 
 
 
 
 
211
  if b["asset"] not in ASSETS:
212
  return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
213
  r = L.submit(u["user"], b["asset"], b["position"], b.get("source", "rest"))
@@ -230,36 +394,55 @@ def api_curves(asset: str, top: int = 12):
230
 
231
 
232
  @app.get("/api/manifesto")
233
- def api_manifesto():
234
- return {"ok": True, **S.MANIFESTO, "anchors": S.ANCHORS,
235
- "types": {k: v for k, v in S.TYPES.items()},
236
- "season2": {k: v for k, v in S.SEASONS.get(2, {}).items()
237
- if k != "assets"}}
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
 
240
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ MCP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
241
- def _call(name, args, key=None):
 
242
  if name == "get_rules":
243
- return rules_doc()
244
  if name == "get_data":
245
  a = args["asset"]
246
  if a not in ASSETS:
247
- return {"error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ. ๊ฐ€๋Šฅ: %s" % list(ASSETS)}
248
- s = prices(a, args.get("interval", "1d"), min(args.get("bars", 500), 5000))
249
- return {"asset": a, "interval": args.get("interval", "1d"),
 
 
 
 
 
 
250
  "bars": [{"t": t.date().isoformat(), "c": round(float(v), 6)}
251
  for t, v in s.items()]}
252
  if name == "submit_position":
253
- u = me_from(key)
254
  a = args["asset"]
255
  if a not in ASSETS:
256
- return {"error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ. ๊ฐ€๋Šฅ: %s" % list(ASSETS)}
257
  r = L.submit(u["user"], a, args["position"], "mcp")
258
  _SC.pop(a, None)
259
  return {"accepted": True, "asset": a, "position": r["p"], "at": r["t"],
260
- "note": "๋‹ค์‹œ ์ œ์ถœํ•  ๋•Œ๊นŒ์ง€ ์ด ํฌ์ง€์…˜์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค"}
261
  if name == "check_score":
262
- u = me_from(key)
263
  want = [args["asset"]] if args.get("asset") else u.get("assets", list(ASSETS))
264
  out = []
265
  for a in want:
@@ -269,19 +452,18 @@ def _call(name, args, key=None):
269
  mine = next((r for r in rows if r["user"] == u["user"]), None)
270
  if not mine:
271
  last = L.latest(a, u["user"])
272
- out.append({"asset": a, "status": "์ง‘๊ณ„ ์ „",
273
  "position": last["p"] if last else None,
274
- "hint": ("ํฌ์ง€์…˜์€ ์ ‘์ˆ˜๋์Šต๋‹ˆ๋‹ค. ์‹œ์ฆŒ์ด ํ•˜๋ฃจ ์ด์ƒ ์ง„ํ–‰๋˜๋ฉด "
275
- "์ˆ˜์ต๋ฅ ์ด ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค." if last else
276
- "์•„์ง ํฌ์ง€์…˜์„ ๋‚ด์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")})
277
  else:
278
  out.append({"asset": a, "rank": mine["rank"], "of": len(rows),
279
  "return": round(mine["ret"], 4),
280
  "luck_ceiling": round(mine["luck_p95"], 4),
281
  "score": round(mine["score"], 2),
282
  "fee_paid": round(mine["fee_paid"], 4),
283
- "hint": ("์šด์˜ ํ•œ๊ณ„์„ ์„ ๋„˜์—ˆ์Šต๋‹ˆ๋‹ค" if mine["ret"] > mine["luck_p95"]
284
- else "์•„์ง ์šด์œผ๋กœ๋„ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ๋Š” ๋ฒ”์œ„์ž…๋‹ˆ๋‹ค")})
285
  return {"user": u["user"], "strategy": u.get("strategy"), "results": out}
286
  return {"error": "๋ฏธ๊ตฌํ˜„"}
287
 
@@ -290,28 +472,47 @@ def _call(name, args, key=None):
290
  async def mcp_endpoint(req: Request, authorization: str = Header(None),
291
  x_finchal_key: str = Header(None)):
292
  body = await req.json()
 
293
  key = ((x_finchal_key or "").strip()
294
  or (authorization or "").replace("Bearer ", "").strip())
295
- resp = MCP.handle(body, lambda n, a: _call(n, a, key))
296
  if resp is None:
297
  return JSONResponse({}, 202)
298
  return JSONResponse(resp)
299
 
300
 
301
  @app.get("/llms.txt", response_class=PlainTextResponse)
302
- def llms():
303
- r = rules_doc()
304
- return (
305
- "# %s\n\n" % r["manifesto"] +
306
- "๋‹น์‹ ์€ ์‹ค์ œ ํฌ์ง€์…˜์„ ์žก์•„ ์ˆ˜์ต๋ฅ ๋กœ ๊ฒจ๋ฃน๋‹ˆ๋‹ค. ๋ฐฉํ–ฅ ์˜ˆ์ธก์ด ์•„๋‹ˆ๋ผ ๋งค๋งค์ž…๋‹ˆ๋‹ค.\n\n"
307
- "์ข…๋ชฉ: %s\n" % ", ".join("%s(%s)" % (k, v["label"]) for k, v in ASSETS.items()) +
308
- "๊ธฐ๊ฐ„: %s ~ %s\n" % (r["opens"], r["closes"]) +
309
- "์ƒ๊ธˆ: %s\n" % r["prize"] +
310
- "ํฌ์ง€์…˜: %s\n" % r["leverage"] +
311
- "๊ฐฑ์‹ : %s\n" % r["update"] +
312
- "์ˆ˜์ˆ˜๋ฃŒ: ํฌ์ง€์…˜ ๋ณ€๊ฒฝ๋ถ„์—๋งŒ. ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์†ํ•ด์ž…๋‹ˆ๋‹ค\n"
313
- "์ฑ„์ : ์ข…๋ชฉ๋ณ„ ์ตœ์ข… ์ˆ˜์ต๋ฅ  ์ˆœ์œ„\n\n"
314
- "์‹œ์ž‘: get_rules() โ†’ get_data() โ†’ ์ง์ ‘ ๋ชจ๋ธ๋ง โ†’ submit_position()\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
315
 
316
 
317
  STATIC = os.path.join(HERE, "static")
 
4
  REST ๋Š” ์‚ฌ๋žŒ๊ณผ ์›น ํ™”๋ฉด์ด ์“ฐ๊ณ , MCP ๋Š” ์—์ด์ „ํŠธ๊ฐ€ ์“ด๋‹ค.
5
  ๋‘˜์ด ๊ฐ™์€ ์›์žฅยท๊ฐ™์€ ์ฑ„์ ๊ธฐ๋ฅผ ๋ณธ๋‹ค. ๊ฒฝ๋กœ๊ฐ€ ๊ฐˆ๋ฆฌ๋ฉด ์„ฑ์ ์ด ๊ฐˆ๋ฆฐ๋‹ค.
6
  """
7
+ import os, sys, json, time, base64, hmac, hashlib, secrets, urllib.parse, urllib.request
8
  import numpy as np, pandas as pd
9
+ from fastapi import FastAPI, Request, Header, HTTPException
10
+ from fastapi.responses import (JSONResponse, PlainTextResponse, FileResponse,
11
+ RedirectResponse)
12
  from fastapi.staticfiles import StaticFiles
13
 
14
  HERE = os.path.dirname(os.path.abspath(__file__))
 
19
  import ledger as L
20
  import mcp as MCP
21
  import fetch as FE
22
+ import i18n as I
23
 
24
  CEIL = json.load(open(os.path.join(os.path.dirname(HERE), "announce_ceiling.json"),
25
  encoding="utf-8"))["ceiling"]
26
 
27
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Hugging Face ๋กœ๊ทธ์ธ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
28
+ # ์‹ ์›์€ ์ฐธ๊ฐ€์ž๊ฐ€ ์œ„์กฐํ•  ์ˆ˜ ์—†๋Š” ๊ณณ์—์„œ ์™€์•ผ ํ•œ๋‹ค. ์ž์œ  ์ž…๋ ฅ ID๋ฅผ ๋‘๋ฉด
29
+ # ๋‚จ์˜ ์ด๋ฆ„์œผ๋กœ ์ œ์ถœํ•  ์ˆ˜ ์žˆ๊ณ , ๊ทธ๋Ÿฌ๋ฉด ์ˆœ์œ„ํ‘œ๊ฐ€ ์˜๋ฏธ๋ฅผ ์žƒ๋Š”๋‹ค.
30
+ OAUTH_ID = os.environ.get("OAUTH_CLIENT_ID", "")
31
+ OAUTH_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
32
+ OAUTH_ISS = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
33
+ SPACE_HOST = os.environ.get("SPACE_HOST", "")
34
+ COOKIE = "finchal_session"
35
+ # ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ iframe ์•ˆ์—์„œ ๋œจ๋ฏ€๋กœ ์šฐ๋ฆฌ ์ฟ ํ‚ค๋Š” 3์ž ์ฟ ํ‚ค๋‹ค. Lax ๋Š” ๊ฑฐ๊ธฐ์„œ ๋ฒ„๋ ค์ง€๊ณ 
36
+ # None ์€ Secure ๋ฅผ ์š”๊ตฌํ•˜๋ฉฐ Secure ๋Š” HTTPS ๋ฅผ ์š”๊ตฌํ•œ๋‹ค โ€” Space ์—์„œ๋Š” ์ฐธ, ๋กœ์ปฌ์—์„œ๋Š” ๊ฑฐ์ง“.
37
+ IN_FRAME = bool(SPACE_HOST)
38
+ COOKIE_KW = ({"samesite": "none", "secure": True} if IN_FRAME
39
+ else {"samesite": "lax", "secure": False})
40
+ # ๐Ÿ”ด ์žฌ์‹œ์ž‘ํ•  ๋•Œ๋งˆ๋‹ค ๋ฐ”๋€Œ๋ฉด ๋ชจ๋“  ์„ธ์…˜์ด ๋Š๊ธด๋‹ค. Space ๋ณ€์ˆ˜๋กœ ๊ณ ์ •ํ•  ๊ฒƒ.
41
+ SESSION_KEY = os.environ.get("FINCHAL_SESSION_KEY") or secrets.token_hex(16)
42
+
43
  app = FastAPI(title="FINCHAL")
44
  S1 = S.SEASONS[1]
45
  # ๋กœ์ปฌ์—์„œ ํ™”๋ฉด์„ ํ™•์ธํ•  ๋•Œ๋งŒ ์“ด๋‹ค. ์‹œ์ฆŒ์ด ์˜ค๋Š˜ ์—ด๋ฆฌ๋ฉด ์ฑ„์ ํ•  ๊ตฌ๊ฐ„์ด ์—†์–ด
 
78
  return s[s.index >= pd.Timestamp(S1["opens"])]
79
 
80
 
81
+ def rules_doc(lang="ko"):
82
+ T = lambda d: I.t(d, lang)
83
  return {
84
+ "lang": lang,
85
  "season": S1["name"], "opens": S1["opens"], "closes": S1["closes"],
86
  "days": (pd.Timestamp(S1["closes"]) - pd.Timestamp(S1["opens"])).days,
87
+ "prize": T(I.RULES["prize"]) % (S1["prize_usd_per_asset"],
88
+ S1["prize_total_usd"]),
89
+ "leverage": T(I.RULES["leverage"]),
90
+ "update": T(I.RULES["update"]),
91
  "fees": {k: SC.fee_of(k) for k in ASSETS},
92
+ "fee_note": T(I.RULES["fee_note"]),
93
+ "scoring": T(I.RULES["scoring"]),
94
+ "participation": T(I.RULES["participation"]),
95
+ "assets": {k: {"label": T(I.ASSET[k]["label"]), "why": T(I.ASSET[k]["why"]),
96
  "vol_annual": v["vol"], "hours": v["hours"]}
97
  for k, v in ASSETS.items()},
98
+ "types": {k: I.t(v, lang) for k, v in I.TYPE.items()},
99
+ "warning": T(I.RULES["warning"]),
100
+ "manifesto": T(I.MANIFESTO["title"]) + " โ€” " + T(I.MANIFESTO["sub"]),
 
101
  # ์šด์˜ ํ•œ๊ณ„์„ . ์‹ค๋ ฅ 0 ์ฐธ๊ฐ€์ž๋ฅผ 20,000ํšŒ ๋Œ๋ ค ๋‚ธ ์ƒ์œ„ 5% ์ง€์ ์ด๋‹ค.
102
  # ์ด ๊ฐ’ ์•„๋ž˜์˜ ์ˆ˜์ต์€ ์šด์œผ๋กœ๋„ ์„ค๋ช…๋œ๋‹ค.
103
  "luck_ceiling": {k: round(v["p95"], 4) for k, v in CEIL.items()},
104
+ "luck_note": T(I.RULES["luck_note"]),
 
105
  }
106
 
107
 
108
+ def _sign(payload):
109
+ raw = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip("=")
110
+ sig = hmac.new(SESSION_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32]
111
+ return raw + "." + sig
112
+
113
+
114
+ def _unsign(token):
115
+ try:
116
+ raw, sig = (token or "").rsplit(".", 1)
117
+ except ValueError:
118
+ return None
119
+ good = hmac.new(SESSION_KEY.encode(), raw.encode(), hashlib.sha256).hexdigest()[:32]
120
+ if not hmac.compare_digest(sig, good):
121
+ return None
122
+ pad = "=" * (-len(raw) % 4)
123
+ try:
124
+ return json.loads(base64.urlsafe_b64decode(raw + pad))
125
+ except Exception:
126
+ return None
127
+
128
+
129
+ def current_user(request):
130
+ return _unsign(request.cookies.get(COOKIE))
131
+
132
+
133
+ def _redirect_uri(request):
134
+ if SPACE_HOST:
135
+ return "https://%s/auth/callback" % SPACE_HOST
136
+ return str(request.base_url).rstrip("/") + "/auth/callback"
137
+
138
+
139
+ def lang_of(request, explicit=None):
140
+ """๋ธŒ๋ผ์šฐ์ € ์–ธ์–ด๋ฅผ ์ž๋™ ์‹๋ณ„ํ•œ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๊ณ ๋ฅธ ๊ฐ’์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒŒ ์ด๊ธด๋‹ค."""
141
+ return I.pick(request.headers.get("accept-language"),
142
+ explicit or request.cookies.get("finchal_lang")
143
+ or request.query_params.get("lang"))
144
+
145
+
146
+ def me_from(key, lang="ko"):
147
  u = L.user_by_key(key or "")
148
  if not u:
149
+ raise PermissionError(I.t(I.HINT["need_key"], lang))
 
 
150
  return u
151
 
152
 
 
228
 
229
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ REST โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
230
  @app.get("/api/rules")
231
+ def api_rules(request: Request, lang: str = None):
232
+ return {"ok": True, **rules_doc(lang_of(request, lang))}
233
+
234
+
235
+ @app.get("/api/me")
236
+ def api_me(request: Request):
237
+ u = current_user(request)
238
+ out = {"signed_in": bool(u), "user": u,
239
+ "oauth_configured": bool(OAUTH_ID and OAUTH_SECRET),
240
+ "lang": lang_of(request)}
241
+ if u:
242
+ # ๋กœ๊ทธ์ธํ•œ ์‚ฌ๋žŒ์€ ๋“ฑ๋ก์„ ๋”ฐ๋กœ ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ํ‚ค๋ฅผ ๋ฐ”๋กœ ์ค€๋‹ค.
243
+ out["key"] = L.make_key(u["name"])
244
+ rec = next((x for x in L.all_users() if x["user"] == u["name"]), None)
245
+ out["strategy"] = (rec or {}).get("strategy", "")
246
+ out["joined"] = (rec or {}).get("joined")
247
+ return out
248
+
249
+
250
+ @app.get("/login")
251
+ def login(request: Request):
252
+ if not (OAUTH_ID and OAUTH_SECRET):
253
+ raise HTTPException(503, "OAuth is not configured (needs hf_oauth in README)")
254
+ state = secrets.token_urlsafe(16)
255
+ q = urllib.parse.urlencode({
256
+ "client_id": OAUTH_ID, "redirect_uri": _redirect_uri(request),
257
+ "response_type": "code", "scope": "openid profile", "state": state})
258
+ r = RedirectResponse("%s/oauth/authorize?%s" % (OAUTH_ISS.rstrip("/"), q))
259
+ # ์ œ๊ณต์ž๊ฐ€ ๋˜๋Œ๋ ค์ฃผ๋Š” state ๋ฅผ ์ด ์ฟ ํ‚ค์™€ ๋Œ€์กฐํ•˜๋Š” ๊ฒƒ์ด,
260
+ # ์ œ3์ž๊ฐ€ ๋‚จ์˜ ๋กœ๊ทธ์ธ์„ ๊ฐ€๋กœ์ฑ„ ์™„๋ฃŒํ•˜๋Š” ๊ฒƒ์„ ๋ง‰๋Š” ์œ ์ผํ•œ ์ˆ˜๋‹จ์ด๋‹ค.
261
+ r.set_cookie("finchal_state", state, max_age=600, httponly=True, **COOKIE_KW)
262
+ return r
263
+
264
+
265
+ @app.get("/auth/callback")
266
+ def callback(request: Request, code: str = "", state: str = ""):
267
+ lang = lang_of(request)
268
+ saved = request.cookies.get("finchal_state")
269
+ if not code:
270
+ raise HTTPException(400, "No authorization code โ€” please sign in again")
271
+ if not saved:
272
+ # ์ฟ ํ‚ค๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š์€ ๊ฒƒ์ด๋‹ค. ์œ„์กฐ๊ฐ€ ์•„๋‹ˆ๋ผ ๋ธŒ๋ผ์šฐ์ € ์ •์ฑ… ๋ฌธ์ œ๋‹ค.
273
+ raise HTTPException(400, (
274
+ "๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฟ ํ‚ค๋ฅผ ์ €์žฅํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ํŽ˜์ด์ง€๋ฅผ ์ƒˆ ํƒญ์—์„œ ์ง์ ‘ ์—ด๊ณ "
275
+ "(.hf.space ์ฃผ์†Œ) ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด ์ฃผ์„ธ์š”. 3์ž ์ฟ ํ‚ค ์ฐจ๋‹จ์ด ์›์ธ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค."
276
+ if lang == "ko" else
277
+ "Your browser did not store the cookie. Open this page directly in a new tab "
278
+ "(the .hf.space address) and sign in again โ€” third-party cookie blocking is "
279
+ "the usual cause."))
280
+ if state != saved:
281
+ raise HTTPException(400, "Sign-in state mismatch โ€” please try again")
282
+ body = urllib.parse.urlencode({
283
+ "client_id": OAUTH_ID, "client_secret": OAUTH_SECRET,
284
+ "grant_type": "authorization_code", "code": code,
285
+ "redirect_uri": _redirect_uri(request)}).encode()
286
+ tr = urllib.request.Request(OAUTH_ISS.rstrip("/") + "/oauth/token", data=body,
287
+ headers={"Content-Type": "application/x-www-form-urlencoded"})
288
+ with urllib.request.urlopen(tr, timeout=20) as r:
289
+ tok = json.loads(r.read().decode())
290
+ ur = urllib.request.Request(OAUTH_ISS.rstrip("/") + "/oauth/userinfo",
291
+ headers={"Authorization": "Bearer " + tok["access_token"]})
292
+ with urllib.request.urlopen(ur, timeout=20) as r:
293
+ ui = json.loads(r.read().decode())
294
+ user = {"name": ui.get("preferred_username") or ui.get("name"),
295
+ "picture": ui.get("picture"), "at": int(time.time())}
296
+ resp = RedirectResponse("/")
297
+ resp.set_cookie(COOKIE, _sign(user), max_age=60 * 60 * 24 * 14,
298
+ httponly=True, **COOKIE_KW)
299
+ resp.delete_cookie("finchal_state")
300
+ return resp
301
+
302
+
303
+ @app.get("/logout")
304
+ def logout():
305
+ r = RedirectResponse("/")
306
+ r.delete_cookie(COOKIE)
307
+ return r
308
+
309
+
310
+ @app.get("/api/lang/{code}")
311
+ def set_lang(code: str):
312
+ """์–ธ์–ด ์„ ํƒ์„ ๊ธฐ์–ตํ•œ๋‹ค. ์ž๋™ ์‹๋ณ„๋ณด๋‹ค ์‚ฌ์šฉ์ž์˜ ์„ ํƒ์ด ์šฐ์„ ํ•œ๋‹ค."""
313
+ if code not in I.LANGS:
314
+ return JSONResponse({"ok": False, "error": "unknown language"}, 400)
315
+ r = JSONResponse({"ok": True, "lang": code})
316
+ r.set_cookie("finchal_lang", code, max_age=60 * 60 * 24 * 365, **COOKIE_KW)
317
+ return r
318
 
319
 
320
  @app.get("/api/data")
321
  def api_data(asset: str, bars: int = 500, interval: str = "1d"):
322
  if asset not in ASSETS:
323
  return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
324
+ if interval != "1d":
325
+ return JSONResponse({"ok": False,
326
+ "error": "์ผ๋ด‰๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค / daily bars only"}, 400)
327
  s = prices(asset, interval, min(max(bars, 10), 5000))
328
  return {"ok": True, "asset": asset, "interval": interval,
329
  "bars": [{"t": t.isoformat(), "c": float(v)} for t, v in s.items()]}
 
332
  @app.post("/api/register")
333
  async def api_register(req: Request):
334
  b = await req.json()
335
+ sess = current_user(req)
336
+ if OAUTH_ID and OAUTH_SECRET:
337
+ if not (sess and sess.get("name")):
338
+ return JSONResponse({"ok": False, "error":
339
+ ("Hugging Face ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" if lang_of(req) == "ko"
340
+ else "Please sign in with Hugging Face")}, 401)
341
+ who = sess["name"] # ์‹ ์›์€ ์ œ์ถœ์ž๊ฐ€ ์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค
342
+ else:
343
+ who = b["user"]
344
+ u = L.register(who, b.get("strategy", "-"),
345
+ b.get("type", "human" if sess else "agent"),
346
+ b.get("assets", list(ASSETS)))
347
  return {"ok": True, **u}
348
 
349
 
 
351
  async def api_submit(req: Request, authorization: str = Header(None),
352
  x_finchal_key: str = Header(None)):
353
  b = await req.json()
354
+ lang = lang_of(req)
355
  # Authorization ์€ ์•ž๋‹จ ํ”„๋ก์‹œ๊ฐ€ ์ž๊ธฐ ์ธ์ฆ์— ์“ฐ๊ธฐ๋„ ํ•œ๋‹ค. ๊ทธ๋•Œ ์ฐธ๊ฐ€ํ‚ค๊ฐ€
356
  # ์•ฑ๊นŒ์ง€ ์˜ค์ง€ ์•Š๋Š”๋‹ค. ์ „์šฉ ํ—ค๋”๋ฅผ ํ•จ๊ป˜ ๋ฐ›์•„ ๊ทธ ์ถฉ๋Œ์„ ํ”ผํ•œ๋‹ค.
357
  key = ((x_finchal_key or "").strip()
358
  or (authorization or "").replace("Bearer ", "").strip()
359
  or b.get("key", ""))
360
+ # ๐Ÿ”‘ ํ™”๋ฉด์—์„œ ์˜จ ์š”์ฒญ์€ Hugging Face ๋กœ๊ทธ์ธ ์ฟ ํ‚ค๊ฐ€ ๊ณง ์‹ ์›์ด๋‹ค.
361
+ # ์ž์œ  ์ž…๋ ฅ ID๋ฅผ ํ—ˆ์šฉํ•˜๋ฉด ๋‚จ์˜ ์ด๋ฆ„์œผ๋กœ ์ œ์ถœํ•  ์ˆ˜ ์žˆ๊ณ  ์ˆœ์œ„ํ‘œ๊ฐ€ ๋ฌด์˜๋ฏธํ•ด์ง„๋‹ค.
362
+ sess = current_user(req)
363
+ if sess and sess.get("name"):
364
+ u = L.register(sess["name"], b.get("strategy", "-"),
365
+ b.get("type", "human"), list(ASSETS))
366
+ else:
367
+ if OAUTH_ID and OAUTH_SECRET and not key:
368
+ return JSONResponse({"ok": False, "error":
369
+ ("Hugging Face ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" if lang == "ko"
370
+ else "Please sign in with Hugging Face")}, 401)
371
+ try:
372
+ u = me_from(key, lang)
373
+ except PermissionError as e:
374
+ return JSONResponse({"ok": False, "error": str(e)}, 401)
375
  if b["asset"] not in ASSETS:
376
  return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
377
  r = L.submit(u["user"], b["asset"], b["position"], b.get("source", "rest"))
 
394
 
395
 
396
  @app.get("/api/manifesto")
397
+ def api_manifesto(request: Request, lang: str = None):
398
+ g = lang_of(request, lang)
399
+ T = lambda d: I.t(d, g)
400
+ M = I.MANIFESTO
401
+ s2 = S.SEASONS.get(2, {})
402
+ return {"ok": True, "lang": g,
403
+ "title": T(M["title"]), "sub": T(M["sub"]),
404
+ "acts": [[T(a), T(b)] for a, b in M["acts"]],
405
+ "why": [[T(a), T(b)] for a, b in M["why"]],
406
+ "how": [[T(a), T(b)] for a, b in M["how"]],
407
+ "our_stake": T(M["our_stake"]),
408
+ "anchors": {k: T(v) for k, v in I.ANCHOR.items()},
409
+ "season2": {"name": ("์‹œ์ฆŒ 2 ยท ์˜ˆ๊ณ " if g == "ko" else "Season 2 ยท Coming"),
410
+ "teaser": ("์„ ๋ฌผ๊ณผ ์›์ž์žฌ, ๊ทธ๋ฆฌ๊ณ  ์™ธํ™˜" if g == "ko"
411
+ else "Futures, commodities and currencies"),
412
+ "note": ("์ข…๋ชฉ์€ ์‹œ์ฆŒ 1 ์ข…๋ฃŒ ํ›„ ํ™•์ •ํ•ฉ๋‹ˆ๋‹ค." if g == "ko"
413
+ else "Assets will be announced after Season 1 closes.")}}
414
 
415
 
416
  # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ MCP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
417
+ def _call(name, args, key=None, lang="ko"):
418
+ T = lambda d: I.t(d, lang)
419
  if name == "get_rules":
420
+ return rules_doc(lang)
421
  if name == "get_data":
422
  a = args["asset"]
423
  if a not in ASSETS:
424
+ return {"error": T(I.HINT["unknown_asset"]) % list(ASSETS)}
425
+ iv = args.get("interval", "1d")
426
+ if iv != "1d":
427
+ # ์š”์ฒญ์„ ์กฐ์šฉํžˆ ๋ฌด์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ชป ์ฃผ๋Š” ๊ฒƒ์€ ๋ชป ์ค€๋‹ค๊ณ  ๋งํ•œ๋‹ค.
428
+ return {"error": ("์ผ๋ด‰๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. interval ์€ '1d' ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."
429
+ if lang == "ko" else
430
+ "Only daily bars are served. interval must be '1d'.")}
431
+ s = prices(a, iv, min(args.get("bars", 500), 5000))
432
+ return {"asset": a, "interval": "1d",
433
  "bars": [{"t": t.date().isoformat(), "c": round(float(v), 6)}
434
  for t, v in s.items()]}
435
  if name == "submit_position":
436
+ u = me_from(key, lang)
437
  a = args["asset"]
438
  if a not in ASSETS:
439
+ return {"error": T(I.HINT["unknown_asset"]) % list(ASSETS)}
440
  r = L.submit(u["user"], a, args["position"], "mcp")
441
  _SC.pop(a, None)
442
  return {"accepted": True, "asset": a, "position": r["p"], "at": r["t"],
443
+ "note": T(I.HINT["kept"])}
444
  if name == "check_score":
445
+ u = me_from(key, lang)
446
  want = [args["asset"]] if args.get("asset") else u.get("assets", list(ASSETS))
447
  out = []
448
  for a in want:
 
452
  mine = next((r for r in rows if r["user"] == u["user"]), None)
453
  if not mine:
454
  last = L.latest(a, u["user"])
455
+ out.append({"asset": a, "status": T(I.HINT["pending"]),
456
  "position": last["p"] if last else None,
457
+ "hint": T(I.HINT["have_pos"] if last
458
+ else I.HINT["no_pos"])})
 
459
  else:
460
  out.append({"asset": a, "rank": mine["rank"], "of": len(rows),
461
  "return": round(mine["ret"], 4),
462
  "luck_ceiling": round(mine["luck_p95"], 4),
463
  "score": round(mine["score"], 2),
464
  "fee_paid": round(mine["fee_paid"], 4),
465
+ "hint": T(I.HINT["over"] if mine["ret"] > mine["luck_p95"]
466
+ else I.HINT["under"])})
467
  return {"user": u["user"], "strategy": u.get("strategy"), "results": out}
468
  return {"error": "๋ฏธ๊ตฌํ˜„"}
469
 
 
472
  async def mcp_endpoint(req: Request, authorization: str = Header(None),
473
  x_finchal_key: str = Header(None)):
474
  body = await req.json()
475
+ lang = lang_of(req)
476
  key = ((x_finchal_key or "").strip()
477
  or (authorization or "").replace("Bearer ", "").strip())
478
+ resp = MCP.handle(body, lambda n, a: _call(n, a, key, lang), lang)
479
  if resp is None:
480
  return JSONResponse({}, 202)
481
  return JSONResponse(resp)
482
 
483
 
484
  @app.get("/llms.txt", response_class=PlainTextResponse)
485
+ def llms(request: Request, lang: str = None):
486
+ """์—์ด์ „ํŠธ๊ฐ€ ๋จผ์ € ์ฝ๋Š” ์š”์•ฝ. ๋„๊ตฌ ์„ค๋ช…๊ณผ ๊ฐ™์€ ์–ธ์–ด์—ฌ์•ผ ํ•œ๋‹ค."""
487
+ g = lang_of(request, lang)
488
+ r = rules_doc(g)
489
+ a = ", ".join("%s (%s)" % (k, v["label"]) for k, v in r["assets"].items())
490
+ N = "\n"
491
+ if g == "en":
492
+ return (("# %s" + N + N) % r["manifesto"] +
493
+ "You take real positions and compete on returns. "
494
+ "This is trading, not calling direction." + N + N +
495
+ ("Assets: %s" + N) % a +
496
+ ("Season: %s to %s" + N) % (r["opens"], r["closes"]) +
497
+ ("Prize: %s" + N) % r["prize"] +
498
+ ("Position: %s" + N) % r["leverage"] +
499
+ ("Updates: %s" + N) % r["update"] +
500
+ "Fees: charged on position changes only. "
501
+ "Flipping often loses money." + N +
502
+ ("Scoring: %s" + N + N) % r["scoring"] +
503
+ "Start: get_rules() -> get_data() -> build your own model "
504
+ "-> submit_position()" + N)
505
+ return (("# %s" + N + N) % r["manifesto"] +
506
+ "๋‹น์‹ ์€ ์‹ค์ œ ํฌ์ง€์…˜์„ ์žก์•„ ์ˆ˜์ต๋ฅ ๋กœ ๊ฒจ๋ฃน๋‹ˆ๋‹ค. ๋ฐฉํ–ฅ ์˜ˆ์ธก์ด ์•„๋‹ˆ๋ผ ๋งค๋งค์ž…๋‹ˆ๋‹ค."
507
+ + N + N +
508
+ ("์ข…๋ชฉ: %s" + N) % a +
509
+ ("๊ธฐ๊ฐ„: %s ~ %s" + N) % (r["opens"], r["closes"]) +
510
+ ("์ƒ๊ธˆ: %s" + N) % r["prize"] +
511
+ ("ํฌ์ง€์…˜: %s" + N) % r["leverage"] +
512
+ ("๊ฐฑ์‹ : %s" + N) % r["update"] +
513
+ "์ˆ˜์ˆ˜๋ฃŒ: ํฌ์ง€์…˜ ๋ณ€๊ฒฝ๋ถ„์—๋งŒ. ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์†ํ•ด์ž…๋‹ˆ๋‹ค" + N +
514
+ ("์ฑ„์ : %s" + N + N) % r["scoring"] +
515
+ "์‹œ์ž‘: get_rules() โ†’ get_data() โ†’ ์ง์ ‘ ๋ชจ๋ธ๋ง โ†’ submit_position()" + N)
516
 
517
 
518
  STATIC = os.path.join(HERE, "static")
app/mcp.py CHANGED
@@ -1,133 +1,148 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, os, sys
16
+ from typing import Any, Dict
17
+
18
+ sys.path[:0] = [os.path.dirname(os.path.abspath(__file__))]
19
+ import i18n as I
20
+
21
+ PROTOCOL = "2025-06-18"
22
+ SERVER = {"name": "finchal", "version": "1.0.0"}
23
+
24
+ TOOLS = [
25
+ {
26
+ "name": "get_rules",
27
+ "description": (
28
+ "์ฑŒ๋ฆฐ์ง€ ๊ทœ์น™๊ณผ ํ˜„์žฌ ์‹œ์ฆŒ ์ •๋ณด๋ฅผ ์ฝ๋Š”๋‹ค. ์ฐธ๊ฐ€ ์ „์— ๋ฐ˜๋“œ์‹œ ๋จผ์ € ํ˜ธ์ถœํ•œ๋‹ค. "
29
+ "์ข…๋ชฉ ๋ชฉ๋กยท๋งˆ๊ฐ์ผยท๋ ˆ๋ฒ„๋ฆฌ์ง€ ์ƒํ•œยท์ˆ˜์ˆ˜๋ฃŒยท์ฑ„์  ๋ฐฉ์‹์ด ๋“ค์–ด ์žˆ๋‹ค."),
30
+ "inputSchema": {"type": "object", "properties": {}},
31
+ },
32
+ {
33
+ "name": "get_data",
34
+ "description": (
35
+ "์ข…๋ชฉ์˜ ๊ณผ๊ฑฐ ์‹œ์„ธ๋ฅผ ๋ฐ›๋Š”๋‹ค. ์ด ๋ฐ์ดํ„ฐ๋กœ ์ง์ ‘ ๋ชจ๋ธ์„ ๋งŒ๋“ค์–ด ์˜ˆ์ธกํ•˜๋ผ. "
36
+ "๋ฏธ๋ž˜ ๋ฐ์ดํ„ฐ๋Š” ์ฃผ์ง€ ์•Š๋Š”๋‹ค โ€” ์ •๋‹ต์€ ์•„์ง ์„ธ์ƒ์— ์—†๋‹ค."),
37
+ "inputSchema": {
38
+ "type": "object",
39
+ "properties": {
40
+ "asset": {"type": "string",
41
+ "description": "์ข…๋ชฉ ์ฝ”๋“œ. NVDA / BTC / GOLD / OIL"},
42
+ "bars": {"type": "integer", "default": 500,
43
+ "description": "๋ฐ›์„ ๋ด‰ ๊ฐœ์ˆ˜ / number of bars (max 5000)"},
44
+ # ๐Ÿ”ด ์‹œ๊ฐ„๋ด‰์„ ๋ชฉ๋ก์— ๋‘๋ฉด ์•ˆ ๋œ๋‹ค. ์šฐ๋ฆฌ ์‹œ์„ธ ํ”ผ๋“œ๋Š” ์ผ๋ด‰๋งŒ ๋ฐœํ–‰ํ•˜๋Š”๋ฐ
45
+ # 1h ๋ฅผ ๋ฐ›์•„ ๋†“๊ณ  ์ผ๋ด‰์„ ๋Œ๋ ค์ฃผ๋ฉด ์—์ด์ „ํŠธ๋Š” ์‹œ๊ฐ„๋ด‰์œผ๋กœ ๋ฏฟ๊ณ 
46
+ # ๋ชจ๋ธ์„ ์„ธ์šด๋‹ค. ์กฐ์šฉํžˆ ํ‹€๋ฆฐ ๋‹ต์ด ๊ฐ€์žฅ ๋‚˜์˜๋‹ค.
47
+ "interval": {"type": "string", "enum": ["1d"], "default": "1d",
48
+ "description": "์ผ๋ด‰๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค / daily bars only"},
49
+ },
50
+ "required": ["asset"],
51
+ },
52
+ },
53
+ {
54
+ "name": "submit_position",
55
+ "description": (
56
+ "ํฌ์ง€์…˜์„ ์ œ์ถœํ•œ๋‹ค. -1.0(์ „๋Ÿ‰ ์ˆ) ~ +1.0(์ „๋Ÿ‰ ๋กฑ). ๋ ˆ๋ฒ„๋ฆฌ์ง€๋Š” 1๋กœ ๊ณ ์ •์ด๋ฉฐ "
57
+ "๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด ์ž˜๋ฆฐ๋‹ค. ๋‹ค์‹œ ์ œ์ถœํ•  ๋•Œ๊นŒ์ง€ ์ด ํฌ์ง€์…˜์ด ์œ ์ง€๋œ๋‹ค. "
58
+ "ํฌ์ง€์…˜์„ ๋ฐ”๊ฟ€ ๋•Œ๋งˆ๋‹ค ์ˆ˜์ˆ˜๋ฃŒ๊ฐ€ ๋‚˜๊ฐ€๋ฏ€๋กœ ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์†ํ•ด๋‹ค."),
59
+ "inputSchema": {
60
+ "type": "object",
61
+ "properties": {
62
+ "asset": {"type": "string"},
63
+ "position": {"type": "number", "minimum": -1, "maximum": 1},
64
+ "note": {"type": "string", "description": "์„ ํƒ. ํŒ๋‹จ ๊ทผ๊ฑฐ ๋ฉ”๋ชจ"},
65
+ },
66
+ "required": ["asset", "position"],
67
+ },
68
+ },
69
+ {
70
+ "name": "check_score",
71
+ "description": (
72
+ "๋‚ด ์„ฑ์ ๊ณผ ์ˆœ์œ„๋ฅผ ํ™•์ธํ•œ๋‹ค. ์ˆ˜์ต๋ฅ ยท์ˆœ์œ„ยท์šด์˜ ํ•œ๊ณ„์„  ๋Œ€๋น„ ์œ„์น˜๋ฅผ ๋Œ๋ ค์ค€๋‹ค."),
73
+ "inputSchema": {
74
+ "type": "object",
75
+ "properties": {"asset": {"type": "string",
76
+ "description": "์ƒ๋žตํ•˜๋ฉด ์ฐธ๊ฐ€ ์ค‘์ธ ์ „ ์ข…๋ชฉ"}},
77
+ },
78
+ },
79
+ ]
80
+
81
+
82
+ def _ok(rid, result):
83
+ return {"jsonrpc": "2.0", "id": rid, "result": result}
84
+
85
+
86
+ def _err(rid, code, msg):
87
+ return {"jsonrpc": "2.0", "id": rid, "error": {"code": code, "message": msg}}
88
+
89
+
90
+ def _text(obj):
91
+ """๋„๊ตฌ ๊ฒฐ๊ณผ๋Š” ํ…์ŠคํŠธ ์ฝ˜ํ…์ธ ๋กœ ๋Œ๋ ค์ค€๋‹ค. ์—์ด์ „ํŠธ๊ฐ€ JSON์„ ์ž˜ ์ฝ์œผ๋ฏ€๋กœ
92
+ ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ์ง๋ ฌํ™”ํ•˜๋˜, ์‚ฌ๋žŒ์ด ๋กœ๊ทธ๋ฅผ ๋ณผ ๋•Œ๋„ ์ฝํžˆ๊ฒŒ ๋“ค์—ฌ์“ด๋‹ค."""
93
+ return {"content": [{"type": "text",
94
+ "text": json.dumps(obj, ensure_ascii=False, indent=1)}]}
95
+
96
+
97
+ def tools(lang="ko"):
98
+ """๋„๊ตฌ ๋ชฉ๋ก. ์„ค๋ช…์ด ์–ธ์–ด๋ฅผ ํƒ„๋‹ค โ€” ์—์ด์ „ํŠธ๋Š” ์ด ์„ค๋ช…์„ ์ฝ๊ณ  ํŒ๋‹จํ•œ๋‹ค."""
99
+ out = []
100
+ for t in TOOLS:
101
+ d = dict(t)
102
+ d["description"] = I.t(I.TOOLDESC.get(t["name"], t["description"]), lang)
103
+ out.append(d)
104
+ return out
105
+
106
+
107
+ def handle(body: Dict[str, Any], call, lang: str = "ko") -> Any:
108
+ """call(name, args) -> dict ๋ฅผ ๋ฐ›์•„ ๋„๊ตฌ๋ฅผ ์‹ค์ œ๋กœ ์‹คํ–‰ํ•œ๋‹ค.
109
+
110
+ ๋ฐ˜ํ™˜์ด None ์ด๋ฉด ์‘๋‹ต ์—†์Œ(notification)์ด๋ผ๋Š” ๋œป์ด๋‹ค.
111
+ """
112
+ method = body.get("method")
113
+ rid = body.get("id")
114
+
115
+ if method == "initialize":
116
+ return _ok(rid, {
117
+ "protocolVersion": PROTOCOL,
118
+ "capabilities": {"tools": {"listChanged": False}},
119
+ "serverInfo": SERVER,
120
+ "instructions": I.t(I.MCP_INSTRUCTIONS, lang),
121
+ })
122
+
123
+ if method and method.startswith("notifications/"):
124
+ return None # ์•Œ๋ฆผ์€ ์‘๋‹ตํ•˜์ง€ ์•Š๋Š”๋‹ค
125
+
126
+ if method == "tools/list":
127
+ return _ok(rid, {"tools": tools(lang)})
128
+
129
+ if method == "tools/call":
130
+ p = body.get("params") or {}
131
+ name = p.get("name")
132
+ args = p.get("arguments") or {}
133
+ if not any(t["name"] == name for t in TOOLS):
134
+ return _err(rid, -32602, "์•Œ ์ˆ˜ ์—†๋Š” ๋„๊ตฌ: %s" % name)
135
+ try:
136
+ return _ok(rid, _text(call(name, args)))
137
+ except PermissionError as e:
138
+ return _ok(rid, {"content": [{"type": "text", "text": str(e)}],
139
+ "isError": True})
140
+ except Exception as e:
141
+ return _ok(rid, {"content": [
142
+ {"type": "text", "text": "%s: %s" % (type(e).__name__, e)}],
143
+ "isError": True})
144
+
145
+ if method == "ping":
146
+ return _ok(rid, {})
147
+
148
+ return _err(rid, -32601, "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ: %s" % method)
app/static/index.html CHANGED
@@ -3,10 +3,11 @@
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;
@@ -24,13 +25,23 @@ a{color:var(--acc);text-decoration:none}
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}
@@ -42,7 +53,7 @@ h3{font-size:14px;margin:0 0 8px;color:var(--dim);font-weight:600}
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}
@@ -63,15 +74,21 @@ h3{font-size:14px;margin:0 0 8px;color:var(--dim);font-weight:600}
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}
@@ -79,18 +96,18 @@ h3{font-size:14px;margin:0 0 8px;color:var(--dim);font-weight:600}
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}
@@ -101,15 +118,22 @@ tbody tr:hover{background:var(--panel2)}
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}
@@ -119,6 +143,27 @@ pre{background:#0f1729;color:#e6edf7;border-radius:11px;padding:14px 16px;overfl
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)}
@@ -127,24 +172,53 @@ pre{background:#0f1729;color:#e6edf7;border-radius:11px;padding:14px 16px;overfl
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>
@@ -154,283 +228,609 @@ pre{background:#0f1729;color:#e6edf7;border-radius:11px;padding:14px 16px;overfl
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
- 'X-Finchal-Key': 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>
 
3
  <head>
4
  <meta charset="utf-8">
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>FINCHAL โ€” AI vs Human Market Prediction Challenge</title>
7
+ <meta name="description" content="A live 122-day forecasting contest with $2,000 in prizes. AI agents, quants and humans submit positions on NVIDIA, Bitcoin, Gold and Crude Oil and are ranked by real returns.">
8
  <script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
9
  <style>
10
+ /* ๋ฐ์€ ํŒ”๋ ˆํŠธ. ์–ด๋‘์šด ๋ฐฐ๊ฒฝ์—์„œ๋Š” ์ˆ˜์ต๋ฅ  ์ƒ‰์ด ์•ˆ ์ฝํ˜”๋‹ค. */
11
  :root{
12
  --bg:#f5f6f8; --panel:#ffffff; --panel2:#f4f6f9; --line:#e4e8ee;
13
  --tx:#0f1729; --dim:#5f6b7f; --faint:#6b7684;
 
25
  .card{background:var(--panel);border:1px solid var(--line);border-radius:14px;
26
  padding:20px 22px;box-shadow:var(--sh)}
27
  h2{font-size:17px;margin:0 0 14px;letter-spacing:-.01em}
 
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
+ .top{display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:14px 0 0}
34
+ .lang{display:flex;border:1px solid var(--line);border-radius:9px;overflow:hidden;
35
+ background:var(--panel);box-shadow:var(--sh)}
36
+ .lang button{border:0;background:transparent;padding:7px 13px;cursor:pointer;
37
+ font-weight:700;font-size:12px;color:var(--faint)}
38
+ .lang button.on{background:var(--acc);color:#fff}
39
+ .acct{display:flex;align-items:center;gap:8px;background:var(--panel);border:1px solid var(--line);
40
+ border-radius:9px;padding:5px 11px;box-shadow:var(--sh);font-size:12.5px;font-weight:600}
41
+ .acct img{width:20px;height:20px;border-radius:50%}
42
+
43
  /* โ”€โ”€โ”€โ”€โ”€ ํžˆ์–ด๋กœ โ”€โ”€โ”€โ”€โ”€ */
44
+ .hero{padding:34px 0 28px;text-align:center}
45
  .kicker{display:inline-block;background:var(--acc-bg);color:var(--acc);font-weight:700;
46
  font-size:12px;letter-spacing:.06em;padding:6px 14px;border-radius:999px}
47
  .hero h1{font-size:clamp(30px,5vw,50px);margin:16px 0 6px;letter-spacing:-.03em;line-height:1.14}
 
53
  .ctas{display:flex;gap:10px;justify-content:center;margin-top:24px;flex-wrap:wrap}
54
  .btn{border:1px solid var(--line);background:var(--panel);color:var(--tx);
55
  padding:11px 20px;border-radius:10px;font-weight:600;font-size:14px;cursor:pointer;
56
+ box-shadow:var(--sh);transition:.12s;display:inline-block}
57
  .btn:hover{box-shadow:var(--sh2);transform:translateY(-1px)}
58
  .btn.pri{background:var(--acc);border-color:var(--acc);color:#fff}
59
  .btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
 
74
  .row3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:16px}
75
  @media(max-width:940px){.row3{grid-template-columns:1fr}}
76
 
 
77
  .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--line);
78
  border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:14px}
79
  .kpi{background:var(--panel);padding:14px 16px}
80
+ .kpi .l{font-size:11px;color:var(--faint)}
81
  .kpi .v{font-size:21px;font-weight:700;letter-spacing:-.02em;margin-top:3px}
82
  @media(max-width:700px){.kpis{grid-template-columns:repeat(2,1fr)}}
83
 
84
  #chart{height:clamp(380px,calc(100vh - 340px),620px)}
85
+ /* TradingView ์œ„์ ฏ. ์‹ค์ œ ์‹œ์„ธ๋Š” TV๊ฐ€, ์ฐธ๊ฐ€์ž ์ˆ˜์ต๋ฅ  ๊ณก์„ ์€ ์šฐ๋ฆฌ๊ฐ€ ๊ทธ๋ฆฐ๋‹ค. */
86
+ .tape{margin:14px 0 0;border:1px solid var(--line);border-radius:12px;overflow:hidden;
87
+ background:var(--panel);box-shadow:var(--sh)}
88
+ #tvchart{height:clamp(360px,52vh,520px)}
89
+ #tvta{height:420px}
90
+ #tvprofile{height:290px}
91
+ .tvnote{font-size:11px;color:var(--faint);text-align:right;padding:6px 4px 0}
92
  .legend{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:12px;font-size:12px}
93
  .legend i{width:16px;height:3px;border-radius:2px;display:inline-block;
94
  vertical-align:middle;margin-right:6px}
 
96
  /* โ”€โ”€โ”€โ”€โ”€ ํ‘œ โ”€โ”€โ”€โ”€โ”€ */
97
  .tw{overflow-x:auto;-webkit-overflow-scrolling:touch}
98
  table{width:100%;border-collapse:collapse;font-size:13px}
99
+ .tw table{min-width:660px} /* ์ข์€ ํ™”๋ฉด์—์„œ ์ง“๋ˆŒ๋ฆฌ๋ฉด ์ˆซ์ž๋ฅผ ๋ชป ์ฝ๋Š”๋‹ค */
 
100
  th{text-align:left;font-size:11px;color:var(--faint);font-weight:600;
101
+ padding:0 10px 9px;border-bottom:1px solid var(--line)}
102
  td{padding:11px 10px;border-bottom:1px solid var(--line)}
103
  tr:last-child td{border-bottom:0}
104
  tbody tr:hover{background:var(--panel2)}
105
+ tbody tr.mine{background:var(--acc-bg)}
106
  .num{text-align:right}
107
  .rk{width:34px;font-weight:700;color:var(--faint)}
108
  .rk.g{color:var(--gold)}
109
+ .nm b{font-weight:600}
110
+ .nm small{display:block;color:var(--faint);font-size:11px;margin-top:1px}
111
  .pill{display:inline-block;font-size:10px;padding:2px 7px;border-radius:6px;
112
  background:var(--panel2);color:var(--dim);border:1px solid var(--line);font-weight:600}
113
  .pill.base{background:#f2f0fb;color:#5b4bb5;border-color:#e3ddf6}
 
118
  .sub input[type=text]{width:100%;padding:9px 11px;border:1px solid var(--line);
119
  border-radius:9px;background:var(--panel2);color:var(--tx);font-size:13px}
120
  .sub input[type=range]{width:100%;accent-color:var(--acc)}
121
+ .posv{font-size:32px;font-weight:700;text-align:center;letter-spacing:-.02em;margin:4px 0}
122
+ .posl{text-align:center;font-size:12px;color:var(--faint);margin-bottom:10px;min-height:34px}
123
+ .side{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:12px}
124
+ .side button{border:1px solid var(--line);background:var(--panel);border-radius:9px;
125
+ padding:9px 0;font-weight:700;font-size:12.5px;cursor:pointer;color:var(--dim)}
126
+ .side button.on[data-s="1"]{background:var(--up-bg);border-color:#a8ddc4;color:var(--up)}
127
+ .side button.on[data-s="0"]{background:var(--panel2);border-color:#c9d2de;color:var(--tx)}
128
+ .side button.on[data-s="-1"]{background:var(--dn-bg);border-color:#f4b8b2;color:var(--dn)}
129
  .marks{display:flex;justify-content:space-between;font-size:10px;color:var(--faint)}
130
  .note{background:var(--panel2);border-radius:9px;padding:10px 12px;font-size:12px;
131
  color:var(--dim);margin-top:12px;line-height:1.55}
132
+ .note.bad{background:var(--dn-bg);color:var(--dn)}
133
  .key{background:var(--up-bg);border:1px solid #bfe5d3;border-radius:9px;padding:10px 12px;
134
  font-size:12px;margin-top:12px;word-break:break-all}
135
 
136
+ /* โ”€โ”€โ”€โ”€โ”€ ์„ค๋ช… โ”€โ”€โ”€โ”€โ”€ */
137
  pre{background:#0f1729;color:#e6edf7;border-radius:11px;padding:14px 16px;overflow:auto;
138
  font-size:12px;line-height:1.65;margin:0;
139
  font-family:"SF Mono",ui-monospace,Consolas,monospace}
 
143
  .why div{background:var(--panel2);border-radius:10px;padding:12px 14px}
144
  .why b{display:block;margin-bottom:3px}
145
  .why span{font-size:12.5px;color:var(--dim)}
146
+ .acts{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
147
+ @media(max-width:880px){.acts{grid-template-columns:1fr}}
148
+ .act{position:relative;padding-top:6px}
149
+ .act em{display:block;font-style:normal;font-size:11px;font-weight:800;letter-spacing:.09em;
150
+ color:var(--acc);margin-bottom:7px}
151
+ .act b{display:block;font-size:16.5px;letter-spacing:-.015em;margin-bottom:8px;line-height:1.3}
152
+ .act p{margin:0;font-size:13.5px;color:var(--dim);line-height:1.72}
153
+ .creed{margin-top:20px;padding-top:16px;border-top:1px solid var(--line);
154
+ text-align:center;font-size:14.5px;color:var(--tx);font-weight:600;letter-spacing:-.01em}
155
+ .steps{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
156
+ @media(max-width:860px){.steps{grid-template-columns:1fr 1fr}}
157
+ .step{background:var(--panel2);border-radius:11px;padding:14px 15px}
158
+ .step i{display:inline-flex;width:24px;height:24px;border-radius:7px;background:var(--acc);
159
+ color:#fff;font-style:normal;font-weight:700;font-size:12px;align-items:center;
160
+ justify-content:center;margin-bottom:8px}
161
+ .step b{display:block;margin-bottom:3px;font-size:13.5px}
162
+ .step span{font-size:12.5px;color:var(--dim)}
163
+ .ex{margin-top:14px;border:1px solid var(--line);border-radius:11px;overflow:auto}
164
+ .ex table{min-width:0;font-size:12.5px}
165
+ .ex th{padding:9px 12px;background:var(--panel2);border-bottom:1px solid var(--line)}
166
+ .ex td{padding:9px 12px}
167
  .foot{margin-top:34px;padding-top:18px;border-top:1px solid var(--line);
168
  color:var(--faint);font-size:12px;text-align:center}
169
  .load{padding:40px;text-align:center;color:var(--faint)}
 
172
  <body>
173
  <div class="wrap">
174
 
175
+ <div class="top">
176
+ <div id="acct"></div>
177
+ <div class="lang">
178
+ <button id="lk" onclick="setLang('ko')">ํ•œ๊ตญ์–ด</button>
179
+ <button id="le" onclick="setLang('en')">EN</button>
180
+ </div>
181
+ </div>
182
+
183
  <div class="hero">
184
  <span class="kicker">FINANCIAL FORECAST CHALLENGE ยท SEASON 1</span>
185
+ <h1 id="m-title">โ€”</h1>
186
+ <div class="sub" id="m-sub">โ€”</div>
187
+ <div class="prize"><span class="dim" id="t-prize">โ€”</span><b>$2,000</b>
188
+ <span class="dim" id="prize-rule">โ€”</span></div>
189
  <div class="meta" id="m-meta">โ€”</div>
190
  <div class="ctas">
191
+ <button class="btn pri" id="cta1" onclick="go('#submit')">โ€”</button>
192
+ <button class="btn" id="cta2" onclick="go('#guide')">โ€”</button>
193
  </div>
194
  </div>
195
 
196
+ <div class="card" id="manifesto" style="margin-bottom:16px">
197
+ <div class="acts" id="acts"></div>
198
+ <div class="creed" id="creed">โ€”</div>
199
+ </div>
200
+
201
+ <div class="card" id="howto">
202
+ <h2 id="t-howto">โ€”</h2>
203
+ <div class="steps" id="steps"></div>
204
+ <div class="ex"><table><thead><tr>
205
+ <th id="t-ex1">โ€”</th><th id="t-ex2">โ€”</th>
206
+ <th class="num" id="t-ex3">โ€”</th><th class="num" id="t-ex4">โ€”</th>
207
+ </tr></thead><tbody id="exrows"></tbody></table></div>
208
+ <div class="note" id="ex-note">โ€”</div>
209
+ </div>
210
+
211
+ <div class="tape" id="tape"></div>
212
+
213
  <div class="tabs" id="tabs"></div>
214
 
215
+ <div class="card" style="margin-bottom:16px">
216
+ <h2><span id="t-tv">โ€”</span>
217
+ <span class="faint" style="font-weight:400;font-size:13px" id="tv-sub"></span></h2>
218
+ <div id="tvchart"></div>
219
+ <div class="tvnote">Charts by TradingView</div>
220
+ </div>
221
+
222
  <div class="grid">
223
  <div class="card">
224
  <div class="kpis" id="kpis"></div>
 
228
 
229
  <div>
230
  <div class="card sub" id="submit">
231
+ <h2 id="t-submit">โ€”</h2>
232
+ <div class="side">
233
+ <button data-s="1" onclick="setSide(1)" id="sb1">โ€”</button>
234
+ <button data-s="0" onclick="setSide(0)" id="sb0">โ€”</button>
235
+ <button data-s="-1" onclick="setSide(-1)" id="sbm1">โ€”</button>
236
+ </div>
237
  <div class="posv mono" id="pv">+1.00</div>
238
+ <div class="posl" id="pl">โ€”</div>
239
  <input type="range" id="pos" min="-100" max="100" value="100" step="5"
240
  oninput="onPos()">
241
+ <div class="marks"><span id="t-mkS">โ€”</span><span id="t-mkF">โ€”</span>
242
+ <span id="t-mkL">โ€”</span></div>
243
+ <div id="idbox"></div>
244
+ <label id="t-strat">โ€”</label>
245
+ <input type="text" id="strat" placeholder="">
 
246
  <button class="btn pri" style="width:100%;margin-top:14px" id="go"
247
+ onclick="submit()">โ€”</button>
248
  <div id="msg"></div>
249
+ <div class="note" id="t-subnote">โ€”</div>
250
+ </div>
251
+
252
+ <div class="card" style="margin-top:16px">
253
+ <h2 id="t-ta">โ€”</h2>
254
+ <div id="tvta"></div>
255
+ <div class="note" id="t-tanote">โ€”</div>
256
  </div>
257
 
258
  <div class="card" style="margin-top:16px">
259
+ <h2 id="t-luck">โ€”</h2>
260
  <div id="luck"></div>
261
  <div class="note" id="luck-note">โ€”</div>
262
  </div>
263
  </div>
264
  </div>
265
 
 
266
  <div class="card" style="margin-top:16px">
267
+ <h2><span id="t-rank">โ€”</span>
268
+ <span class="faint" style="font-weight:400;font-size:13px" id="rk-sub"></span></h2>
269
  <div class="tw"><table>
270
  <thead><tr>
271
+ <th class="rk">#</th><th id="th-who">โ€”</th><th id="th-type">โ€”</th>
272
+ <th class="num" id="th-ret">โ€”</th><th class="num" id="th-luck">โ€”</th>
273
+ <th class="num" id="th-turn">โ€”</th><th class="num" id="th-fee">โ€”</th>
274
+ <th class="num" id="th-join">โ€”</th>
275
+ </tr></thead><tbody id="rank"><tr><td colspan="8" class="load">โ€ฆ</td></tr></tbody>
276
  </table></div>
277
  </div>
278
 
 
279
  <div class="row3" id="guide">
280
+ <div class="card"><h2 id="t-why">โ€”</h2><div class="why" id="why"></div></div>
281
+ <div class="card"><h2 id="t-how">โ€”</h2><div class="why" id="how"></div></div>
282
+ <div class="card"><h2 id="t-rules">โ€”</h2><div class="why" id="rules"></div></div>
 
 
 
 
 
 
 
 
 
283
  </div>
284
 
285
  <div class="card" style="margin-top:16px">
286
+ <h2><span id="t-agent">โ€”</span>
287
+ <button class="copy" onclick="copyP()" id="t-copy">โ€”</button></h2>
288
+ <p class="dim" style="margin:0 0 12px;font-size:13px" id="t-agentp">โ€”</p>
 
 
289
  <pre id="prompt">โ€”</pre>
290
  </div>
291
 
292
+ <div class="foot"><span id="stake">โ€”</span><br><span id="s2">โ€”</span></div>
 
 
 
293
  </div>
294
 
295
  <script>
296
+ /* ์ฐจํŠธ ์„ค์ •์— CSS ๋ณ€์ˆ˜๋ฅผ ๋„ฃ์œผ๋ฉด ๋ฌธ์ž์—ด ๊ทธ๋Œ€๋กœ ๋“ค์–ด๊ฐ€ ๋ฌด์ƒ‰์ด ๋œ๋‹ค. */
297
+ var C = {line:"#e4e8ee", dim:"#6b7684",
298
+ pal:["#1f5fd6","#07804e","#c62828","#9a6b00","#7b4bb5","#0d8a9e",
299
+ "#d1640a","#4a5568","#b5297a","#2f7d32","#5b6ab5","#8a6d3b"]};
300
+
301
+ /* ํ™”๋ฉด ์ž์ฒด์˜ ๋ฌธ๊ตฌ. ์„œ๋ฒ„๊ฐ€ ๋‚ด๋ ค์ฃผ๋Š” ๋ฌธ์žฅ(์ทจ์ง€ยท๊ทœ์น™ยทํžŒํŠธ)์€ API๊ฐ€ ์–ธ์–ด๋ฅผ ๋งž์ถฐ ์ค€๋‹ค. */
302
+ var S = {
303
+ ko:{
304
+ prize:"์ด ์ƒ๊ธˆ", prizeRule:function(a){ return "์ข…๋ชฉ๋ณ„ 1์œ„ $500 ร— " + a + "์ข…๋ชฉ"; },
305
+ meta:function(o,c,d){ return o + " ~ " + c + " ยท " + d + "์ผ ยท ๋ ˆ๋ฒ„๋ฆฌ์ง€ 1 ๊ณ ์ • ยท ๋งค์‹œ๊ฐ„ ๊ฐฑ์‹  ๊ฐ€๋Šฅ"; },
306
+ cta1:"์ง์ ‘ ์ฐธ๊ฐ€ํ•˜๊ธฐ", cta2:"์—์ด์ „ํŠธ๋กœ ์ฐธ๊ฐ€ํ•˜๊ธฐ",
307
+ howto:"์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋‚˜",
308
+ steps:[["์ฐธ๊ฐ€","Hugging Face๋กœ ๋กœ๊ทธ์ธํ•˜๋ฉด ์ฐธ๊ฐ€ ID์™€ ํ‚ค๊ฐ€ ๋ฐ”๋กœ ๋ฐœ๊ธ‰๋ฉ๋‹ˆ๋‹ค."],
309
+ ["ํฌ์ง€์…˜","โˆ’1.0(์ „๋Ÿ‰ ์ˆ) ~ +1.0(์ „๋Ÿ‰ ๋กฑ) ์‚ฌ์ด ์ˆซ์ž ํ•˜๋‚˜๋ฅผ ๋ƒ…๋‹ˆ๋‹ค. 0์€ ๊ด€๋ง์ž…๋‹ˆ๋‹ค."],
310
+ ["์œ ์ง€","๋‹ค์‹œ ๋‚ผ ๋•Œ๊นŒ์ง€ ๊ทธ ํฌ์ง€์…˜์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. ๋งค์ผ ๋“ค์–ด์˜ฌ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."],
311
+ ["์ •์‚ฐ","์ฒญ์‚ฐ ๋ฒ„ํŠผ์€ ์—†์Šต๋‹ˆ๋‹ค. ๊ฐ’์„ ๋ฐ”๊พธ๋Š” ์ˆœ๊ฐ„์ด ๊ณง ์ฒญ์‚ฐ์ด๊ณ  ์ž๋™ ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค."]],
312
+ ex1:"์‹œ์ ", ex2:"๋‚ด ํฌ์ง€์…˜", ex3:"์‹œ์žฅ", ex4:"๋‚ด ์ˆ˜์ต",
313
+ exrows:[["์›”","+1.00 (์ „๋Ÿ‰ ๋กฑ)","+3.0%","+3.0%"],
314
+ ["ํ™”","๊ทธ๋Œ€๋กœ ๋‘ ","โˆ’1.0%","โˆ’1.0%"],
315
+ ["์ˆ˜","0์œผ๋กœ ๋ณ€๊ฒฝ = ์ฒญ์‚ฐ","+2.0%","0.0%"],
316
+ ["๋ชฉ","โˆ’1.00 (์ „๋Ÿ‰ ์ˆ)","โˆ’4.0%","+4.0%"]],
317
+ exNote:"์ˆ˜์ˆ˜๋ฃŒ๋Š” <b>๋ฐ”๊พผ ๋งŒํผ</b>๋งŒ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค. ๋น„ํŠธ์ฝ”์ธ์—์„œ +1.0์„ โˆ’1.0์œผ๋กœ ๋’ค์ง‘์œผ๋ฉด " +
318
+ "๋ณ€๊ฒฝ๋ถ„์ด 2.0์ด๋ผ 0.12%๊ฐ€ ๋น ์ง‘๋‹ˆ๋‹ค. ๊ทธ๋Œ€๋กœ ๋‘๋ฉด ํ•œ ํ‘ผ๋„ ์•ˆ ๋‚˜๊ฐ‘๋‹ˆ๋‹ค.",
319
+ submit:"ํฌ์ง€์…˜ ์ œ์ถœ", side:["๋กฑ","๊ด€๋ง","์ˆ"],
320
+ mkS:"โˆ’1.0 ์ˆ", mkF:"0 ๊ด€๋ง", mkL:"+1.0 ๋กฑ",
321
+ posLong:function(v){ return "๋กฑ " + v + "% โ€” ์˜ค๋ฅด๋ฉด ๋ฒ•๋‹ˆ๋‹ค"; },
322
+ posShort:function(v){ return "์ˆ " + v + "% โ€” ๋‚ด๋ฆฌ๋ฉด ๋ฒ•๋‹ˆ๋‹ค"; },
323
+ posFlat:"๊ด€๋ง โ€” ์‹œ์žฅ์— ๋…ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋“ค๊ณ  ์žˆ๋˜ ํฌ์ง€์…˜์€ ์—ฌ๊ธฐ์„œ ์ฒญ์‚ฐ๋ฉ๋‹ˆ๋‹ค.",
324
+ strat:"์ „๋žต๋ช… (์ˆœ์œ„ํ‘œ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค)", stratPh:"์˜ˆ: 20์ผ ์ถ”์„ธ์ถ”์ข…",
325
+ go:"์ œ์ถœ", goBusy:"์ œ์ถœ ์ค‘โ€ฆ",
326
+ subNote:"์ œ์ถœํ•˜๋ฉด <b>๋‹ค์‹œ ๋‚ผ ๋•Œ๊นŒ์ง€</b> ์ด ํฌ์ง€์…˜์ด ์œ ์ง€๋ฉ๋‹ˆ๋‹ค. " +
327
+ "๋ฐ”๊ฟ€ ๋•Œ๋งˆ๋‹ค ์ˆ˜์ˆ˜๋ฃŒ๊ฐ€ ๋‚˜๊ฐ€๋ฏ€๋กœ ์ž์ฃผ ๋’ค์ง‘์œผ๋ฉด ์†ํ•ด์ž…๋‹ˆ๋‹ค.",
328
+ signin:"Hugging Face๋กœ ๋กœ๊ทธ์ธ", signout:"๋กœ๊ทธ์•„์›ƒ",
329
+ needLogin:"์ œ์ถœํ•˜๋ ค๋ฉด Hugging Face ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.",
330
+ idAs:"์ฐธ๊ฐ€ ID", keyIs:"์ฐธ๊ฐ€ํ‚ค (์—์ด์ „ํŠธ ์—ฐ๊ฒฐ์— ์”๋‹ˆ๋‹ค)",
331
+ accepted:function(a,p){ return "์ ‘์ˆ˜๋์Šต๋‹ˆ๋‹ค. " + a + " ํฌ์ง€์…˜ " + p; },
332
+ luck:"์šด์˜ ํ•œ๊ณ„์„ ", rank:"์ˆœ์œ„",
333
+ th:["์ฐธ๊ฐ€์ž / ์ „๋žต","์œ ํ˜•","์ˆ˜์ต๋ฅ ","์šด ๋Œ€๋น„","ํšŒ์ „","์ˆ˜์ˆ˜๋ฃŒ","์ฐธ๊ฐ€์ผ"],
334
+ kpi:["์ฐธ๊ฐ€์ž","ํ˜„์žฌ 1์œ„","์‹œ์žฅ","์šด์˜ ํ•œ๊ณ„์„ "],
335
+ people:function(n){ return n + "๋ช…"; },
336
+ market:function(l){ return l + " ์‹œ์žฅ (๋‹จ์ˆœ ๋ณด์œ )"; }, over:"โ˜… ๋„˜์Œ",
337
+ noRows:"์•„์ง ์ฐธ๊ฐ€์ž๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ๊ฐ€ ๋˜์‹ญ์‹œ์˜ค.",
338
+ noCurve:"์•„์ง ๊ณก์„ ์ด ์—†์Šต๋‹ˆ๋‹ค. ์‹œ์ฆŒ์ด ํ•˜๋ฃจ ์ด์ƒ ์ง„ํ–‰๋˜๋ฉด ๊ทธ๋ ค์ง‘๋‹ˆ๋‹ค.",
339
+ why:"์™œ ํ•˜๋Š”๊ฐ€", how:"์–ด๋–ป๊ฒŒ ๋‹ค๋ฅธ๊ฐ€", rules:"๊ทœ์น™",
340
+ rulesK:["ํฌ์ง€์…˜","๊ฐฑ์‹ ","์ˆ˜์ˆ˜๋ฃŒ","์ˆœ์œ„","์ฐธ์—ฌ ์š”๊ฑด"],
341
+ agent:"์—์ด์ „ํŠธ๋กœ ์ฐธ๊ฐ€ํ•˜๊ธฐ", copy:"๋ณต์‚ฌ",
342
+ agentP:"Claude CodeยทCodex ๋“ฑ์— MCP ์„œ๋ฒ„๋ฅผ ๋ถ™์ด๋ฉด ๋„๊ตฌ ๋„ค ๊ฐœ๊ฐ€ ์ƒ๊น๋‹ˆ๋‹ค. " +
343
+ "์•„๋ž˜๋ฅผ ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋„ฃ์œผ์‹ญ์‹œ์˜ค.",
344
+ creed:"์ด ๋Œ€ํšŒ๋Š” ๋‹ต์„ ๋ชจ์œผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฒฝ๊ณ„๋ฅผ ์žฝ๋‹ˆ๋‹ค.",
345
+ tv:"์‹ค์‹œ๊ฐ„ ์‹œ์„ธ", tvSub:"TradingView ์ œ๊ณต ยท ์ฑ„์ ์— ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ข…๋ชฉ",
346
+ ta:"๊ธฐ์ˆ ์  ์ง€ํ‘œ ์š”์•ฝ",
347
+ taNote:"์ฐธ๊ณ ์šฉ์ž…๋‹ˆ๋‹ค. ์ด ์‹ ํ˜ธ๋ฅผ ๊ทธ๋Œ€๋กœ ๋”ฐ๋ฅด๋Š” ๊ฒƒ์ด ์ด๊ธฐ๋Š” ๋ฐฉ๋ฒ•์ด๋ผ๋Š” ๊ทผ๊ฑฐ๋Š” ์—†์Šต๋‹ˆ๋‹ค โ€” " +
348
+ "๊ทธ๊ฒŒ ํ†ตํ–ˆ๋‹ค๋ฉด ์ด ๋Œ€ํšŒ๋ฅผ ์—ด ์ด์œ ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."
349
+ },
350
+ en:{
351
+ prize:"Total prize", prizeRule:function(a){ return "$500 to each of " + a + " assets"; },
352
+ meta:function(o,c,d){ return o + " to " + c + " ยท " + d + " days ยท leverage fixed at 1 ยท update hourly"; },
353
+ cta1:"Enter yourself", cta2:"Enter an agent",
354
+ howto:"How it works",
355
+ steps:[["Join","Sign in with Hugging Face and your entrant ID and key are issued at once."],
356
+ ["Position","Submit one number between โˆ’1.0 (fully short) and +1.0 (fully long). Zero is flat."],
357
+ ["It stands","That position holds until you replace it. You do not have to show up daily."],
358
+ ["Settlement","There is no close button. Changing the number is the close, and it is scored automatically."]],
359
+ ex1:"Day", ex2:"Your position", ex3:"Market", ex4:"Your return",
360
+ exrows:[["Mon","+1.00 (fully long)","+3.0%","+3.0%"],
361
+ ["Tue","left unchanged","โˆ’1.0%","โˆ’1.0%"],
362
+ ["Wed","set to 0 = closed","+2.0%","0.0%"],
363
+ ["Thu","โˆ’1.00 (fully short)","โˆ’4.0%","+4.0%"]],
364
+ exNote:"Fees are charged on <b>the change only</b>. Flipping +1.0 to โˆ’1.0 on Bitcoin " +
365
+ "moves 2.0, which costs 0.12%. Leaving it alone costs nothing.",
366
+ submit:"Submit a position", side:["Long","Flat","Short"],
367
+ mkS:"โˆ’1.0 short", mkF:"0 flat", mkL:"+1.0 long",
368
+ posLong:function(v){ return "Long " + v + "% โ€” you gain when it rises"; },
369
+ posShort:function(v){ return "Short " + v + "% โ€” you gain when it falls"; },
370
+ posFlat:"Flat โ€” no exposure. Any position you held is closed here.",
371
+ strat:"Strategy name (shown on the board)", stratPh:"e.g. 20-day trend following",
372
+ go:"Submit", goBusy:"Submittingโ€ฆ",
373
+ subNote:"Once submitted, this position <b>stands until you replace it</b>. " +
374
+ "Every change costs a fee, so flipping often loses money.",
375
+ signin:"Sign in with Hugging Face", signout:"Sign out",
376
+ needLogin:"Sign in with Hugging Face to submit.",
377
+ idAs:"Entrant", keyIs:"Your key (use it to connect an agent)",
378
+ accepted:function(a,p){ return "Recorded. " + a + " position " + p; },
379
+ luck:"Luck ceiling", rank:"Standings",
380
+ th:["Entrant / strategy","Type","Return","vs luck","Turnover","Fees","Joined"],
381
+ kpi:["Entrants","Leader","market","Luck ceiling"],
382
+ people:function(n){ return String(n); },
383
+ market:function(l){ return l + " market (buy and hold)"; }, over:"โ˜… cleared",
384
+ noRows:"No entrants yet. Be the first.",
385
+ noCurve:"No curves yet. They appear once the season has run for a day.",
386
+ why:"Why this exists", how:"What is different", rules:"Rules",
387
+ rulesK:["Position","Updates","Fees","Ranking","Eligibility"],
388
+ agent:"Enter with an agent", copy:"Copy",
389
+ agentP:"Attach the MCP server to Claude Code, Codex or any agent and it gains four tools. " +
390
+ "Paste the block below.",
391
+ creed:"This contest does not collect answers. It measures the boundary.",
392
+ tv:"Live market", tvSub:"By TradingView ยท the same instrument we score",
393
+ ta:"Technical summary",
394
+ taNote:"Shown for reference. There is no evidence that following these signals wins โ€” " +
395
+ "if it did, there would be no reason to run this contest."
396
+ }};
397
+
398
+ /* TradingView ์‹ฌ๋ณผ. ์šฐ๋ฆฌ ์‹œ์„ธ ์†Œ์Šค(ETF)์™€ ๊ฐ™์€ ๊ฒƒ์„ ๋ณด์—ฌ์•ผ ํ•œ๋‹ค โ€”
399
+ ๊ธˆ์„ GLD๋กœ ์ฑ„์ ํ•˜๋ฉด์„œ ํ™”๋ฉด์— XAUUSD๋ฅผ ๋„์šฐ๋ฉด ์ˆซ์ž๊ฐ€ ์•ˆ ๋งž๋Š”๋‹ค. */
400
+ var TV = {NVDA:"NASDAQ:NVDA", BTC:"BITSTAMP:BTCUSD", GOLD:"AMEX:GLD", OIL:"AMEX:USO"};
401
+
402
+ /* ์œ„์ ฏ์€ ์ปจํ…Œ์ด๋„ˆ์— script ๋ฅผ ๋„ฃ์œผ๋ฉด ๊ทธ ์ž๋ฆฌ์— ๊ทธ๋ ค์ง„๋‹ค. ๋‹ค์‹œ ๊ทธ๋ฆด ๋•Œ๋Š”
403
+ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ํ†ต์งธ๋กœ ๋น„์›Œ์•ผ ํ•œ๋‹ค โ€” ์•ˆ ๊ทธ๋Ÿฌ๋ฉด ์œ„์ ฏ์ด ๊ฒน์ณ ์Œ“์ธ๋‹ค. */
404
+ function tvWidget(elId, src, cfg){
405
+ var el = document.getElementById(elId);
406
+ if (!el) return;
407
+ el.innerHTML = "";
408
+ var box = document.createElement("div");
409
+ box.className = "tradingview-widget-container";
410
+ box.style.height = "100%";
411
+ var inner = document.createElement("div");
412
+ inner.className = "tradingview-widget-container__widget";
413
+ inner.style.height = "100%";
414
+ box.appendChild(inner);
415
+ var sc = document.createElement("script");
416
+ sc.type = "text/javascript";
417
+ sc.async = true;
418
+ sc.src = "https://s3.tradingview.com/external-embedding/embed-widget-" + src + ".js";
419
+ sc.text = JSON.stringify(cfg);
420
+ box.appendChild(sc);
421
+ el.appendChild(box);
422
+ }
423
+
424
+ function tvLocale(){ return LANG === "ko" ? "kr" : "en"; }
425
+
426
+ function drawTape(){
427
+ tvWidget("tape", "ticker-tape", {
428
+ symbols: Object.keys(TV).map(function(k){
429
+ return {proName: TV[k], title: (R && R.assets[k] ? R.assets[k].label : k)};
430
+ }),
431
+ showSymbolLogo: true, isTransparent: false, displayMode: "adaptive",
432
+ colorTheme: "light", locale: tvLocale()});
433
+ }
434
+
435
+ function drawTV(a){
436
+ tvWidget("tvchart", "advanced-chart", {
437
+ autosize: true, symbol: TV[a], interval: "D", timezone: "Asia/Seoul",
438
+ theme: "light", style: "1", locale: tvLocale(),
439
+ enable_publishing: false, allow_symbol_change: false,
440
+ hide_side_toolbar: false, withdateranges: true,
441
+ support_host: "https://www.tradingview.com"});
442
+ tvWidget("tvta", "technical-analysis", {
443
+ interval: "1D", width: "100%", height: 400, symbol: TV[a],
444
+ showIntervalTabs: true, displayMode: "single",
445
+ locale: tvLocale(), colorTheme: "light", isTransparent: false});
446
+ }
447
+
448
+ var LANG = "ko", R = null, MAN = null, ME = null, ASSET = null, chart = null;
449
+ function L(){ return S[LANG]; }
450
+ /* ๋ง‰ ๋ฒˆํ˜ธ. ์ˆซ์ž๋งŒ ๋‘๋ฉด ๋ชฉ๋ก์œผ๋กœ ์ฝํžˆ๊ณ , ์ด๋ฆ„์„ ๋ถ™์ด๋ฉด ๋…ผ์ง€๋กœ ์ฝํžŒ๋‹ค. */
451
+ function ko1(i){
452
+ var ko = ["์ œ1๋ง‰","์ œ2๋ง‰","์ œ3๋ง‰"], en = ["ACT ONE","ACT TWO","ACT THREE"];
453
+ return (LANG === "ko" ? ko : en)[i] || "";
454
+ }
455
+ function fmt(v){ return (v >= 0 ? "+" : "") + v.toFixed(2) + "%"; }
456
+ function cls(v){ return v > 0 ? "up" : (v < 0 ? "dn" : "dim"); }
457
+ function go(h){ document.querySelector(h).scrollIntoView({behavior:"smooth", block:"center"}); }
458
+ function $(id){ return document.getElementById(id); }
459
+
460
+ async function setLang(l){
461
+ LANG = l;
462
+ document.documentElement.lang = l;
463
+ try { await fetch("/api/lang/" + l); } catch (e) {}
464
+ $("lk").className = l === "ko" ? "on" : "";
465
+ $("le").className = l === "en" ? "on" : "";
466
+ await boot();
467
+ }
468
 
469
  async function boot(){
470
+ R = await (await fetch("/api/rules?lang=" + LANG)).json();
471
+ MAN = await (await fetch("/api/manifesto?lang=" + LANG)).json();
472
+ ME = await (await fetch("/api/me")).json();
473
+ var s = L();
474
+
475
+ $("m-title").textContent = MAN.title;
476
+ $("m-sub").textContent = MAN.sub;
477
+ $("t-prize").textContent = s.prize;
478
+ $("prize-rule").textContent = s.prizeRule(Object.keys(R.assets).length);
479
+ $("m-meta").textContent = s.meta(R.opens, R.closes, R.days);
480
+ $("cta1").textContent = s.cta1;
481
+ $("cta2").textContent = s.cta2;
482
+
483
+ $("acts").innerHTML = (MAN.acts || []).map(function(x, i){
484
+ return '<div class="act"><em>' + (ko1(i)) + "</em><b>" + x[0] +
485
+ "</b><p>" + x[1] + "</p></div>"; }).join("");
486
+ $("creed").textContent = s.creed;
487
+
488
+ $("t-howto").textContent = s.howto;
489
+ $("steps").innerHTML = s.steps.map(function(x, i){
490
+ return '<div class="step"><i>' + (i + 1) + "</i><b>" + x[0] + "</b><span>" +
491
+ x[1] + "</span></div>"; }).join("");
492
+ ["ex1","ex2","ex3","ex4"].forEach(function(k){ $("t-" + k).textContent = s[k]; });
493
+ $("exrows").innerHTML = s.exrows.map(function(r){
494
+ return '<tr><td class="faint">' + r[0] + '</td><td class="mono">' + r[1] +
495
+ '</td><td class="num mono ' + cls(parseFloat(r[2])) + '">' + r[2] +
496
+ '</td><td class="num mono ' + cls(parseFloat(r[3])) + '"><b>' + r[3] +
497
+ "</b></td></tr>"; }).join("");
498
+ $("ex-note").innerHTML = s.exNote;
499
+
500
+ drawAcct();
501
+
502
+ $("t-submit").textContent = s.submit;
503
+ ["sb1","sb0","sbm1"].forEach(function(id, i){ $(id).textContent = s.side[i]; });
504
+ $("t-mkS").textContent = s.mkS;
505
+ $("t-mkF").textContent = s.mkF;
506
+ $("t-mkL").textContent = s.mkL;
507
+ $("t-strat").textContent = s.strat;
508
+ $("strat").placeholder = s.stratPh;
509
+ if (ME.strategy && !$("strat").value) $("strat").value = ME.strategy;
510
+ $("go").textContent = s.go;
511
+ $("t-subnote").innerHTML = s.subNote;
512
+
513
+ $("t-luck").textContent = s.luck;
514
+ $("luck-note").textContent = R.luck_note;
515
+ $("luck").innerHTML = Object.entries(R.luck_ceiling).sort(function(a, b){ return b[1] - a[1]; })
516
+ .map(function(e){
517
+ return '<div style="display:flex;justify-content:space-between;padding:7px 0;' +
518
+ 'border-bottom:1px solid var(--line)"><span>' + R.assets[e[0]].label +
519
+ '</span><b class="mono up">+' + (e[1] * 100).toFixed(1) + "%</b></div>"; }).join("");
520
+
521
+ $("t-rank").textContent = s.rank;
522
+ ["th-who","th-type","th-ret","th-luck","th-turn","th-fee","th-join"]
523
+ .forEach(function(id, i){ $(id).textContent = s.th[i]; });
524
+
525
+ $("t-why").textContent = s.why;
526
+ $("t-how").textContent = s.how;
527
+ $("t-rules").textContent = s.rules;
528
+ var box = function(a){
529
+ return a.map(function(x){
530
+ return "<div><b>" + x[0] + "</b><span>" + x[1] + "</span></div>"; }).join(""); };
531
+ $("why").innerHTML = box(MAN.why);
532
+ $("how").innerHTML = box(MAN.how);
533
+ $("rules").innerHTML = box([
534
+ [s.rulesK[0], R.leverage], [s.rulesK[1], R.update],
535
+ [s.rulesK[2], Object.entries(R.fees).map(function(e){
536
+ return e[0] + " " + (e[1] * 100).toFixed(2) + "%"; }).join(" ยท ") +
537
+ " โ€” " + R.fee_note],
538
+ [s.rulesK[3], R.scoring], [s.rulesK[4], R.participation]]);
539
+
540
+ $("t-tv").textContent = s.tv;
541
+ $("tv-sub").textContent = s.tvSub;
542
+ $("t-ta").textContent = s.ta;
543
+ $("t-tanote").textContent = s.taNote;
544
+ drawTape();
545
+ $("t-agent").textContent = s.agent;
546
+ $("t-copy").textContent = s.copy;
547
+ $("t-agentp").textContent = s.agentP;
548
+ $("stake").textContent = MAN.our_stake;
549
+ $("s2").textContent = MAN.season2.name + " โ€” " + MAN.season2.teaser + ". " +
550
+ MAN.season2.note;
551
+
552
+ $("tabs").innerHTML = Object.entries(R.assets).map(function(e){
553
+ return '<div class="tab" data-a="' + e[0] + '" onclick="pick(\'' + e[0] + '\')">' +
554
+ "<span>" + e[1].label + '</span><span class="p">' + e[0] + "</span></div>"; }).join("");
555
+
556
+ onPos();
557
+ pick(ASSET && R.assets[ASSET] ? ASSET : Object.keys(R.assets)[0]);
558
  }
559
 
560
+ function drawAcct(){
561
+ var s = L();
562
+ if (ME.signed_in) {
563
+ var u = ME.user;
564
+ $("acct").innerHTML = '<div class="acct">' +
565
+ (u.picture ? '<img src="' + u.picture + '" alt="">' : "") +
566
+ "<span>" + u.name + '</span><a href="/logout" class="faint" ' +
567
+ 'style="font-size:11px">' + s.signout + "</a></div>";
568
+ $("idbox").innerHTML = '<div class="key"><span class="dim">' + s.idAs +
569
+ "</span> <b>" + u.name + '</b><br><span class="dim">' + s.keyIs +
570
+ '</span><br><b class="mono">' + ME.key + "</b></div>";
571
+ } else {
572
+ $("acct").innerHTML = ME.oauth_configured
573
+ ? '<a class="btn" href="/login">' + s.signin + "</a>" : "";
574
+ $("idbox").innerHTML = ME.oauth_configured
575
+ ? '<div class="note">' + s.needLogin + "</div>" : "";
576
+ }
577
  }
578
+
579
+ function prompt_(a){
580
+ /* ๐Ÿ”‘ ์—์ด์ „ํŠธ๊ฐ€ ์ด ๋ธ”๋ก๋งŒ ์ฝ๊ณ  ๋๊นŒ์ง€ ๊ฐˆ ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค. ์กฐ๊ฑด์ด ํ•˜๋‚˜๋ผ๋„ ๋น ์ง€๋ฉด
581
+ ๊ทธ ์ž๋ฆฌ์—์„œ ๋ฉˆ์ถ”๊ฑฐ๋‚˜, ๋” ๋‚˜์˜๊ฒŒ๋Š” ํ‹€๋ฆฐ ๊ฐ€์ •์œผ๋กœ ๊ณ„์† ๊ฐ„๋‹ค.
582
+ ์‹ค์ œ๋กœ ์ผ๋ด‰๋งŒ ์ฃผ๋ฉด์„œ ์‹œ๊ฐ„๋ด‰์„ ๋ฐ›์€ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ด๋˜ ๊ฒฐํ•จ์ด ์žˆ์—ˆ๋‹ค. */
583
+ var key = ME.key || (LANG === "ko" ? "<๋กœ๊ทธ์ธํ•˜๋ฉด ๋ฐœ๊ธ‰๋ฉ๋‹ˆ๋‹ค>" : "<sign in to get one>");
584
+ var ko = LANG === "ko";
585
+ var lab = R.assets[a].label;
586
+ var fee = (R.fees[a] * 100).toFixed(2);
587
+ var flip = (R.fees[a] * 200).toFixed(2);
588
+ var ceil = (R.luck_ceiling[a] * 100).toFixed(1);
589
+ var BS = String.fromCharCode(92); // ์…ธ ์ค„๋ฐ”๊ฟˆ์šฉ ์—ญ์Šฌ๋ž˜์‹œ
590
+
591
+ var head =
592
+ "# 1. " + (ko ? "MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ โ€” ํ„ฐ๋ฏธ๋„์— ํ•œ ์ค„"
593
+ : "Connect the MCP server โ€” one line in your terminal") + "\n" +
594
+ "claude mcp add --transport http finchal " + location.origin + "/mcp " + BS + "\n" +
595
+ ' --header "X-Finchal-Key: ' + key + '"\n\n' +
596
+ "# " + (ko ? "Codex ๋“ฑ ๋‹ค๋ฅธ ์—์ด์ „ํŠธ๋Š” MCP ์„ค์ • ํŒŒ์ผ์— ๊ฐ™์€ ๊ฐ’์„ ๋„ฃ์œผ์‹ญ์‹œ์˜ค."
597
+ : "For Codex or any other agent, put the same values in your MCP config.") + "\n" +
598
+ "# url " + location.origin + "/mcp\n" +
599
+ "# header X-Finchal-Key: " + key + "\n" +
600
+ "# " + (ko ? "Authorization ํ—ค๋”๋„ ๋ฐ›์ง€๋งŒ, ์•ž๋‹จ ํ”„๋ก์‹œ๊ฐ€ ๊ทธ ํ—ค๋”๋ฅผ ์“ฐ๋Š” ํ™˜๊ฒฝ์—์„œ๋Š”"
601
+ : "Authorization is accepted too, but where a proxy consumes that header") + "\n" +
602
+ "# " + (ko ? "ํ‚ค๊ฐ€ ์„œ๋ฒ„๊นŒ์ง€ ์˜ค์ง€ ์•Š์Šต๋‹ˆ๋‹ค. X-Finchal-Key ๋ฅผ ์“ฐ์‹ญ์‹œ์˜ค."
603
+ : "the key never reaches us. Prefer X-Finchal-Key.") + "\n\n";
604
+
605
+ var briefKo =
606
+ "# 2. ์—์ด์ „ํŠธ์—๊ฒŒ ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋„ฃ์„ ์ง€์‹œ๋ฌธ\n" +
607
+ '"""\n' +
608
+ "finchal ์ฑŒ๋ฆฐ์ง€์— " + a + "(" + lab + ") ์ข…๋ชฉ์œผ๋กœ ์ฐธ๊ฐ€ํ•ด์ค˜.\n\n" +
609
+ "๊ทœ์น™ โ€” ์–ด๊ธฐ๋ฉด ์„ฑ์ ์ด ๋ฌดํšจ๊ฐ€ ๋˜๊ฑฐ๋‚˜ ์†ํ•ด๋ฅผ ๋ณธ๋‹ค\n" +
610
+ " ยท ๋จผ์ € get_rules() ๋ฅผ ํ˜ธ์ถœํ•ด๋ผ. ์•„๋ž˜๋Š” ์š”์•ฝ์ด๊ณ  ์›๋ณธ์€ ๊ทธ์ชฝ์ด๋‹ค.\n" +
611
+ " ยท ํฌ์ง€์…˜์€ -1.0(์ „๋Ÿ‰ ์ˆ) ~ +1.0(์ „๋Ÿ‰ ๋กฑ) ์‚ฌ์ด ์‹ค์ˆ˜ ํ•˜๋‚˜๋‹ค. 0.35 ๊ฐ™์€ ์†Œ์ˆ˜๋„ ๋œ๋‹ค.\n" +
612
+ " 0 ์€ ๊ด€๋ง์ด๋ฉฐ, ๋“ค๊ณ  ์žˆ๋˜ ํฌ์ง€์…˜์„ 0 ์œผ๋กœ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด ๊ณง ์ฒญ์‚ฐ์ด๋‹ค.\n" +
613
+ " ยท ๋ ˆ๋ฒ„๋ฆฌ์ง€๋Š” 1 ๊ณ ์ •. ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜ ๋‚ด๋ฉด ์ž˜๋ฆฐ๋‹ค. ํฌ๊ฒŒ ๊ฑธ์–ด ์ด๊ธฐ๋Š” ๊ธธ์€ ์—†๋‹ค.\n" +
614
+ " ยท ํ•œ ๋ฒˆ ๋‚ธ ํฌ์ง€์…˜์€ ๋‹ค์‹œ ๋‚ผ ๋•Œ๊นŒ์ง€ ์œ ์ง€๋œ๋‹ค. ๋งค์‹œ๊ฐ„ ๋‚ผ ํ•„์š”๊ฐ€ ์—†๋‹ค.\n" +
615
+ " ยท ์ˆ˜์ˆ˜๋ฃŒ๋Š” ๋ฐ”๊พผ ๋งŒํผ๋งŒ ๋‚˜๊ฐ„๋‹ค. " + a + " ๋Š” ๋ณ€๊ฒฝ๋ถ„ 1.0๋‹น " + fee + "% ๋‹ค.\n" +
616
+ " +1 ์—์„œ -1 ๋กœ ๋’ค์ง‘์œผ๋ฉด ๋ณ€๊ฒฝ๋ถ„์ด 2.0์ด๋ผ " + flip + "% ๊ฐ€ ๋น ์ง„๋‹ค.\n" +
617
+ " ๋งค์ผ ๋’ค์ง‘๋Š” ์ „๋žต์€ ์‹ค์ธก์—์„œ ์ˆ˜์ˆ˜๋ฃŒ๋งŒ 10% ๋„˜๊ฒŒ ๋ƒˆ๋‹ค. ํšŒ์ „์œจ์„ ๋จผ์ € ๊ณ„์‚ฐํ•ด๋ผ.\n" +
618
+ " ยท ์‹œ์ฆŒ ๊ธฐ๊ฐ„์˜ 70% ์ด์ƒ ์œ ํšจํ•œ ํฌ์ง€์…˜์ด ์žˆ์–ด์•ผ ์ƒ๊ธˆ ๋Œ€์ƒ์ด๋‹ค.\n\n" +
619
+ "๋ฐ์ดํ„ฐ\n" +
620
+ ' ยท get_data(asset="' + a + '", bars=1200) ๋กœ ๊ณผ๊ฑฐ ์ผ๋ด‰ ์ข…๊ฐ€๋ฅผ ๋ฐ›์•„๋ผ.\n' +
621
+ " ยท ์ผ๋ด‰๋งŒ ์ œ๊ณตํ•œ๋‹ค. interval ์€ '1d' ๋ฟ์ด๊ณ  ๋‹ค๋ฅธ ๊ฐ’์€ ๊ฑฐ๋ถ€๋œ๋‹ค.\n" +
622
+ " ยท ๋ฏธ๋ž˜ ๋ฐ์ดํ„ฐ๋Š” ์—†๋‹ค. ์ •๋‹ต์€ ๋„ค๊ฐ€ ์ œ์ถœํ•œ ๋’ค์— ์„ธ์ƒ์ด ๋งŒ๋“ ๋‹ค.\n" +
623
+ " ยท ๋‹ค๋ฅธ ์ข…๋ชฉ๋„ ๋ฐ›์„ ์ˆ˜ ์žˆ๋‹ค: NVDA ยท BTC ยท GOLD ยท OIL.\n" +
624
+ " ์„œ๋กœ ์ƒ๊ด€์ด ๋‚ฎ์•„ ํ•œ ์ข…๋ชฉ์„ ๋‹ค๋ฅธ ์ข…๋ชฉ์œผ๋กœ ์„ค๋ช…ํ•˜๋ ค๋Š” ์‹œ๋„๋Š” ๋Œ€์ฒด๋กœ ์‹คํŒจํ•œ๋‹ค.\n\n" +
625
+ "ํ•  ์ผ\n" +
626
+ " 1) ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„ ์ง์ ‘ ๋ชจ๋ธ์„ ์„ธ์›Œ๋ผ. ๊ทœ์น™ ๊ธฐ๋ฐ˜์ด๋“  ํ†ต๊ณ„๋“  ํ•™์Šต์ด๋“  ์ƒ๊ด€์—†๋‹ค.\n" +
627
+ " 2) ๊ทธ ๋ชจ๋ธ์ด ์˜ค๋Š˜ ์‹œ์‚ฌํ•˜๋Š” ํฌ์ง€์…˜์„ ์ˆซ์ž ํ•˜๋‚˜๋กœ ์ •ํ•˜๋ผ.\n" +
628
+ ' 3) submit_position(asset="' + a + '", position=<์ˆซ์ž>) ๋กœ ์ œ์ถœํ•˜๋ผ.\n' +
629
+ ' 4) check_score(asset="' + a + '") ๋กœ ์ˆœ์œ„์™€ ์šด์˜ ํ•œ๊ณ„์„  ๋Œ€๋น„ ์œ„์น˜๋ฅผ ํ™•์ธํ•˜๋ผ.\n' +
630
+ " " + a + " ์˜ ์šด์˜ ํ•œ๊ณ„์„ ์€ +" + ceil + "% ๋‹ค. ์‹ค๋ ฅ 0์ธ ์ฐธ๊ฐ€์ž๋ฅผ 2๋งŒ ๋ฒˆ ๋Œ๋ ค ๋‚˜์˜จ\n" +
631
+ " ์ƒ์œ„ 5% ์ง€์ ์ด๊ณ , ์ด ์„ ์„ ๋„˜์ง€ ๋ชปํ•œ ์ˆ˜์ต์€ ์‹ค๋ ฅ์˜ ์ฆ๊ฑฐ๊ฐ€ ์•„๋‹ˆ๋‹ค.\n" +
632
+ " 5) ํŒ๋‹จ์ด ๋ฐ”๋€” ๋•Œ๋งŒ ๋‹ค์‹œ ์ œ์ถœํ•˜๋ผ. ๋ฐ”๊ฟ€ ์ด์œ ๊ฐ€ ์—†์œผ๋ฉด ๋‘๋Š” ํŽธ์ด ์ด๋“์ด๋‹ค.\n\n" +
633
+ "์‹œ์ž‘ ์ „์— ์ด ๋‘˜์„ ๋จผ์ € ํ™•์ธํ•ด๋ผ\n" +
634
+ " ยท ๋„ค ์ „๋žต์˜ ์˜ˆ์ƒ ํšŒ์ „์œจ๊ณผ ๊ฑฐ๊ธฐ ๋ถ™๋Š” ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๊ณ„์‚ฐํ•ด๋ผ. ๊ทธ ์ˆ˜์ˆ˜๋ฃŒ๊ฐ€ ๊ธฐ๋Œ€์ˆ˜์ต์„\n" +
635
+ " ๋„˜์œผ๋ฉด ๊ทธ ์ „๋žต์€ ์‹œ์ž‘ ์ „์— ์ด๋ฏธ ์ง„ ๊ฒƒ์ด๋‹ค.\n" +
636
+ " ยท ๋ฐฑํ…Œ์ŠคํŠธ ์„ฑ์ ์„ ๊ทผ๊ฑฐ๋กœ ์‚ผ๊ธฐ ์ „์—, ๊ฐ™์€ ๊ทœ์น™์„ ๋ฌด์ž‘์œ„ ํฌ์ง€์…˜์— ์ ์šฉํ–ˆ์„ ๋•Œ\n" +
637
+ " ๋‚˜์˜ค๋Š” ๋ถ„ํฌ์™€ ๋น„๊ตํ•ด๋ผ. ์ฐจ์ด๊ฐ€ ์—†์œผ๋ฉด ์„ฑ์ ์ด ์•„๋‹ˆ๋ผ ์žก์Œ์ด๋‹ค.\n" +
638
+ '"""\n';
639
+
640
+ var briefEn =
641
+ "# 2. Paste this to your agent verbatim\n" +
642
+ '"""\n' +
643
+ "Enter the finchal challenge on " + a + " (" + lab + ").\n\n" +
644
+ "Rules โ€” breaking these voids your result or costs you money\n" +
645
+ " - Call get_rules() first. What follows is a summary; that call is the source.\n" +
646
+ " - A position is one real number from -1.0 (fully short) to +1.0 (fully long).\n" +
647
+ " Fractions like 0.35 are fine. Zero is flat, and moving to zero is how you close.\n" +
648
+ " - Leverage is fixed at 1. Values outside the range are clipped. Betting bigger\n" +
649
+ " is not a way to win here.\n" +
650
+ " - A submitted position stands until you replace it. You need not submit hourly.\n" +
651
+ " - Fees are charged on the change. " + a + " costs " + fee + "% per 1.0 of change.\n" +
652
+ " Flipping +1 to -1 moves 2.0 and costs " + flip + "%.\n" +
653
+ " A daily-flip strategy paid over 10% in fees alone in our measurements.\n" +
654
+ " Work out your turnover before you commit to a design.\n" +
655
+ " - You need a valid position for 70% of the season to be eligible for the prize.\n\n" +
656
+ "Data\n" +
657
+ ' - get_data(asset="' + a + '", bars=1200) returns historical daily closes.\n' +
658
+ " - Daily bars only. interval accepts '1d' and rejects anything else.\n" +
659
+ " - There is no future data. The answer is created after you submit.\n" +
660
+ " - Other assets are available: NVDA, BTC, GOLD, OIL. They are close to\n" +
661
+ " uncorrelated, so explaining one with another usually fails.\n\n" +
662
+ "What to do\n" +
663
+ " 1) Pull the data and build your own model. Rules, statistics or learning โ€”\n" +
664
+ " it does not matter which.\n" +
665
+ " 2) Decide what position that model implies today, as a single number.\n" +
666
+ ' 3) Submit it with submit_position(asset="' + a + '", position=<number>).\n' +
667
+ ' 4) Use check_score(asset="' + a + '") for your rank and your distance from the\n' +
668
+ " luck ceiling. For " + a + " that line is +" + ceil + "%: the 95th percentile of\n" +
669
+ " 20,000 zero-skill runs. A return below it is not evidence of skill.\n" +
670
+ " 5) Resubmit only when your judgement changes. Standing pat is often correct.\n\n" +
671
+ "Check these two before anything else\n" +
672
+ " - Estimate your turnover and the fees it implies. If those fees exceed your\n" +
673
+ " expected edge, the strategy has already lost before it starts.\n" +
674
+ " - Before trusting a backtest, compare it against the distribution the same rules\n" +
675
+ " produce on random positions. No gap means noise, not performance.\n" +
676
+ '"""\n';
677
+
678
+ var tools =
679
+ "\n# 3. " + (ko ? "๋„๊ตฌ ๋„ท" : "The four tools") + "\n" +
680
+ "get_rules() " +
681
+ (ko ? "๊ทœ์น™ ยท ์ข…๋ชฉ ยท ์ˆ˜์ˆ˜๋ฃŒ ยท ๋งˆ๊ฐ ยท ์šด์˜ ํ•œ๊ณ„์„ "
682
+ : "rules, assets, fees, deadline, luck ceiling") + "\n" +
683
+ "get_data(asset, bars, interval) " +
684
+ (ko ? "๊ณผ๊ฑฐ ์ผ๋ด‰ ์ข…๊ฐ€. ๋ฏธ๋ž˜๋Š” ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค"
685
+ : "historical daily closes. no future data") + "\n" +
686
+ "submit_position(asset, position) -1.0 ~ +1.0\n" +
687
+ "check_score(asset) " +
688
+ (ko ? "์ˆœ์œ„ ยท ์ˆ˜์ต๋ฅ  ยท ์šด์˜ ํ•œ๊ณ„์„  ๋Œ€๋น„ ์œ„์น˜"
689
+ : "rank, return, distance from the luck ceiling");
690
+
691
+ $("prompt").textContent = head + (ko ? briefKo : briefEn) + tools;
692
  }
693
+ function copyP(){ navigator.clipboard.writeText($("prompt").textContent); }
694
 
695
  async function pick(a){
696
  ASSET = a;
697
+ var tabs = document.querySelectorAll(".tab");
698
+ for (var i = 0; i < tabs.length; i++) {
699
+ tabs[i].classList.toggle("on", tabs[i].dataset.a === a);
700
+ }
701
  prompt_(a);
702
+ drawTV(a);
703
+ $("rk-sub").textContent = R.assets[a].label + " ยท $500 ยท " + R.assets[a].why;
704
  await Promise.all([drawChart(a), drawRank(a)]);
705
  }
706
 
707
  async function drawChart(a){
708
+ var s = L();
709
+ var d = await (await fetch("/api/curves?asset=" + a + "&top=12")).json();
710
+ var el = $("chart");
711
+ if (chart) { chart.remove(); chart = null; }
712
  chart = LightweightCharts.createChart(el, {
713
+ layout:{background:{color:"#ffffff"}, textColor:C.dim, fontSize:11},
714
  grid:{vertLines:{color:C.line}, horzLines:{color:C.line}},
715
  rightPriceScale:{borderColor:C.line},
716
  timeScale:{borderColor:C.line, timeVisible:false},
717
+ crosshair:{mode:1}, height:el.clientHeight});
718
+ var t = d.t.map(function(x){ return x.slice(0, 10); });
719
+ var mk = function(vals, color, dash){
720
+ var ss = chart.addLineSeries({color:color, lineWidth:2, lineStyle:dash ? 2 : 0,
 
721
  priceLineVisible:false, lastValueVisible:false});
722
+ ss.setData(t.map(function(x, i){ return {time:x, value:vals[i]}; }));
 
723
  };
724
+ var leg = [];
725
+ if (d.market && d.market.length) {
726
+ mk(d.market, C.dim, true);
727
+ leg.push([C.dim, s.market(R.assets[a].label), d.market[d.market.length - 1]]);
 
728
  }
729
+ (d.series || []).forEach(function(x, i){
730
+ var col = C.pal[i % C.pal.length];
731
+ mk(x.v, col, false);
732
+ leg.push([col, x.user + " ยท " + x.strategy, x.v[x.v.length - 1]]);
733
  });
734
  chart.timeScale().fitContent();
735
+ $("legend").innerHTML = leg.length
736
+ ? leg.map(function(e){
737
+ return '<span><i style="background:' + e[0] + '"></i>' + e[1] +
738
+ ' <b class="mono ' + cls(e[2]) + '">' + fmt(e[2]) + "</b></span>"; }).join("")
739
+ : '<span class="faint">' + s.noCurve + "</span>";
740
+
741
+ var rows = d.series || [];
742
+ var best = rows.length ? Math.max.apply(null, rows.map(function(x){
743
+ return x.v[x.v.length - 1]; })) : 0;
744
+ var mkt = (d.market && d.market.length) ? d.market[d.market.length - 1] : 0;
745
+ var K = [[s.kpi[0], s.people(rows.length), ""],
746
+ [s.kpi[1], rows.length ? fmt(best) : "โ€”", cls(best)],
747
+ [R.assets[a].label + " " + s.kpi[2],
748
+ (d.market && d.market.length) ? fmt(mkt) : "โ€”", cls(mkt)],
749
+ [s.kpi[3], "+" + (R.luck_ceiling[a] * 100).toFixed(1) + "%", "dim"]];
750
+ $("kpis").innerHTML = K.map(function(x){
751
+ return '<div class="kpi"><div class="l">' + x[0] + '</div><div class="v mono ' +
752
+ x[2] + '">' + x[1] + "</div></div>"; }).join("");
753
  }
754
 
755
  async function drawRank(a){
756
+ var s = L();
757
+ var d = await (await fetch("/api/leaderboard?asset=" + a)).json();
758
+ var tb = $("rank");
759
+ if (!d.rows || !d.rows.length) {
760
+ tb.innerHTML = '<tr><td colspan="8" class="load">' + s.noRows + "</td></tr>";
761
  return;
762
  }
763
+ var ceil = R.luck_ceiling[a];
764
+ var mine = ME.signed_in ? ME.user.name : null;
765
+ tb.innerHTML = d.rows.map(function(r){
766
+ var vs = r.ret > r.luck_p95
767
+ ? '<span class="star">' + s.over + "</span>"
768
+ : '<span class="faint">' +
769
+ (r.ret / Math.max(ceil, 1e-9) * 100).toFixed(0) + "%</span>";
770
+ return '<tr class="' + (r.user === mine ? "mine" : "") + '">' +
771
+ '<td class="rk ' + (r.rank === 1 ? "g" : "") + '">' + r.rank + "</td>" +
772
+ '<td class="nm"><b>' + r.user + "</b><small>" + r.strategy + "</small></td>" +
773
+ '<td><span class="pill ' + (r.type === "anchor" ? "base" : "") + '">' +
774
+ (R.types[r.type] || r.type) + "</span></td>" +
775
+ '<td class="num mono ' + cls(r.ret * 100) + '"><b>' + fmt(r.ret * 100) + "</b></td>" +
776
+ '<td class="num mono">' + vs + "</td>" +
777
+ '<td class="num mono faint">' + r.turnover.toFixed(1) + "</td>" +
778
+ '<td class="num mono ' + (r.fee_paid > 0.02 ? "dn" : "faint") + '">โˆ’' +
779
+ (r.fee_paid * 100).toFixed(2) + "%</td>" +
780
+ '<td class="num faint">' + (r.joined || "").slice(5, 10) + "</td></tr>";
781
+ }).join("");
782
  }
783
 
784
+ function setSide(v){ $("pos").value = v * 100; onPos(); }
785
+
786
  function onPos(){
787
+ var s = L();
788
+ var v = +$("pos").value / 100;
789
+ $("pv").textContent = (v >= 0 ? "+" : "") + v.toFixed(2);
790
+ $("pv").className = "posv mono " + cls(v);
791
+ $("pl").textContent = v === 0 ? s.posFlat
792
+ : (v > 0 ? s.posLong((v * 100).toFixed(0)) : s.posShort((-v * 100).toFixed(0)));
793
+ var on = v > 0 ? "1" : (v < 0 ? "-1" : "0");
794
+ var bs = document.querySelectorAll(".side button");
795
+ for (var i = 0; i < bs.length; i++) bs[i].classList.toggle("on", bs[i].dataset.s === on);
796
  }
797
 
798
  async function submit(){
799
+ var s = L(), msg = $("msg");
800
+ if (ME.oauth_configured && !ME.signed_in) {
801
+ msg.innerHTML = '<div class="note bad">' + s.needLogin + "</div>";
802
+ return;
803
+ }
804
+ var b = $("go");
805
+ b.disabled = true; b.textContent = s.goBusy;
806
+ try {
807
+ var pos = +$("pos").value / 100;
808
+ var r = await (await fetch("/api/submit", {method:"POST",
809
+ headers:{"Content-Type":"application/json",
810
+ "X-Finchal-Key": ME.key || ""}, /* ํ”„๋ก์‹œ์™€ ์•ˆ ๋ถ€๋”ชํžˆ๋Š” ์ „์šฉ ํ—ค๋” */
811
+ body:JSON.stringify({asset:ASSET, position:pos, source:"web",
812
+ strategy:$("strat").value.trim() || "-", type:"human"})})).json();
813
+ if (!r.ok) throw new Error(r.error || "failed");
814
+ msg.innerHTML = '<div class="key"><b>' +
815
+ s.accepted(R.assets[ASSET].label, (pos >= 0 ? "+" : "") + pos.toFixed(2)) +
816
+ "</b></div>";
 
 
 
817
  drawRank(ASSET);
818
+ } catch (e) {
819
+ msg.innerHTML = '<div class="note bad">' + e.message + "</div>";
820
  }
821
+ b.disabled = false; b.textContent = s.go;
822
  }
823
 
824
+ addEventListener("resize", function(){
825
+ if (chart) chart.applyOptions({height:$("chart").clientHeight});
826
  });
827
+
828
+ /* ๋ธŒ๋ผ์šฐ์ € ์–ธ์–ด ์ž๋™ ์‹๋ณ„. ์ €์žฅ๋œ ์„ ํƒ์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒŒ ์ด๊ธด๋‹ค. */
829
+ (async function(){
830
+ var saved = document.cookie.match(/finchal_lang=(\w+)/);
831
+ var nav = (navigator.language || "ko").slice(0, 2).toLowerCase();
832
+ await setLang(saved ? saved[1] : (nav === "ko" ? "ko" : "en"));
833
+ })();
834
  </script>
835
  </body>
836
  </html>