# -*- coding: utf-8 -*-
r"""몰빵 대 나눠 담기 전수 재현 — 영상 「코인 자동매매 전략, 잘나가는 하나에 몰빵하면 되지 않나요?」

이 파일 하나로 영상에 나온 숫자를 전부 다시 만듭니다.
  (8.5년 통짜 · 워크포워드 46번 · 눈 감고 찍기 기준선 · 찍기의 분포 · 순위 분포 ·
   켈트너 하나를 뺀 셋 · 겹치지 않는 2년 블록)

  python run_mb.py

같이 들어 있는 것
  strat_pool.py / strat_new.py / overlays.py / runlib.py / sweep.py   전략·오버레이·러너
  data_asof/                                                          2026-09-09 시점으로 얼린 시세

★ 왜 시세를 같이 넣었나
  4시간봉은 마지막 봉이 아직 닫히지 않은 채로 받아지면 8년 경로 숫자가 1퍼센트쯤 어긋납니다.
  그래서 영상을 만든 시점의 캔들을 그대로 얼려 동봉했습니다. 최신 데이터로 보고 싶으시면
  showcase 캐시를 직접 갱신해 SHOW 경로만 바꾸시면 됩니다(숫자는 당연히 달라집니다).

무엇을 재는가
  ① 사다리 — 전략 하나에 몰빵한 네 가지 / 넷을 똑같이 나눠 담은 한 가지를
     **같은 날짜 격자 위에서** 짝비교합니다.
  ② 워크포워드 선택 — "제일 잘나가는 하나를 고르면 되지 않나"를 실제로 굴려봅니다.
     직전 2년 연복리 1등을 골라 다음 1년을 그 전략에만 맡기고, 분기마다 다시 고릅니다.
     선택은 **그 시점까지의 데이터만** 봅니다(미래를 보지 않습니다).
  ③ 겹침 반증 — 롤링 창은 서로 겹치므로, 겹치지 않는 2년 블록으로 따로 셉니다.

잣대
  · 연복리(CAGR)  — 얼마나 벌었나
  · 최대낙폭(MDD) — 중간에 얼마나 크게 잃었나
  · 두 시장(업비트 원화 · 바이낸스 USDT)을 각각 계산해 **나쁜 쪽**을 씁니다
    (min CAGR / worst MDD). 한 시장에서만 좋은 결과는 채택하지 않습니다.

한계 (영상과 같은 표기)
  · 매매 비용 편도 0.1퍼센트. 레그 안 매매와 **레그 사이 비중 되돌리기 회전에 각각** 물립니다
  · 체결은 봉이 닫힌 종가 기준 · 호가 밀림(슬리피지) 미모형 · 전부 매수만(공매도 없음)
  · 지금 상장돼 있는 두 시장의 비트코인·이더리움만. 사라진 종목은 들어 있지 않습니다
  · 전략 네 개는 앞선 검증에서 두 시장을 모두 통과한 것들입니다.
    결론은 **이미 살아남은 넷 사이에서**의 이야기입니다
  · 1등은 연복리로 골랐습니다. 낙폭이나 칼마로 고르면 결과가 달라질 수 있습니다
"""
import datetime
import json
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 strat_new import squeeze_break, fvg_pullback   # noqa: E402
from runlib import series_data, make_sig, growth    # noqa: E402
from sweep import span_stats                    # noqa: E402

ASOF = datetime.date(2026, 9, 9)                # 영상을 만든 날. 얼린 시세가 여기까지입니다
SHOW = os.path.join(HERE, "data_asof")          # 최신으로 보려면 이 경로를 캐시 폴더로 바꾸세요
FEE = 0.001                                     # 편도 0.1퍼센트
BPD = 6                                         # 4시간봉 → 하루 6봉
PER = BPD * 7                                   # 1주마다 비중 되돌리기
COMMON_START = "2017-12-05"                     # 두 시장 공통 시작(시간대 가로지르기 방지)

FILES = {("업비트", "btc"): "sc_btc_4h.csv", ("업비트", "eth"): "sc_eth_4h.csv",
         ("바이낸스", "btc"): "sc_btcusdt_4h.csv", ("바이낸스", "eth"): "sc_ethusdt_4h.csv"}
SER = {k: series_data(os.path.join(SHOW, f), "4h", 400, start=COMMON_START)
       for k, f in FILES.items()}
