# -*- coding: utf-8 -*-
"""AlgoLab 재현 코드 — S&P500 적립식, 월급날 바로 살까 떨어질 때까지 모았다 살까 (algolab.co.kr/dipwait)
ASOF = 2026-10-12 (얼린 자료의 마지막 날). 돌리는 법: pip install numpy pandas → python run_dipwait.py

설계:
- 자료: 야후 VFINX(뱅가드 S&P500 인덱스펀드) 수정종가(배당 재투자) 1985-01 ~ · 달러 기준
- 매달 첫 거래일에 50만원(달러 기준 비율을 만원으로 환산 · 환율 미반영)
  ① 바로: 그날 종가에 전부 산다
  ② 기다리기(−20%): 그달 돈을 현금으로 모아 둔다. 전날 종가가 그때까지의 최고 종가보다 20% 이상
     낮은 첫 거래일 종가에 모아 둔 돈을 전부 산다. 기다리는 기간 상한 없음.
     모아 둔 현금에는 미국 3개월 국채 금리(^IRX) 이자를 붙인다.
- 10년 창: 시작 달을 한 달씩 밀며 120번 넣는다. 끝 날 = 120번째 넣은 다음 달 첫 거래일 전날.
  끝 날 계좌 = 주식 평가액 + 아직 안 쓴 현금(이자 포함).
- 반론 ① 짧게: −10% · 최대 12개월 / 반론 ② 기다리는 방법 50가지(−5·10·15·20·30% × 최대 3·6·12·24개월·무한 × 전부/절반)
- 반론 ③ 기간: 5·15·20년 계좌
- 한계: 코스피200 ETF(KODEX200·TIGER200 · 2009-05~ · 현금 이자 0%·3%) · 코스피 지수(1997~ · 배당 0%/연 1.8% 어림 × 이자 0%/3%)
        · S&P500 원화(SPY × 원달러 · 2003-12~) · 나스닥100(QQQ)
        · 수수료·세금 어림(수수료 0.25% · 현금 이자세 15.4% · 배당세 15%)
"""
import json
import os
import sys

import numpy as np
import pandas as pd

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data_dipwait")
ASOF = "2026-10-12"
MONTHLY = 50          # 만원
H = 120               # 10년


def load(name):
    df = pd.read_csv(os.path.join(DATA, name + ".csv"), parse_dates=["date"])
    df = df[df["date"] <= ASOF]
    s = df.set_index("date")["close"].astype(float)
    return s[s > 0]


IRX = load("_IRX")
RAWNAME, DIVNAME, ASOF_RAW = "VFINX_raw.csv", "VFINX_div.csv", None


def cash_index(idx, mode):
    if mode == "irx":
        r = IRX.reindex(idx, method="ffill").bfill().values / 100.0
        daily = (1 + r) ** (1 / 252.0)
    else:
        daily = np.full(len(idx), (1 + mode) ** (1 / 252.0))
    daily[0] = 1.0
    return np.cumprod(daily)


def month_starts(idx):
    s = pd.Series(np.arange(len(idx)), index=idx)
    return s.groupby([idx.year, idx.month]).first().values


def deploy_days(cond, contrib, K, n):
    nxt = np.full(n + 1, n, dtype=np.int64)
    for t in range(n - 1, -1, -1):
        nxt[t] = t if cond[t] else nxt[t + 1]
    out = []
    for i, c in enumerate(contrib):
        d = nxt[c]
        if K is not None:
            j = i + K
            cap = contrib[j] if j < len(contrib) else n
            d = min(d, cap)
        out.append(d)
    return np.array(out)


