# -*- coding: utf-8 -*-
# =============================================================================
# AlgoLab · 리밸런싱 봇(자산배분 + 리밸런싱) 백테스트 — 공개용
# "폭락에 팔지 않는 규칙"(40/30/30 + 연 1회 리밸런싱)의 숫자를 그대로 재현합니다.
# 믿지 말고 직접 돌려보세요. 비율/자산/기간을 바꿔 여러분만의 실험을 해보세요.
#
# 필요: python 3, numpy  (설치:  pip install numpy)
# 실행:  python discipline_bot_backtest.py
# 데이터: 야후 파이낸스 무료 API(배당·이자 재투자 반영 adjclose = 총수익 기준).
# (교육·검증용, 투자 권유 아님)
# =============================================================================
import urllib.request
import json
import datetime
import numpy as np

# ── 여기만 바꿔서 실험하세요 ────────────────────────────────────────────────
TICKERS = {"미국S&P500": "SPY", "금": "GLD", "미국장기국채": "TLT"}
WEIGHTS = [0.40, 0.30, 0.30]      # 위 순서대로의 목표 비중 (합 1.0)
START = (2005, 1, 1)              # 시작일
END = (2026, 7, 17)              # 종료일(as-of 고정 → 언제 돌려도 같은 결과 재현). None이면 오늘까지.
REBAL_YEARS = 1                   # 리밸런싱 주기(년)
PANIC_DD = -0.20                  # 감정적 인간: 주식이 고점 대비 이만큼 빠지면 패닉셀
# ───────────────────────────────────────────────────────────────────────────


def fetch_total_return(sym, start, end=None):
    """야후 adjclose(배당 재투자 반영) 일별 시계열."""
    p1 = int(datetime.datetime(*start).timestamp())
    p2 = int(datetime.datetime(*end).timestamp()) if end else int(datetime.datetime.now().timestamp())
    url = (f"https://query1.finance.yahoo.com/v8/finance/chart/{sym}"
           f"?period1={p1}&period2={p2}&interval=1d")
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    d = json.loads(urllib.request.urlopen(req, timeout=30).read().decode())
    r = d["chart"]["result"][0]
    ts = r["timestamp"]
    adj = r["indicators"]["adjclose"][0]["adjclose"]
    return {datetime.datetime.utcfromtimestamp(t).date(): v
            for t, v in zip(ts, adj) if v is not None}


def metrics(eq, years):
    dret = np.diff(eq) / eq[:-1]
    peak = np.maximum.accumulate(eq)
    return {
        "총수익": eq[-1] - 1,
        "CAGR": eq[-1] ** (1 / years) - 1,
        "MDD": float(np.min(eq / peak - 1)),
        "샤프": (np.mean(dret) * 252) / (np.std(dret) * np.sqrt(252)) if np.std(dret) > 0 else 0,
    }


def main():
    # 데이터 정렬(공통 날짜)
    series = {k: fetch_total_return(v, START, END) for k, v in TICKERS.items()}
    common = set.intersection(*[set(s) for s in series.values()])
    dates = sorted(common)
    mat = np.array([[series[k][d] for k in TICKERS] for d in dates])
    n, years = len(dates), len(dates) / 252
    w = np.array(WEIGHTS)
    rets = np.zeros_like(mat)
    rets[1:] = mat[1:] / mat[:-1] - 1
    STK = 0
    print(f"기간 {dates[0]} ~ {dates[-1]} ({years:.1f}년, 배당·이자 포함)\n")

    # 기준: 주식 100% 보유
    eq_bh = np.cumprod(1 + rets[:, STK])

    # 리밸런싱 포트폴리오(annual=True) / 방치(annual=False)
    def portfolio(annual):
        h = w.copy(); ly = dates[0].year; eq = [1.0]
        for i in range(1, n):
            h = h * (1 + rets[i])
            pv = h.sum()
            if annual and dates[i].year - ly >= REBAL_YEARS:
                h = w * pv; ly = dates[i].year
            eq.append(pv)
        return np.array(eq)

    eq_bot = portfolio(annual=True)     # (A) 자동 리밸런싱 봇
    eq_drift = portfolio(annual=False)  # (B) 방치

    # (C) 감정적 인간: 주식 고점 대비 PANIC_DD 넘게 빠지면 전량 채권으로, 고점 회복 시 재매수
    BND = len(TICKERS) - 1
    h = w.copy(); ly = dates[0].year; eqC = [1.0]; peak = mat[0, STK]; in_stock = True
    for i in range(1, n):
        peak = max(peak, mat[i, STK])
        if in_stock and mat[i, STK] / peak - 1 < PANIC_DD:
            h[BND] += h[STK]; h[STK] = 0.0; in_stock = False
        elif (not in_stock) and mat[i, STK] >= peak * 0.999:
            move = h[BND] * (w[STK] / (w[STK] + w[BND]))
            h[BND] -= move; h[STK] += move; in_stock = True
        h = h * (1 + rets[i]); pv = h.sum()
        if dates[i].year - ly >= REBAL_YEARS:
            tgt = w.copy()
            if not in_stock:
                tgt = w.copy(); tgt[BND] += tgt[STK]; tgt[STK] = 0.0
            h = tgt * pv; ly = dates[i].year
        eqC.append(pv)
    eq_panic = np.array(eqC)

    print(f"{'방식':22} {'총수익':>9} {'CAGR':>7} {'MDD':>7} {'샤프':>6}")
    for name, eq in [("주식 100% 보유", eq_bh), ("리밸런싱 봇(분산+리밸런싱)", eq_bot),
                     ("방치(드리프트)", eq_drift), ("감정적 인간(패닉셀)", eq_panic)]:
        m = metrics(eq, years)
        print(f"{name:22} {m['총수익']*100:>8,.0f}% {m['CAGR']*100:>6.1f}% {m['MDD']*100:>6.0f}% {m['샤프']:>6.2f}")

    print("\n핵심: 리밸런싱 봇은 조금 덜 벌지만(CAGR↓) 낙폭은 절반 이하, 샤프(위험대비수익)는 크게 개선.")
    print("     그리고 같은 전략인데 '감정적 인간'이 봇보다 한참 뒤처짐 = 자동화(규율 실행)의 진짜 값.")


if __name__ == "__main__":
    main()
