Spaces:
Running
Running
bilingual UI with HF sign-in, TradingView widgets, three-act manifesto
Browse files- README.md +116 -18
- app/i18n.py +254 -0
- app/main.py +265 -64
- app/mcp.py +148 -133
- 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
|
|
|
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
|
|
|
| 18 |
|
| 19 |
-
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
**
|
|
|
|
|
|
|
|
|
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
```
|
| 26 |
-
claude mcp add --transport http finchal https://<space>/mcp \
|
| 27 |
-
--header "Authorization: Bearer <์ฐธ๊ฐํค>"
|
| 28 |
-
```
|
| 29 |
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
| 38 |
|
| 39 |
-
|
|
|
|
| 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": "
|
| 68 |
-
|
| 69 |
-
"leverage":
|
| 70 |
-
"update":
|
| 71 |
"fees": {k: SC.fee_of(k) for k in ASSETS},
|
| 72 |
-
"fee_note":
|
| 73 |
-
"scoring":
|
| 74 |
-
"participation": "
|
| 75 |
-
"assets": {k: {"label":
|
| 76 |
"vol_annual": v["vol"], "hours": v["hours"]}
|
| 77 |
for k, v in ASSETS.items()},
|
| 78 |
-
"types": {k: v
|
| 79 |
-
"warning": (
|
| 80 |
-
|
| 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": (
|
| 86 |
-
"์์๋ฃ๋ฅผ ๋๊ฐ์ด ๋ฌผ๋ ธ์ต๋๋ค. ์ด ์ ์ ๋์ด์ผ ์ค๋ ฅ์ ์ฆ๊ฑฐ๊ฐ ๋ฉ๋๋ค."),
|
| 87 |
}
|
| 88 |
|
| 89 |
|
| 90 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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 |
-
|
| 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": ("
|
| 284 |
-
|
| 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 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
"
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
"
|
| 35 |
-
"
|
| 36 |
-
"
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
"
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def
|
| 91 |
-
"""
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""MCP ์๋ฒ โ ์์ด์ ํธ๊ฐ ๋๊ตฌ๋ก ์ง์ ์ฐธ๊ฐํ๋ค.
|
| 3 |
+
|
| 4 |
+
์ด๊ฒ ์ด ์ฑ๋ฆฐ์ง์ ์ ์ฒด์ฑ์ด๋ค. Numerai๋ M6๋ ์ฌ๋์ด CSV๋ฅผ ์ฌ๋ฆฐ๋ค.
|
| 5 |
+
์ฐธ๊ฐ์๋ ์ด ํ ์ค๋ง ์น๋ฉด ๋๋ค:
|
| 6 |
+
|
| 7 |
+
claude mcp add finchal https://<space>/mcp --header "Authorization: Bearer fc_xxx"
|
| 8 |
+
|
| 9 |
+
๊ทธ๋ฌ๋ฉด ์์ด์ ํธ์๊ฒ ๋๊ตฌ ๋ค ๊ฐ๊ฐ ์๊ธฐ๊ณ , ์ฌ๋์ "์ฑ๋ฆฐ์ง ์ฐธ๊ฐํด์ค" ํ ๋ง๋๋ง ํ๋ฉด ๋๋ค.
|
| 10 |
+
|
| 11 |
+
ํ๋กํ ์ฝ์ JSON-RPC 2.0 ์์ Streamable HTTP. ์ธ ๋ฉ์๋๋ง ๊ตฌํํ๋ฉด ๋ถ๋๋ค:
|
| 12 |
+
initialize / tools/list / tools/call
|
| 13 |
+
notifications/* ๋ ์๋ต ์์ด 202 ๋ก ๋ฐ๋๋ค.
|
| 14 |
+
"""
|
| 15 |
+
import json, 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
|
|
|
|
| 7 |
<script src="https://unpkg.com/lightweight-charts@4.1.3/dist/lightweight-charts.standalone.production.js"></script>
|
| 8 |
<style>
|
| 9 |
-
/*
|
| 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:
|
| 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)
|
| 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 |
-
/* ์ข์ ํ๋ฉด์์
|
| 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;
|
| 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 |
-
.
|
| 93 |
-
.
|
| 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:
|
| 105 |
-
.posl{text-align:center;font-size:12px;color:var(--faint);margin-bottom:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 134 |
-
<div class="sub" id="m-sub">
|
| 135 |
-
<div class="prize"><span class="dim"
|
| 136 |
-
<span class="dim" id="prize-rule">
|
| 137 |
<div class="meta" id="m-meta">โ</div>
|
| 138 |
<div class="ctas">
|
| 139 |
-
<button class="btn pri" onclick="go('#submit')">
|
| 140 |
-
<button class="btn" onclick="go('#guide')">
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
<div class="posv mono" id="pv">+1.00</div>
|
| 159 |
-
<div class="posl" id="pl">
|
| 160 |
<input type="range" id="pos" min="-100" max="100" value="100" step="5"
|
| 161 |
oninput="onPos()">
|
| 162 |
-
<div class="marks"><span
|
| 163 |
-
|
| 164 |
-
<
|
| 165 |
-
<
|
| 166 |
-
<
|
| 167 |
-
<input type="text" id="strat" placeholder="์: 20์ผ ์ถ์ธ์ถ์ข
">
|
| 168 |
<button class="btn pri" style="width:100%;margin-top:14px" id="go"
|
| 169 |
-
onclick="submit()">
|
| 170 |
<div id="msg"></div>
|
| 171 |
-
<div class="note">
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
| 175 |
</div>
|
| 176 |
|
| 177 |
<div class="card" style="margin-top:16px">
|
| 178 |
-
<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>
|
| 188 |
-
id="rk-sub"></span></h2>
|
| 189 |
<div class="tw"><table>
|
| 190 |
<thead><tr>
|
| 191 |
-
<th class="rk">#</th><th
|
| 192 |
-
<th class="num">
|
| 193 |
-
<th class="num"
|
| 194 |
-
|
| 195 |
-
<tbody id="rank"><tr><td colspan="8" class="load">
|
| 196 |
</table></div>
|
| 197 |
</div>
|
| 198 |
|
| 199 |
-
<!-- ๊ท์น ยท ๊ฐ์ด๋ -->
|
| 200 |
<div class="row3" id="guide">
|
| 201 |
-
<div class="card">
|
| 202 |
-
|
| 203 |
-
|
| 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()">
|
| 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 |
-
/* ์ฐจํธ ์ค์ ์
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
|
| 243 |
async function boot(){
|
| 244 |
-
R
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
-
function
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
"
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
| 300 |
}
|
| 301 |
-
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
}
|
|
|
|
| 304 |
|
| 305 |
async function pick(a){
|
| 306 |
ASSET = a;
|
| 307 |
-
document.querySelectorAll(
|
| 308 |
-
|
|
|
|
|
|
|
| 309 |
prompt_(a);
|
| 310 |
-
|
| 311 |
-
|
| 312 |
await Promise.all([drawChart(a), drawRank(a)]);
|
| 313 |
}
|
| 314 |
|
| 315 |
async function drawChart(a){
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
| 319 |
chart = LightweightCharts.createChart(el, {
|
| 320 |
-
layout:{background:{color:
|
| 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 |
-
|
| 327 |
-
|
| 328 |
-
const s = chart.addLineSeries({color, lineWidth:w, lineStyle:dash?2:0,
|
| 329 |
priceLineVisible:false, lastValueVisible:false});
|
| 330 |
-
|
| 331 |
-
return s;
|
| 332 |
};
|
| 333 |
-
|
| 334 |
-
if(d.market && d.market.length){
|
| 335 |
-
mk(
|
| 336 |
-
leg.push([C.dim,
|
| 337 |
-
d.market[d.market.length-1]]);
|
| 338 |
}
|
| 339 |
-
(d.series||[]).forEach((
|
| 340 |
-
|
| 341 |
-
mk(
|
| 342 |
-
leg.push([col,
|
| 343 |
});
|
| 344 |
chart.timeScale().fitContent();
|
| 345 |
-
|
| 346 |
-
? leg.map((
|
| 347 |
-
<
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
|
|
|
| 362 |
}
|
| 363 |
|
| 364 |
async function drawRank(a){
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
return;
|
| 371 |
}
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
tb.innerHTML = d.rows.map(r
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
<
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
<td class="
|
| 381 |
-
<td class="
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
<td class="num mono
|
| 385 |
-
|
| 386 |
-
<td class="num faint">
|
| 387 |
-
|
| 388 |
-
|
|
|
|
|
|
|
| 389 |
}
|
| 390 |
|
|
|
|
|
|
|
| 391 |
function onPos(){
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
: v > 0 ?
|
| 398 |
-
|
|
|
|
|
|
|
| 399 |
}
|
| 400 |
|
| 401 |
async function submit(){
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 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 =
|
| 426 |
}
|
| 427 |
-
b.disabled = false; b.textContent =
|
| 428 |
}
|
| 429 |
|
| 430 |
-
addEventListener(
|
| 431 |
-
if(chart) chart.applyOptions({height:
|
| 432 |
});
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|