"""
변동성 타기팅 — 최근 변동성으로 코스피 비중을 조절하면 (실험 스크립트)

사용법:
    pip install pandas numpy matplotlib finance-datareader
    python vol_targeting.py
    python vol_targeting.py --synthetic     # 난수 데이터로 동작 확인

데이터: FinanceDataReader 의 코스피(KS11) 일별 종가 (1995-05 ~ 2026-10-19, 글과 같은 기간), 가격 지수(배당 제외)
규칙:   비중 = min(목표 변동성 / 최근 20거래일 변동성(연율), 상한).
        t일 종가로 정한 비중을 t+1일 수익률에 적용한다. 비중이 0.1 넘게 바뀔 때만 다시 맞춘다.
        비중이 바뀐 만큼 편도 0.1% 비용, 현금 이자는 0%.
비교:   매수 후 보유, 200일 이동평균(실전 ⑧과 같은 규칙).
결과:   output/vol_targeting_results.txt 와 차트 PNG
"""
import argparse
import os

import numpy as np
import pandas as pd

HERE = os.path.dirname(os.path.abspath(__file__))
OUT = os.path.join(HERE, "output")
END = "2026-10-19"                                # 글의 표와 같은 기간(2026년 10월 19일까지)
os.makedirs(OUT, exist_ok=True)
COST = 0.001          # 편도 0.1%
BAND = 0.1            # 비중 차이가 이보다 클 때만 다시 맞춘다
WINDOWS = {
    "IT 버블 붕괴 (2000.1~2001.9)": ("2000-01-04", "2001-09-28"),
    "금융위기 (2007.11~2008.10)": ("2007-11-01", "2008-10-31"),
    "코로나 급락 (2020.2.17~3.19)": ("2020-02-17", "2020-03-19"),
    "코로나 반등 (2020.3.20~2021.6)": ("2020-03-20", "2021-06-30"),
    "박스권 (2011.5~2020.6)": ("2011-05-02", "2020-06-30"),
}


def load(synthetic=False):
    if synthetic:
        rng = np.random.default_rng(9)
        idx = pd.bdate_range("1995-05-02", "2026-10-19")
        vol = 0.01 * np.exp(np.cumsum(rng.normal(0, 0.05, len(idx))).clip(-1, 1))
        return pd.Series(1000 * np.exp(np.cumsum(rng.normal(0.0002, 1, len(idx)) * vol)), index=idx)
    import FinanceDataReader as fdr
    return fdr.DataReader("KS11", "1995-01-01", END)["Close"].dropna()


def years(ix):
    return (ix[-1] - ix[0]).days / 365.25


def run(r, w):
    """w[t]: t일 종가에 정한 비중 → t+1일 수익률에 적용. 첫날은 보유(1)로 출발."""
    held = np.r_[1.0, w[:-1]]
    trades = np.abs(np.diff(np.r_[1.0, held]))
    return pd.Series(held * r.values - trades * COST, index=r.index), held, trades


def stats(s):
    y = years(s.index)
    eq = (1 + s).cumprod()
    cagr = eq.iloc[-1] ** (1 / y) - 1
    vol = s.std() * np.sqrt(len(s) / y)
    return cagr, vol, (eq / eq.cummax() - 1).min(), cagr / vol


