# -*- coding: utf-8 -*-
"""260927 연금저축 반반 계좌 — 되돌리는 주기 실측 (공개 코드).

계좌: KODEX 200(069500) 반 + KODEX 국고채3년(114260) 반으로 시작하는 1,000만원.
팔:  안함 · 1년에 한 번(1~12월 각각, 그 달 첫 거래일) · 한 달에 한 번(그 달 k번째 거래일, k=1..15) · 매일.
규약: 신호는 점검일 종가, 체결은 다음 거래일 종가(한 봉 지연) · 수수료는 **실제로 옮긴 금액에만**
      편도 0.1%(파는 쪽·사는 쪽 둘 다) · 가격은 야후 수정주가(분배금 재투자). 단 국고채3년(114260)은 야후 분배 기록이 2018-12 에서 끊겨 그 뒤 분배금(예: 2025-12 807원)이 빠져 있다 — 채권을 더 든 계좌에 불리한 쪽이다.
기준일 ASOF 고정 — 이 날짜까지의 봉만 쓴다(영상과 같은 숫자가 나오게).

실행: python run_banban.py          (같은 폴더의 data_banban/ 을 읽는다)
데이터: 야후 파이낸스 수정주가(분배금 재투자)를 ASOF 에서 잘라 얼린 것이다. 수정주가는 새 분배금이
      나올 때마다 과거 값이 조금씩 바뀌므로, 영상과 같은 숫자를 내려고 파일째 함께 싣는다.
"""
import argparse
import json
import os

import numpy as np
import pandas as pd

ASOF = "2026-09-23"
START = "2009-08-01"
원금 = 10_000_000
편도 = 0.001
위험, 안전 = "069500_KS", "114260_KS"

HERE = os.path.dirname(os.path.abspath(__file__))
기본폴더 = os.path.join(HERE, "data_banban")   # 함께 받은 zip 을 풀면 이 폴더가 생긴다


def 읽기(폴더, sym):
    d = pd.read_csv(os.path.join(폴더, sym + "_1d.csv"), parse_dates=["date"])
    d = d[["date", "close"]].dropna()
    d = d[(d["close"] > 0) & (d["date"] <= pd.Timestamp(ASOF))]
    return d.drop_duplicates("date").sort_values("date").reset_index(drop=True)


def 판(폴더):
    a, b = 읽기(폴더, 위험), 읽기(폴더, 안전)
    m = a.merge(b, on="date", suffixes=("_r", "_s"))       # 두 종목이 모두 거래된 날만
    m = m[m["date"] >= pd.Timestamp(START)].reset_index(drop=True)
    return m


def 점검일(dates, 방식, 인자=None):
    """점검(신호)이 켜지는 날 = True. 체결은 그다음 거래일."""
    n = len(dates)
    on = np.zeros(n, dtype=bool)
    if 방식 == "안함":
        return on
    if 방식 == "매일":
        on[1:] = True
        return on
    ym = dates.dt.year.values * 100 + dates.dt.month.values
    # 그 달에서 몇 번째 거래일인가 (1부터)
    k = np.ones(n, dtype=int)
    for i in range(1, n):
        k[i] = k[i - 1] + 1 if ym[i] == ym[i - 1] else 1
    if 방식 == "매월":                       # 인자 = 그 달 몇 번째 거래일
        on = k == 인자
    elif 방식 == "연1회":                    # 인자 = 몇 월 (그 달 첫 거래일)
        on = (k == 1) & (dates.dt.month.values == 인자)
    on[0] = False
    return on


def 돌리기(m, 방식, 인자=None, w0=0.5, fee=편도):
    r = m["close_r"].values
    s = m["close_s"].values
    n = len(r)
    on = 점검일(m["date"], 방식, 인자)
    r_amt, s_amt = 원금 * w0, 원금 * (1 - w0)
    eq = np.empty(n)
    wp = np.empty(n)
    eq[0], wp[0] = 원금, w0
    횟수, 수수료합, 옮긴합 = 0, 0.0, 0.0
    대기 = False
    for i in range(1, n):
        r_amt *= r[i] / r[i - 1]
        s_amt *= s[i] / s[i - 1]
        if 대기:
            tot = r_amt + s_amt
            d = abs(tot * w0 - r_amt)
            if d > 0:
                c = d * fee * 2.0
                tot -= c
                수수료합 += c
                옮긴합 += d
                횟수 += 1
                r_amt, s_amt = tot * w0, tot * (1 - w0)
            대기 = False
        tot = r_amt + s_amt
        eq[i] = tot
        wp[i] = r_amt / tot
        대기 = bool(on[i])
    return eq, wp, 횟수, 수수료합, 옮긴합


