"""
백테스트 시작일 민감도 실측 — 같은 전략, 시작일만 바꿔 다시 돌린다
https://algolab.co.kr/blog/backtest-start-date-sensitivity-2026

전략 : 코스피200 지수(KS200) 종가가 120일 이동평균 위면 보유, 아래면 현금
       (전일 신호를 다음 날 반영 = shift(1), 보유<->현금 전환 1회당 0.1% 차감)
비교 : 단순 보유(Buy & Hold)

실험 A : 2020년 1~12월 각 월 첫 거래일에 시작 -> ASOF 까지 (시작 월 12개)
실험 B : 2010-01 ~ 2021-09 매월 시작, 각 5년(1,260거래일)씩 -> 데이터가 다 차는 139개 구간
실험 D : 끝나는 날짜만 바꾸기 — 2020-01 시작, 끝 3가지
실험 C : 워밍업 함정 — 데이터를 '시작일부터' 받으면 처음 120일이 강제로 현금

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

ASOF = "2026-09-17"     # 기준일 고정(데이터 마지막 거래일) — None 이면 실행 시점까지
MA, FEE, YEARS = 120, 0.001, 5

px = fdr.DataReader("KS200", "2002-01-01", ASOF)["Close"].astype(float).dropna()
if ASOF:
    px = px[px.index <= ASOF]          # 캐시·신규 다운로드 모두 같은 자르기
ret = px.pct_change().fillna(0)

def strat_returns(p):
    """p 전체로 신호를 만든다 (이동평균 워밍업은 p 의 앞부분이 쓴다)."""
    r = p.pct_change().fillna(0)
    sig = (p > p.rolling(MA).mean()).shift(1, fill_value=False).astype(int)
    sig.iloc[:MA] = 0
    return r * sig - sig.diff().abs().fillna(0) * FEE

SR = strat_returns(px)

def stats(r):
    eq = (1 + r).cumprod().values
    cagr = eq[-1] ** (252 / len(eq)) - 1
    mdd = (eq / np.maximum.accumulate(eq) - 1).min()
    return cagr, mdd

def window(start, n=None):
    idx = px.index[px.index >= start][0]
    i = px.index.get_loc(idx)
    sl = slice(i + 1, i + 1 + n) if n else slice(i + 1, None)  # 시작일 종가 매수 -> 다음 날부터 수익
    return idx, SR.iloc[sl], ret.iloc[sl]

print(f"data {px.index[0].date()} ~ {px.index[-1].date()}  ({len(px):,} days)")

# ---------- 실험 A
print("\n[A] start month in 2020 -> ASOF")
print("start       strat_CAGR  strat_MDD   B&H_CAGR   B&H_MDD   winner")
rowsA = []
for m in range(1, 13):
    d, s, b = window(f"2020-{m:02d}-01")
    sc, sm = stats(s); bc, bm = stats(b)
    rowsA.append((d.date(), sc, sm, bc, bm))
    print(f"{d.date()}  {sc:9.2%}  {sm:9.2%}  {bc:9.2%}  {bm:9.2%}   {'strat' if sc > bc else 'B&H'}")
a = np.array([[r[1], r[2], r[3], r[4]] for r in rowsA])
print(f"strat CAGR range {a[:,0].min():.2%} ~ {a[:,0].max():.2%}  |  MDD range {a[:,1].min():.2%} ~ {a[:,1].max():.2%}")
eq = (1 + window("2020-01-01")[1]).cumprod()
dd = eq / eq.cummax() - 1
print(f"strat MDD trough {dd.idxmin().date()}  peak {eq[:dd.idxmin()].idxmax().date()}")
print(f"B&H   CAGR range {a[:,2].min():.2%} ~ {a[:,2].max():.2%}  |  MDD range {a[:,3].min():.2%} ~ {a[:,3].max():.2%}")

# ---------- 실험 B
print(f"\n[B] monthly starts, {YEARS}-year windows")
n = 252 * YEARS
starts = pd.date_range("2010-01-01", "2021-09-01", freq="MS")
rowsB = []
for st in starts:
    d, s, b = window(st, n)
    if len(s) < n: continue
    sc, sm = stats(s); bc, bm = stats(b)
    rowsB.append((d.date(), sc, sm, bc, bm))
B = pd.DataFrame(rowsB, columns=["start", "sC", "sM", "bC", "bM"])
print(f"windows {len(B)}")
for col, name in [("sC", "strat CAGR"), ("sM", "strat MDD"), ("bC", "B&H CAGR"), ("bM", "B&H MDD")]:
    q = B[col].quantile([0, .25, .5, .75, 1]).values
    print(f"{name:11s} min {q[0]:7.2%}  q1 {q[1]:7.2%}  med {q[2]:7.2%}  q3 {q[3]:7.2%}  max {q[4]:7.2%}")
win = (B.sC > B.bC)
print(f"strat CAGR > B&H : {win.sum()}/{len(B)} ({win.mean():.1%})")
print(f"strat MDD shallower than B&H : {(B.sM > B.bM).sum()}/{len(B)} ({(B.sM > B.bM).mean():.1%})")
best = B.loc[B.sC.idxmax()]; worst = B.loc[B.sC.idxmin()]
print(f"best start  {best.start}  strat {best.sC:.2%} / B&H {best.bC:.2%}")
print(f"worst start {worst.start}  strat {worst.sC:.2%} / B&H {worst.bC:.2%}")
# 연도별로 묶어 본 승률
B["year"] = pd.to_datetime(B.start).dt.year
print("by start year: strat wins / 12, median strat CAGR, median B&H CAGR")
for y, g in B.groupby("year"):
    print(f"  {y}  {int((g.sC > g.bC).sum()):2d}/{len(g)}  {g.sC.median():7.2%}  {g.bC.median():7.2%}")

# ---------- 실험 C
print("\n[C] warm-up trap: download from start date only")
for st in ["2020-01-01", "2020-04-01"]:
    full = window(st)[1]
    p2 = px[px.index >= st]
    cut = strat_returns(p2).iloc[1:]
    print(f"{st}  full-history signal CAGR {stats(full)[0]:.2%}  |  start-only download CAGR {stats(cut)[0]:.2%}"
          f"  (first {MA} days forced cash)")

# ---------- 실험 D
print("\n[D] same start 2020-01-02, different end dates")
print("end         strat_CAGR  strat_MDD   B&H_CAGR   B&H_MDD")
for end in ["2025-12-30", "2026-06-22", px.index[-1].strftime("%Y-%m-%d")]:
    d, s, b = window("2020-01-01")
    sc, sm = stats(s[s.index <= end]); bc, bm = stats(b[b.index <= end])
    print(f"{end}  {sc:9.2%}  {sm:9.2%}  {bc:9.2%}  {bm:9.2%}")
