# -*- coding: utf-8 -*-
"""알고랩 — 코인 봇 매수, 나눌까 한 번에 넣을까 (2026-09-21 공개)

영상이 말한 **비트코인 숫자는 전부** 이 파일이 냅니다. 시세는 data_asof/ 에 얼려 두었으니
인터넷 없이 그대로 돌아갑니다.

⚠ 이 묶음 밖에 있는 것이 하나 있습니다 — 영상 한계 챕터의 「이더리움과 알트코인 여섯 종
  가운데 두 종에서는 물타기가 이긴 경우가 있었습니다」는 **여기 없는 시세**(ETH·알트 6종 ×
  두 거래소)로 센 값이라 이 파일이 재현하지 못합니다. 세는 규칙과 자리 수는 재현 페이지
  https://algolab.co.kr/split 의 '한계' 절에 적어 두었습니다.

    python run_split.py

봇 설정 : 압축 돌파 — 비트코인 4시간봉 · 최근 20봉 고가를 종가로 뚫으면 사고,
          50봉 지수이동평균 아래로 닫히면 판다 · 직전 20봉 밴드폭이 최근 252봉 하위 30%
          구간일 때(=압축)나 그 직후 5봉 안에만 진입 · 일봉 50일 이동평균 위에서만
          · 편도 수수료 0.1%
넣는 방법: 한 번에(비중 k) / 나눠 사기(물타기) / 오를 때 더 사기(불타기) / 시간분할
기준일(ASOF): 2026-09-20T20:00:00 — 이 날짜를 바꾸면 영상과 다른 숫자가 나옵니다.

⚠ 헤드라인은 **업비트와 바이낸스 중 나쁜 쪽**입니다(사전 등록 규약). 금액을 두 시장에서
  견주려면 창이 같아야 하므로 공통 시작 2019-09-01 로 자르고, 워밍업 400봉을 뺀
  2019-11-06 부터를 성적 구간으로 씁니다.
"""
import os
import sys

import numpy as np

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
HERE = os.path.dirname(os.path.abspath(__file__))

ASOF = "2026-09-20T20:00:00"
FEE = 0.001                 # 편도 0.1% — 비중이 움직인 만큼 물린다
WARM = 400                  # 워밍업 봉 수(지표가 자리 잡기 전은 성적에서 뺀다)
BARS_PER_DAY = 6            # 4시간봉
SEED = 1000.0               # 만원 — 1,000만원
Q, BRK_N, EXIT_N, BB_N, WIN = 0.3, 20, 50, 20, 252
FILT_DAYS = 50              # 일봉 50일 이동평균

FILES = {"업비트": os.path.join(HERE, "data_asof", "KRW-BTC_4h.csv"),
         "바이낸스": os.path.join(HERE, "data_asof", "BTCUSDT_4h.csv")}
# 긴 창 — 업비트만 2017년부터. 바이낸스는 2017년 자료가 없어서 헤드라인에는 못 쓴다.
LONG_FILE = os.path.join(HERE, "data_asof", "KRW-BTC_4h_long.csv")


# ────────────────────────────────────────────────────────── 시세
def load(path):
    d, o, h, l, c = [], [], [], [], []
    with open(path, encoding="utf-8") as f:
        next(f)
        for line in f:
            p = line.rstrip("\n").split(",")
            if len(p) < 5:
                continue
            d.append(p[0]); o.append(float(p[1])); h.append(float(p[2]))
            l.append(float(p[3])); c.append(float(p[4]))
    return d, np.array(o), np.array(h), np.array(l), np.array(c)


def series(path):
    d, o, h, l, c = load(path)
    n = len(c)
    ret = np.zeros(n)
    ret[1:] = c[1:] / c[:-1] - 1
    import datetime
    yrs = (datetime.date.fromisoformat(d[-1][:10])
           - datetime.date.fromisoformat(d[0][:10])).days / 365.25
    return dict(d=d, o=o, h=h, l=l, c=c, n=n, ret=ret, ppy=n / max(yrs, 1e-9))


