# -*- coding: utf-8 -*-
r"""미국 ETF — 「이름 끝의 H, 붙은 걸 살까 그냥 살까」 편 재현 코드 (AlgoLab, 2026-09-18)

영상에서 말한 숫자를 **이 파일 하나로** 그대로 다시 냅니다.

  python run_fx.py

필요한 것: 파이썬 표준 라이브러리만. (numpy·pandas 불필요)
데이터: 옆 폴더 `data_fx/` 에 그날 시세를 얼려 두었습니다. 그 폴더가 있으면 그것을 쓰고,
없으면 야후 파이낸스에서 새로 받습니다. **새로 받으면 분배금 보정이 갱신돼 끝자리가
조금 달라질 수 있습니다** — 영상과 똑같은 숫자를 보려면 얼린 시세를 그대로 두십시오.

무엇을 견줬나
  · 같은 지수를 담는 국내상장 ETF 두 개 — 이름 끝에 (H) 가 붙은 것과 안 붙은 것
  · 짝비교 5번 (상품 8개)
      S&P500    360750(그냥) ↔ 143850(H·선물)  ·  379800(그냥) ↔ 219480(H·선물)
      S&P500    360750(그냥) ↔ 448290(H·현물)  ← 선물 구조를 걷어낸 짝
      나스닥100  133690(그냥) ↔ 304940(H·선물)  ·  379810(그냥) ↔ 304940(H·선물)
  · ★ (H) 상품 넷 중 셋이 선물로 지수를 따라가는 상품입니다(상품명 조회로 확인).
    그래서 선물이 아닌 현물 환헤지 상품(448290)으로 한 짝을 더 만들었습니다.
  · 헤드라인은 **구간이 가장 긴 짝** — 고르는 규칙을 먼저 정하고 결과를 봤습니다
    (나스닥100 · 133690 ↔ 304940 · 2018-08-30 ~ 2026-09-17 · 8.05년)

정직하게 잰 방법
  · 분배금 보정(총수익) 종가 · 사고팔 때 편도 0.1%
  · 두 종목이 모두 거래된 날만 씁니다(같은 날짜·같은 구간)
  · 적립식 — 매달 첫 거래일 종가에 100만원. 거치식 결과도 같이 냅니다
  · (H) 여부는 **상품 이름이 아니라 데이터로** 갈랐습니다. 하루치 수익률을
    「미국 지수 수익률」과 「원/달러 수익률」에 최소제곱으로 회귀해, 원/달러 계수가
    0.12 아래면 (H), 위면 그냥형입니다. 실제로는 -0.03~-0.01 과 +0.32~+0.38 로
    중간값 없이 두 무리로 갈립니다.

안 넣은 것: 호가 스프레드·괴리율·세금(양도세·배당소득세)·연금계좌 과세이연.
매수만 하고 하락에 거는 기능은 없습니다. 여덟 종목 다 지금 상장돼 있습니다(생존편향).
환율이 위기에 오른다는 것은 지난 8년의 성질입니다. 투자 권유가 아닙니다.
"""
import csv
import datetime
import io
import json
import os
import sys

# 한국어 윈도 기본 콘솔(cp949)에서 대시·화살표가 UnicodeEncodeError 를 낸다.
# 시청자가 받아 가는 파일이라 첫 줄에서 죽으면 안 된다.
try:
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

ASOF = "2026-09-17"          # 영상을 만든 날의 직전 거래일. None 으로 바꾸면 오늘까지
COST = 0.001                 # 편도 0.1%
DEP = 1000000.0              # 적립식 매달 납입
HERE = os.path.dirname(os.path.abspath(__file__))
FROZEN = os.path.join(HERE, "data_fx")

PAIRS = [("S&P500",   "360750.KS", "143850.KS", "선물"),
         ("S&P500",   "379800.KS", "219480.KS", "선물"),
         ("S&P500",   "360750.KS", "448290.KS", "현물"),
         ("나스닥100", "133690.KS", "304940.KS", "선물"),
         ("나스닥100", "379810.KS", "304940.KS", "선물")]