def 요약(m, eq, wp, 횟수, 수수료합, 옮긴합):
    dates = m["date"]
    yrs = (dates.iloc[-1] - dates.iloc[0]).days / 365.25
    peak = np.maximum.accumulate(eq)
    dd = eq / peak - 1
    j = int(dd.argmin())
    i = int(np.argmax(eq[: j + 1]))
    return {
        "끝금액": int(round(eq[-1])),
        "연수익률": round(((eq[-1] / eq[0]) ** (1 / yrs) - 1) * 100, 2),
        "최대낙폭": round(float(dd.min()) * 100, 2),
        "낙폭_고점일": str(dates.iloc[i].date()),
        "낙폭_바닥일": str(dates.iloc[j].date()),
        "낙폭_고점잔고": int(round(eq[i])),
        "낙폭_바닥잔고": int(round(eq[j])),
        "끝주식비중": round(float(wp[-1]) * 100, 1),
        "주식비중_최대": round(float(wp.max()) * 100, 1),
        "되돌린횟수": 횟수,
        "수수료합": int(round(수수료합)),
        "옮긴금액합": int(round(옮긴합)),
    }


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--csv-dir", default=기본폴더)
    ap.add_argument("--json", default=os.path.join(HERE, "결과_banban.json"))
    a = ap.parse_args()
    m = 판(a.csv_dir)
    yrs = (m["date"].iloc[-1] - m["date"].iloc[0]).days / 365.25
    out = {"구간": [str(m["date"].iloc[0].date()), str(m["date"].iloc[-1].date())],
           "연수": round(yrs, 1), "봉": len(m), "원금": 원금, "편도수수료": 편도, "팔": {}}

    print(f"① 판 {out['구간'][0]} ~ {out['구간'][1]} · {out['연수']}년 · {len(m)}거래일 · 원금 {원금:,}원")
    print("   종목: KODEX 200(069500) 반 + KODEX 국고채3년(114260) 반 · 편도 0.1% · 체결은 다음 거래일\n")

    def 기록(이름, 방식, 인자=None):
        g = 요약(m, *돌리기(m, 방식, 인자))
        out["팔"][이름] = g
        return g

    print("② 세 가지 계좌")
    for 이름, 방식, 인자 in [("안함", "안함", None), ("1년에한번_1월", "연1회", 1),
                           ("한달에한번_첫거래일", "매월", 1), ("매일", "매일", None)]:
        g = 기록(이름, 방식, 인자)
        print(f"   {이름:<18} 끝 {g['끝금액']:>12,}원 · 연 {g['연수익률']:5.2f}% · 최대낙폭 {g['최대낙폭']:6.2f}% "
              f"({g['낙폭_고점일']}→{g['낙폭_바닥일']}, {g['낙폭_고점잔고']:,}→{g['낙폭_바닥잔고']:,}원) · "
              f"끝 주식비중 {g['끝주식비중']}% · 되돌린 {g['되돌린횟수']}번 · 수수료 {g['수수료합']:,}원")

    print("\n③ 1년에 한 번 — 되돌리는 달만 1~12월로 바꿔 본다")
    연 = {}
    for 달 in range(1, 13):
        g = 기록(f"1년에한번_{달}월", "연1회", 달)
        연[달] = g
        print(f"   {달:>2}월  끝 {g['끝금액']:>12,}원 · 최대낙폭 {g['최대낙폭']:6.2f}% ({g['낙폭_고점일']}→{g['낙폭_바닥일']})")
    lo = min(연, key=lambda d: 연[d]["최대낙폭"])
    hi = max(연, key=lambda d: 연[d]["최대낙폭"])
    print(f"   → 가장 깊은 달 {lo}월 {연[lo]['최대낙폭']}% · 가장 얕은 달 {hi}월 {연[hi]['최대낙폭']}% · "
          f"폭 {연[hi]['최대낙폭'] - 연[lo]['최대낙폭']:.2f}")
    끝s = [연[d]["끝금액"] for d in 연]
    print(f"   → 끝금액 범위 {min(끝s):,} ~ {max(끝s):,}원")

    print("\n④ 한 달에 한 번 — 그 달 몇 번째 거래일에 되돌리나만 바꿔 본다(1~15번째)")
    월 = {}
    for k in range(1, 16):
        g = 기록(f"한달에한번_{k}번째", "매월", k)
        월[k] = g
        print(f"   {k:>2}번째  끝 {g['끝금액']:>12,}원 · 최대낙폭 {g['최대낙폭']:6.2f}% · 되돌린 {g['되돌린횟수']}번 · 수수료 {g['수수료합']:,}원")
    mdds = [월[k]["최대낙폭"] for k in 월]
    끝m = [월[k]["끝금액"] for k in 월]
    print(f"   → 최대낙폭 범위 {min(mdds)} ~ {max(mdds)}% · 폭 {max(mdds) - min(mdds):.2f} · 끝금액 {min(끝m):,} ~ {max(끝m):,}원")

    print("\n⑤ 올여름 폭락 — KODEX 200 의 2026년 최고 종가일 → 그 뒤 최저 종가일")
    r = m["close_r"].values
    y26 = np.where(m["date"].dt.year.values == 2026)[0]
    p = int(y26[np.argmax(r[y26])])
    t = int(p + np.argmin(r[p:]))
    print(f"   {m['date'].iloc[p].date()} → {m['date'].iloc[t].date()} · KODEX 200 {r[t] / r[p] * 100 - 100:.1f}%")
    폭락 = {"고점일": str(m["date"].iloc[p].date()), "바닥일": str(m["date"].iloc[t].date()),
          "KODEX200": round(r[t] / r[p] * 100 - 100, 1), "팔": {}}
    for 이름, 방식, 인자 in ([("안함", "안함", None)] + [(f"1년에한번_{d}월", "연1회", d) for d in range(1, 13)]
                          + [("한달에한번_첫거래일", "매월", 1), ("매일", "매일", None)]):
        eq, wp, *_ = 돌리기(m, 방식, 인자)
        g = {"고점날_주식비중": round(float(wp[p]) * 100, 1),
             "천만원이": int(round(원금 * eq[t] / eq[p], -4))}
        폭락["팔"][이름] = g
        print(f"   {이름:<18} 그날 주식 {g['고점날_주식비중']:5.1f}% · 그날 1,000만원이던 계좌가 → {g['천만원이']:,}원")
    out["폭락"] = 폭락

    print("\n⑥ 올해를 빼면? — 2025-12-31 까지만 잘라 같은 표를 다시 잰다")
    m25 = m[m["date"] <= pd.Timestamp("2025-12-31")].reset_index(drop=True)
    앞 = {}
    for 이름, 방식, 인자 in ([("안함", "안함", None)] + [(f"1년에한번_{d}월", "연1회", d) for d in range(1, 13)]
                          + [(f"한달에한번_{k}번째", "매월", k) for k in range(1, 16)]):
        앞[이름] = 요약(m25, *돌리기(m25, 방식, 인자))
    연25 = [앞[f"1년에한번_{d}월"]["최대낙폭"] for d in range(1, 13)]
    월25 = [앞[f"한달에한번_{k}번째"]["최대낙폭"] for k in range(1, 16)]
    print(f"   안함 최대낙폭 {앞['안함']['최대낙폭']}% ({앞['안함']['낙폭_바닥일']}) · 끝 주식비중 {앞['안함']['끝주식비중']}%")
    print(f"   1년에 한 번 12달: {min(연25)} ~ {max(연25)}% (폭 {max(연25) - min(연25):.2f})")
    print(f"   한 달에 한 번 15날: {min(월25)} ~ {max(월25)}% (폭 {max(월25) - min(월25):.2f})")
    out["2025까지"] = {"안함": 앞["안함"], "1년에한번_낙폭범위": [min(연25), max(연25)],
                     "한달에한번_낙폭범위": [min(월25), max(월25)]}

    print("\n⑦ 되돌리기 한 번 — 한 달에 한 번(첫 거래일) 계좌의 2026년 체결 기록")
    r_, s_ = m["close_r"].values, m["close_s"].values
    on = 점검일(m["date"], "매월", 1)
    r_amt, s_amt = 원금 * 0.5, 원금 * 0.5
    기록들 = []
    대기 = False
    for i in range(1, len(m)):
        r_amt *= r_[i] / r_[i - 1]
        s_amt *= s_[i] / s_[i - 1]
        if 대기:
            tot = r_amt + s_amt
            d = tot * 0.5 - r_amt                  # + 면 주식을 산다 · − 면 판다
            c = abs(d) * 편도 * 2.0
            if m["date"].iloc[i].year == 2026:
                기록들.append({"체결일": str(m["date"].iloc[i].date()), "체결전_주식": int(round(r_amt)),
                            "체결전_채권": int(round(s_amt)), "주식_사고판금액": int(round(d)),
                            "체결전_주식비중": round(r_amt / tot * 100, 1), "수수료": int(round(c))})
            tot -= c
            r_amt, s_amt = tot * 0.5, tot * 0.5
            대기 = False
        대기 = bool(on[i])
    for g in 기록들:
        방향 = "주식 팔고 채권 삼" if g["주식_사고판금액"] < 0 else "주식 사고 채권 팜"
        print(f"   {g['체결일']}  체결 전 주식 {g['체결전_주식']:>11,} · 채권 {g['체결전_채권']:>11,} "
              f"(주식 {g['체결전_주식비중']}%) → {방향} {abs(g['주식_사고판금액']):,}원 · 수수료 {g['수수료']:,}원")
    out["되돌리기_2026"] = 기록들

    out["요약"] = {
        "1년에한번_가장깊은달": lo, "1년에한번_가장얕은달": hi,
        "1년에한번_낙폭범위": [연[lo]["최대낙폭"], 연[hi]["최대낙폭"]],
        "한달에한번_낙폭범위": [min(mdds), max(mdds)],
        "한달에한번_끝금액범위": [min(끝m), max(끝m)],
        "1년에한번_끝금액범위": [min(끝s), max(끝s)],
    }
    with open(a.json, "w", encoding="utf-8") as f:
        json.dump(out, f, ensure_ascii=False, indent=1)
    print(f"\n→ {os.path.basename(a.json)}")

    # 수치표 — 대본·화면에 들어가는 숫자는 전부 여기서 나온다(손으로 적지 않는다).
    만 = lambda v: int(round(v / 10_000))                      # noqa: E731
    폭 = out["폭락"]["팔"]
    표 = {
        "설정": {"주식ETF": "KODEX 200 (069500)", "채권ETF": "KODEX 국고채3년 (114260)",
               "목표비중_%": 50, "편도수수료_%": 0.1, "원금_만원": 만(원금),
               "체결": "점검일 종가 신호 → 다음 거래일 종가", "매달_몇번째거래일_잰범위": [1, 15]},
        "구간": {"시작": out["구간"][0], "끝": out["구간"][1], "연수": out["연수"], "거래일": out["봉"]},
        "폭락": {"고점일": out["폭락"]["고점일"], "바닥일": out["폭락"]["바닥일"],
               "KODEX200_하락_%": abs(out["폭락"]["KODEX200"]),
               "그냥둠_고점날_주식비중_%": 폭["안함"]["고점날_주식비중"],
               "매달_고점날_주식비중_%": 폭["한달에한번_첫거래일"]["고점날_주식비중"],
               "1월_고점날_주식비중_%": 폭["1년에한번_1월"]["고점날_주식비중"],
               "그냥둠_천만원이_만원": 만(폭["안함"]["천만원이"]),
               "매달_천만원이_만원": 만(폭["한달에한번_첫거래일"]["천만원이"]),
               "매일_천만원이_만원": 만(폭["매일"]["천만원이"]),
               "1년에한번_달별_천만원이_만원": {f"{d}월": 만(폭[f"1년에한번_{d}월"]["천만원이"])
                                         for d in range(1, 13)}},
        "끝": {"그냥둠_끝금액_만원": 만(out["팔"]["안함"]["끝금액"]),
              "매달_끝금액_만원": 만(out["팔"]["한달에한번_첫거래일"]["끝금액"]),
              "그냥둠_끝주식비중_%": out["팔"]["안함"]["끝주식비중"],
              "매달_수수료합_원": out["팔"]["한달에한번_첫거래일"]["수수료합"],
              "매달_수수료합_만원_상한": int(np.ceil(out["팔"]["한달에한번_첫거래일"]["수수료합"] / 10_000)),
              "매달_되돌린횟수": out["팔"]["한달에한번_첫거래일"]["되돌린횟수"]},
        "매달_몇번째거래일": {"최대낙폭_범위_%": out["요약"]["한달에한번_낙폭범위"]},
        "2025까지": out["2025까지"]["1년에한번_낙폭범위"] + out["2025까지"]["한달에한번_낙폭범위"],
        "되돌리기_2026": [{"체결일": g["체결일"], "주식_사고판금액_원": g["주식_사고판금액"],
                        "체결전_주식비중_%": g["체결전_주식비중"]} for g in 기록들],
        "대본용_기타": {"폭락까지_주": 3, "달": list(range(1, 13))},
    }
    with open(os.path.join(HERE, "수치표_banban.json"), "w", encoding="utf-8") as f:
        json.dump(표, f, ensure_ascii=False, indent=1)
    print("→ 수치표_banban.json")


if __name__ == "__main__":
    main()
