"""
손절 직후 다시 사면 — 재진입 대기 0일 / 3일 / 10일(+5·20일)로 바꿔 10년 돌린 실측
https://algolab.co.kr/blog/stop-loss-reentry-cooldown-backtest-2026

진입 : 종가가 직전 20거래일 고가를 넘으면(20일 돌파) -> 다음 날 시가 매수
청산 : ① 손절 — 진입가 -5%. 장중 저가가 닿으면 손절가 체결
              (시가부터 손절가 아래로 갭이면 시가 체결)
       ② 추세 이탈 — 종가가 직전 10거래일 저가를 깨면 다음 날 시가 매도
재진입 대기 : '손절'로 나온 뒤에만 N거래일 동안 새 신호를 무시한다.
              추세 이탈 청산 뒤에는 대기 없음(모든 설정 공통).
              N = 0 이면 손절 다음 날 신호가 나와도 바로 다시 산다.
비용 : 왕복 0.3% (수수료+세금+슬리피지 뭉뚱그린 가정)
종목 : 2016년 이전 상장 국내 종목 22개 (지금 살아 있는 종목 = 생존 편향 있음)

같은 날 매수하고 그 날 저가가 손절가 아래면 그날 손절로 친다(보수적).

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

ASOF = "2026-10-23"     # 기준일 고정 — None 이면 실행 시점까지
START = "2016-01-01"
COST = 0.003
STOP = 0.05
WAITS = [0, 3, 5, 10, 20]

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-01-01", ASOF)[["Open", "High", "Low", "Close"]].astype(float)
    if ASOF:
        df = df[df.index <= ASOF]          # 캐시·신규 다운로드 모두 같은 자르기
    return df[(df["Open"] > 0) & (df["Low"] > 0)]


def run(df, wait):
    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
    ma200 = df["Close"].rolling(200).mean().values
    start_i = max(int(np.searchsorted(df.index.values, np.datetime64(START))), 200)
    trades, i, n = [], start_i, len(df)
    blocked_until = -1                     # 이 인덱스까지는 신호 무시
    prev_stop = False                      # 직전 거래가 손절이었나
    while i < n - 1:
        if i > blocked_until and c[i] > hi20[i]:      # 신호(종가 확인) -> 다음 날 시가 진입
            e = i + 1
            entry = o[e]
            stop_px = entry * (1 - STOP)
            regime = "down" if c[i] < ma200[i] else "up"   # 신호일 종가가 200일선 아래 = 하락 구간
            after_stop = prev_stop
            j, exit_px, why = e, None, None
            while j < n:
                if l[j] <= stop_px:
                    exit_px, why = (o[j] if o[j] < stop_px else stop_px), "stop"
                    break
                if j + 1 < n and c[j] < lo10[j]:
                    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
            ret = exit_px / entry - 1 - COST
            trades.append(dict(entry_dt=df.index[e], exit_dt=df.index[j], ret=ret, why=why,
                               regime=regime, after_stop=after_stop))
            prev_stop = why == "stop"
            blocked_until = j + wait if why == "stop" else j
            i = j + 1 if why == "stop" else j
            continue
        i += 1
    return trades


def equity_stats(tr):
    """거래를 순서대로 복리로 이어 붙인 자산곡선 — 누적수익·MDD·최장 연속 손절."""
    eq = np.cumprod(1 + np.array([t["ret"] for t in tr])) if tr else np.array([1.0])
    peak = np.maximum.accumulate(np.r_[1.0, eq])
    mdd = (np.r_[1.0, eq] / peak - 1).min()
    run_, best = 0, 0
    for t in tr:
        run_ = run_ + 1 if t["why"] == "stop" else 0
        best = max(best, run_)
    return eq[-1] - 1, mdd, best


if __name__ == "__main__":
    data = {k: load(k) for k in TICKERS}
    allres = {w: {k: run(df, w) for k, df in data.items()} for w in WAITS}
    total = sum(len(v) for v in allres[0].values())
    print(f"ASOF {ASOF}  tickers {len(TICKERS)}  stop -{STOP:.0%}  trades(wait 0) {total:,}\n")

    print("[1] 전체 (22종목 거래 합산)")
    print("wait  trades   win%  stop_hit%  expectancy  med_cum   med_mdd  max_stop_streak")
    for w in WAITS:
        tr = [t for v in allres[w].values() for t in v]
        r = np.array([t["ret"] for t in tr])
        st = [equity_stats(v) for v in allres[w].values()]
        print(f"{w:>3}d  {len(tr):>6} {np.mean(r > 0):6.1%}  {np.mean([t['why'] == 'stop' for t in tr]):8.1%}"
              f"  {r.mean():9.2%}  {np.median([s[0] for s in st]):8.1%}  {np.median([s[1] for s in st]):7.1%}"
              f"  {max(s[2] for s in st):>6}")

    print("\n[2] 손절 직후 들어간 거래만 (직전 거래가 손절)")
    print("wait  trades   win%  expectancy")
    for w in WAITS:
        tr = [t for v in allres[w].values() for t in v if t["after_stop"]]
        r = np.array([t["ret"] for t in tr])
        print(f"{w:>3}d  {len(tr):>6} {np.mean(r > 0):6.1%}  {r.mean():9.2%}")

    print("\n[3] 구간별 — 신호일 종가 vs 200일 이동평균")
    print("regime wait  trades   win%  expectancy  sum_ret")
    for reg in ("down", "up"):
        for w in WAITS:
            tr = [t for v in allres[w].values() for t in v if t["regime"] == reg]
            r = np.array([t["ret"] for t in tr])
            print(f"{reg:>5} {w:>3}d  {len(tr):>6} {np.mean(r > 0):6.1%}  {r.mean():9.2%}  {r.sum():8.1%}")

    print("\n[4] 종목별 누적수익 1등 횟수 (동률 제외)")
    wins = {w: 0 for w in WAITS}
    for k in TICKERS:
        cums = {w: equity_stats(allres[w][k])[0] for w in WAITS}
        best = max(cums.values())
        top = [w for w, v in cums.items() if v == best]
        if len(top) == 1:
            wins[top[0]] += 1
    print("  ".join(f"{w}d:{wins[w]}" for w in WAITS))

    print("\n[5] 기간별 거래당 기대값")
    for lab, a, b in (("2016-2020", "2016-01-01", "2020-12-31"), ("2021-", "2021-01-01", "2100-01-01")):
        row = []
        for w in WAITS:
            r = np.array([t["ret"] for v in allres[w].values() for t in v if a <= str(t["entry_dt"].date()) <= b])
            row.append(f"{w}d {r.mean():.2%}({len(r)})")
        print(f"{lab:>10}: " + "  ".join(row))

    print("\n[6] 대기 0일 기준 — 손절 뒤 며칠 만에 다시 들어갔나 (재진입까지 거래일)")
    print("gap       trades   win%  expectancy  down%")
    gaps = []
    for k, df in data.items():
        idx = {d: n for n, d in enumerate(df.index)}
        tr = allres[0][k]
        for a, b in zip(tr, tr[1:]):
            if a["why"] == "stop":
                gaps.append((idx[b["entry_dt"]] - idx[a["exit_dt"]], b))
    for lab, lo, hi in (("2-3d", 0, 3), ("4-5d", 4, 5), ("6-10d", 6, 10), ("11-20d", 11, 20), ("21d+", 21, 10**6)):
        g = [b for d, b in gaps if lo <= d <= hi]
        r = np.array([b["ret"] for b in g])
        print(f"{lab:>7}  {len(g):>6} {np.mean(r > 0):6.1%}  {r.mean():9.2%}  {np.mean([b['regime'] == 'down' for b in g]):5.1%}")

    print("\n[7] 200일선 필터를 더하면 (신호일 종가 < 200일선이면 진입 안 함)")
    print("wait  trades   win%  expectancy  med_cum   med_mdd  max_stop_streak")
    for w in (0, 10):
        per = {k: [t for t in v if t["regime"] == "up"] for k, v in allres[w].items()}
        tr = [t for v in per.values() for t in v]
        r = np.array([t["ret"] for t in tr])
        st = [equity_stats(v) for v in per.values()]
        print(f"{w:>3}d  {len(tr):>6} {np.mean(r > 0):6.1%}  {r.mean():9.2%}  {np.median([s[0] for s in st]):8.1%}"
              f"  {np.median([s[1] for s in st]):7.1%}  {max(s[2] for s in st):>6}")

    print("\n[8] 2~3거래일 만의 재진입 상위 거래 + 에코프로 제외 재계산")
    short = sorted(((b["ret"], k, str(b["entry_dt"].date()), b["why"]) for d, b in gaps
                    for k in [next(k for k, v in allres[0].items() if b in v)] if d <= 3), reverse=True)
    for r_, k, dt_, why in short[:3]:
        print(f"  {TICKERS[k]} {k}  {dt_} 진입  {r_:+.1%}  {why}")
    r = np.array([x[0] for x in short])
    print(f"  mean {r.mean():.2%}  median {np.median(r):.2%}  상위 2건 제외 {np.sort(r)[:-2].mean():.2%}")
    print("wait  trades  expectancy(에코프로 제외)  상위 1% 거래도 제외")
    for w in WAITS:
        r = np.array([t["ret"] for k, v in allres[w].items() if k != "086520" for t in v])
        q = np.sort(r)[:-int(len(r) * 0.01)]
        print(f"{w:>3}d  {len(r):>6}        {r.mean():.2%}                  {q.mean():.2%}")

    print("\n[9] 종목별 — 대기 0일 vs 10일 (누적수익 · MDD · 최장 연속 손절)")
    for k in TICKERS:
        a, b = equity_stats(allres[0][k]), equity_stats(allres[10][k])
        print(f"  {TICKERS[k]:<8} {a[0]:+8.1%} {b[0]:+8.1%}   mdd {a[1]:.1%} {b[1]:.1%}   streak {a[2]} {b[2]}")