# ────────────────────────────────────────────────────────── 지표
def ema(c, n):
    out = np.empty_like(c, dtype=float)
    k = 2 / (n + 1)
    out[0] = c[0]
    for i in range(1, len(c)):
        out[i] = c[i] * k + out[i - 1] * (1 - k)
    return out


def sma(x, n):
    out = np.full(len(x), np.nan)
    if len(x) >= n:
        cs = np.cumsum(np.insert(x, 0, 0.0))
        out[n - 1:] = (cs[n:] - cs[:-n]) / n
    return out


def squeeze_break(o, h, l, c):
    """압축 돌파. 밴드폭(20봉 표준편차/평균)이 최근 252봉 하위 30% 면 '압축'.
    압축 중이거나 직후 5봉 안에 직전 20봉 고가를 종가로 뚫으면 매수. 청산은 50봉 EMA 아래."""
    N = len(c)
    e = ema(c, EXIT_N)
    width = np.full(N, np.nan)
    for t in range(BB_N, N):
        seg = c[t - BB_N + 1:t + 1]
        width[t] = seg.std() / max(seg.mean(), 1e-12)
    sig = np.zeros(N)
    pos, armed = 0.0, -10 ** 9
    for t in range(BB_N + WIN, N):
        if width[t] <= np.nanquantile(width[t - WIN + 1:t + 1], Q):
            armed = t
        if pos == 0.0:
            if t - armed <= 5 and c[t] > h[t - BRK_N:t].max():
                pos = 1.0
        elif c[t] < e[t]:
            pos = 0.0
        sig[t] = pos
    return sig


def make_sig(S):
    base = squeeze_break(S["o"], S["h"], S["l"], S["c"])
    m_ = sma(S["c"], FILT_DAYS * BARS_PER_DAY)
    f = np.zeros(S["n"])
    ok = ~np.isnan(m_)
    f[ok] = (S["c"][ok] > m_[ok]).astype(float)
    return (base * f > 0).astype(float)


# ────────────────────────────────────────────────────────── 넣는 방법
def build_path(S, sig, kind, f0=0.5, add=0.3, step=0.05, h=3):
    """0/1 신호를 비중 경로로. 청산은 신호 그대로(전량). 되팔지 않는다(래칫).
    S 한 번에 / U 오를 때 더 / D 내릴 때 더(나눠 사기) / T 시간분할."""
    c = S["c"]
    inpos = sig > 0
    w = np.zeros(S["n"])
    ref, entry_i, cur = 0.0, -1, 0.0
    for i in range(S["n"]):
        if not inpos[i]:
            cur, entry_i, w[i] = 0.0, -1, 0.0
            continue
        if entry_i < 0:
            entry_i, ref = i, c[i]
            cur = 1.0 if kind == "S" else f0
        else:
            if kind == "S":
                tgt = 1.0
            elif kind == "U":
                tgt = f0 + add * np.floor(max(0.0, c[i] / ref - 1.0) / step)
            elif kind == "D":
                tgt = f0 + add * np.floor(max(0.0, 1.0 - c[i] / ref) / step)
            elif kind == "T":
                tgt = f0 + add * np.floor((i - entry_i) / float(h))
            else:
                raise ValueError(kind)
            cur = max(cur, min(1.0, tgt))
        w[i] = cur
    return w


