SeaWolf-AI commited on
Commit
588034e
ยท
verified ยท
1 Parent(s): eed8012

hourly scoring grid and 13 reference strategies since Jan 1

Browse files
Files changed (5) hide show
  1. app/baselines.py +158 -0
  2. app/fetch.py +29 -0
  3. app/main.py +66 -7
  4. app/mcp.py +4 -2
  5. app/static/index.html +68 -5
app/baselines.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """์ฐธ๊ณ  ์ „๋žต โ€” ๊ต๊ณผ์„œ์— ๋‚˜์˜ค๋Š” ๊ทœ์น™๋“ค์„ ๊ฐ™์€ ์ฑ„์ ๊ธฐ๋กœ ๋Œ๋ฆฐ ๋˜๋Œ์•„๋ณด๊ธฐ.
3
+
4
+ ์™œ ๋„ฃ๋Š”๊ฐ€
5
+ ์‹œ์ฆŒ ์ดˆ์—๋Š” ์ฐธ๊ฐ€์ž ๊ณก์„ ์ด ์—†์–ด ๋น„๊ต ์ฐจํŠธ๊ฐ€ ๋น„์–ด ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด "์–ผ๋งˆ๋ฅผ ๋‚ด์•ผ
6
+ ์ž˜ํ•œ ๊ฒƒ์ธ๊ฐ€"๋ฅผ ๊ฐ€๋Š ํ•  ๊ธฐ์ค€์ด ํ™”๋ฉด์— ์—†๋‹ค. ๋„๋ฆฌ ์•Œ๋ ค์ง„ ๊ทœ์น™ ๋ช‡ ๊ฐœ๋ฅผ ์ตœ๊ทผ
7
+ ๊ตฌ๊ฐ„์— ์ ์šฉํ•ด ๊น”์•„ ๋‘๋ฉด ๊ทธ ์ž๋ฆฌ๊ฐ€ ์ฒ™๋„๊ฐ€ ๋œ๋‹ค.
8
+
9
+ ๐Ÿ”ด ์ด๊ฒƒ์€ **๊ณผ๊ฑฐ ๊ตฌ๊ฐ„ ์žฌํ˜„**์ด๋‹ค. ๋ฏธ๋ž˜ ์„ฑ๊ณผ๊ฐ€ ์•„๋‹ˆ๊ณ , ์ฐธ๊ฐ€์ž ์„ฑ์ ๋„ ์•„๋‹ˆ๋‹ค.
10
+ ํ™”๋ฉด์— ๊ทธ ์‚ฌ์‹ค์„ ๋ฐ˜๋“œ์‹œ ํ•จ๊ป˜ ์ ๋Š”๋‹ค. ๋ฐฑํ…Œ์ŠคํŠธ๋ฅผ ์„ฑ์ ์ฒ˜๋Ÿผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด
11
+ ์ด ๋Œ€ํšŒ๊ฐ€ ์—†์• ๋ ค๋Š” ๋ฌธ์ œ ๊ทธ ์ž์ฒด๋‹ค.
12
+
13
+ ๐Ÿ”‘ ์ฐธ๊ฐ€์ž์™€ **์™„์ „ํžˆ ๊ฐ™์€ ์ฑ„์ ๊ธฐยท๊ฐ™์€ ์ˆ˜์ˆ˜๋ฃŒ**๋กœ ๋Œ๋ฆฐ๋‹ค. ๊ทœ์น™๋งŒ ๋‹ค๋ฅด๊ณ 
14
+ ๋‚˜๋จธ์ง€ ์กฐ๊ฑด์ด ๊ฐ™์•„์•ผ ๋น„๊ต๊ฐ€ ์„ฑ๋ฆฝํ•œ๋‹ค. ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋นผ๊ณ  ๊ทธ๋ฆฌ๋ฉด ํšŒ์ „์ด ๋†’์€
15
+ ๊ทœ์น™์ด ๋ถ€๋‹นํ•˜๊ฒŒ ์ข‹์•„ ๋ณด์ธ๋‹ค.
16
+
17
+ ๐Ÿ”‘ ์–ด๋–ค ๊ทœ์น™๋„ ๋ฏธ๋ž˜๋ฅผ ๋ณด์ง€ ์•Š๋Š”๋‹ค. ๋ชจ๋“  ์‹ ํ˜ธ๋Š” shift(1) ์ดํ›„์— ์“ด๋‹ค.
18
+ ์ด๋™ํ‰๊ท ์„ ๊ทธ๋‚  ์ข…๊ฐ€๋กœ ๊ณ„์‚ฐํ•ด ๊ทธ๋‚  ํฌ์ง€์…˜์— ์“ฐ๋ฉด ์„ฑ์ ์ด ํ†ต์งธ๋กœ ๊ฑฐ์ง“์ด ๋œ๋‹ค.
19
+ """
20
+ import numpy as np
21
+ import pandas as pd
22
+
23
+ # ์˜ฌํ•ด ์ฒซ ๊ฑฐ๋ž˜์ผ๋ถ€ํ„ฐ ์žฐ๋‹ค. "1์›” 1์ผ์— ์ด ๊ทœ์น™์œผ๋กœ ์ถœ๋ฐœํ–ˆ๋‹ค๋ฉด ์ง€๊ธˆ ์–ด๋””์ธ๊ฐ€" ๊ฐ€
24
+ # ๊ตด๋Ÿฌ๊ฐ€๋Š” 90์ผ ์ฐฝ๋ณด๋‹ค ํ›จ์”ฌ ์ฝ๊ธฐ ์‰ฝ๊ณ , ๊ธฐ์ค€์ ์ด ๊ณ ์ •๋˜์–ด ๋งค์ผ ๊ฐ’์ด ํ”๋“ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
25
+ SINCE = "2026-01-01"
26
+
27
+
28
+ def _rsi(p, n=14):
29
+ d = p.diff()
30
+ up = d.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
31
+ dn = (-d.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
32
+ return 100 - 100 / (1 + up / dn.replace(0, np.nan))
33
+
34
+
35
+ def _weights(px):
36
+ """๊ทœ์น™๋ณ„ ํฌ์ง€์…˜ ์‹œ๊ณ„์—ด. ์ „๋ถ€ ์ „๋‚ ๊นŒ์ง€์˜ ์ •๋ณด๋งŒ ์“ด๋‹ค."""
37
+ p = pd.Series(px, dtype=float).sort_index()
38
+ out = {}
39
+
40
+ out["BUYHOLD"] = pd.Series(1.0, index=p.index)
41
+
42
+ # 200์ผ์„  โ€” ๋‹จ์ผ ๊ทœ์น™์œผ๋กœ๋Š” ๊ฐ€์žฅ ๋„๋ฆฌ ์•Œ๋ ค์ง„ ๊ฒƒ. ์œ„๋ฉด ๋“ค๊ณ  ์•„๋ž˜๋ฉด ๋น„์šด๋‹ค.
43
+ s200 = p.rolling(200).mean()
44
+ out["SMA200"] = (p > s200).astype(float).shift(1)
45
+
46
+ # ๊ณจ๋“ /๋ฐ๋“œ ํฌ๋กœ์Šค โ€” 50์ผ์„ ์ด 200์ผ์„ ์„ ๋„˜์œผ๋ฉด ๋กฑ, ๋‚ด๋ ค๊ฐ€๋ฉด ์ˆ
47
+ s50x = p.rolling(50).mean()
48
+ out["GOLDEN50_200"] = np.sign(s50x - s200).shift(1)
49
+
50
+ # ์ด๋™ํ‰๊ท  ๊ต์ฐจ โ€” ๊ฐ€์žฅ ๋„๋ฆฌ ์•Œ๋ ค์ง„ ์ถ”์„ธ ๊ทœ์น™
51
+ s20, s50 = p.rolling(20).mean(), p.rolling(50).mean()
52
+ out["SMA20_50"] = np.sign(s20 - s50).shift(1)
53
+
54
+ # 60์ผ ๋ชจ๋ฉ˜ํ…€ โ€” ํ•™์ˆ  ๋ฌธํ—Œ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ๊ฒ€์ฆ๋œ ์ถ•
55
+ out["MOM60"] = np.sign(p / p.shift(60) - 1).shift(1)
56
+
57
+ # ๋ˆ์น˜์•ˆ 20์ผ ์ฑ„๋„ ๋ŒํŒŒ
58
+ hi, lo = p.rolling(20).max(), p.rolling(20).min()
59
+ br = pd.Series(0.0, index=p.index)
60
+ br[p >= hi] = 1.0
61
+ br[p <= lo] = -1.0
62
+ out["DONCHIAN20"] = br.replace(0.0, np.nan).ffill().fillna(0.0).shift(1)
63
+
64
+ # RSI ์—ญ์ถ”์„ธ โ€” ๊ณผ๋งค๋„์— ์‚ฌ๊ณ  ๊ณผ๋งค์ˆ˜์— ํŒ๋‹ค
65
+ r = _rsi(p)
66
+ out["RSI14"] = ((50 - r) / 20).clip(-1, 1).shift(1)
67
+
68
+ # ๋ณผ๋ฆฐ์ € ๋ณต๊ท€ โ€” ํ‰๊ท ์—์„œ ๋ฉ€์–ด์ง„ ๋งŒํผ ๋ฐ˜๋Œ€๋กœ
69
+ m, sd = p.rolling(20).mean(), p.rolling(20).std()
70
+ out["BBAND20"] = (-(p - m) / (2 * sd.replace(0, np.nan))).clip(-1, 1).shift(1)
71
+
72
+ # MACD (12,26,9) โ€” ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ค‘ ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ๊ฒƒ.
73
+ # ํžˆ์Šคํ† ๊ทธ๋žจ ๋ถ€ํ˜ธ๋กœ ๋ฐฉํ–ฅ์„ ์ •ํ•œ๋‹ค.
74
+ e12 = p.ewm(span=12, adjust=False).mean()
75
+ e26 = p.ewm(span=26, adjust=False).mean()
76
+ macd = e12 - e26
77
+ out["MACD"] = np.sign(macd - macd.ewm(span=9, adjust=False).mean()).shift(1)
78
+
79
+ # ์ผ๋ชฉ๊ท ํ˜•ํ‘œ ๊ตฌ๋ฆ„ โ€” ์ข…๊ฐ€๋งŒ ์žˆ์œผ๋ฏ€๋กœ ๊ณ ์ € ๋Œ€์‹  ์ข…๊ฐ€์˜ ์ด๋™ ์ตœ๊ณ ยท์ตœ์ €๋ฅผ ์“ด๋‹ค.
80
+ # ์›๋ณธ ์ •์˜์™€ ์™„์ „ํžˆ ๊ฐ™์ง€๋Š” ์•Š์ง€๋งŒ ๊ตฌ๋ฆ„์˜ ์œ„์น˜ ํŒ์ •์€ ๊ฐ™์€ ๋œป์„ ์œ ์ง€ํ•œ๋‹ค.
81
+ conv = (p.rolling(9).max() + p.rolling(9).min()) / 2
82
+ base = (p.rolling(26).max() + p.rolling(26).min()) / 2
83
+ spa = ((conv + base) / 2).shift(26)
84
+ spb = ((p.rolling(52).max() + p.rolling(52).min()) / 2).shift(26)
85
+ top, bot = pd.concat([spa, spb], axis=1).max(axis=1), pd.concat([spa, spb], axis=1).min(axis=1)
86
+ ich = pd.Series(0.0, index=p.index)
87
+ ich[p > top] = 1.0
88
+ ich[p < bot] = -1.0
89
+ out["ICHIMOKU"] = ich.shift(1)
90
+
91
+ # 12๊ฐœ์›” ์ ˆ๋Œ€ ๋ชจ๋ฉ˜ํ…€ โ€” ์ง€๋‚œ 1๋…„ ์ˆ˜์ต์ด ์–‘์ˆ˜๋ฉด ๋ณด์œ , ์•„๋‹ˆ๋ฉด ๋น„์šด๋‹ค.
92
+ out["ABSMOM12M"] = (p / p.shift(252) - 1 > 0).astype(float).shift(1)
93
+
94
+ # ์Šคํ† ์บ์Šคํ‹ฑ (14,3) โ€” ๊ณผ๋งค์ˆ˜ยท๊ณผ๋งค๋„ ํŒ์ •์˜ ๊ณ ์ „
95
+ lo14, hi14 = p.rolling(14).min(), p.rolling(14).max()
96
+ k = 100 * (p - lo14) / (hi14 - lo14).replace(0, np.nan)
97
+ out["STOCH14"] = ((50 - k.rolling(3).mean()) / 25).clip(-1, 1).shift(1)
98
+
99
+ # ๋ณ€๋™์„ฑ ํƒ€๊ฒŸํŒ… โ€” ๋ฐฉํ–ฅ์€ ๋กฑ ๊ณ ์ •, ํฌ๊ธฐ๋งŒ ์กฐ์ ˆ
100
+ sig = np.log(p).diff().ewm(span=60).std()
101
+ tgt = sig.iloc[:max(len(sig) // 2, 60)].median()
102
+ out["VOLTARGET"] = (tgt / sig).clip(0, 1).shift(1)
103
+
104
+ return {k: v.fillna(0.0) for k, v in out.items()}
105
+
106
+
107
+ LABEL = {
108
+ "BUYHOLD": {"ko": "๋‹จ์ˆœ ๋ณด์œ ", "en": "Buy and hold"},
109
+ "SMA200": {"ko": "200์ผ์„  ์œ„์—์„œ๋งŒ ๋ณด์œ ",
110
+ "en": "Hold only above the 200-day MA"},
111
+ "GOLDEN50_200": {"ko": "๊ณจ๋“ ํฌ๋กœ์Šค 50/200", "en": "Golden cross 50/200"},
112
+ "SMA20_50": {"ko": "์ด๋™ํ‰๊ท  20/50 ๊ต์ฐจ", "en": "SMA 20/50 crossover"},
113
+ "MOM60": {"ko": "60์ผ ๋ชจ๋ฉ˜ํ…€", "en": "60-day momentum"},
114
+ "DONCHIAN20": {"ko": "๋ˆ์น˜์•ˆ 20์ผ ๋ŒํŒŒ", "en": "Donchian 20 breakout"},
115
+ "RSI14": {"ko": "RSI 14 ์—ญ์ถ”์„ธ", "en": "RSI 14 mean reversion"},
116
+ "BBAND20": {"ko": "๋ณผ๋ฆฐ์ € 20 ๋ณต๊ท€", "en": "Bollinger 20 reversion"},
117
+ "MACD": {"ko": "MACD 12/26/9", "en": "MACD 12/26/9"},
118
+ "ICHIMOKU": {"ko": "์ผ๋ชฉ๊ท ํ˜•ํ‘œ ๊ตฌ๋ฆ„", "en": "Ichimoku cloud"},
119
+ "ABSMOM12M": {"ko": "12๊ฐœ์›” ์ ˆ๋Œ€ ๋ชจ๋ฉ˜ํ…€", "en": "12-month absolute momentum"},
120
+ "STOCH14": {"ko": "์Šคํ† ์บ์Šคํ‹ฑ 14/3", "en": "Stochastic 14/3"},
121
+ "VOLTARGET": {"ko": "๋ณ€๋™์„ฑ ํƒ€๊ฒŸํŒ…", "en": "Volatility targeting"},
122
+ }
123
+ ORDER = ["BUYHOLD", "SMA200", "GOLDEN50_200", "SMA20_50", "MACD", "ICHIMOKU",
124
+ "MOM60", "ABSMOM12M", "DONCHIAN20", "RSI14", "STOCH14", "BBAND20",
125
+ "VOLTARGET"]
126
+
127
+
128
+ def curves(px, asset, scoring, lang="ko", since=SINCE):
129
+ """์˜ฌํ•ด ์ฒซ ๊ฑฐ๋ž˜์ผ๋ถ€ํ„ฐ ์ง€๊ธˆ๊นŒ์ง€. ์ฐธ๊ฐ€์ž์™€ ๊ฐ™์€ ์—”์ง„ยท๊ฐ™์€ ์ˆ˜์ˆ˜๋ฃŒ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค.
130
+
131
+ ๐Ÿ”‘ ์ง€ํ‘œ ๊ณ„์‚ฐ์—๋Š” ๊ทธ ์ด์ „ ๋ฐ์ดํ„ฐ๋„ ์“ด๋‹ค โ€” 1์›” 2์ผ์˜ 200์ผ์„ ์€ ์ž‘๋…„ ๊ฐ’์ด
132
+ ์žˆ์–ด์•ผ ๋‚˜์˜จ๋‹ค. ์ž˜๋ผ๋‚ธ ๋’ค์— ์ง€ํ‘œ๋ฅผ ๋งŒ๋“ค๋ฉด ์—ฐ์ดˆ ๊ตฌ๊ฐ„์ด ํ†ต์งธ๋กœ ๋น„๊ณ ,
133
+ ๊ทธ๋Ÿฌ๋ฉด ๊ทœ์น™์ด ์‹œ์ž‘ํ•˜์ง€๋„ ์•Š์€ ์ƒํƒœ๋กœ ๊ทธ๋ ค์ง„๋‹ค.
134
+ """
135
+ p = pd.Series(px, dtype=float).sort_index()
136
+ if len(p) < 300: # 252์ผ ๋ชจ๋ฉ˜ํ…€ + ์—ฌ์œ 
137
+ return {"t": [], "series": []}
138
+ w = _weights(p) # ์ „ ๊ตฌ๊ฐ„์œผ๋กœ ์ง€ํ‘œ๋ฅผ ๋งŒ๋“  ๋’ค์—
139
+ idx = p.index[p.index >= pd.Timestamp(since)] # ํ‘œ์‹œ ๊ตฌ๊ฐ„๋งŒ ์ž๋ฅธ๋‹ค
140
+ if len(idx) < 5:
141
+ return {"t": [], "series": []}
142
+ seg = p.loc[idx]
143
+ out = []
144
+ for k in ORDER:
145
+ pos = {t: float(w[k].loc[t]) for t in idx}
146
+ eq, _ = scoring.equity(pos, seg, asset)
147
+ out.append({
148
+ "key": k,
149
+ "label": LABEL[k]["en" if lang == "en" else "ko"],
150
+ "v": [round(float(x - 1) * 100, 4) for x in eq],
151
+ "ret": round(float(eq.iloc[-1] - 1) * 100, 2),
152
+ "turnover": round(scoring.turnover(pos, seg), 1),
153
+ "fee": round(scoring.fee_paid(pos, seg, asset) * 100, 2),
154
+ })
155
+ out.sort(key=lambda x: -x["ret"])
156
+ return {"t": [t.isoformat() for t in idx], "series": out,
157
+ "since": str(idx[0].date()), "window": len(idx),
158
+ "market": [round(float(x), 4) for x in (seg / seg.iloc[0] - 1) * 100]}
app/fetch.py CHANGED
@@ -91,6 +91,35 @@ def _gecko(cid, days):
91
  return s[~s.index.duplicated(keep="last")]
92
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def _feed(sym, days):
95
  """์šฐ๋ฆฌ๊ฐ€ ๋ฐœํ–‰ํ•œ ์‹œ์„ธ ํ”ผ๋“œ. ๊ณต๊ฐœ ์†Œ์Šค๊ฐ€ ์ „๋ถ€ ๋ง‰ํžŒ ์„œ๋ฒ„์—์„œ๋Š” ์ด๊ฒŒ ์œ ์ผํ•œ ๊ธธ์ด๋‹ค.
96
 
 
91
  return s[~s.index.duplicated(keep="last")]
92
 
93
 
94
+ def _feed_h(sym, bars):
95
+ """์‹œ๊ฐ„๋ด‰ ํ”ผ๋“œ. ์ฑ„์  ๊ฒฉ์ž๊ฐ€ ์ด๊ฑธ ์“ด๋‹ค.
96
+
97
+ ๐Ÿ”‘ ์ผ๋ด‰์œผ๋กœ ์ฑ„์ ํ•˜๋ฉด ๊ฐœ๋ง‰์ผ์— ํ•˜๋ฃจ๋ฅผ ํ†ต์งธ๋กœ ๊ธฐ๋‹ค๋ ค์•ผ ์ฒซ ์ˆ˜์ต๋ฅ ์ด ๋‚˜์˜จ๋‹ค.
98
+ ๊ทœ์น™์—๋Š” '๋งค์‹œ๊ฐ„ ๊ฐฑ์‹  ๊ฐ€๋Šฅ'์ด๋ผ๊ณ  ์จ ๋†“๊ณ  ์ฑ„์ ๋งŒ ์ผ ๋‹จ์œ„์ด๋ฉด ์–ด๊ธ‹๋‚œ๋‹ค.
99
+ """
100
+ import os
101
+ from huggingface_hub import hf_hub_download
102
+ repo = os.environ.get("FINCHAL_FEED", "FINAL-Bench/finchal-prices")
103
+ tok = os.environ.get("HF_TOKEN") or os.environ.get("FINCHAL_HF_TOKEN")
104
+ f = hf_hub_download(repo, "prices_1h/%s.csv" % sym, repo_type="dataset", token=tok)
105
+ d = pd.read_csv(f)
106
+ s = pd.Series(d["close"].values, index=pd.to_datetime(d["ts"])).dropna()
107
+ s = s[~s.index.duplicated(keep="last")].sort_index()
108
+ return s.iloc[-bars:]
109
+
110
+
111
+ def hourly(asset, bars=2000):
112
+ """์‹œ๊ฐ„๋ด‰. ํ”ผ๋“œ์—๋งŒ ์žˆ๋‹ค. ์—†์œผ๋ฉด None ์„ ๋Œ๋ ค์ฃผ๊ณ  ํ˜ธ์ถœ๋ถ€๊ฐ€ ์ผ๋ด‰์œผ๋กœ ๋‚ด๋ ค๊ฐ„๋‹ค."""
113
+ sym = MAP.get(asset, {}).get("feed")
114
+ if not sym:
115
+ return None
116
+ try:
117
+ s = _feed_h(sym, bars)
118
+ return s if len(s) >= 24 else None
119
+ except Exception:
120
+ return None
121
+
122
+
123
  def _feed(sym, days):
124
  """์šฐ๋ฆฌ๊ฐ€ ๋ฐœํ–‰ํ•œ ์‹œ์„ธ ํ”ผ๋“œ. ๊ณต๊ฐœ ์†Œ์Šค๊ฐ€ ์ „๋ถ€ ๋ง‰ํžŒ ์„œ๋ฒ„์—์„œ๋Š” ์ด๊ฒŒ ์œ ์ผํ•œ ๊ธธ์ด๋‹ค.
125
 
app/main.py CHANGED
@@ -20,6 +20,7 @@ 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"]
@@ -72,10 +73,34 @@ def prices(asset, interval="1d", bars=800):
72
  return _PX["d"][k].iloc[-bars:]
73
 
74
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  def season_px(asset):
76
- """์‹œ์ฆŒ ๊ฐœ๋ง‰ ์ดํ›„ ๊ตฌ๊ฐ„๋งŒ. ์ฑ„์ ๊ณผ ๊ธฐ์ค€๋ถ„ํฌ๊ฐ€ ์ด๊ฑธ ๋ณธ๋‹ค."""
77
- s = prices(asset, "1d", 2000)
78
- return s[s.index >= pd.Timestamp(S1["opens"])]
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
 
81
  def rules_doc(lang="ko"):
@@ -342,9 +367,17 @@ def set_lang(code: str):
342
  def api_data(asset: str, bars: int = 500, interval: str = "1d"):
343
  if asset not in ASSETS:
344
  return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
 
 
 
 
 
 
 
 
345
  if interval != "1d":
346
  return JSONResponse({"ok": False,
347
- "error": "์ผ๋ด‰๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค / daily bars only"}, 400)
348
  s = prices(asset, interval, min(max(bars, 10), 5000))
349
  return {"ok": True, "asset": asset, "interval": interval,
350
  "bars": [{"t": t.isoformat(), "c": float(v)} for t, v in s.items()]}
@@ -414,6 +447,22 @@ def api_curves(asset: str, top: int = 12):
414
  return {"ok": True, "asset": asset, **curves(asset, top)}
415
 
416
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  @app.get("/api/manifesto")
418
  def api_manifesto(request: Request, lang: str = None):
419
  g = lang_of(request, lang)
@@ -444,12 +493,22 @@ def _call(name, args, key=None, lang="ko"):
444
  if a not in ASSETS:
445
  return {"error": T(I.HINT["unknown_asset"]) % list(ASSETS)}
446
  iv = args.get("interval", "1d")
 
 
 
 
 
 
 
 
 
 
447
  if iv != "1d":
448
  # ์š”์ฒญ์„ ์กฐ์šฉํžˆ ๋ฌด์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ชป ์ฃผ๋Š” ๊ฒƒ์€ ๋ชป ์ค€๋‹ค๊ณ  ๋งํ•œ๋‹ค.
449
- return {"error": ("์ผ๋ด‰๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. interval ์€ '1d' ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."
450
  if lang == "ko" else
451
- "Only daily bars are served. interval must be '1d'.")}
452
- s = prices(a, iv, min(args.get("bars", 500), 5000))
453
  return {"asset": a, "interval": "1d",
454
  "bars": [{"t": t.date().isoformat(), "c": round(float(v), 6)}
455
  for t, v in s.items()]}
 
20
  import mcp as MCP
21
  import fetch as FE
22
  import i18n as I
23
+ import baselines as BL
24
 
25
  CEIL = json.load(open(os.path.join(os.path.dirname(HERE), "announce_ceiling.json"),
26
  encoding="utf-8"))["ceiling"]
 
73
  return _PX["d"][k].iloc[-bars:]
74
 
75
 
76
+ _HX = {"at": 0.0, "d": {}}
77
+
78
+
79
+ def hourly_px(asset):
80
+ """์‹œ๊ฐ„๋ด‰ ์บ์‹œ. ์ฑ„์  ๊ฒฉ์ž๊ฐ€ ์ด๊ฑธ ์“ด๋‹ค."""
81
+ if time.time() - _HX["at"] > 600:
82
+ _HX["d"].clear(); _HX["at"] = time.time()
83
+ if asset not in _HX["d"]:
84
+ _HX["d"][asset] = FE.hourly(asset, 2000)
85
+ return _HX["d"][asset]
86
+
87
+
88
  def season_px(asset):
89
+ """์‹œ์ฆŒ ๊ฐœ๋ง‰ ์ดํ›„ ๊ตฌ๊ฐ„. ์ฑ„์ ๊ณผ ๊ธฐ์ค€๋ถ„ํฌ๊ฐ€ ์ด๊ฑธ ๋ณธ๋‹ค.
90
+
91
+ ๐Ÿ”‘ ์‹œ๊ฐ„๋ด‰์ด ์žˆ์œผ๋ฉด ์‹œ๊ฐ„๋ด‰์œผ๋กœ ์žฐ๋‹ค. ์ผ๋ด‰์œผ๋กœ ์ฑ„์ ํ•˜๋ฉด ๊ฐœ๋ง‰์ผ์— ํ•˜๋ฃจ๋ฅผ
92
+ ํ†ต์งธ๋กœ ๊ธฐ๋‹ค๋ ค์•ผ ์ฒซ ์ˆ˜์ต๋ฅ ์ด ๋‚˜์˜ค๋Š”๋ฐ, ๊ทœ์น™์€ '๋งค์‹œ๊ฐ„ ๊ฐฑ์‹  ๊ฐ€๋Šฅ'์ด๋ผ๊ณ 
93
+ ๋งํ•˜๊ณ  ์žˆ๋‹ค. ์žฌ๋Š” ๋‹จ์œ„์™€ ์•ฝ์†ํ•œ ๋‹จ์œ„๊ฐ€ ๋‹ค๋ฅด๋ฉด ์•ˆ ๋œ๋‹ค.
94
+ ์‹œ๊ฐ„๋ด‰์ด ์•„์ง ์—†๋Š” ์ข…๋ชฉ์€ ์ผ๋ด‰์œผ๋กœ ๋‚ด๋ ค๊ฐ„๋‹ค โ€” ๋น„์–ด ์žˆ๋Š” ๊ฒƒ๋ณด๋‹ค ๋‚ซ๋‹ค.
95
+ """
96
+ op = pd.Timestamp(S1["opens"])
97
+ h = hourly_px(asset)
98
+ if h is not None and len(h):
99
+ sub = h[h.index >= op]
100
+ if len(sub) >= 2:
101
+ return sub
102
+ d = prices(asset, "1d", 2000)
103
+ return d[d.index >= op]
104
 
105
 
106
  def rules_doc(lang="ko"):
 
367
  def api_data(asset: str, bars: int = 500, interval: str = "1d"):
368
  if asset not in ASSETS:
369
  return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
370
+ if interval == "1h":
371
+ h = hourly_px(asset)
372
+ if h is None or not len(h):
373
+ return JSONResponse({"ok": False,
374
+ "error": "์‹œ๊ฐ„๋ด‰ ์—†์Œ / no hourly bars yet"}, 400)
375
+ return {"ok": True, "asset": asset, "interval": "1h",
376
+ "bars": [{"t": t.isoformat(), "c": float(v)}
377
+ for t, v in h.iloc[-min(max(bars, 10), 5000):].items()]}
378
  if interval != "1d":
379
  return JSONResponse({"ok": False,
380
+ "error": "1d ๋˜๋Š” 1h / 1d or 1h only"}, 400)
381
  s = prices(asset, interval, min(max(bars, 10), 5000))
382
  return {"ok": True, "asset": asset, "interval": interval,
383
  "bars": [{"t": t.isoformat(), "c": float(v)} for t, v in s.items()]}
 
447
  return {"ok": True, "asset": asset, **curves(asset, top)}
448
 
449
 
450
+ _BLC = {}
451
+
452
+
453
+ @app.get("/api/baselines")
454
+ def api_baselines(request: Request, asset: str, lang: str = None):
455
+ """๊ต๊ณผ์„œ ๊ทœ์น™๋“ค์„ ์ตœ๊ทผ ๊ตฌ๊ฐ„์— ์ ์šฉํ•œ ๋˜๋Œ์•„๋ณด๊ธฐ. ์ฐธ๊ฐ€์ž ๊ณก์„ ์˜ ์ฒ™๋„๋กœ ์“ด๋‹ค."""
456
+ if asset not in ASSETS:
457
+ return JSONResponse({"ok": False, "error": "์•Œ ์ˆ˜ ์—†๋Š” ์ข…๋ชฉ"}, 400)
458
+ g = lang_of(request, lang)
459
+ k = "%s|%s" % (asset, g)
460
+ e = _BLC.get(k)
461
+ if not e or time.time() - e[0] > 1800:
462
+ _BLC[k] = (time.time(), BL.curves(prices(asset, "1d", 400), asset, SC, g))
463
+ return {"ok": True, "asset": asset, **_BLC[k][1]}
464
+
465
+
466
  @app.get("/api/manifesto")
467
  def api_manifesto(request: Request, lang: str = None):
468
  g = lang_of(request, lang)
 
493
  if a not in ASSETS:
494
  return {"error": T(I.HINT["unknown_asset"]) % list(ASSETS)}
495
  iv = args.get("interval", "1d")
496
+ n = min(args.get("bars", 500), 5000)
497
+ if iv == "1h":
498
+ h = hourly_px(a)
499
+ if h is None or not len(h):
500
+ return {"error": ("์ด ์ข…๋ชฉ์€ ์•„์ง ์‹œ๊ฐ„๋ด‰์ด ์—†์Šต๋‹ˆ๋‹ค. '1d' ๋ฅผ ์“ฐ์‹ญ์‹œ์˜ค."
501
+ if lang == "ko" else
502
+ "No hourly bars for this asset yet. Use '1d'.")}
503
+ return {"asset": a, "interval": "1h",
504
+ "bars": [{"t": t.isoformat(sep=" "), "c": round(float(v), 6)}
505
+ for t, v in h.iloc[-n:].items()]}
506
  if iv != "1d":
507
  # ์š”์ฒญ์„ ์กฐ์šฉํžˆ ๋ฌด์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค. ๋ชป ์ฃผ๋Š” ๊ฒƒ์€ ๋ชป ์ค€๋‹ค๊ณ  ๋งํ•œ๋‹ค.
508
+ return {"error": ("interval ์€ '1d' ๋˜๋Š” '1h' ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค."
509
  if lang == "ko" else
510
+ "interval must be '1d' or '1h'.")}
511
+ s = prices(a, iv, n)
512
  return {"asset": a, "interval": "1d",
513
  "bars": [{"t": t.date().isoformat(), "c": round(float(v), 6)}
514
  for t, v in s.items()]}
app/mcp.py CHANGED
@@ -44,8 +44,10 @@ TOOLS = [
44
  # ๐Ÿ”ด ์‹œ๊ฐ„๋ด‰์„ ๋ชฉ๋ก์— ๋‘๋ฉด ์•ˆ ๋œ๋‹ค. ์šฐ๋ฆฌ ์‹œ์„ธ ํ”ผ๋“œ๋Š” ์ผ๋ด‰๋งŒ ๋ฐœํ–‰ํ•˜๋Š”๋ฐ
45
  # 1h ๋ฅผ ๋ฐ›์•„ ๋†“๊ณ  ์ผ๋ด‰์„ ๋Œ๋ ค์ฃผ๋ฉด ์—์ด์ „ํŠธ๋Š” ์‹œ๊ฐ„๋ด‰์œผ๋กœ ๋ฏฟ๊ณ 
46
  # ๋ชจ๋ธ์„ ์„ธ์šด๋‹ค. ์กฐ์šฉํžˆ ํ‹€๋ฆฐ ๋‹ต์ด ๊ฐ€์žฅ ๋‚˜์˜๋‹ค.
47
- "interval": {"type": "string", "enum": ["1d"], "default": "1d",
48
- "description": "์ผ๋ด‰๋งŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค / daily bars only"},
 
 
49
  },
50
  "required": ["asset"],
51
  },
 
44
  # ๐Ÿ”ด ์‹œ๊ฐ„๋ด‰์„ ๋ชฉ๋ก์— ๋‘๋ฉด ์•ˆ ๋œ๋‹ค. ์šฐ๋ฆฌ ์‹œ์„ธ ํ”ผ๋“œ๋Š” ์ผ๋ด‰๋งŒ ๋ฐœํ–‰ํ•˜๋Š”๋ฐ
45
  # 1h ๋ฅผ ๋ฐ›์•„ ๋†“๊ณ  ์ผ๋ด‰์„ ๋Œ๋ ค์ฃผ๋ฉด ์—์ด์ „ํŠธ๋Š” ์‹œ๊ฐ„๋ด‰์œผ๋กœ ๋ฏฟ๊ณ 
46
  # ๋ชจ๋ธ์„ ์„ธ์šด๋‹ค. ์กฐ์šฉํžˆ ํ‹€๋ฆฐ ๋‹ต์ด ๊ฐ€์žฅ ๋‚˜์˜๋‹ค.
47
+ "interval": {"type": "string", "enum": ["1d", "1h"], "default": "1d",
48
+ "description": "์ผ๋ด‰ ๋˜๋Š” ์‹œ๊ฐ„๋ด‰. ์ฑ„์ ์€ ์‹œ๊ฐ„๋ด‰ ๊ฒฉ์ž์—์„œ "
49
+ "์ด๋ค„์ง‘๋‹ˆ๋‹ค / daily or hourly; scoring runs "
50
+ "on the hourly grid"},
51
  },
52
  "required": ["asset"],
53
  },
