Spaces:
Running
Running
| # -*- 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]} | |