class Market:
    def __init__(self, price, cash_mode, start="1985-01-01"):
        p = price[price.index >= start]
        self.idx = p.index
        self.price = p.values
        self.n = len(self.price)
        self.C = cash_index(self.idx, cash_mode)
        self.contrib = month_starts(self.idx)
        runmax = np.maximum.accumulate(self.price)
        dd = self.price / runmax - 1
        self.dd_prev = np.concatenate([[0.0], dd[:-1]])   # 전날 종가 기준(룩어헤드 없음)
        self.nwin = len(self.contrib) - H
        self.E = np.array([self.contrib[w + H] - 1 for w in range(self.nwin)])

    def values(self, d):
        """창마다 끝 날 계좌(1 단위 납입 기준) · 창마다 끝까지 안 쓴 현금 비중."""
        n, C, price, contrib = self.n, self.C, self.price, self.contrib
        vals = np.zeros(self.nwin)
        cashfrac = np.zeros(self.nwin)
        for w in range(self.nwin):
            e = self.E[w]
            cs = contrib[w:w + H]
            ds = d[w:w + H]
            bought = ds <= e
            dd_ = np.minimum(ds, n - 1)
            stock = np.where(bought, (C[dd_] / C[cs]) / price[dd_] * price[e], 0.0)
            cash = np.where(bought, 0.0, C[e] / C[cs])
            vals[w] = stock.sum() + cash.sum()
            cashfrac[w] = cash.sum() / vals[w]
        return vals, cashfrac

    def arm(self, X, K):
        if X is None:
            return self.contrib.copy()
        return deploy_days(self.dd_prev <= -X / 100.0, self.contrib, K, self.n)

    def date(self, i):
        return str(self.idx[min(i, self.n - 1)].date())