def metrics(S, w):
    pos = np.r_[0.0, w[:-1]]
    g = (1 + pos * S["ret"]) * (1 - FEE * np.abs(np.diff(np.r_[0.0, pos])))
    seg = g[WARM:S["n"]]
    eq = np.cumprod(seg)
    yrs = len(seg) / S["ppy"]
    cagr = eq[-1] ** (1 / yrs) - 1 if eq[-1] > 0 else -1.0
    mdd = float((eq / np.maximum.accumulate(eq) - 1).min())
    # 오간 금액(회전) — 비용을 무는 자리다. 매매 '횟수'가 아니라 |비중 변화| 의 합이다.
    dpos = np.abs(np.diff(np.r_[0.0, pos]))[WARM:S["n"]]
    # 진입 횟수 — 비중이 0 에서 올라온 지점
    up = (pos[WARM:S["n"]] > 0) & (np.r_[0.0, pos[WARM:S["n"]][:-1]] == 0)
    return dict(mult=float(eq[-1]), cagr=float(cagr), mdd=mdd,
                expo=float(np.mean(pos[WARM:S["n"]])),
                turn=float(dpos.sum()), entries=int(up.sum()),
                calmar=float(cagr / abs(mdd)) if mdd < -1e-9 else 0.0)


def expo_matched(S, sig, target):
    """같은 평균 노출을 고정비중으로 복제한 계좌 — '덜 넣어서'인지 가르는 기준선."""
    base = float(np.mean(np.r_[0.0, sig[:-1]][WARM:S["n"]]))
    return metrics(S, sig * min(1.0, target / base)) if base > 1e-9 else None


def 금액(mult):
    v = int(round(SEED * mult))
    if v >= 10000:
        억, 나머지 = divmod(v, 10000)
        return "{:d}억 {:,d}만원".format(억, 나머지) if 나머지 else "{:d}억원".format(억)
    return "{:,d}만원".format(v)


def 바닥(mdd):
    """⚠ 최대낙폭을 원금에 곱한 **환산값**이다. 계좌가 실제로 그 금액이었던 적은 없다
    (낙폭은 그때까지의 고점에서 잰다). 영상은 이 환산값을 말하지 않는다 — 화면과
    이 출력에서만 참고로 보여 준다."""
    return "{:,d}만원".format(int(round(SEED * (1.0 + mdd))))


