# -*- coding: utf-8 -*-
"""미국주식 양도세 — 매년 12월 이익 250만원어치만 팔고 그날 다시 사기(리셋) 대 끝까지 그냥 두기.
AlgoLab · 2026-11-01 · 표준 라이브러리만 씁니다.  돌리는 법: python run_ustax.py

가정(영상·설명란과 같음)
 · 가격 = 미국 ETF 종가(배당 제외) × 그날 원/달러 환율(FRED DEXKOUS, 없는 날은 직전 값). 자료는 2026-10-02 까지 얼림.
 · 수수료 편도 0.25%(살 때 값에 더하고, 팔 때 값에서 뺌). 소수점 수량.
 · 양도세 = 22% × max(0, 그해 실현 이익 − 250만원). 취득가는 먼저 산 것부터(선입선출).
 · 리셋: 해마다 12월 15일 이후 첫 거래일, 실현 이익이 250만원이 될 만큼만 먼저 산 주식부터 팔고 그날 판 돈으로 다시 산다.
         판 돈의 10분의 1도 이익이 아니면 그해는 건너뛴다. 마지막으로 다 파는 해에는 리셋하지 않는다(공제를 마지막 매도가 쓴다).
 · 마지막 날 두 계좌 모두 전부 팔고 양도세를 낸 뒤 손에 쥔 돈을 견준다.
"""
import bisect, csv, datetime as dt, os, sys

try:
    sys.stdout.reconfigure(encoding="utf-8")
except Exception:
    pass
HERE = os.path.dirname(os.path.abspath(__file__))
TAX, DED = 0.22, 250.0          # 세율 · 공제(만원)


def load(sym):
    def rd(f, col):
        out = {}
        with open(os.path.join(HERE, "data", f), encoding="utf-8") as fh:
            r = csv.reader(fh); next(r)
            for row in r:
                try:
                    out[dt.date.fromisoformat(row[0])] = float(row[col])
                except (ValueError, IndexError):
                    pass
        return out
    px, fx = rd(sym + ".csv", 1), rd("DEXKOUS.csv", 1)
    fd = sorted(fx)
    D, P = [], []
    for d in sorted(px):
        i = bisect.bisect_right(fd, d) - 1
        if i >= 0:
            D.append(d); P.append(px[d] * fx[fd[i]] / 1e4)   # 1주 값(만원)
    return D, P


class Acct:
    def __init__(self, C, M):
        self.C, self.M, self.lots, self.cash = C, M, [], 0.0

    def buy(self, money, p):
        money += self.cash
        unit = p * (1 + self.C)
        sh = money / unit
        if sh > 0:
            self.lots.append([sh, unit])
        self.cash = money - sh * unit

    def gain(self, p):
        return sum(sh * (p * (1 - self.C) - b) for sh, b in self.lots)

    def reset(self, p):
        """반환 (판 돈, 실현 이익, 했나)"""
        if self.gain(p) <= 0:
            return 0.0, 0.0, False
        realized = proceeds = sold = 0.0
        keep, lots = [], [l[:] for l in self.lots]
        for i, (sh, b) in enumerate(lots):
            if realized >= DED:
                keep.extend(lots[i:]); break
            g = p * (1 - self.C) - b
            if g > 0 and realized + sh * g > DED:
                q = (DED - realized) / g
                realized += q * g; proceeds += q * p * (1 - self.C); sold += q
                keep.append([sh - q, b]); keep.extend(lots[i + 1:]); break
            realized += sh * g; proceeds += sh * p * (1 - self.C); sold += sh
        if sold <= 0 or realized <= 0:
            return 0.0, 0.0, False
        if realized / (sold * p) < self.M:
            return sold * p, realized, False
        self.lots = keep
        self.buy(proceeds, p)
        return sold * p, realized, True

    def settle(self, p):
        val = sum(sh for sh, _ in self.lots) * p * (1 - self.C) + self.cash
        tax = TAX * max(0.0, self.gain(p) - DED)
        return val, tax


def reset_days(D):
    out = {}
    for y in sorted({d.year for d in D}):
        k = bisect.bisect_left(D, dt.date(y, 12, 15))
        if k < len(D) and D[k].year == y:
            out[k] = y
    return out


def case(D, P, start, amt=1000.0, C=0.0025, M=0.10, show=False):
    a, z = bisect.bisect_left(D, start), len(D) - 1
    H, R = Acct(C, M), Acct(C, M)
    H.buy(amt, P[a]); R.buy(amt, P[a])
    if show:
        print(f"  {D[a]} 에 두 계좌 모두 {amt:,.0f}만원어치 삼")
    for y in range(D[a].year, D[z].year):
        k = bisect.bisect_left(D, dt.date(y, 12, 15))
        sold, real, did = R.reset(P[k])
        if show:
            if did:
                print(f"  {D[k]}  리셋: {sold:,.1f}만원어치 팔고 다시 삼 · 실현 이익 {real:,.1f}만원 · 그해 양도세 0원")
            elif sold > 0:
                print(f"  {D[k]}  건너뜀: 다 팔아도 이익 {real:,.1f}만원 / 판 돈 {sold:,.1f}만원 (10분의 1 미만)")
            else:
                print(f"  {D[k]}  건너뜀: 이익 없음")
    vh, th = H.settle(P[z]); vr, tr = R.settle(P[z])
    return dict(끝=D[z], 판값_보유=vh, 세금_보유=th, 세후_보유=vh - th, 판값_리셋=vr, 세금_리셋=tr, 세후_리셋=vr - tr)


