Spaces:
Running
Running
hourly scoring grid and 13 reference strategies since Jan 1
Browse files- app/baselines.py +158 -0
- app/fetch.py +29 -0
- app/main.py +66 -7
- app/mcp.py +4 -2
- 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 |
-
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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": "
|
| 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": ("
|
| 450 |
if lang == "ko" else
|
| 451 |
-
"
|
| 452 |
-
s = prices(a, iv,
|
| 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": "์ผ๋ด
|
|
|
|
|
|
|
| 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.
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
| 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();
|