sq = json.load(open(os.path.join(SHOW, "champion_squeeze.json"), encoding="utf-8"))
SQP = dict(sq["param"])


def fvg_vt(S):
    """빈틈 되돌림 + 변동성 타깃 — 신호는 그대로 두고 비중만 변동성에 맞춰 조절."""
    base = make_sig(S, fvg_pullback, {"look": 20, "exit_n": 20}, 50)
    return OV.vol_target(S["o"], S["h"], S["l"], S["c"], base, target=0.5, ppy=S["ppy"])


LEGS = {
    "켈트너 비트코인": ("btc", lambda S: make_sig(S, P.keltner_break, {"n": 30, "k": 1.5}, 100)),
    "켈트너 이더리움": ("eth", lambda S: make_sig(S, P.keltner_break, {"n": 15, "k": 1.25}, 50)),
    "압축 돌파": (sq["asset"], lambda S: make_sig(S, squeeze_break, SQP, sq["filt"])),
    "빈틈 되돌림": ("eth", fvg_vt),
}
NAMES = list(LEGS)
MKTS = ("업비트", "바이낸스")
CACHE, GRID = {}, {}


def get_leg(mkt, name):
    if (mkt, name) not in CACHE:
        asset, mk = LEGS[name]
        S = SER[(mkt, asset)]
        sig = mk(S)
        _, g = growth(S, sig, FEE)
        CACHE[(mkt, name)] = (dict(zip(S["d"], g)), S["d"][S["warm"]])
    return CACHE[(mkt, name)]


def grid(mkt):
    """모든 레그의 공통 날짜 격자 — 팀끼리 같은 자 위에서 비교하기 위함."""
    if mkt not in GRID:
        gds = [get_leg(mkt, n) for n in NAMES]
        start = max(w for _, w in gds)
        dates = [d for d in sorted(set.intersection(*[set(g) for g, _ in gds])) if d >= start]
        GRID[mkt] = ({n: np.array([get_leg(mkt, n)[0][d] for d in dates]) for n in NAMES}, dates)
    return GRID[mkt]


def ppy_of(dates):
    yrs = (datetime.date.fromisoformat(dates[-1][:10])
           - datetime.date.fromisoformat(dates[0][:10])).days / 365.25
    return len(dates) / yrs


def run_port(M, period=PER, fee=FEE):
    """동일 비중 · period 봉마다 원래 비중으로 되돌린다. 회전에 편도 fee 를 실부과."""
    n, T = M.shape
    e = np.full(n, 1.0 / n)
    out = np.ones(T)
    for t in range(T):
        e = e * M[:, t]
        tot = float(e.sum())
        if tot <= 1e-12:
            break
        e = e / tot
        gross = tot
        if period > 0 and (t + 1) % period == 0 and t + 1 < T:
            tgt = np.full(n, 1.0 / n)
            gross *= (1.0 - float(np.abs(tgt - e).sum()) * fee)
            e = tgt
        out[t] = gross
    return out


def stats(mkt, legs, i0=0, i1=None):
    M, dates = grid(mkt)
    sl = slice(i0, i1)
    g = run_port(np.array([M[n][sl] for n in legs]))
    dts = dates[sl]
    st = span_stats(g, ppy_of(dts))
    return st["cagr"], st["mdd"], dts


# ── ① 사다리 ────────────────────────────────────────────────────────────────
print("=" * 78)
print("① 전 구간 사다리 (4시간봉 · 1주 되돌리기 · 편도 0.1%%)   기준일 %s" % ASOF)
print("=" * 78)
TEAMS = [(n, [n]) for n in NAMES] + [("넷을 다 담기", NAMES)]
ladder = {}
for label, legs in TEAMS:
    row = {}
    for mkt in MKTS:
        c, m, dts = stats(mkt, legs)
        row[mkt] = (c, m)
    ladder[label] = row
    print("  %-14s 업비트 %6.1f%% / %6.1f%%   바이낸스 %6.1f%% / %6.1f%%   | 나쁜 쪽 %5.1f%% / %6.1f%%"
          % (label, row["업비트"][0] * 100, row["업비트"][1] * 100,
             row["바이낸스"][0] * 100, row["바이낸스"][1] * 100,
             min(row[m][0] for m in MKTS) * 100, min(row[m][1] for m in MKTS) * 100))