# 국내 **정식 종목명**이다. 해외 데이터 제공처의 영문명은 143850 에서 (H) 를 빠뜨린다.
NAMES = {"133690": "TIGER 미국나스닥100 (현물·환노출)",
         "379810": "KODEX 미국나스닥100 (현물·환노출)",
         "360750": "TIGER 미국S&P500 (현물·환노출)",
         "379800": "KODEX 미국S&P500 (현물·환노출)",
         "304940": "KODEX 미국나스닥100선물(H) (선물·환헤지)",
         "143850": "TIGER 미국S&P500선물(H) (선물·환헤지)",
         "219480": "KODEX 미국S&P500선물(H) (선물·환헤지)",
         "448290": "TIGER 미국S&P500(H) (현물·환헤지)"}
IDX = {"S&P500": "^GSPC", "나스닥100": "^NDX"}   # ^IXIC 는 나스닥 종합 — 100 이 아니다
FX = "KRW=X"


# ── 시세 ──────────────────────────────────────────────────────────────
def _path(sym):
    safe = sym.replace(".", "_").replace("^", "_").replace("=", "_")
    return os.path.join(FROZEN, safe + "_1d.csv")


def load(sym):
    if not os.path.exists(_path(sym)):
        fetch(sym)
    out = []
    with io.open(_path(sym), encoding="utf-8") as f:
        for row in csv.reader(f):
            if not row or row[0] == "date":
                continue
            d = row[0][:10]
            if ASOF and d > ASOF:
                continue
            try:
                out.append((d, float(row[4])))
            except (ValueError, IndexError):
                pass
    if not out:
        raise SystemExit("시세가 비었습니다: " + sym)
    return out


def fetch(sym):
    import urllib.parse
    import urllib.request
    os.makedirs(FROZEN, exist_ok=True)
    url = ("https://query1.finance.yahoo.com/v8/finance/chart/%s"
           "?range=30y&interval=1d&events=div%%2Csplit" % urllib.parse.quote(sym))
    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    j = json.load(urllib.request.urlopen(req, timeout=60))["chart"]["result"][0]
    ts, q = j["timestamp"], j["indicators"]["quote"][0]
    adj = (j["indicators"].get("adjclose") or [{}])[0].get("adjclose") or q["close"]
    with io.open(_path(sym), "w", encoding="utf-8", newline="") as f:
        w = csv.writer(f)
        w.writerow(["date", "open", "high", "low", "close", "volume"])
        for i, t in enumerate(ts):
            if q["close"][i] is None or adj[i] is None:
                continue
            k = adj[i] / q["close"][i]
            d = datetime.datetime.utcfromtimestamp(t).strftime("%Y-%m-%d")
            w.writerow([d,
                        q["open"][i] * k if q["open"][i] else adj[i],
                        q["high"][i] * k if q["high"][i] else adj[i],
                        q["low"][i] * k if q["low"][i] else adj[i],
                        adj[i], q["volume"][i] or 0])
    print("  받았습니다 %-12s" % sym)


# ── 계산 ──────────────────────────────────────────────────────────────
def 보유(dates, px):
    g = [1.0 - COST]
    for i in range(1, len(px)):
        g.append(g[-1] * px[i] / px[i - 1])
    d0 = datetime.date(*map(int, dates[0].split("-")))
    d1 = datetime.date(*map(int, dates[-1].split("-")))
    yrs = (d1 - d0).days / 365.25
    cagr = g[-1] ** (1 / yrs) - 1
    peak, mdd = g[0], 0.0
    for v in g:
        peak = max(peak, v)
        mdd = min(mdd, v / peak - 1)
    return {"CAGR": cagr, "MDD": mdd, "Calmar": cagr / abs(mdd), "배수": g[-1], "년": yrs}