def windows(D, P, mode, amt, years, C=0.0025, M=0.10):
    first = {}
    for i, d in enumerate(D):
        first.setdefault((d.year, d.month), i)
    ml = sorted(first)
    rs = reset_days(D)
    H = years * 12
    res = []
    for i0 in range(len(ml) - H):
        (y0, m0), (y1, m1) = ml[i0], ml[i0 + H]
        if (y1 * 12 + m1) - (y0 * 12 + m0) != H:
            continue
        a, z = first[ml[i0]], first[ml[i0 + H]]
        zy = D[z].year
        contrib = {a: amt} if mode == "일시금" else {first[ml[i0 + j]]: amt for j in range(H)}
        ev = sorted(set(contrib) | {k for k in rs if a <= k < z and rs[k] != zy})
        A, B = Acct(C, M), Acct(C, M)
        for k in ev:
            if k in contrib:
                A.buy(contrib[k], P[k]); B.buy(contrib[k], P[k])
            if k in rs and rs[k] != zy:
                B.reset(P[k])
        vh, th = A.settle(P[z]); vr, tr = B.settle(P[z])
        res.append((vr - tr) - (vh - th))
    win = sum(1 for d in res if d > 0); lose = sum(1 for d in res if d < 0)
    return len(res), win, len(res) - win - lose, lose, min(res)


def man(x):
    x = round(x)
    return f"{x // 10000}억 {x % 10000:,}만원" if x >= 10000 else f"{x:,}만원"


def show_case(name, D, P, **kw):
    r = case(D, P, dt.date(2016, 1, 1), **kw)
    print(f"  그냥 둔 계좌   판 돈 {man(r['판값_보유'])} · 양도세 {man(r['세금_보유'])} · 손에 쥔 돈 {man(r['세후_보유'])}")
    print(f"  12월마다 리셋  판 돈 {man(r['판값_리셋'])} · 양도세 {man(r['세금_리셋'])} · 손에 쥔 돈 {man(r['세후_리셋'])}")
    print(f"  차이: 양도세 {man(r['세금_보유'] - r['세금_리셋'])} 덜 냄 · 손에 쥔 돈 {man(r['세후_리셋'] - r['세후_보유'])} 더")
    return r


if __name__ == "__main__":
    SPY, QQQ = load("SPY"), load("QQQ")
    print("■ 사례 · S&P500(SPY) 2016년 1월 1,000만원 → 2026-10-02 전부 매도 · 편도 0.25% · 10분의 1 문턱")
    case(*SPY, dt.date(2016, 1, 1), show=True)
    show_case("SPY", *SPY)
    print()
    print("■ 같은 사례, 조건을 바꾸면")
    for lab, kw in (("편도 0.1%", dict(C=0.001)), ("10분의 1 문턱 없이 해마다", dict(M=0.0))):
        r = case(*SPY, dt.date(2016, 1, 1), **kw)
        print(f"  {lab:<16} 손에 쥔 돈 차이 {man(r['세후_리셋'] - r['세후_보유'])} (양도세 {man(r['세금_보유'])} → {man(r['세금_리셋'])})")
    print()
    print("■ 나스닥100(QQQ) 같은 사례")
    show_case("QQQ", *QQQ)
    print()
    print("■ 시작 달만 바꿔 다시 (편도 0.25% · 10분의 1 문턱) — 리셋이 더 남음 / 똑같음 / 짐 · 가장 크게 진 금액")
    for lab, S, mode, amt, yrs, kw in (
            ("S&P500 1,000만원 10년", SPY, "일시금", 1000, 10, {}),
            ("S&P500 3억원 20년", SPY, "일시금", 30000, 20, {}),
            ("S&P500 3억원 20년 · 문턱 없이", SPY, "일시금", 30000, 20, dict(M=0.0)),
            ("S&P500 매달 100만원 10년", SPY, "적립", 100, 10, {}),
            ("나스닥100 1,000만원 10년", QQQ, "일시금", 1000, 10, {})):
        n, w, t, l, worst = windows(*S, mode, amt, yrs, **kw)
        print(f"  {lab:<26} {n}번 중 더 남음 {w} · 똑같음 {t} · 짐 {l} · 가장 크게 진 금액 {worst:,.1f}만원")
    print()
    print("끝.")