app/static/index.html CHANGED
@@ -228,6 +228,8 @@ pre{background:#0f1729;color:#e6edf7;border-radius:11px;padding:14px 16px;overfl
228
  <div class="kpis" id="kpis"></div>
229
  <div id="chart"></div>
230
  <div class="legend" id="legend"></div>
 
 
231
  </div>
232
 
233
  <div>
@@ -348,6 +350,11 @@ var S = {
348
  "์•„๋ž˜๋ฅผ ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋„ฃ์œผ์‹ญ์‹œ์˜ค.",
349
  creed:"์ด ๋Œ€ํšŒ๋Š” ๋‹ต์„ ๋ชจ์œผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฒฝ๊ณ„๋ฅผ ์žฝ๋‹ˆ๋‹ค.",
350
  pending:"์ง‘๊ณ„ ์ „",
 
 
 
 
 
351
  pendingNote:"์‹œ์ฆŒ์ด ์˜ค๋Š˜ ์—ด๋ ธ์Šต๋‹ˆ๋‹ค. ์ ‘์ˆ˜๋œ ํฌ์ง€์…˜์€ ์•„๋ž˜์™€ ๊ฐ™๊ณ , ์ˆ˜์ต๋ฅ ์€ " +
352
  "ํ•˜๋ฃจ์น˜ ๊ตฌ๊ฐ„์ด ์Œ“์ด๋Š” ๋Œ€๋กœ ๊ณ„์‚ฐ๋˜์–ด ์ˆœ์œ„๊ฐ€ ๋งค๊ฒจ์ง‘๋‹ˆ๋‹ค.",
353
  tv:"์‹ค์‹œ๊ฐ„ ์‹œ์„ธ", tvSub:"TradingView ์ œ๊ณต ยท ์ฑ„์ ์— ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ข…๋ชฉ",
@@ -398,6 +405,12 @@ var S = {
398
  "Paste the block below.",
399
  creed:"This contest does not collect answers. It measures the boundary.",
400
  pending:"pending",
 
 
 
 
 
 
401
  pendingNote:"The season opened today. Positions received so far are listed below; " +
402
  "returns and ranks appear once a full day of data accumulates.",
403
  tv:"Live market", tvSub:"By TradingView ยท the same instrument we score",
@@ -743,11 +756,14 @@ async function drawChart(a){
743
  leg.push([col, x.user + " ยท " + x.strategy, x.v[x.v.length - 1]]);
744
  });
745
  chart.timeScale().fitContent();
746
- $("legend").innerHTML = leg.length
747
- ? leg.map(function(e){
748
- return '<span><i style="background:' + e[0] + '"></i>' + e[1] +
749
- ' <b class="mono ' + cls(e[2]) + '">' + fmt(e[2]) + "</b></span>"; }).join("")
750
- : '<span class="faint">' + s.noCurve + "</span>";
 
 
 
751
 
752
  var rows = d.series || [];
753
  var best = rows.length ? Math.max.apply(null, rows.map(function(x){
@@ -763,6 +779,53 @@ async function drawChart(a){
763
  x[2] + '">' + x[1] + "</div></div>"; }).join("");
764
  }
765
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  async function drawRank(a){
767
  var s = L();
768
  var d = await (await fetch("/api/leaderboard?asset=" + a)).json();
 
228
  <div class="kpis" id="kpis"></div>
229
  <div id="chart"></div>
230
  <div class="legend" id="legend"></div>
231
+ <div class="note" id="bl-note" style="margin-top:14px"></div>
232
+ <div id="bl-table" style="margin-top:10px"></div>
233
  </div>
234
 
235
  <div>
 
350
  "์•„๋ž˜๋ฅผ ๊ทธ๋Œ€๋กœ ๋ถ™์—ฌ๋„ฃ์œผ์‹ญ์‹œ์˜ค.",
351
  creed:"์ด ๋Œ€ํšŒ๋Š” ๋‹ต์„ ๋ชจ์œผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ฒฝ๊ณ„๋ฅผ ์žฝ๋‹ˆ๋‹ค.",
352
  pending:"์ง‘๊ณ„ ์ „",
353
+ blNote:function(d, l){ return "<b>์ฐธ๊ณ  ยท " + d + " ๋ถ€ํ„ฐ ์ง€๊ธˆ๊นŒ์ง€</b> โ€” ์˜ฌํ•ด ์ฒซ ๊ฑฐ๋ž˜์ผ์— " +
354
+ "์ด ๊ทœ์น™๋“ค๋กœ " + l + " ์„ ์‹œ์ž‘ํ–ˆ๋‹ค๋ฉด ์ง€๊ธˆ ์–ด๋””์ธ์ง€๋ฅผ, <b>์ฐธ๊ฐ€์ž์™€ ๋˜‘๊ฐ™์€ ์ฑ„์ ๊ธฐยท" +
355
+ "๋˜‘๊ฐ™์€ ์ˆ˜์ˆ˜๋ฃŒ</b>๋กœ ๊ณ„์‚ฐํ•œ ๊ฐ’์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  ์‹ ํ˜ธ๋Š” ์ „๋‚ ๊นŒ์ง€์˜ ์ •๋ณด๋งŒ ์”๋‹ˆ๋‹ค. " +
356
+ "์ด๊ฒƒ์€ ๊ณผ๊ฑฐ ๊ตฌ๊ฐ„ ์žฌํ˜„์ด๋ฉฐ ๋ฏธ๋ž˜ ์„ฑ๊ณผ๋„, ์ฐธ๊ฐ€์ž ์„ฑ์ ๋„ ์•„๋‹™๋‹ˆ๋‹ค."; },
357
+ blTh:["์ฐธ๊ณ  ์ „๋žต","์—ฐ์ดˆ ๋Œ€๋น„","ํšŒ์ „์œจ","์ˆ˜์ˆ˜๋ฃŒ"],
358
  pendingNote:"์‹œ์ฆŒ์ด ์˜ค๋Š˜ ์—ด๋ ธ์Šต๋‹ˆ๋‹ค. ์ ‘์ˆ˜๋œ ํฌ์ง€์…˜์€ ์•„๋ž˜์™€ ๊ฐ™๊ณ , ์ˆ˜์ต๋ฅ ์€ " +
359
  "ํ•˜๋ฃจ์น˜ ๊ตฌ๊ฐ„์ด ์Œ“์ด๋Š” ๋Œ€๋กœ ๊ณ„์‚ฐ๋˜์–ด ์ˆœ์œ„๊ฐ€ ๋งค๊ฒจ์ง‘๋‹ˆ๋‹ค.",
360
  tv:"์‹ค์‹œ๊ฐ„ ์‹œ์„ธ", tvSub:"TradingView ์ œ๊ณต ยท ์ฑ„์ ์— ์“ฐ๋Š” ๊ฒƒ๊ณผ ๊ฐ™์€ ์ข…๋ชฉ",
 
405
  "Paste the block below.",
406
  creed:"This contest does not collect answers. It measures the boundary.",
407
  pending:"pending",
408
+ blNote:function(d, l){ return "<b>Reference ยท since " + d + "</b> โ€” where these rules " +
409
+ "would stand today had you started them on " + l + " at the first trading day of the " +
410
+ "year, computed through <b>the same scorer and the same fees</b> entrants face. Every " +
411
+ "signal uses information available the day before. This is a replay of the past, not " +
412
+ "future performance and not an entrant result."; },
413
+ blTh:["Reference strategy","Since Jan 1","Turnover","Fees"],
414
  pendingNote:"The season opened today. Positions received so far are listed below; " +
415
  "returns and ranks appear once a full day of data accumulates.",
416
  tv:"Live market", tvSub:"By TradingView ยท the same instrument we score",
 
756
  leg.push([col, x.user + " ยท " + x.strategy, x.v[x.v.length - 1]]);
757
  });
758
  chart.timeScale().fitContent();
759
+ $("legend").innerHTML = leg.map(function(e){
760
+ return '<span><i style="background:' + e[0] + '"></i>' + e[1] +
761
+ ' <b class="mono ' + cls(e[2]) + '">' + fmt(e[2]) + "</b></span>"; }).join("");
762
+
763
+ /* ์ฐธ๊ฐ€์ž ๊ณก์„ ์ด ์•„์ง ์—†์œผ๋ฉด ๊ต๊ณผ์„œ ๊ทœ์น™๋“ค์„ ๋Œ€์‹  ๊น”์•„ ์ฒ™๋„๋ฅผ ๋งŒ๋“ ๋‹ค.
764
+ ๐Ÿ”ด ์ด๊ฑด ๊ณผ๊ฑฐ ์žฌํ˜„์ด์ง€ ์ฐธ๊ฐ€์ž ์„ฑ์ ์ด ์•„๋‹ˆ๋‹ค. ๊ทธ ์‚ฌ์‹ค์„ ํ™”๋ฉด์— ์ ๋Š”๋‹ค. */
765
+ if (!(d.series || []).length) { drawBaselines(a); }
766
+ else { $("bl-note").textContent = ""; $("bl-table").innerHTML = ""; }
767
 
768
  var rows = d.series || [];
769
  var best = rows.length ? Math.max.apply(null, rows.map(function(x){
 
779
  x[2] + '">' + x[1] + "</div></div>"; }).join("");
780
  }
781
 
782
+ async function drawBaselines(a){
783
+ var s = L();
784
+ var d = await (await fetch("/api/baselines?asset=" + a + "&lang=" + LANG)).json();
785
+ if (!d.series || !d.series.length) {
786
+ $("legend").innerHTML = '<span class="faint">' + s.noCurve + "</span>";
787
+ return;
788
+ }
789
+ var el = $("chart");
790
+ if (chart) { chart.remove(); chart = null; }
791
+ chart = LightweightCharts.createChart(el, {
792
+ layout:{background:{color:"#ffffff"}, textColor:C.dim, fontSize:11},
793
+ grid:{vertLines:{color:C.line}, horzLines:{color:C.line}},
794
+ rightPriceScale:{borderColor:C.line},
795
+ timeScale:{borderColor:C.line, timeVisible:false},
796
+ crosshair:{mode:1}, height:el.clientHeight});
797
+ var t = d.t.map(function(x){ return x.slice(0, 10); });
798
+ var add = function(vals, color, w, dash){
799
+ var ss = chart.addLineSeries({color:color, lineWidth:w, lineStyle:dash ? 2 : 0,
800
+ priceLineVisible:false, lastValueVisible:false});
801
+ ss.setData(t.map(function(x, i){ return {time:x, value:vals[i]}; }));
802
+ };
803
+ var leg = [];
804
+ if (d.market) {
805
+ add(d.market, C.dim, 2, true);
806
+ leg.push([C.dim, s.market(R.assets[a].label), d.market[d.market.length - 1]]);
807
+ }
808
+ d.series.forEach(function(x, i){
809
+ var col = C.pal[i % C.pal.length];
810
+ add(x.v, col, 2, false);
811
+ leg.push([col, x.label, x.ret]);
812
+ });
813
+ chart.timeScale().fitContent();
814
+ $("legend").innerHTML = leg.map(function(e){
815
+ return '<span><i style="background:' + e[0] + '"></i>' + e[1] +
816
+ ' <b class="mono ' + cls(e[2]) + '">' + fmt(e[2]) + "</b></span>"; }).join("");
817
+ $("bl-note").innerHTML = s.blNote(d.since || "", R.assets[a].label);
818
+ $("bl-table").innerHTML =
819
+ '<div class="tw"><table><thead><tr><th>' + s.blTh[0] + "</th>" +
820
+ '<th class="num">' + s.blTh[1] + '</th><th class="num">' + s.blTh[2] +
821
+ '</th><th class="num">' + s.blTh[3] + "</th></tr></thead><tbody>" +
822
+ d.series.map(function(x){
823
+ return "<tr><td>" + x.label + '</td><td class="num mono ' + cls(x.ret) +
824
+ '"><b>' + fmt(x.ret) + '</b></td><td class="num mono faint">' + x.turnover +
825
+ '</td><td class="num mono ' + (x.fee > 2 ? "dn" : "faint") + '">โˆ’' +
826
+ x.fee.toFixed(2) + "%</td></tr>"; }).join("") + "</tbody></table></div>";
827
+ }
828
+
829
  async function drawRank(a){
830
  var s = L();
831
  var d = await (await fetch("/api/leaderboard?asset=" + a)).json();