finchal / app /baselines.py
SeaWolf-AI's picture
hourly scoring grid and 13 reference strategies since Jan 1
588034e verified
Raw
History Blame Contribute Delete
7.93 kB
# -*- coding: utf-8 -*-
"""์ฐธ๊ณ  ์ „๋žต โ€” ๊ต๊ณผ์„œ์— ๋‚˜์˜ค๋Š” ๊ทœ์น™๋“ค์„ ๊ฐ™์€ ์ฑ„์ ๊ธฐ๋กœ ๋Œ๋ฆฐ ๋˜๋Œ์•„๋ณด๊ธฐ.
์™œ ๋„ฃ๋Š”๊ฐ€
์‹œ์ฆŒ ์ดˆ์—๋Š” ์ฐธ๊ฐ€์ž ๊ณก์„ ์ด ์—†์–ด ๋น„๊ต ์ฐจํŠธ๊ฐ€ ๋น„์–ด ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด "์–ผ๋งˆ๋ฅผ ๋‚ด์•ผ
์ž˜ํ•œ ๊ฒƒ์ธ๊ฐ€"๋ฅผ ๊ฐ€๋Š ํ•  ๊ธฐ์ค€์ด ํ™”๋ฉด์— ์—†๋‹ค. ๋„๋ฆฌ ์•Œ๋ ค์ง„ ๊ทœ์น™ ๋ช‡ ๊ฐœ๋ฅผ ์ตœ๊ทผ
๊ตฌ๊ฐ„์— ์ ์šฉํ•ด ๊น”์•„ ๋‘๋ฉด ๊ทธ ์ž๋ฆฌ๊ฐ€ ์ฒ™๋„๊ฐ€ ๋œ๋‹ค.
๐Ÿ”ด ์ด๊ฒƒ์€ **๊ณผ๊ฑฐ ๊ตฌ๊ฐ„ ์žฌํ˜„**์ด๋‹ค. ๋ฏธ๋ž˜ ์„ฑ๊ณผ๊ฐ€ ์•„๋‹ˆ๊ณ , ์ฐธ๊ฐ€์ž ์„ฑ์ ๋„ ์•„๋‹ˆ๋‹ค.
ํ™”๋ฉด์— ๊ทธ ์‚ฌ์‹ค์„ ๋ฐ˜๋“œ์‹œ ํ•จ๊ป˜ ์ ๋Š”๋‹ค. ๋ฐฑํ…Œ์ŠคํŠธ๋ฅผ ์„ฑ์ ์ฒ˜๋Ÿผ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด
์ด ๋Œ€ํšŒ๊ฐ€ ์—†์• ๋ ค๋Š” ๋ฌธ์ œ ๊ทธ ์ž์ฒด๋‹ค.
๐Ÿ”‘ ์ฐธ๊ฐ€์ž์™€ **์™„์ „ํžˆ ๊ฐ™์€ ์ฑ„์ ๊ธฐยท๊ฐ™์€ ์ˆ˜์ˆ˜๋ฃŒ**๋กœ ๋Œ๋ฆฐ๋‹ค. ๊ทœ์น™๋งŒ ๋‹ค๋ฅด๊ณ 
๋‚˜๋จธ์ง€ ์กฐ๊ฑด์ด ๊ฐ™์•„์•ผ ๋น„๊ต๊ฐ€ ์„ฑ๋ฆฝํ•œ๋‹ค. ์ˆ˜์ˆ˜๋ฃŒ๋ฅผ ๋นผ๊ณ  ๊ทธ๋ฆฌ๋ฉด ํšŒ์ „์ด ๋†’์€
๊ทœ์น™์ด ๋ถ€๋‹นํ•˜๊ฒŒ ์ข‹์•„ ๋ณด์ธ๋‹ค.
๐Ÿ”‘ ์–ด๋–ค ๊ทœ์น™๋„ ๋ฏธ๋ž˜๋ฅผ ๋ณด์ง€ ์•Š๋Š”๋‹ค. ๋ชจ๋“  ์‹ ํ˜ธ๋Š” shift(1) ์ดํ›„์— ์“ด๋‹ค.
์ด๋™ํ‰๊ท ์„ ๊ทธ๋‚  ์ข…๊ฐ€๋กœ ๊ณ„์‚ฐํ•ด ๊ทธ๋‚  ํฌ์ง€์…˜์— ์“ฐ๋ฉด ์„ฑ์ ์ด ํ†ต์งธ๋กœ ๊ฑฐ์ง“์ด ๋œ๋‹ค.
"""
import numpy as np
import pandas as pd
# ์˜ฌํ•ด ์ฒซ ๊ฑฐ๋ž˜์ผ๋ถ€ํ„ฐ ์žฐ๋‹ค. "1์›” 1์ผ์— ์ด ๊ทœ์น™์œผ๋กœ ์ถœ๋ฐœํ–ˆ๋‹ค๋ฉด ์ง€๊ธˆ ์–ด๋””์ธ๊ฐ€" ๊ฐ€
# ๊ตด๋Ÿฌ๊ฐ€๋Š” 90์ผ ์ฐฝ๋ณด๋‹ค ํ›จ์”ฌ ์ฝ๊ธฐ ์‰ฝ๊ณ , ๊ธฐ์ค€์ ์ด ๊ณ ์ •๋˜์–ด ๋งค์ผ ๊ฐ’์ด ํ”๋“ค๋ฆฌ์ง€ ์•Š๋Š”๋‹ค.
SINCE = "2026-01-01"
def _rsi(p, n=14):
d = p.diff()
up = d.clip(lower=0).ewm(alpha=1 / n, adjust=False).mean()
dn = (-d.clip(upper=0)).ewm(alpha=1 / n, adjust=False).mean()
return 100 - 100 / (1 + up / dn.replace(0, np.nan))
def _weights(px):
"""๊ทœ์น™๋ณ„ ํฌ์ง€์…˜ ์‹œ๊ณ„์—ด. ์ „๋ถ€ ์ „๋‚ ๊นŒ์ง€์˜ ์ •๋ณด๋งŒ ์“ด๋‹ค."""
p = pd.Series(px, dtype=float).sort_index()
out = {}
out["BUYHOLD"] = pd.Series(1.0, index=p.index)
# 200์ผ์„  โ€” ๋‹จ์ผ ๊ทœ์น™์œผ๋กœ๋Š” ๊ฐ€์žฅ ๋„๋ฆฌ ์•Œ๋ ค์ง„ ๊ฒƒ. ์œ„๋ฉด ๋“ค๊ณ  ์•„๋ž˜๋ฉด ๋น„์šด๋‹ค.
s200 = p.rolling(200).mean()
out["SMA200"] = (p > s200).astype(float).shift(1)
# ๊ณจ๋“ /๋ฐ๋“œ ํฌ๋กœ์Šค โ€” 50์ผ์„ ์ด 200์ผ์„ ์„ ๋„˜์œผ๋ฉด ๋กฑ, ๋‚ด๋ ค๊ฐ€๋ฉด ์ˆ
s50x = p.rolling(50).mean()
out["GOLDEN50_200"] = np.sign(s50x - s200).shift(1)
# ์ด๋™ํ‰๊ท  ๊ต์ฐจ โ€” ๊ฐ€์žฅ ๋„๋ฆฌ ์•Œ๋ ค์ง„ ์ถ”์„ธ ๊ทœ์น™
s20, s50 = p.rolling(20).mean(), p.rolling(50).mean()
out["SMA20_50"] = np.sign(s20 - s50).shift(1)
# 60์ผ ๋ชจ๋ฉ˜ํ…€ โ€” ํ•™์ˆ  ๋ฌธํ—Œ์—์„œ ๊ฐ€์žฅ ๋งŽ์ด ๊ฒ€์ฆ๋œ ์ถ•
out["MOM60"] = np.sign(p / p.shift(60) - 1).shift(1)
# ๋ˆ์น˜์•ˆ 20์ผ ์ฑ„๋„ ๋ŒํŒŒ
hi, lo = p.rolling(20).max(), p.rolling(20).min()
br = pd.Series(0.0, index=p.index)
br[p >= hi] = 1.0
br[p <= lo] = -1.0
out["DONCHIAN20"] = br.replace(0.0, np.nan).ffill().fillna(0.0).shift(1)
# RSI ์—ญ์ถ”์„ธ โ€” ๊ณผ๋งค๋„์— ์‚ฌ๊ณ  ๊ณผ๋งค์ˆ˜์— ํŒ๋‹ค
r = _rsi(p)
out["RSI14"] = ((50 - r) / 20).clip(-1, 1).shift(1)
# ๋ณผ๋ฆฐ์ € ๋ณต๊ท€ โ€” ํ‰๊ท ์—์„œ ๋ฉ€์–ด์ง„ ๋งŒํผ ๋ฐ˜๋Œ€๋กœ
m, sd = p.rolling(20).mean(), p.rolling(20).std()
out["BBAND20"] = (-(p - m) / (2 * sd.replace(0, np.nan))).clip(-1, 1).shift(1)
# MACD (12,26,9) โ€” ์˜ค์‹ค๋ ˆ์ดํ„ฐ ์ค‘ ๊ฐ€์žฅ ๋„๋ฆฌ ์“ฐ์ด๋Š” ๊ฒƒ.
# ํžˆ์Šคํ† ๊ทธ๋žจ ๋ถ€ํ˜ธ๋กœ ๋ฐฉํ–ฅ์„ ์ •ํ•œ๋‹ค.
e12 = p.ewm(span=12, adjust=False).mean()
e26 = p.ewm(span=26, adjust=False).mean()
macd = e12 - e26
out["MACD"] = np.sign(macd - macd.ewm(span=9, adjust=False).mean()).shift(1)
# ์ผ๋ชฉ๊ท ํ˜•ํ‘œ ๊ตฌ๋ฆ„ โ€” ์ข…๊ฐ€๋งŒ ์žˆ์œผ๋ฏ€๋กœ ๊ณ ์ € ๋Œ€์‹  ์ข…๊ฐ€์˜ ์ด๋™ ์ตœ๊ณ ยท์ตœ์ €๋ฅผ ์“ด๋‹ค.
# ์›๋ณธ ์ •์˜์™€ ์™„์ „ํžˆ ๊ฐ™์ง€๋Š” ์•Š์ง€๋งŒ ๊ตฌ๋ฆ„์˜ ์œ„์น˜ ํŒ์ •์€ ๊ฐ™์€ ๋œป์„ ์œ ์ง€ํ•œ๋‹ค.
conv = (p.rolling(9).max() + p.rolling(9).min()) / 2
base = (p.rolling(26).max() + p.rolling(26).min()) / 2
spa = ((conv + base) / 2).shift(26)
spb = ((p.rolling(52).max() + p.rolling(52).min()) / 2).shift(26)
top, bot = pd.concat([spa, spb], axis=1).max(axis=1), pd.concat([spa, spb], axis=1).min(axis=1)
ich = pd.Series(0.0, index=p.index)
ich[p > top] = 1.0
ich[p < bot] = -1.0
out["ICHIMOKU"] = ich.shift(1)
# 12๊ฐœ์›” ์ ˆ๋Œ€ ๋ชจ๋ฉ˜ํ…€ โ€” ์ง€๋‚œ 1๋…„ ์ˆ˜์ต์ด ์–‘์ˆ˜๋ฉด ๋ณด์œ , ์•„๋‹ˆ๋ฉด ๋น„์šด๋‹ค.
out["ABSMOM12M"] = (p / p.shift(252) - 1 > 0).astype(float).shift(1)
# ์Šคํ† ์บ์Šคํ‹ฑ (14,3) โ€” ๊ณผ๋งค์ˆ˜ยท๊ณผ๋งค๋„ ํŒ์ •์˜ ๊ณ ์ „
lo14, hi14 = p.rolling(14).min(), p.rolling(14).max()
k = 100 * (p - lo14) / (hi14 - lo14).replace(0, np.nan)
out["STOCH14"] = ((50 - k.rolling(3).mean()) / 25).clip(-1, 1).shift(1)
# ๋ณ€๋™์„ฑ ํƒ€๊ฒŸํŒ… โ€” ๋ฐฉํ–ฅ์€ ๋กฑ ๊ณ ์ •, ํฌ๊ธฐ๋งŒ ์กฐ์ ˆ
sig = np.log(p).diff().ewm(span=60).std()
tgt = sig.iloc[:max(len(sig) // 2, 60)].median()
out["VOLTARGET"] = (tgt / sig).clip(0, 1).shift(1)
return {k: v.fillna(0.0) for k, v in out.items()}
LABEL = {
"BUYHOLD": {"ko": "๋‹จ์ˆœ ๋ณด์œ ", "en": "Buy and hold"},
"SMA200": {"ko": "200์ผ์„  ์œ„์—์„œ๋งŒ ๋ณด์œ ",
"en": "Hold only above the 200-day MA"},
"GOLDEN50_200": {"ko": "๊ณจ๋“ ํฌ๋กœ์Šค 50/200", "en": "Golden cross 50/200"},
"SMA20_50": {"ko": "์ด๋™ํ‰๊ท  20/50 ๊ต์ฐจ", "en": "SMA 20/50 crossover"},
"MOM60": {"ko": "60์ผ ๋ชจ๋ฉ˜ํ…€", "en": "60-day momentum"},
"DONCHIAN20": {"ko": "๋ˆ์น˜์•ˆ 20์ผ ๋ŒํŒŒ", "en": "Donchian 20 breakout"},
"RSI14": {"ko": "RSI 14 ์—ญ์ถ”์„ธ", "en": "RSI 14 mean reversion"},
"BBAND20": {"ko": "๋ณผ๋ฆฐ์ € 20 ๋ณต๊ท€", "en": "Bollinger 20 reversion"},
"MACD": {"ko": "MACD 12/26/9", "en": "MACD 12/26/9"},
"ICHIMOKU": {"ko": "์ผ๋ชฉ๊ท ํ˜•ํ‘œ ๊ตฌ๋ฆ„", "en": "Ichimoku cloud"},
"ABSMOM12M": {"ko": "12๊ฐœ์›” ์ ˆ๋Œ€ ๋ชจ๋ฉ˜ํ…€", "en": "12-month absolute momentum"},
"STOCH14": {"ko": "์Šคํ† ์บ์Šคํ‹ฑ 14/3", "en": "Stochastic 14/3"},
"VOLTARGET": {"ko": "๋ณ€๋™์„ฑ ํƒ€๊ฒŸํŒ…", "en": "Volatility targeting"},
}
ORDER = ["BUYHOLD", "SMA200", "GOLDEN50_200", "SMA20_50", "MACD", "ICHIMOKU",
"MOM60", "ABSMOM12M", "DONCHIAN20", "RSI14", "STOCH14", "BBAND20",
"VOLTARGET"]
def curves(px, asset, scoring, lang="ko", since=SINCE):
"""์˜ฌํ•ด ์ฒซ ๊ฑฐ๋ž˜์ผ๋ถ€ํ„ฐ ์ง€๊ธˆ๊นŒ์ง€. ์ฐธ๊ฐ€์ž์™€ ๊ฐ™์€ ์—”์ง„ยท๊ฐ™์€ ์ˆ˜์ˆ˜๋ฃŒ๋กœ ๊ณ„์‚ฐํ•œ๋‹ค.
๐Ÿ”‘ ์ง€ํ‘œ ๊ณ„์‚ฐ์—๋Š” ๊ทธ ์ด์ „ ๋ฐ์ดํ„ฐ๋„ ์“ด๋‹ค โ€” 1์›” 2์ผ์˜ 200์ผ์„ ์€ ์ž‘๋…„ ๊ฐ’์ด
์žˆ์–ด์•ผ ๋‚˜์˜จ๋‹ค. ์ž˜๋ผ๋‚ธ ๋’ค์— ์ง€ํ‘œ๋ฅผ ๋งŒ๋“ค๋ฉด ์—ฐ์ดˆ ๊ตฌ๊ฐ„์ด ํ†ต์งธ๋กœ ๋น„๊ณ ,
๊ทธ๋Ÿฌ๋ฉด ๊ทœ์น™์ด ์‹œ์ž‘ํ•˜์ง€๋„ ์•Š์€ ์ƒํƒœ๋กœ ๊ทธ๋ ค์ง„๋‹ค.
"""
p = pd.Series(px, dtype=float).sort_index()
if len(p) < 300: # 252์ผ ๋ชจ๋ฉ˜ํ…€ + ์—ฌ์œ 
return {"t": [], "series": []}
w = _weights(p) # ์ „ ๊ตฌ๊ฐ„์œผ๋กœ ์ง€ํ‘œ๋ฅผ ๋งŒ๋“  ๋’ค์—
idx = p.index[p.index >= pd.Timestamp(since)] # ํ‘œ์‹œ ๊ตฌ๊ฐ„๋งŒ ์ž๋ฅธ๋‹ค
if len(idx) < 5:
return {"t": [], "series": []}
seg = p.loc[idx]
out = []
for k in ORDER:
pos = {t: float(w[k].loc[t]) for t in idx}
eq, _ = scoring.equity(pos, seg, asset)
out.append({
"key": k,
"label": LABEL[k]["en" if lang == "en" else "ko"],
"v": [round(float(x - 1) * 100, 4) for x in eq],
"ret": round(float(eq.iloc[-1] - 1) * 100, 2),
"turnover": round(scoring.turnover(pos, seg), 1),
"fee": round(scoring.fee_paid(pos, seg, asset) * 100, 2),
})
out.sort(key=lambda x: -x["ret"])
return {"t": [t.isoformat() for t in idx], "series": out,
"since": str(idx[0].date()), "window": len(idx),
"market": [round(float(x), 4) for x in (seg / seg.iloc[0] - 1) * 100]}