def 적립(dates, px):
    first, seen = set(), set()
    for i, d in enumerate(dates):
        if d[:7] not in seen:
            seen.add(d[:7])
            first.add(i)
    units, bal = 0.0, []
    for i in range(len(dates)):
        if i in first:
            units += DEP * (1 - COST) / px[i]
        bal.append(units * px[i])
    peak, mdd = 0.0, 0.0
    for v in bal:
        peak = max(peak, v)
        if peak > 0:
            mdd = min(mdd, v / peak - 1)
    cf = [-DEP if i in first else 0.0 for i in range(len(dates))]
    cf[-1] += bal[-1]
    lo, hi = -0.99, 3.0
    for _ in range(200):
        mid = (lo + hi) / 2
        v = sum(x / ((1 + mid) ** (i / 252.0)) for i, x in enumerate(cf))
        lo, hi = (mid, hi) if v > 0 else (lo, mid)
    return {"최종잔고": bal[-1], "총납입": DEP * len(first), "배수": bal[-1] / (DEP * len(first)),
            "IRR": (lo + hi) / 2, "MDD": mdd, "납입횟수": len(first)}


def 환율베타(etf, idx, fx):
    """일간수익률 r_etf = a + b1*r_idx + b2*r_fx 를 최소제곱으로 푼다(3x3 정규방정식)."""
    em = {d: i for i, (d, _) in enumerate(etf)}
    im = {d: i for i, (d, _) in enumerate(idx)}
    fm = {d: i for i, (d, _) in enumerate(fx)}
    days = sorted(set(em) & set(im) & set(fm))
    X, y = [], []
    for k in range(1, len(days)):
        p, q = days[k - 1], days[k]
        if em[q] - em[p] != 1 or im[q] < 1 or im[p] < 1:
            continue
        X.append([1.0,
                  idx[im[q] - 1][1] / idx[im[p] - 1][1] - 1,
                  fx[fm[q]][1] / fx[fm[p]][1] - 1])
        y.append(etf[em[q]][1] / etf[em[p]][1] - 1)
    n = 3
    A = [[sum(X[r][i] * X[r][j] for r in range(len(X))) for j in range(n)] for i in range(n)]
    b = [sum(X[r][i] * y[r] for r in range(len(X))) for i in range(n)]
    for i in range(n):                                   # 가우스 소거
        p = max(range(i, n), key=lambda r: abs(A[r][i]))
        A[i], A[p] = A[p], A[i]
        b[i], b[p] = b[p], b[i]
        for r in range(i + 1, n):
            f = A[r][i] / A[i][i]
            for c in range(i, n):
                A[r][c] -= f * A[i][c]
            b[r] -= f * b[i]
    x = [0.0] * n
    for i in range(n - 1, -1, -1):
        x[i] = (b[i] - sum(A[i][c] * x[c] for c in range(i + 1, n))) / A[i][i]
    return x[2], len(X)


