# walkforward_ma_backtest.py — 이동평균 기간 '최적값' vs 워크포워드 (알고랩)
# 규칙: 코스피 종가 > N일 이동평균이면 보유, 아니면 현금. 신호는 종가, 체결은 다음 거래일.
# 필요: pip install finance-datareader pandas numpy
import numpy as np
import pandas as pd
import FinanceDataReader as fdr

# 기준일 고정 — 이 글의 숫자를 재현하려면 그대로 둔다.
# 고정하지 않으면 실행하는 날의 데이터까지 받아 글과 다른 숫자가 나온다.
# 최신 데이터로 보려면 ASOF = None
ASOF = "2026-08-31"
START = "2010-01-01"              # 200일 이동평균 워밍업 포함
COST = 0.0005                     # 편도 비용 가정 0.05% (수수료+슬리피지)
GRID = list(range(5, 205, 5))     # 후보 N: 5, 10, ..., 200 (40개)
TRAIN_YEARS = 3                   # 학습 구간 3년 → 다음 1년에 적용
TEST_FROM, TEST_TO = 2015, 2026   # 평가 구간 2015-01 ~ ASOF

px = fdr.DataReader("KS11", START)["Close"].astype(float)
px = px[:ASOF] if ASOF else px    # 받은 뒤 자른다 — 캐시를 쓰든 새로 받든 같은 구간
ret = px.pct_change().fillna(0.0)

def daily_returns(n):
    """N일선 위면 1, 아니면 0. 오늘 신호 → 내일 수익률에 적용, 전환 시 비용 차감."""
    pos = (px > px.rolling(n).mean()).astype(float)
    pos[px.rolling(n).mean().isna()] = 0.0
    held = pos.shift(1).fillna(0.0)
    turn = held.diff().abs().fillna(0.0)
    return held * ret - turn * COST

ALL = pd.DataFrame({n: daily_returns(n) for n in GRID})

def cagr(r):
    years = len(r) / 252
    return (1 + r).prod() ** (1 / years) - 1

def mdd(r):
    eq = (1 + r).cumprod()
    return (eq / eq.cummax() - 1).min()

test = ALL.loc[f"{TEST_FROM}-01-01":]
bh = ret.loc[f"{TEST_FROM}-01-01":]

# ① 사후 최적: 평가 구간 전체를 보고 가장 좋았던 N (현실에선 불가능)
full_scores = test.apply(cagr)
best_n = int(full_scores.idxmax())

# ② 워크포워드: 매년 1월, 직전 3년에서 최고였던 N을 그해에만 쓴다
wf_parts, rows = [], []
for y in range(TEST_FROM, TEST_TO + 1):
    train = ALL.loc[f"{y - TRAIN_YEARS}-01-01":f"{y - 1}-12-31"]
    pick = int(train.apply(cagr).idxmax())
    yr = ALL.loc[f"{y}-01-01":f"{y}-12-31"]
    if yr.empty:
        continue
    yr_tot = (1 + yr).prod() - 1                       # 그해 40개 N 각각의 수익률
    rank = int(yr_tot.rank(ascending=False)[pick])     # 고른 N이 그해 몇 등이었나
    wf_parts.append(yr[pick])
    rows.append({"year": y, "picked_N": pick, "rank_of_40": rank,
                 "picked_ret": yr_tot[pick], "best_N_that_year": int(yr_tot.idxmax()),
                 "hindsight_N_ret": yr_tot[best_n], "kospi_ret": (1 + bh.loc[str(y)]).prod() - 1})
wf = pd.concat(wf_parts)
table = pd.DataFrame(rows)

print(f"기간 {test.index[0].date()} ~ {test.index[-1].date()}  ({len(test)/252:.2f}년)")
print(f"① 사후 최적 N={best_n}  CAGR {cagr(test[best_n]):.2%}  MDD {mdd(test[best_n]):.2%}")
print(f"② 워크포워드        CAGR {cagr(wf):.2%}  MDD {mdd(wf):.2%}")
print(f"③ 코스피 보유       CAGR {cagr(bh):.2%}  MDD {mdd(bh):.2%}")
print(f"   40개 N 중앙값    CAGR {full_scores.median():.2%}")
print(table.to_string(index=False, formatters={
    "picked_ret": "{:.1%}".format, "hindsight_N_ret": "{:.1%}".format, "kospi_ret": "{:.1%}".format}))