solo = [l for l, _ in TEAMS if l != "넷을 다 담기"]
best = max(solo, key=lambda l: min(ladder[l][m][0] for m in MKTS))
wrst = min(solo, key=lambda l: min(ladder[l][m][0] for m in MKTS))
print("\n  가장 좋았던 몰빵 %s — 연 %.1f%%" % (best, min(ladder[best][m][0] for m in MKTS) * 100))
print("  가장 나빴던 몰빵 %s — 연 %.1f%%" % (wrst, min(ladder[wrst][m][0] for m in MKTS) * 100))
print("  가장 깊었던 몰빵 낙폭 %.1f%%  ->  넷을 다 담기 %.1f%%"
      % (min(min(ladder[l][m][1] for m in MKTS) for l in solo) * 100,
         min(ladder["넷을 다 담기"][m][1] for m in MKTS) * 100))

# ── ② 워크포워드 선택 ───────────────────────────────────────────────────────
print("\n" + "=" * 78)
print("② 워크포워드 — 직전 2년 1등을 골라 다음 1년. 분기마다 다시 고른다")
print("=" * 78)
SEL, RUN, STEP = 730 * BPD, 365 * BPD, 91 * BPD
rows = []
for mkt in MKTS:
    _, dates = grid(mkt)
    i = 0
    while i + SEL + RUN <= len(dates):
        sel = {n: stats(mkt, [n], i, i + SEL)[0] for n in NAMES}
        pick = max(sel, key=lambda n: sel[n])
        pc, pm, dts = stats(mkt, [pick], i + SEL, i + SEL + RUN)
        tc, tm, _ = stats(mkt, NAMES, i + SEL, i + SEL + RUN)
        each = {n: stats(mkt, [n], i + SEL, i + SEL + RUN) for n in NAMES}
        post = {n: v[0] for n, v in each.items()}
        cs = [each[n][0] for n in NAMES]
        ms = [each[n][1] for n in NAMES]
        # 눈 감고 하나 찍기 = 균등확률 픽. 그 기대값은 네 레그의 평균이다.
        # 켈트너 둘은 같은 규칙이라, 하나를 뺀 셋도 같은 창에서 같이 돌린다.
        a3 = stats(mkt, ["켈트너 비트코인", "압축 돌파", "빈틈 되돌림"], i + SEL, i + SEL + RUN)
        b3 = stats(mkt, ["켈트너 이더리움", "압축 돌파", "빈틈 되돌림"], i + SEL, i + SEL + RUN)
        rows.append(dict(mkt=mkt, run=dts[0][:10] + "~" + dts[-1][:10], pick=pick,
                         hit=pick == max(post, key=lambda n: post[n]),
                         rank=sorted(NAMES, key=lambda n: -post[n]).index(pick) + 1,
                         pc=pc, pm=pm, tc=tc, tm=tm,
                         rc=float(np.mean(cs)), rm=float(np.mean(ms)),
                         lo=min(cs), hi=max(cs), each=cs,
                         a3m=a3[1], b3m=b3[1]))
        i += STEP
hit = sum(1 for r in rows if r["hit"])
print("  %d번 중 고른 전략이 다음 해에도 1등이었던 건 %d번 (%.0f%%)"
      % (len(rows), hit, hit / len(rows) * 100))
print("  직전 성적으로 고른 쪽은 눈 감고 찍은 쪽을 넘지 못했습니다")
print("  넷을 다 담은 쪽이 이긴 횟수 — 수익 %d/%d · 낙폭 %d/%d"
      % (sum(1 for r in rows if r["tc"] > r["pc"]), len(rows),
         sum(1 for r in rows if r["tm"] > r["pm"]), len(rows)))
med = lambda k: float(np.median([r[k] for r in rows]))
print("  한가운데   고른 하나 %5.1f%% / %6.1f%%      넷을 다 담기 %5.1f%% / %6.1f%%"
      % (med("pc") * 100, med("pm") * 100, med("tc") * 100, med("tm") * 100))
print("  가장 나빴던 한 해   고른 하나 %5.1f%%      넷을 다 담기 %5.1f%%"
      % (min(r["pc"] for r in rows) * 100, min(r["tc"] for r in rows) * 100))
