File size: 7,931 Bytes
588034e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# -*- 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]}