def compare(mk, X, K, half=False):
    A, _ = mk.values(mk.arm(None, None))
    d = mk.arm(X, K)
    B, cf = mk.values(d)
    if half:
        B = 0.5 * A + 0.5 * B
    r = B / A
    order = np.argsort(r)
    mid = int(order[len(order) // 2])
    wait = np.minimum(d, mk.n - 1) - mk.contrib
    return {
        "창수": int(mk.nwin),
        "첫창": mk.date(mk.contrib[0]) + "~" + mk.date(mk.E[0]),
        "끝창": mk.date(mk.contrib[mk.nwin - 1]) + "~" + mk.date(mk.E[-1]),
        "기다림이더남음": int((B > A).sum()),
        "바로가더남음": int((B < A).sum()),
        "같음": int((B == A).sum()),
        "한가운데": {
            "창": mk.date(mk.contrib[mid]) + "~" + mk.date(mk.E[mid]),
            "순위": f"{len(order) // 2 + 1}/{len(order)}",
            "넣은돈_만원": MONTHLY * H,
            "바로_만원": int(round(A[mid] * MONTHLY)),
            "기다림_만원": int(round(B[mid] * MONTHLY)),
            "끝날현금비중": round(float(cf[mid]), 3),
        },
        "기다림_가장많이이김_만원": int(round(((B - A) * MONTHLY).max())),
        "기다림_가장많이짐_만원": int(round(((B - A) * MONTHLY).min())),
        "평균대기_거래일": round(float(wait.mean()), 1),
        "1년넘게기다린달_비율": round(float((wait > 252).mean()), 3),
        "끝까지한번도안산창": int(sum(1 for w in range(mk.nwin) if (d[w:w + H] > mk.E[w]).all())),
        "끝날현금이남은창": int((cf > 0).sum()),
        "_r": r, "_A": A * MONTHLY, "_B": B * MONTHLY,
    }


def paid_more(mk, X=20):
    """기다렸다 산 달 가운데, 산 값이 그달 첫 거래일(월급날) 값보다 비쌌던 달 — 1985~ 전체 달 기준."""
    d = mk.arm(X, None)
    ok = d < mk.n
    pc = mk.price[mk.contrib[ok]]
    pb = mk.price[d[ok]]
    return {"산달": int(ok.sum()), "아직못산달": int((~ok).sum()), "월급날보다비싸게산달": int((pb > pc).sum()),
            "월급날보다싸게산달": int((pb < pc).sum()), "같은날산달": int((pb == pc).sum()),
            "비율": round(float((pb > pc).mean()), 3)}


def replay(mk, start_ym, X=20):
    """한 계좌의 거래 리플레이 — 어느 달 돈이 언제 들어갔나, 들어간 값이 월급날 값보다 비쌌나."""
    d = mk.arm(X, None)
    w = next(i for i in range(mk.nwin) if mk.date(mk.contrib[i]).startswith(start_ym))
    e = mk.E[w]
    cs, ds = mk.contrib[w:w + H], d[w:w + H]
    buys = []
    for day in sorted(set(int(x) for x in ds if x <= e)):
        months = [int(ci) for ci, di in zip(cs, ds) if di == day]
        p = mk.price[day]
        buys.append({"산날": mk.date(day), "달수": len(months), "첫달": mk.date(months[0]), "끝달": mk.date(months[-1]),
                     "전날낙폭": round(float(mk.dd_prev[day]), 3),
                     "월급날값이더쌌던달": sum(1 for ci in months if mk.price[ci] < p)})
    left = [int(ci) for ci, di in zip(cs, ds) if di > e]
    return {"창": mk.date(cs[0]) + "~" + mk.date(e), "산날들": buys,
            "끝까지현금": {"달수": len(left), "첫달": mk.date(left[0]) if left else None,
                        "끝달": mk.date(left[-1]) if left else None},
            "산달": int(sum(b["달수"] for b in buys)),
            "월급날보다비싸게산달": int(sum(1 for ci, di in zip(cs, ds) if di <= e and mk.price[di] > mk.price[ci]))}


def compare_cost(price_tr, irx_scale, fee, X=20):
    """수수료(산 몫에 fee) · 현금 이자세(이자 × irx_scale) · 배당세(price_tr 로) 를 넣은 판 — 바로/기다리기 승수만."""
    global IRX
    keep = IRX
    IRX = keep * irx_scale
    try:
        mk = Market(price_tr, "irx")
    finally:
        IRX = keep
    res = []
    for d in (mk.arm(None, None), mk.arm(X, None)):
        vals = np.zeros(mk.nwin)
        for w in range(mk.nwin):
            e = mk.E[w]
            cs, ds = mk.contrib[w:w + H], d[w:w + H]
            b = ds <= e
            dd = np.minimum(ds, mk.n - 1)
            st = np.where(b, (mk.C[dd] / mk.C[cs]) / mk.price[dd] * mk.price[e] * (1 - fee), 0.0)
            ca = np.where(b, 0.0, mk.C[e] / mk.C[cs])
            vals[w] = st.sum() + ca.sum()
        res.append(vals)
    A, B = res
    return {"창수": int(mk.nwin), "바로가더남음": int((A > B).sum()), "기다림이더남음": int((B > A).sum())}


def after_tax_tr(tax):
    """VFINX 원래 종가 + 배당(야후 events) 로 배당세를 뗀 총수익 지수를 짓는다."""
    raw = pd.read_csv(os.path.join(DATA, RAWNAME), parse_dates=["date"]).set_index("date")
    raw = raw[raw.index <= ASOF_RAW] if ASOF_RAW else raw
    dv = pd.read_csv(os.path.join(DATA, DIVNAME), parse_dates=["date"]).set_index("date")["amount"]
    P = raw["close"].astype(float)
    D = dv.reindex(P.index).fillna(0.0)
    r = (P + D * (1 - tax)) / P.shift(1)
    r.iloc[0] = 1.0
    return pd.Series(np.cumprod(r.values), index=P.index)


def strip(x):
    return {k: v for k, v in x.items() if not k.startswith("_")}


def main():
    global H
    out = {"입력": {"매달_만원": MONTHLY, "기간_년": H // 12, "넣은돈_만원": MONTHLY * H,
                    "기다림_문턱": 20, "짧은기다림_문턱": 10, "짧은기다림_최대개월": 12}}
    vf = Market(load("VFINX"), "irx")
    out["자료"] = {"VFINX": f"{vf.date(0)}~{vf.date(vf.n - 1)}", "IRX끝": str(IRX.index[-1].date())}

    main_ = compare(vf, 20, None)
    out["본편_SP500_기다림20"] = strip(main_)
    short = compare(vf, 10, 12)
    out["반론_짧게_10_12개월"] = strip(short)
    # 반론 ③ 10년이 아니면? — 같은 규칙을 5·15·20년 계좌로
    for yrs in (5, 15, 20):
        H = yrs * 12
        out[f"반론_기간_{yrs}년"] = strip(compare(Market(load("VFINX"), "irx"), 20, None))
    H = 120
    out["왜_비싸게산달_SP500"] = paid_more(vf)
    out["리플레이_한가운데"] = replay(vf, main_["한가운데"]["창"][:7])

    # 한계 ④ 수수료·세금 어림 — 수수료 0.25%(산 몫) · 현금 이자세 15.4% · 미국 배당세 15%
    tr15 = after_tax_tr(0.15)
    tr0 = after_tax_tr(0.0)
    out["한계_비용"] = {
        "기준_원래종가재구성": compare_cost(tr0[tr0.index >= "1985-01-01"], 1.0, 0.0),
        "수수료0.25": compare_cost(tr0[tr0.index >= "1985-01-01"], 1.0, 0.0025),
        "이자세15.4": compare_cost(tr0[tr0.index >= "1985-01-01"], 0.846, 0.0),
        "배당세15": compare_cost(tr15[tr15.index >= "1985-01-01"], 1.0, 0.0),
        "셋다": compare_cost(tr15[tr15.index >= "1985-01-01"], 0.846, 0.0025),
    }

    # 50가지
    cells = []
    for X in (5, 10, 15, 20, 30):
        for K in (3, 6, 12, 24, None):
            for half in (False, True):
                c = compare(vf, X, K, half)
                cells.append({"X": X, "K": K if K else "inf", "절반": half,
                              "기다림이더남음": c["기다림이더남음"], "창수": c["창수"],
                              "중앙비율": round(float(np.median(c["_r"])), 4)})
    out["반론_50가지"] = {
        "가지수": len(cells),
        "절반넘게이긴가지": sum(1 for c in cells if c["기다림이더남음"] > c["창수"] / 2),
        "가장많이이긴가지": max(cells, key=lambda c: c["기다림이더남음"]),
        "중앙비율1넘은가지": sum(1 for c in cells if c["중앙비율"] > 1),
        "cells": cells,
    }

    # 한계: 코스피200 ETF · 원화 S&P500 · 나스닥100
    # ⚠ 야후 069500.KS 는 2007-02-07 다음 봉이 2009-04-17 이다(2008년 전체 공백) — 2009-05 부터만 쓴다(261013 심사 지적)
    kx = load("069500_KS")
    out["한계_KODEX200_이자0"] = strip(compare(Market(kx, 0.0, "2009-05-01"), 20, None))
    out["왜_비싸게산달_KODEX200"] = paid_more(Market(kx, 0.0, "2009-05-01"))
    out["한계_KODEX200_이자3"] = strip(compare(Market(kx, 0.03, "2009-05-01"), 20, None))
    out["한계_TIGER200_이자0"] = strip(compare(Market(load("102110_KS"), 0.0, "2009-05-01"), 20, None))
    # 길게: 코스피 지수(^KS11 · 가격지수라 배당 없음) 1997~ — 배당 0%·연 1.8%(어림) × 현금 이자 0%·3%
    k11 = load("_KS11")
    yrs = np.arange(len(k11)) / 252.0
    k11d = pd.Series(k11.values * np.exp(np.log1p(0.018) * yrs), index=k11.index)
    out["한계_코스피지수_배당0_이자0"] = strip(compare(Market(k11, 0.0, "1997-01-01"), 20, None))
    out["한계_코스피지수_배당0_이자3"] = strip(compare(Market(k11, 0.03, "1997-01-01"), 20, None))
    out["한계_코스피지수_배당18_이자0"] = strip(compare(Market(k11d, 0.0, "1997-01-01"), 20, None))
    out["한계_코스피지수_배당18_이자3"] = strip(compare(Market(k11d, 0.03, "1997-01-01"), 20, None))
    spy = load("SPY")
    fx = load("USDKRW_X")
    j = spy.index.intersection(fx.index)
    spy_krw = (spy.reindex(j) * fx.reindex(j)).dropna()
    out["한계_SP500원화_이자0"] = strip(compare(Market(spy_krw, 0.0, "2003-12-01"), 20, None))
    out["한계_SP500원화_이자3"] = strip(compare(Market(spy_krw, 0.03, "2003-12-01"), 20, None))
    out["한계_나스닥100"] = strip(compare(Market(load("QQQ"), "irx", "1999-01-01"), 20, None))


    def won(v):
        v = int(v)
        if abs(v) >= 10000:
            r = abs(v) % 10000
            s = f"{abs(v) // 10000}억" + (f" {r:,}만원" if r else "원")
            return ("-" if v < 0 else "") + s
        return f"{v:,}만원"

    def line(name, v, yrs=10):
        g = v["한가운데"]
        print(f"[{name}] {yrs}년짜리 계좌 {v['창수']:,}개 ({v['첫창']} … {v['끝창']})")
        print(f"  {yrs}년 뒤 더 많이 남은 쪽: 월급날 바로 {v['바로가더남음']:,}번 · 기다리기 {v['기다림이더남음']:,}번 · 같음 {v['같음']}번")
        print(f"  가운데 계좌({g['순위']}): {g['창']} · 넣은 돈 {won(g['넣은돈_만원'])} → 바로 {won(g['바로_만원'])} · 기다리기 {won(g['기다림_만원'])}")

    print(f"자료: VFINX {out['자료']['VFINX']} · ^IRX 끝 {out['자료']['IRX끝']} · ASOF {ASOF}\n")
    m = out["본편_SP500_기다림20"]
    line("S&P500 · 매달 50만원 · 20% 떨어지면 사기(상한 없음)", m)
    print(f"  기다린 돈이 산 날까지 평균 {m['평균대기_거래일']}거래일 · 1년 넘게 기다린 돈 {m['1년넘게기다린달_비율'] * 100:.1f}%")
    print(f"  10년 내내 한 번도 못 산 계좌 {m['끝까지한번도안산창']}개 · 끝날 현금이 남은 계좌 {m['끝날현금이남은창']}개")
    print(f"  기다리기가 가장 크게 이긴 계좌 +{won(m['기다림_가장많이이김_만원'])} · 가장 크게 진 계좌 {won(m['기다림_가장많이짐_만원'])}")
    pm = out["왜_비싸게산달_SP500"]
    print(f"  1985년부터 넣은 달: 기다렸다 산 달 {pm['산달']}개 중 월급날 값보다 비싸게 산 달 {pm['월급날보다비싸게산달']}개 ({pm['비율'] * 100:.1f}%)"
          f" · 싸게 산 달 {pm['월급날보다싸게산달']}개 · 같은 날 산 달 {pm['같은날산달']}개 · 아직 못 산 달 {pm['아직못산달']}개")
    rp = out["리플레이_한가운데"]
    print(f"\n[가운데 계좌 리플레이] {rp['창']}")
    for b in rp["산날들"]:
        print(f"  {b['산날']} 샀다(전날 최고점 대비 {b['전날낙폭'] * 100:.1f}%) — {b['첫달']}~{b['끝달']} 에 모은 {b['달수']}달치 · 그중 월급날 값이 더 쌌던 달 {b['월급날값이더쌌던달']}개")
    lf = rp["끝까지현금"]
    print(f"  끝까지 현금: {lf['첫달']}~{lf['끝달']} 에 모은 {lf['달수']}달치")
    print(f"  산 달 {rp['산달']}개 중 월급날보다 비싸게 산 달 {rp['월급날보다비싸게산달']}개")
    s = out["반론_짧게_10_12개월"]
    print()
    line("반론 · 10% 떨어지면 사기 · 최대 12개월", s)
    f = out["반론_50가지"]
    t = f["가장많이이긴가지"]
    print(f"\n[반론 · 기다리는 방법 {f['가지수']}가지] 382개 계좌 중 절반 넘게 이긴 방법 {f['절반넘게이긴가지']}가지 · "
          f"가장 많이 이긴 방법 {t['기다림이더남음']}번(−{t['X']}% · 최대 {t['K']}개월 · {'절반' if t['절반'] else '전부'})")
    for c in f["cells"]:
        print(f"  −{c['X']:>2}% · 최대 {str(c['K']):>3}개월 · {'절반' if c['절반'] else '전부'}: 기다리기가 더 남은 {c['기다림이더남음']:>3}/{c['창수']} · 가운데 비율 {c['중앙비율']:.4f}")
    print()
    for yrs in (5, 15, 20):
        line(f"반론 · 기간을 {yrs}년으로 · 20% 떨어지면 사기", out[f"반론_기간_{yrs}년"], yrs)
    print("\n[한계 · 수수료·세금 어림] VFINX 원래 종가 + 배당 기록으로 다시 지은 총수익 · 10년짜리 계좌")
    for k, v in out["한계_비용"].items():
        print(f"  {k}: 월급날 바로 {v['바로가더남음']}번 · 기다리기 {v['기다림이더남음']}번 (계좌 {v['창수']}개)")
    print("  (수수료 = 산 몫의 0.25% · 이자세 = 현금 이자의 15.4% · 배당세 = 배당의 15%)")
    print("\n※ 야후 069500.KS 는 2007-02-07 다음 봉이 2009-04-17 이다(2008년 전체 공백) — 코스피200 ETF 는 2009-05 부터만 쓴다")
    line("한계 · 코스피200 ETF(KODEX200) · 2009-05~ · 원화 · 현금 이자 0%", out["한계_KODEX200_이자0"])
    pk = out["왜_비싸게산달_KODEX200"]
    print(f"  기다렸다 산 달 {pk['산달']}개 중 월급날 값보다 싸게 산 달 {pk['월급날보다싸게산달']}개 · 비싸게 산 달 {pk['월급날보다비싸게산달']}개 · 같은 날 산 달 {pk['같은날산달']}개")
    line("한계 · 코스피200 ETF(KODEX200) · 2009-05~ · 원화 · 현금 이자 3%", out["한계_KODEX200_이자3"])
    line("한계 · 코스피200 ETF(TIGER200) · 2009-05~ · 원화 · 현금 이자 0%", out["한계_TIGER200_이자0"])
    line("한계 · 코스피 지수(가격지수 · 배당 0%) · 1997~ · 현금 이자 0%", out["한계_코스피지수_배당0_이자0"])
    line("한계 · 코스피 지수(가격지수 · 배당 0%) · 1997~ · 현금 이자 3%", out["한계_코스피지수_배당0_이자3"])
    line("한계 · 코스피 지수 + 배당 연 1.8%(어림) · 1997~ · 현금 이자 0%", out["한계_코스피지수_배당18_이자0"])
    line("한계 · 코스피 지수 + 배당 연 1.8%(어림) · 1997~ · 현금 이자 3%", out["한계_코스피지수_배당18_이자3"])
    line("한계 · S&P500 원화로(SPY × 원달러) · 현금 이자 0%", out["한계_SP500원화_이자0"])
    line("한계 · S&P500 원화로(SPY × 원달러) · 현금 이자 3%", out["한계_SP500원화_이자3"])
    line("한계 · 나스닥100(QQQ) · 미국 단기 국채 이자", out["한계_나스닥100"])
    print("\n끝.")


if __name__ == "__main__":
    main()