# ────────────────────────────────────────────────────────── 실행
R = {}
print("=" * 78)
print(" 코인 봇 매수, 나눌까 한 번에 넣을까 — 재현 (ASOF %s)" % ASOF)
print("=" * 78)
for market, path in FILES.items():
    S = series(path)
    sig = make_sig(S)
    R[market] = dict(S=S, sig=sig, m={})
    R[market]["m"]["그냥 보유"] = metrics(S, np.ones(S["n"]))
    for k in (1.0, 0.9, 0.8, 0.7):
        R[market]["m"]["한 번에 %.0f%%" % (k * 100)] = metrics(S, sig * k)
    for kind, name in (("D", "나눠 사기"), ("U", "오를 때 더"), ("T", "시간분할")):
        kw = dict(f0=0.5, add=0.3, step=0.05) if kind in ("U", "D") else dict(f0=0.5, add=0.3, h=3)
        w = build_path(S, sig, kind, **kw)
        mm = metrics(S, w)
        # 진입마다 비중이 어디까지 올라갔나 — 이 편의 중심 숫자
        진입, 더넣음 = 0, 0
        i, prev, peak = 0, 0.0, 0.0
        for v in w:
            if v > 0 and prev == 0:
                진입 += 1; peak = v
            elif v > 0:
                peak = max(peak, v)
            elif v == 0 and prev > 0:
                더넣음 += 1 if peak > 0.501 else 0
                peak = 0.0
            prev = v
        if prev > 0 and peak > 0.501:
            더넣음 += 1
        mm["진입"], mm["더넣음"] = 진입, 더넣음
        # 규칙은 **종가** 로 판정한다. 봉 안 저가로는 몇 번이나 그 선을 넘었는지도 같이 센다
        # — 지정가를 미리 걸어 뒀다면 그만큼 증량이 걸렸을 것이다(영상 한계 문장).
        저가넘음, 보유 = 0, []
        i2 = 0
        while i2 < S["n"]:
            if w[i2] > 0:
                j2 = i2
                while j2 < S["n"] and w[j2] > 0:
                    j2 += 1
                if float(1 - S["l"][i2:j2].min() / S["c"][i2]) >= 0.05:
                    저가넘음 += 1
                보유.append((j2 - i2) / float(BARS_PER_DAY))
                i2 = j2
            else:
                i2 += 1
        mm["저가넘음"] = 저가넘음
        보유.sort()
        mm["보유중앙일"] = 보유[len(보유) // 2] if 보유 else 0.0
        mm["보유최장일"] = 보유[-1] if 보유 else 0.0
        mm["동노출"] = expo_matched(S, sig, mm["expo"])
        R[market]["m"][name] = mm
    print("\n── %s · %s ~ %s (%.1f년 · %d봉)"
          % (market, S["d"][WARM][:10], S["d"][-1][:10],
             (S["n"] - WARM) / S["ppy"], S["n"] - WARM))
    for k, v in R[market]["m"].items():
        x = ""
        if "진입" in v:
            x = (" · 진입 %d번 중 더 넣은 건 %d번(종가 기준) · 봉 안 저가로는 %d번"
                 " · 보유 중앙 %.1f일 · 최장 %.1f일 · 같은노출 기준선 CAGR %.1f%% MDD %.1f%%") % (
                v["진입"], v["더넣음"], v["저가넘음"], v["보유중앙일"], v["보유최장일"],
                v["동노출"]["cagr"] * 100, v["동노출"]["mdd"] * 100)
        print("   %-12s %12s · 연 %5.1f%% · 최대낙폭 고점에서 %6.1f%% (원금 환산 %8s) · 평균 비중 %.3f%s"
              % (k, 금액(v["mult"]), v["cagr"] * 100, v["mdd"] * 100, 바닥(v["mdd"]), v["expo"], x))

print("\n" + "=" * 78)
print(" 영상이 말한 숫자 — 두 시장 중 나쁜 쪽")
print("=" * 78)
keys = list(R["업비트"]["m"].keys())
요약 = {}
for k in keys:
    mult = min(R[m]["m"][k]["mult"] for m in R)
    cagr = min(R[m]["m"][k]["cagr"] for m in R)
    mdd = min(R[m]["m"][k]["mdd"] for m in R)
    요약[k] = (mult, cagr, mdd)
    print("   %-12s %12s · 연 %5.1f%% · 최대낙폭 고점에서 %6.1f%% (원금 환산 %8s) %s"
          % (k, 금액(mult), cagr * 100, mdd * 100, 바닥(mdd),
             "· 잣대 통과" if (cagr >= 0.30 and mdd >= -0.35) else ""))

# ── 긴 창 — 창을 바꾸면 무엇이 뒤집히나 ──────────────────────────────────────
#    영상의 한계 문장이 여기서 나온다. 헤드라인(공통 창·두 시장 나쁜 쪽)에서는
#    '오를 때 더'가 비중 80%보다 조금 더 벌고 조금 덜 내려갔다. 창을 늘리면
#    **금액 우위는 남고 낙폭 우위가 뒤집힌다.**
print("\n" + "=" * 78)
print(" 창을 2017년까지 늘리면 — 업비트만(바이낸스는 2017년 자료가 없다)")
print("=" * 78)
SL = series(LONG_FILE)
sigL = make_sig(SL)
긴창 = {}
for k in (1.0, 0.8):
    긴창["한 번에 %.0f%%" % (k * 100)] = metrics(SL, sigL * k)
for kind, name in (("D", "나눠 사기"), ("U", "오를 때 더")):
    긴창[name] = metrics(SL, build_path(SL, sigL, kind))
print("   %s ~ %s (%.1f년 · %d봉)"
      % (SL["d"][WARM][:10], SL["d"][-1][:10],
         (SL["n"] - WARM) / SL["ppy"], SL["n"] - WARM))
for k, v in 긴창.items():
    print("   %-12s %12s · 연 %5.1f%% · 최대낙폭 고점에서 %6.1f%% (원금 환산 %8s)"
          % (k, 금액(v["mult"]), v["cagr"] * 100, v["mdd"] * 100, 바닥(v["mdd"])))
print("   → 오를 때 더 사기는 %s 더 벌었지만, 낙폭은 %.1f%% 대 %.1f%% 로 비중 80%% 가 얕다."
      % ("여전히" if 긴창["오를 때 더"]["mult"] > 긴창["한 번에 80%"]["mult"] else "이제 못",
         긴창["오를 때 더"]["mdd"] * 100, 긴창["한 번에 80%"]["mdd"] * 100))

# ── 간격을 바꾸면 — 무동작 칸을 강건성으로 세지 않기 위해 전부 찍는다 ────────────
#    사전 등록 격자의 간격은 5% 와 10% 둘뿐인데, **10% 칸은 6.9년 동안 증량이 한 번도
#    안 걸린다**(고정비중 기준선과 소수점까지 같다). 격자 밖 3% 도 같이 찍어 둔다.
print("\n" + "=" * 78)
print(" 얼마나 내려가면 더 넣을지(간격)를 바꾸면")
print("=" * 78)
격자 = {}
for market in FILES:
    S, sig = R[market]["S"], R[market]["sig"]
    for f0 in (0.5, 0.4):
        for step in (0.03, 0.05, 0.10):
            w = build_path(S, sig, "D", f0=f0, add=0.3, step=step)
            mm = metrics(S, w)
            더넣음, prev, peak = 0, 0.0, 0.0
            for v in w:
                if v > 0 and prev == 0:
                    peak = v
                elif v > 0:
                    peak = max(peak, v)
                elif v == 0 and prev > 0:
                    더넣음 += 1 if peak > f0 + 1e-9 else 0
                    peak = 0.0
                prev = v
            if prev > 0 and peak > f0 + 1e-9:
                더넣음 += 1
            em = expo_matched(S, sig, mm["expo"])
            격자[(market, f0, step)] = (더넣음, mm, em)
            print("   %-5s 첫 %.0f%% · %2.0f%%마다 내려갈 때마다 추가 · 더 넣은 거래 %2d번"
                  " · 연 %5.2f%% 대 같은노출 기준선 %5.2f%% · 낙폭 %6.2f%% 대 %6.2f%% · 물타기 승? %s"
                  % (market, f0 * 100, step * 100, 더넣음,
                     mm["cagr"] * 100, em["cagr"] * 100, mm["mdd"] * 100, em["mdd"] * 100,
                     "예" if (mm["cagr"] > em["cagr"] and mm["mdd"] > em["mdd"]) else "아니오"))
print("   → 10%마다 칸은 더 넣은 거래가 0번이라 기준선과 **같은 계좌**다."
      " 좁혀도(3%) 결론은 안 바뀐다.")

# ── 비중 80% 가 낙폭을 얼마나 깎나 — 대본이 '오분의 일 가까이'라고 말하는 값
깎임 = {m: 1 - abs(R[m]["m"]["한 번에 80%"]["mdd"]) / abs(R[m]["m"]["한 번에 100%"]["mdd"])
        for m in R}
print("\n   비중 80%의 낙폭 감소율 — " + " · ".join(
    "%s %.1f%%" % (m, v * 100) for m, v in 깎임.items())
    + "  (대본: '오분의 일 가까이')")

print("""
─ 영상 문장과 대조 ─
  「한 번에 넣은 계좌가 %s 이 됐습니다」        → 한 번에 100%%
  「물타기로 나눠 산 계좌는 %s 이 됐습니다」       → 나눠 사기(물타기)
  「비트코인을 %d번 샀습니다」                        → 나눠 사기 · 진입 수(바이낸스)
  「남겨 둔 절반에 손댄 거래는, 봉이 닫힌 가격으로 세면 한 번」 → 종가 기준 증량 거래 %d번
  「봉 안에서 잠깐 더 살 자리까지 내려갔던 거래를 세도 7번」    → 봉 안 저가 기준 %d번
  「보통 %.0f일 만에 정리합니다 · 가장 오래 %.0f일」      → 보유 기간 중앙·최장
  「낙폭도 거의 같고, 번 돈도 거의 같았습니다」        → 나눠 사기 대 같은노출 기준선
  「돈의 80퍼센트만 넣으면 계좌는 %s 이 됐습니다」   → 한 번에 80%%
  「내려가는 폭이 오분의 일 가까이 줄어듭니다」        → %.1f%% 감소(-%.1f%% → -%.1f%%)
  「가장 많이 번 건 다 넣은 계좌였습니다」              → 한 번에 100%% %s
  「오를 때 더 산 계좌도 평균 투입은 한 번에 넣는 계좌보다 적었습니다」
                                                      → 평균 비중 %.3f 대 %.3f (바이낸스)
  「그런데도 같은 만큼 넣은 계좌보다 더 벌었고, 대신 더 깊이 내려갔습니다」
                                                      → 오를 때 더 대 같은노출 기준선
  「제가 권하는 80퍼센트보다도 조금 더 벌고 조금 덜 내려갔습니다」
                                                      → %s 대 %s · %.1f%% 대 %.1f%%
  「업비트 자료로 2017년까지 늘려 보면, 그 낙폭 우위가 뒤집힙니다」
                                                      → 긴 창 %.1f%% 대 %.1f%%
  「사고판 횟수는 두 쪽이 같은데, 오간 금액은 한 번에 넣는 쪽이 두 배입니다」
                                                      → 진입 %d번 대 %d번 · 오간 금액 %.1f 대 %.1f
  「간격을 좁혀도 넓혀도 결론은 같았습니다 / 넓히면 더 넣을 일이 아예 없었습니다」
                                                      → 바로 위 간격 표(10%%마다 = 더 넣은 거래 0번)
  ⚠ 가장 많이 번 건 비중을 안 줄인 계좌입니다.
    비중은 더 버는 값이 아니라 **견딜 낙폭을 사는 값**입니다.""" % (
    금액(요약["한 번에 100%"][0]), 금액(요약["나눠 사기"][0]),
    R["바이낸스"]["m"]["나눠 사기"]["진입"], R["바이낸스"]["m"]["나눠 사기"]["더넣음"],
    R["바이낸스"]["m"]["나눠 사기"]["저가넘음"],
    R["바이낸스"]["m"]["나눠 사기"]["보유중앙일"], R["바이낸스"]["m"]["나눠 사기"]["보유최장일"],
    금액(요약["한 번에 80%"][0]),
    깎임["바이낸스"] * 100, abs(R["바이낸스"]["m"]["한 번에 100%"]["mdd"]) * 100,
    abs(R["바이낸스"]["m"]["한 번에 80%"]["mdd"]) * 100,
    금액(요약["한 번에 100%"][0]),
    R["바이낸스"]["m"]["오를 때 더"]["expo"], R["바이낸스"]["m"]["한 번에 100%"]["expo"],
    금액(요약["오를 때 더"][0]), 금액(요약["한 번에 80%"][0]),
    요약["오를 때 더"][2] * 100, 요약["한 번에 80%"][2] * 100,
    긴창["오를 때 더"]["mdd"] * 100, 긴창["한 번에 80%"]["mdd"] * 100,
    R["바이낸스"]["m"]["나눠 사기"]["entries"], R["바이낸스"]["m"]["한 번에 100%"]["entries"],
    R["바이낸스"]["m"]["나눠 사기"]["turn"], R["바이낸스"]["m"]["한 번에 100%"]["turn"]))
