"""
손절선 -3% / -5% / -8% 고정 vs ATR 2배 — 같은 진입, 손절만 바꿔 10년 돌린 실측
https://algolab.co.kr/blog/atr-stop-vs-fixed-percent-stop-backtest-2026

진입 : 종가가 직전 20거래일 고가를 넘으면(20일 돌파) -> 다음 날 시가 매수
청산 : ① 손절 — 장중 저가가 손절가에 닿으면 손절가 체결
              (시가부터 손절가 아래로 갭이면 시가 체결)
       ② 추세 이탈 — 종가가 직전 10거래일 저가를 깨면 다음 날 시가 매도
손절 4종 : 진입가 -3% / -5% / -8% / 진입가 - ATR(14, Wilder) x 2 (신호일 기준, 진입 후 고정)
비용 : 왕복 0.3% (수수료+세금+슬리피지 뭉뚱그린 가정)
종목 : 2016년 이전 상장 국내 종목 22개 (지금 살아 있는 종목 = 생존 편향 있음)

같은 날 매수하고 그 날 저가가 손절가 아래면 그날 손절로 친다(보수적).
일봉만으로는 장중 순서를 모른다 — 관련 글: /blog/daily-bar-stop-loss-take-profit-same-day-backtest-2026

ASOF 를 고정한 이유: 기준일 없이 돌리면 실행 시점까지 데이터를 받아
글의 숫자와 다르게 나온다. 최신으로 보려면 ASOF = None (숫자 달라짐).
수정주가(FinanceDataReader 기본값) 기준, 배당 미포함. 과거 결과는 미래를 보장하지 않는다.
"""
import numpy as np, pandas as pd, FinanceDataReader as fdr

ASOF = "2026-09-30"     # 기준일 고정 — None 이면 실행 시점까지
START = "2016-01-01"
COST = 0.003
STOPS = {"-3%": ("pct", 0.03), "-5%": ("pct", 0.05), "-8%": ("pct", 0.08), "ATR x2": ("atr", 2.0)}

TICKERS = {
    "005930": "삼성전자", "000660": "SK하이닉스", "005380": "현대차", "000270": "기아",
    "012330": "현대모비스", "035420": "NAVER", "035720": "카카오", "051910": "LG화학",
    "006400": "삼성SDI", "068270": "셀트리온", "105560": "KB금융", "055550": "신한지주",
    "015760": "한국전력", "017670": "SK텔레콤", "030200": "KT", "033780": "KT&G",
    "009150": "삼성전기", "086520": "에코프로", "028300": "HLB", "196170": "알테오젠",
    "041510": "에스엠", "035900": "JYP Ent.",
}


def load(code):
    df = fdr.DataReader(code, "2015-06-01", ASOF)[["Open", "High", "Low", "Close"]].astype(float)
    if ASOF:
        df = df[df.index <= ASOF]          # 캐시·신규 다운로드 모두 같은 자르기
    return df[(df["Open"] > 0) & (df["Low"] > 0)]


def atr(df, n=14):
    pc = df["Close"].shift(1)
    tr = pd.concat([df["High"] - df["Low"], (df["High"] - pc).abs(), (df["Low"] - pc).abs()], axis=1).max(axis=1)
    return tr.ewm(alpha=1 / n, adjust=False).mean()      # Wilder 평활


def run(df, kind, k):
    o, h, l, c = (df[x].values for x in ("Open", "High", "Low", "Close"))
    hi20 = df["High"].rolling(20).max().shift(1).values
    lo10 = df["Low"].rolling(10).min().shift(1).values
    a = atr(df).values
    start_i = int(np.searchsorted(df.index.values, np.datetime64(START)))
    trades, i, n = [], max(start_i, 21), len(df)
    while i < n - 1:
        if c[i] > hi20[i]:                         # 신호(종가 확인) -> 다음 날 시가 진입
            e = i + 1
            entry = o[e]
            stop = entry * (1 - k) if kind == "pct" else entry - k * a[i]
            j, exit_px, why = e, None, None
            while j < n:
                if j > e and o[j] <= stop:        # 갭으로 손절가 아래 출발 -> 시가 체결
                    exit_px, why = o[j], "stop"; break
                if l[j] <= stop:
                    exit_px, why = min(stop, o[j]) if j > e else stop, "stop"; break
                if c[j] < lo10[j] and j + 1 < n:  # 추세 이탈 -> 다음 날 시가
                    exit_px, why = o[j + 1], "trend"; j += 1; break
                j += 1
            if exit_px is None:
                exit_px, why, j = c[n - 1], "open", n - 1
            trades.append((df.index[e], df.index[j], entry, exit_px, (entry - stop) / entry, why,
                           exit_px / entry - 1 - COST))
            i = j + 1
        else:
            i += 1
    return pd.DataFrame(trades, columns=["in", "out", "entry", "exit", "stop_dist", "why", "ret"])


