# -*- coding: utf-8 -*-
r"""손절선·익절선 전수 재현 — 영상 「교과서대로 손절했는데, 왜 수익만 사라졌을까?」

이 파일 하나로 영상에 나온 숫자를 전부 다시 만듭니다.

  python run_stop.py

같이 들어 있는 것
  strat_pool.py / overlays.py / runlib.py / sweep.py   전략·오버레이·러너
  data_asof/                                           2026-09-08 시점으로 얼린 시세

★ 왜 시세를 같이 넣었나
  4시간봉은 마지막 봉이 아직 닫히지 않은 채로 받아지면 5년 경로 금액이 1퍼센트쯤 어긋납니다.
  그래서 영상을 만든 시점의 캔들을 그대로 얼려 동봉했습니다. 최신 데이터로 보고 싶으시면
  showcase 캐시를 직접 갱신해 SHOW 경로만 바꾸시면 됩니다(숫자는 당연히 달라집니다).

무엇을 재는가
  같은 신호·같은 파라미터·같은 구간에서 **손절선과 익절선만** 갈아끼웁니다.
  비교는 항상 '같은 조합 안에서' 합니다. 다른 전략끼리 순위를 매기는 실험이 아닙니다.

잣대
  · 연복리(CAGR)  — 얼마나 벌었나
  · 최대낙폭(MDD) — 중간에 얼마나 크게 잃었나
  · 칼마 = 연복리 ÷ 최대낙폭 — 손절은 낙폭을 낮추는 게 당연하므로 둘을 같이 봅니다
  · 평균 노출     — 돈이 실제로 시장에 걸려 있던 비율

한계 (영상과 같은 표기)
  · 매매 비용 편도 0.1퍼센트. 세금 미반영(손절·익절은 실현이고 그냥 두는 쪽은 이연이라
    세금을 넣으면 격차는 더 벌어집니다 — 손절에 유리한 방향의 생략이 아닙니다)
  · 손절 판정은 종가 마감 기준. 봉내 저가 터치 모델도 같이 잽니다(자가반박)
  · 호가 밀림(슬리피지) 미모형 · 전부 매수만(공매도 없음)
  · 지금 상장돼 있는 17개 시장만. 사라진 종목은 들어 있지 않습니다
"""
import datetime
import os
import sys

import numpy as np

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)

import strat_pool as P                     # noqa: E402
import overlays as OV                      # noqa: E402
from runlib import series_data, growth, stats  # noqa: E402

ASOF = datetime.date(2026, 9, 8)           # 영상을 만든 날. 얼린 시세가 여기까지입니다
SHOW = os.path.join(HERE, "data_asof")     # 최신으로 보려면 이 경로를 캐시 폴더로 바꾸세요
COST = 0.001                               # 편도 0.1퍼센트
SEED = 10_000_000                          # 천만원

SERIES = [
    ("비트코인(업비트)",   "sc_btc_4h.csv",     "4h", 400),
    ("비트코인(바이낸스)",  "sc_btcusdt_4h.csv", "4h", 400),
    ("이더리움(업비트)",   "sc_eth_4h.csv",     "4h", 400),
    ("이더리움(바이낸스)",  "sc_ethusdt_4h.csv", "4h", 400),
    ("비트코인(일봉)",     "sc_btc_1d.csv",     "1d", 260),
    ("이더리움(일봉)",     "sc_eth_1d.csv",     "1d", 260),
    ("SPY",              "sc_spy_1d.csv",     "1d", 260),
    ("QQQ",              "sc_qqq_1d.csv",     "1d", 260),
    ("TQQQ(3배)",        "sc_tqqq_1d.csv",    "1d", 260),
    ("SOXL(3배)",        "sc_soxl_1d.csv",    "1d", 260),
    ("엔비디아",          "sc_nvda_1d.csv",    "1d", 260),
    ("테슬라",            "sc_tsla_1d.csv",    "1d", 260),
    ("애플",              "sc_aapl_1d.csv",    "1d", 260),
    ("마이크로소프트",      "sc_msft_1d.csv",    "1d", 260),
    ("삼성전자",          "sc_samsung_1d.csv", "1d", 260),
    ("SK하이닉스",        "sc_hynix_1d.csv",   "1d", 260),
    ("코스피",            "sc_kospi_1d.csv",   "1d", 260),
]
STRATS = [
    ("이평크로스50/200", P.ma_cross,     dict(fast=50, slow=200)),
    ("돈치안55",        P.donchian,      dict(n=55)),
    ("슈퍼트렌드10/3",   P.supertrend,    dict(n=10, mult=3.0)),
    ("UT봇2.0",         P.ut_bot,        dict(key=2.0, atr_n=10)),
    ("켈트너돌파20/1.5", P.keltner_break, dict(n=20, k=1.5)),
    ("RSI역추세",       P.rsi_meanrev,   dict(n=14, lo=30, hi=70)),
]
SL_LADDER = [0.03, 0.05, 0.10, 0.30]
TP_MAIN = 0.06