N = len(rows)
print("")
print("  ── 견줄 기준선: 눈 감고 하나 찍기 ──")
print("     한가운데  연복리 %5.1f%%  낙폭 %6.1f%%"
      % (med("rc") * 100, med("rm") * 100))
w = lambda a, b: sum(1 for r in rows if r[a] > r[b])
print("     직전 1등 픽이 찍기를 이긴 횟수 — 수익 %d/%d · 낙폭 %d/%d"
      % (w("pc", "rc"), N, w("pm", "rm"), N))
print("     넷 담기가   찍기를 이긴 횟수 — 수익 %d/%d · 낙폭 %d/%d"
      % (w("tc", "rc"), N, w("tm", "rm"), N))
flat = [c for r in rows for c in r["each"]]
q = np.percentile(flat, [25, 50, 75])
print("     찍기의 분포(%d개) — 하위25 %5.1f%% · 한가운데 %5.1f%% · 상위25 %5.1f%%"
      % (len(flat), q[0] * 100, q[1] * 100, q[2] * 100))
print("     창마다 꼴찌만 잡았다면 %5.1f%% · 1등만 잡았다면 %5.1f%%"
      % (med("lo") * 100, med("hi") * 100))
print("")
print("  ── 고른 전략이 실행창에서 몇 등이었나 ──")
rk = [sum(1 for r in rows if r["rank"] == k) for k in (1, 2, 3, 4)]
print("     1등 %d · 2등 %d · 3등 %d · 꼴찌 %d   (2등 이내 %d/%d)"
      % (rk[0], rk[1], rk[2], rk[3], rk[0] + rk[1], N))
print("")
print("  ── 넷이라지만 규칙은 셋 아닌가 (켈트너 하나를 빼고) ──")
print("     낙폭에서 찍기를 이긴 횟수 — 넷 %d/%d · 이더리움쪽 뺌 %d/%d · 비트코인쪽 뺌 %d/%d"
      % (w("tm", "rm"), N, w("a3m", "rm"), N, w("b3m", "rm"), N))
print("\n  구간별:")
for r in rows:
    print("   %-5s %s  고른것 %-9s -> %6.1f%% / %6.1f%%   넷 %6.1f%% / %6.1f%%  %s"
          % (r["mkt"], r["run"], r["pick"], r["pc"] * 100, r["pm"] * 100,
             r["tc"] * 100, r["tm"] * 100, "적중" if r["hit"] else ""))

# ── ③ 겹침 반증 ─────────────────────────────────────────────────────────────
print("\n" + "=" * 78)
print("③ 겹침 반증 — 위 창들은 서로 겹친다. 겹치지 않는 2년 블록으로 다시 센다")
print("=" * 78)
keep = 0
tot = 0
wc = wm = 0
for mkt in MKTS:
    _, dates = grid(mkt)
    W = 730 * BPD
    i, prev = 0, None
    while i + W <= len(dates):
        post = {n: stats(mkt, [n], i, i + W)[0] for n in NAMES}
        b = max(post, key=lambda n: post[n])
        tc, tm, dts = stats(mkt, NAMES, i, i + W)
        tag = ""
        if prev is not None:
            tot += 1
            keep += int(prev == b)
            # 대본이 견주는 상대는 '직전 블록 1등에 몰빵'이다. 그 창에서 그것도 돌린다.
            pc2, pm2, _ = stats(mkt, [prev], i, i + W)
            wc += int(tc > pc2)
            wm += int(tm > pm2)
            tag = "직전 1등 %s -> %s" % (prev, "유지" if prev == b else "탈락")
        print("  %-5s %s~%s  1등 %-9s  넷 %6.1f%% / %6.1f%%  %s"
              % (mkt, dts[0][:10], dts[-1][:10], b, tc * 100, tm * 100, tag))
        prev = b
        i += W
print("\n  직전 2년 1등이 다음 2년에도 1등이었던 경우: %d / %d" % (keep, tot))
print("  겹치지 않는 %d번에서 넷을 다 담은 쪽이 직전 1등 몰빵을 이긴 횟수 — "
      "수익 %d/%d · 낙폭 %d/%d" % (tot, wc, tot, wm, tot))
print("\n영상의 한 문장 — 나눠 담기는 수익을 늘리는 장치가 아니라, 잘못 고를 위험을 줄이는 장치입니다.")
