# -*- coding: utf-8 -*-
"""
오버나이트(종가→다음날 시가) vs 장중(시가→종가) 수익률 분해 백테스트
알고랩 https://algolab.co.kr/blog/kospi-overnight-vs-intraday-return-backtest-2026

설치: pip install finance-datareader pandas numpy
실행: python overnight_intraday_backtest.py

- 대상: KODEX 200(069500), TIGER 200(102110), SPY(비교용)
- 가격: FinanceDataReader 일봉. 국내 ETF는 호가 단위에 맞지 않는 값이 많아 수정 가격으로 보인다.
- 비용: cost_bp 는 '왕복 1회'에 드는 총비용(수수료+슬리피지, 단위 0.01%)
- 투자 권유가 아니다. 과거 수치는 미래를 보장하지 않는다.
"""
import os
import numpy as np
import pandas as pd
import FinanceDataReader as fdr

# 기준일 고정: 비워 두면 실행하는 날의 데이터가 섞여 글과 다른 숫자가 나온다.
# 최신 데이터로 보려면 ASOF = None 으로 바꾼다.
ASOF = "2026-10-30"
START = "2015-01-01"
CACHE = "cache_overnight"
TICKERS = {"KODEX 200": "069500", "TIGER 200": "102110", "SPY": "SPY"}
COSTS_BP = [0, 3, 5, 10, 20]
TDAYS = 252


def load(code):
    os.makedirs(CACHE, exist_ok=True)
    path = os.path.join(CACHE, code + ".csv")
    if os.path.exists(path):
        df = pd.read_csv(path, index_col=0, parse_dates=True)
    else:
        df = fdr.DataReader(code, START)
        df.to_csv(path)
    # 자르기는 캐시를 쓰든 새로 받든 똑같이 적용한다
    if ASOF:
        df = df.loc[:ASOF]
    df = df.loc[START:]
    df = df[(df["Open"] > 0) & (df["Close"] > 0)]
    return df[["Open", "Close"]].copy()


def split_returns(df):
    out = pd.DataFrame(index=df.index)
    out["night"] = df["Open"] / df["Close"].shift(1) - 1   # 어제 종가 → 오늘 시가
    out["day"] = df["Close"] / df["Open"] - 1              # 오늘 시가 → 오늘 종가
    out["hold"] = df["Close"] / df["Close"].shift(1) - 1   # 어제 종가 → 오늘 종가
    return out.dropna()


def stats(r):
    eq = (1 + r).cumprod()
    years = len(r) / TDAYS
    cagr = eq.iloc[-1] ** (1 / years) - 1
    mdd = (eq / eq.cummax() - 1).min()
    sharpe = r.mean() / r.std() * np.sqrt(TDAYS) if r.std() > 0 else 0.0
    return dict(total=eq.iloc[-1] - 1, cagr=cagr, mdd=mdd, sharpe=sharpe, up=(r > 0).mean())


def line(name, s):
    print(f"  {name:<8} 누적 {s['total']*100:>8.1f}%  CAGR {s['cagr']*100:>6.2f}%  "
          f"MDD {s['mdd']*100:>7.1f}%  샤프 {s['sharpe']:>5.2f}  상승일 {s['up']*100:>5.1f}%")


def main():
    for name, code in TICKERS.items():
        df = load(code)
        r = split_returns(df)
        print(f"\n=== {name} ({code})  {r.index[0].date()} ~ {r.index[-1].date()}  {len(r)}거래일")
        line("보유", stats(r["hold"]))
        line("오버나이트", stats(r["night"]))
        line("장중", stats(r["day"]))

        print("  [구간별 누적 %]  구간 / 보유 / 오버나이트 / 장중")
        blocks = [("2015-19", "2015", "2019"), ("2020-22", "2020", "2022"),
                  ("2023-25", "2023", "2025"), ("2026", "2026", "2026")]
        for label, a, b in blocks:
            x = r.loc[a:b]
            print(f"  {label}  {((1+x['hold']).prod()-1)*100:>8.1f}  "
                  f"{((1+x['night']).prod()-1)*100:>8.1f}  {((1+x['day']).prod()-1)*100:>8.1f}")

        yearly = (1 + r).groupby(r.index.year).prod() - 1
        wins = (yearly["night"] > yearly["day"]).sum()
        print(f"  오버나이트가 장중을 이긴 해: {wins}/{len(yearly)}  "
              f"(진 해: {list(yearly.index[yearly['night'] <= yearly['day']])})")

        print("  [왕복비용별]  bp / 오버나이트 CAGR / MDD / 장중 CAGR")
        for bp in COSTS_BP:
            c = bp / 10000
            sn = stats(r["night"] - c)
            print(f"  {bp:>4}bp  {sn['cagr']*100:>8.2f}  {sn['mdd']*100:>8.1f}  "
                  f"{stats(r['day'] - c)['cagr']*100:>8.2f}")

        # 복리 기준 손익분기: 비용을 뺀 일수익률의 로그 합이 0이 되는 왕복비용
        lo, hi = 0.0, 0.01
        for _ in range(60):
            mid = (lo + hi) / 2
            if np.log1p(r["night"] - mid).sum() > 0:
                lo = mid
            else:
                hi = mid
        print(f"  오버나이트 복리 손익분기 왕복비용: {lo*10000:.2f}bp")

        # 집중도: 오버나이트 수익이 가장 컸던 날들을 빼면?
        n = r["night"].sort_values(ascending=False)
        for k in (10, 30):
            x = r["night"].drop(n.index[:k])
            print(f"  상위 {k}일 제외 오버나이트 CAGR {stats(x)['cagr']*100:.2f}% "
                  f"(누적 {stats(x)['total']*100:.1f}%)")
        x = r.loc[:"2025-12-31"]
        print("  2025년까지만:")
        line("보유", stats(x["hold"]))
        line("오버나이트", stats(x["night"]))
        line("장중", stats(x["day"]))
        print(f"  큰 갭 |오버나이트|>=2%: {(r['night'].abs()>=0.02).sum()}일, 그중 2026년 "
              f"{(r.loc['2026':,'night'].abs()>=0.02).sum()}일 / 최악 {r['night'].min()*100:.2f}% "
              f"({r['night'].idxmin().date()})")
        print(f"  오버나이트 일수익률 표준편차 {r['night'].std()*100:.2f}% / 장중 {r['day'].std()*100:.2f}%")


def index_check():
    """지수 시가로 분해하면 왜 안 되는지: 같은 기간 코스피 지수와 KODEX 200 비교"""
    a = split_returns(load("KS11"))["night"]
    b = split_returns(load("069500"))["night"]
    j = pd.concat([a, b], axis=1, keys=["kospi", "kodex"]).dropna()
    years = len(j) / TDAYS
    print("\n=== 지수 시가 점검 (KS11 vs KODEX 200)")
    print(f"  오버나이트 CAGR  지수 {((1+j['kospi']).prod()**(1/years)-1)*100:.2f}%  "
          f"ETF {((1+j['kodex']).prod()**(1/years)-1)*100:.2f}%  상관 {j.corr().iloc[0,1]:.3f}")
    for d in ["2026-03-05", "2026-06-08", "2026-07-31"]:
        print(f"  {d}  지수 {j.loc[d,'kospi']*100:+.2f}%  ETF {j.loc[d,'kodex']*100:+.2f}%")
    px = load("069500")["Close"]
    print(f"  KODEX 200 종가 중 5원 배수가 아닌 비율: {((px % 5) != 0).mean()*100:.1f}%")


if __name__ == "__main__":
    index_check()
    main()