def cal(cg, m):
    return cg / abs(m) if m and m < 0 else float("nan")


def bracket(c, sig, sl, tp, low=None):
    """진입 종가 대비 -sl 손절 / +tp 익절. low 를 주면 봉내 저가 터치 판정."""
    st = {}

    def entry(t):
        st["e"] = c[t]

    def hold(t):
        e = st["e"]
        if low is not None and low[t] <= e * (1 - sl):
            return False, 1.0
        if c[t] / e - 1 <= -sl:
            return False, 1.0
        if tp is not None and c[t] / e - 1 >= tp:
            return False, 1.0
        return True, 1.0

    return OV._iterate(sig, entry, hold)


def med(xs):
    v = [float(x) for x in xs if x == x]
    return float(np.median(v)) if v else float("nan")


def run():
    rows = []
    for label, fname, tf, warm in SERIES:
        S = series_data(os.path.join(SHOW, fname), tf, warm=warm)
        for sname, fn, prm in STRATS:
            base = np.nan_to_num(fn(S["o"], S["h"], S["l"], S["c"], **prm), nan=0.0)
            pb, gb = growth(S, base, COST)
            rb = stats(S, gb)
            eb = float((pb[warm:] > 0).mean())
            rec = dict(자산=label, 전략=sname, 시작=S["d"][warm][:10], 끝=S["d"][-1][:10],
                       기준_cagr=rb["cagr"], 기준_mdd=rb["mdd"],
                       기준_cal=cal(rb["cagr"], rb["mdd"]), 기준_노출=eb)
            for sl in SL_LADDER:
                sig = bracket(S["c"], base, sl, None)
                ps, gs = growth(S, sig, COST)
                r = stats(S, gs)
                rec["sl%d" % int(sl * 100)] = dict(
                    cagr=r["cagr"], mdd=r["mdd"], calmar=cal(r["cagr"], r["mdd"]),
                    노출=float((ps[warm:] > 0).mean()))
            sig = bracket(S["c"], base, 0.03, TP_MAIN)
            ps, gs = growth(S, sig, COST)
            r = stats(S, gs)
            rec["격언"] = dict(cagr=r["cagr"], mdd=r["mdd"], calmar=cal(r["cagr"], r["mdd"]),
                              노출=float((ps[warm:] > 0).mean()))
            _, g_ib = growth(S, bracket(S["c"], base, 0.03, TP_MAIN, low=S["l"]), COST)
            r_ib = stats(S, g_ib)
            rec["격언_봉내"] = dict(cagr=r_ib["cagr"])
            e3 = rec["sl3"]["노출"]
            w = min(e3 / eb, 1.0) if eb > 0 else 0.0
            pw, gw = growth(S, base * w, COST)
            rw = stats(S, gw)
            rec["비중"] = dict(cagr=rw["cagr"], mdd=rw["mdd"],
                              calmar=cal(rw["cagr"], rw["mdd"]), w=w)
            rows.append(rec)
    return rows


def 대표칸():
    """영상에 나온 천만원 경로 — 미국 대표 지수 + 이동평균선 50/200 교차."""
    S = series_data(os.path.join(SHOW, "sc_spy_1d.csv"), "1d", warm=260)
    base = np.nan_to_num(P.ma_cross(S["o"], S["h"], S["l"], S["c"], fast=50, slow=200), nan=0.0)
    pb, gb = growth(S, base, COST)
    eb = float((pb[260:] > 0).mean())
    sl = bracket(S["c"], base, 0.03, None)
    psl, gsl = growth(S, sl, COST)
    esl = float((psl[260:] > 0).mean())
    tp = bracket(S["c"], base, 0.03, TP_MAIN)
    ptp, gtp = growth(S, tp, COST)
    w = min(esl / eb, 1.0)
    pw, gw = growth(S, base * w, COST)
    out = []
    for name, pos, g in (("신호대로", pb, gb), ("손절 -3%", psl, gsl),
                         ("손절 -3% + 익절 +6%", ptp, gtp), ("비중 %.0f%% 한 줄" % (w * 100), pw, gw)):
        eq = np.cumprod(g[260:])
        r = stats(S, g)
        out.append((name, SEED * float(eq[-1]), r["cagr"], r["mdd"],
                    float((pos[260:] > 0).mean())))
    inb, ins = pb[260:] > 0, psl[260:] > 0
    return S["d"][260][:10], S["d"][-1][:10], out, int(inb.sum()), int(ins.sum())