def vol_target(r, target, n=20, cap=1.0):
    sig = r.rolling(n).std() * np.sqrt(250)
    raw = (target / sig).clip(upper=cap).fillna(1.0).values
    w, cur = np.empty_like(raw), 1.0
    for i, x in enumerate(raw):
        if abs(x - cur) > BAND:
            cur = x
        w[i] = cur
    return w


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--synthetic", action="store_true")
    a = ap.parse_args()
    close = load(a.synthetic)
    r = close.pct_change().dropna()
    close = close.loc[r.index]
    ma = close.rolling(200).mean()
    rules = {
        "매수 후 보유": np.ones(len(r)),
        "200일 이동평균": np.where(ma.isna(), 1.0, (close > ma).astype(float)),
        "변동성 타기팅 15%": vol_target(r, 0.15),
        "변동성 타기팅 20%": vol_target(r, 0.20),
        "변동성 타기팅 20% (최대 1.5배)": vol_target(r, 0.20, cap=1.5),
    }
    res = {k: run(r, w) for k, w in rules.items()}
    y = years(r.index)
    lines = [f"기간 {r.index[0].date()} ~ {r.index[-1].date()}, {len(r)}거래일 (편도 비용 {COST:.1%}, 현금 이자 0%)", ""]
    lines.append(f"{'규칙':<24}{'CAGR':>8}{'변동성':>8}{'MDD':>9}{'CAGR/변동성':>12}{'평균 비중':>9}{'연 회전':>8}")
    for k, (s, held, tr) in res.items():
        c, v, m, rv = stats(s)
        lines.append(f"{k:<24}{c:8.2%}{v:8.1%}{m:9.1%}{rv:12.2f}{held.mean():9.2f}{tr.sum() / y:8.1f}")
    lines.append("\n[구간별 CAGR]")
    keys = ["매수 후 보유", "200일 이동평균", "변동성 타기팅 15%", "변동성 타기팅 20%"]
    for lab, (s0, e0) in {"1995~2008": ("1995", "2008"), "2009~": ("2009", "2100")}.items():
        lines.append(f"  {lab:<10}" + "  ".join(f"{k} {stats(res[k][0].loc[s0:e0])[0]:6.1%}" for k in keys))
    lines.append("\n[사건 구간 누적 수익률]")
    for lab, (s0, e0) in WINDOWS.items():
        lines.append(f"  {lab:<28}" + "  ".join(f"{k} {(1 + res[k][0].loc[s0:e0]).prod() - 1:7.1%}" for k in keys))
    s20, _, _ = res["변동성 타기팅 20%"]
    eq = (1 + s20).cumprod()
    dd = eq / eq.cummax() - 1
    trough = dd.idxmin()
    lines.append(f"\n변동성 타기팅 20%의 MDD 구간: {eq.loc[:trough].idxmax().date()} ~ {trough.date()}")
    # 변동성이 높을 때 다음 20거래일 수익률은 낮았나
    v20 = r.rolling(20).std() * np.sqrt(250)
    nxt = (1 + r).rolling(20).apply(np.prod, raw=True).shift(-20) - 1
    past_vol = r.rolling(20).std()
    fut_vol = r[::-1].rolling(20).std()[::-1].shift(-1)
    df = pd.DataFrame({"v": v20, "n": nxt}).dropna()
    df["q"] = pd.qcut(df.v, 5, labels=["1(낮음)", "2", "3", "4", "5(높음)"])
    g = df.groupby("q", observed=True)
    tab = pd.DataFrame({"직전 변동성": g.v.mean(), "다음 20일 평균(%)": g.n.mean() * 100,
                        "다음 20일 표준편차(%)": g.n.std() * 100})
    tab["평균/표준편차"] = tab["다음 20일 평균(%)"] / tab["다음 20일 표준편차(%)"]
    lines.append("\n[직전 20일 변동성 5분위별 다음 20거래일 수익률]\n" + tab.round(2).to_string())
    lines.append(f"직전 20일 변동성과 다음 20일 변동성의 상관: {past_vol.corr(fut_vol):.2f}")
    text = "\n".join(lines)
    print(text)
    with open(os.path.join(OUT, "vol_targeting_results.txt"), "w", encoding="utf-8") as fh:
        fh.write(text + "\n")

    try:
        import matplotlib
        matplotlib.use("Agg")
        import matplotlib.pyplot as plt
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6.5), sharex=True, gridspec_kw={"height_ratios": [3, 1.3]})
        for k, lab, col in [("매수 후 보유", "Buy & hold", "#555555"), ("200일 이동평균", "200-day MA", "#b22222"),
                            ("변동성 타기팅 20%", "Vol target 20%", "#1f5fbf")]:
            ax1.plot((1 + res[k][0]).cumprod(), label=lab, color=col, linewidth=1.0)
        ax1.set_yscale("log"); ax1.grid(alpha=0.3); ax1.legend()
        ax1.set_title("KOSPI: buy & hold vs 200-day MA vs volatility targeting (20%), costs 0.1%")
        ax2.plot(r.index, res["변동성 타기팅 20%"][1], color="#1f5fbf", linewidth=0.8)
        ax2.set_ylabel("equity weight"); ax2.set_ylim(0, 1.05); ax2.grid(alpha=0.3)
        fig.tight_layout(); fig.savefig(os.path.join(OUT, "53_vol_targeting.png"), dpi=120); plt.close(fig)
    except ImportError:
        pass


if __name__ == "__main__":
    main()