def summarize(t):
    eq = (1 + t["ret"]).cumprod()
    mdd = (eq / eq.cummax() - 1).min() if len(t) else 0
    return dict(n=len(t), win=(t["ret"] > 0).mean(), avg_win=t.loc[t["ret"] > 0, "ret"].mean(),
                avg_loss=t.loc[t["ret"] <= 0, "ret"].mean(), stops=(t["why"] == "stop").mean(),
                total=eq.iloc[-1] - 1 if len(t) else 0, mdd=mdd, exp=t["ret"].mean())


if __name__ == "__main__":
    rows, per = [], []
    for code, name in TICKERS.items():
        df = load(code)
        d = df[df.index >= START]
        vol = (atr(df) / df["Close"])[df.index >= START].median()   # 종목 변동성 = ATR% 중앙값
        for label, (kind, k) in STOPS.items():
            t = run(df, kind, k)
            s = summarize(t)
            per.append(dict(code=code, name=name, atrpct=vol, stop=label, **s,
                            stop_dist=t["stop_dist"].median()))
            t["code"], t["stop"], t["atrpct"] = code, label, vol
            rows.append(t)
    P = pd.DataFrame(per)
    T = pd.concat(rows)
    print(f"ASOF {ASOF}  tickers {len(TICKERS)}  trades total {len(T):,}")

    print("\n[1] 전체 (22종목 거래 합산)")
    print("stop      trades   win%   avg_win  avg_loss  stop_hit%  expectancy")
    for label in STOPS:
        x = T[T["stop"] == label]
        print(f"{label:7s} {len(x):7d} {(x.ret>0).mean():6.1%} {x.loc[x.ret>0,'ret'].mean():8.2%} "
              f"{x.loc[x.ret<=0,'ret'].mean():8.2%} {(x.why=='stop').mean():9.1%} {x.ret.mean():9.2%}")

    print("\n[2] 종목별 누적수익 중앙값 / MDD 중앙값 / 이긴 종목 수(누적수익 1위)")
    best = P.loc[P.groupby("code")["total"].idxmax(), "stop"].value_counts()
    for label in STOPS:
        x = P[P["stop"] == label]
        print(f"{label:7s} total_med {x.total.median():8.1%}  mdd_med {x.mdd.median():7.1%}  best_in {best.get(label,0):2d}")

    P["tier"] = pd.qcut(P["atrpct"], 3, labels=["저변동", "중변동", "고변동"])
    print("\n[3] 변동성 구간별 (ATR% 3분위, 각 7~8종목) — 거래당 기대값 / 손절 비율 / ATR 손절폭 중앙값")
    T["tier"] = T["code"].map(P.drop_duplicates("code").set_index("code")["tier"])
    for tier in ["저변동", "중변동", "고변동"]:
        codes = P[P.tier == tier].drop_duplicates("code")
        rng = f"{codes.atrpct.min():.1%}~{codes.atrpct.max():.1%}"
        print(f"  {tier} (ATR% {rng})")
        for label in STOPS:
            x = T[(T.tier == tier) & (T.stop == label)]
            print(f"    {label:7s} n {len(x):4d}  win {(x.ret>0).mean():5.1%}  exp {x.ret.mean():6.2%}  "
                  f"stop_hit {(x.why=='stop').mean():5.1%}  dist_med {x.stop_dist.median():5.1%}")

    print("\n[4] 종목별 ATR x2 손절폭(중앙값) vs 고정 -5%")
    for _, r in P[P.stop == "ATR x2"].sort_values("atrpct").iterrows():
        print(f"  {r['name']:8s} ATR% {r.atrpct:5.2%}  ATRx2 stop {r.stop_dist:6.2%}")

    g = T[T.stop.isin(["-5%", "ATR x2"])]
    print("\n[5] 갭 손절(손절가보다 아래에서 체결) 비율")
    for label in ["-3%", "-5%", "-8%", "ATR x2"]:
        x = T[(T.stop == label) & (T.why == "stop")]
        gap = (x["exit"] < x["entry"] * (1 - x["stop_dist"]) - 1e-9).mean()
        print(f"  {label:7s} stops {len(x):4d}  gap-through {gap:5.1%}  worst {x.ret.min():7.2%}")