if __name__ == "__main__":
    print("기준일 %s · 매매 비용 편도 %.1f%% · 세금 미반영\n" % (ASOF, COST * 100))

    a, b, rep, hold_base, hold_sl = 대표칸()
    yrs = (datetime.date.fromisoformat(b) - datetime.date.fromisoformat(a)).days / 365.25
    print("■ 대표 칸 — 미국 대표 지수 + 이동평균선 50/200 교차")
    print("   %s ~ %s (%.1f년) · 천만원으로 시작" % (a, b, yrs))
    for name, money, cg, md, ex in rep:
        print("   %-20s %14s원 · 연복리 %5.1f%% · 최대낙폭 %6.1f%% · 노출 %.2f"
              % (name, format(int(round(money)), ","), cg * 100, md * 100, ex))
    print("   시장에 들어가 있던 날: 신호대로 %s일 → 손절 -3%% %s일 (차이 %s일)\n"
          % (format(hold_base, ","), format(hold_sl, ","), format(hold_base - hold_sl, ",")))

    rows = run()
    n = len(rows)
    print("■ 전 조합 %d개 (시장 17 × 전략 6) — 같은 조합 안에서만 비교" % n)
    print("   %-22s %9s %9s %7s %9s %9s %10s"
          % ("장치", "중앙 연복리", "중앙 낙폭", "노출", "연복리 승", "낙폭 승", "칼마 승"))

    def line(name, get):
        cg = med([get(r)["cagr"] for r in rows])
        md = med([get(r)["mdd"] for r in rows])
        ex = med([get(r).get("노출", float("nan")) for r in rows])
        w1 = sum(1 for r in rows if get(r)["cagr"] > r["기준_cagr"])
        w2 = sum(1 for r in rows if get(r)["mdd"] > r["기준_mdd"])
        w3 = sum(1 for r in rows if get(r)["calmar"] > r["기준_cal"])
        print("   %-22s %8.1f%% %8.1f%% %7.2f %6d/%-3d %6d/%-3d %7d/%-3d"
              % (name, cg * 100, md * 100, ex, w1, n, w2, n, w3, n))

    print("   %-22s %8.1f%% %8.1f%% %7.2f"
          % ("(기준선 · 장치 없음)", med([r["기준_cagr"] for r in rows]) * 100,
             med([r["기준_mdd"] for r in rows]) * 100, med([r["기준_노출"] for r in rows])))
    for sl in SL_LADDER:
        line("손절 -%d%%" % int(sl * 100), (lambda k: (lambda r: r[k]))("sl%d" % int(sl * 100)))
    line("손절 -3% + 익절 +6%", lambda r: r["격언"])

    ib = med([r["격언_봉내"]["cagr"] for r in rows])
    ibw = sum(1 for r in rows if r["격언_봉내"]["cagr"] > r["기준_cagr"])
    print("\n   [자가반박] 봉내 저가 터치로 손절해도  중앙 연복리 %.1f%% · 연복리 승 %d/%d"
          % (ib * 100, ibw, n))

    w_cal = sum(1 for r in rows if r["비중"]["calmar"] > r["sl3"]["calmar"])
    w_cg = sum(1 for r in rows if r["비중"]["cagr"] > r["sl3"]["cagr"])
    print("\n■ 처방 — 손절선 대신 '같은 노출을 만드는 비중 한 줄'")
    print("   중앙 연복리 %.1f%% · 중앙 낙폭 %.1f%%"
          % (med([r["비중"]["cagr"] for r in rows]) * 100,
             med([r["비중"]["mdd"] for r in rows]) * 100))
    print("   손절 -3%% 대비 — 칼마 승 %d/%d · 연복리 승 %d/%d" % (w_cal, n, w_cg, n))

    win = sum(1 for r in rows if r["격언"]["cagr"] > r["기준_cagr"])
    print("\n한 줄 요약: 손절 -3%% + 익절 +6%% 를 얹어 수익이 올라간 조합은 %d개 / %d개." % (win, n))
    print("           덜 잃고 싶다면 손절선이 아니라 비중을 줄이는 쪽이 쌉니다.")