def won(v, 올림=False):
    # 영상·썸네일은 **화면에 뜬 두 금액의 차이**로 말한다(2억 5,387만 − 1억 9,775만 = 5,612만원).
    # 차이만 그 규약을 따르고, 잔고는 그대로 버린다.
    man = int(round(v / 10000)) if 올림 else int(v // 10000)
    e, m = divmod(man, 10000)
    return ("%d억 %s만원" % (e, format(m, ","))) if e else ("%s만원" % format(man, ","))


def main():
    syms = sorted({s for _, a, b, _c in PAIRS for s in (a, b)} | set(IDX.values()) | {FX})
    if not os.path.isdir(FROZEN):
        print("얼린 시세가 없어 야후에서 받습니다 (%d종목)" % len(syms))
    series = {s: load(s) for s in syms}
    fx = series[FX]

    print("\n── (H) 여부를 이름이 아니라 데이터로 가른다 ──────────────────")
    print("   일간수익률의 원/달러 계수 — 0 에 붙으면 (H), 0.3 을 넘으면 그냥형")
    for name, idxsym in IDX.items():
        for sym in sorted({s for n, a, b, _c in PAIRS if n == name for s in (a, b)}):
            beta, cnt = 환율베타(series[sym], series[idxsym], fx)
            print("   %-12s 원/달러 계수 %+.3f  (표본 %5d일)  → %-9s %s"
                  % (sym, beta, cnt, "(H) 환헤지" if beta < 0.12 else "그냥형",
                     NAMES.get(sym.split(".")[0], "")))

    print("\n── 짝비교 %d번 ───────────────────────────────────────────" % len(PAIRS))
    승 = {"보유 해마다": 0, "보유 낙폭": 0, "적립 남은 돈": 0, "적립 낙폭": 0}
    rows = []
    for name, un, hd, 구조 in PAIRS:
        du, dh = dict(series[un]), dict(series[hd])
        days = sorted(set(du) & set(dh))
        pu, ph = [du[d] for d in days], [dh[d] for d in days]
        hu, hh = 보유(days, pu), 보유(days, ph)
        au, ah = 적립(days, pu), 적립(days, ph)
        승["보유 해마다"] += hu["CAGR"] > hh["CAGR"]
        승["보유 낙폭"] += hu["MDD"] > hh["MDD"]
        승["적립 남은 돈"] += au["최종잔고"] > ah["최종잔고"]
        승["적립 낙폭"] += au["MDD"] > ah["MDD"]
        rows.append((name, un, hd, days, hu, hh, au, ah, 구조))
        print("\n  %s  %s(그냥) ↔ %s(H·%s)   %s ~ %s  %.2f년"
              % (name, un[:6], hd[:6], 구조, days[0], days[-1], hu["년"]))
        print("    거치식  그냥 해마다 %5.2f%% · 가장 많이 빠졌을 때 %6.2f%%"
              "   |  (H) 해마다 %5.2f%% · %6.2f%%"
              % (hu["CAGR"] * 100, hu["MDD"] * 100, hh["CAGR"] * 100, hh["MDD"] * 100))
        print("    적립식  그냥 %14s · 가장 많이 빠졌을 때 %6.2f%%"
              "   |  (H) %14s · %6.2f%%"
              % (won(au["최종잔고"]), au["MDD"] * 100, won(ah["최종잔고"]), ah["MDD"] * 100))
        print("            매달 100만원 %d번 · 넣은 돈 %s" % (au["납입횟수"], won(au["총납입"])))

    n = len(PAIRS)
    print("\n── 집계 — 그냥형(H 없는 쪽)이 이긴 횟수 ──────────────────────")
    for k, v in 승.items():
        print("   %-12s %d/%d" % (k, v, n))

    head = max(rows, key=lambda r: r[4]["년"])          # 규칙: 구간이 가장 긴 짝
    name, un, hd, days, hu, hh, au, ah, 구조 = head
    print("\n── 영상이 말한 숫자 (헤드라인 = 구간이 가장 긴 짝) ───────────")
    print("   %s · %s(그냥) ↔ %s(H·%s) · %s ~ %s · %.2f년"
          % (name, un[:6], hd[:6], 구조, days[0], days[-1], hu["년"]))
    print("   ※ 이 짝이 다섯 짝 중 가장 크게 벌어진 짝입니다. 나머지 넷은 그보다 작습니다:")
    for nm, u, h, dd, _hu, _hh, _au, _ah, st in rows:
        if (nm, u, h) != (name, un, hd):
            print("      %-8s %s↔%s(%s)  %s" % (nm, u[:6], h[:6], st, won(_au["최종잔고"] - _ah["최종잔고"], 올림=True)))
    print("   매달 100만원씩 %d번 · 넣은 돈 %s" % (au["납입횟수"], won(au["총납입"])))
    print("   H 없는 쪽 남은 돈   %s" % won(au["최종잔고"]))
    print("   H 붙은 쪽 남은 돈   %s" % won(ah["최종잔고"]))
    print("   차이               %s   (영상·썸네일 표기)" % won(au["최종잔고"] - ah["최종잔고"], 올림=True))
    print("   가장 많이 빠졌을 때  H 없는 쪽 %.2f%% · H 붙은 쪽 %.2f%%"
          % (au["MDD"] * 100, ah["MDD"] * 100))
    print("\n   ※ 영상은 만원 미만을 버리고 낙폭은 소수를 버려 말합니다"
          " — 둘 다 우리 주장에 불리한 쪽입니다.")


if __name__ == "__main__":
    main()
