# -*- coding: utf-8 -*-
"""알고랩 — 「AI 자동매매 봇, 빠진 두 줄」 편 재현 코드 (2026-09-13 공개)

영상에서 말한 숫자를 **전부** 이 파일 하나가 냅니다. 시세는 data_ai2/ 에 얼려 두었으니
인터넷 없이 그대로 돌아갑니다.

    python run_ai2.py

표본 : AI 에게 "코인 자동매매 봇 짜줘"를 시키면 흔히 돌아오는 **지표 기본값** 12종.
       파라미터는 전부 그 지표의 공표된 기본값입니다 — 우리가 고른 값이 하나도 없습니다.
자산 : 이더리움 4시간봉 · 업비트 원화(KRW-ETH) + 바이낸스 USDT(ETHUSDT)
두 줄: 1) 4시간봉 종가가 '30일치 평균'(30일 x 6봉 = 180봉 단순이동평균) 위일 때만 매수 허용
          (아래면 전량 청산). 판정은 하루 한 번이 아니라 봉마다 한다.
       2) 진입 비중은 계좌의 60퍼센트(나머지는 현금).
          ⚠ 한 번 넣고 두는 게 아니라 **봉마다 그때 잔고의 60퍼센트로 되맞춘다**(pos 는 잔고 대비 비율).
          되맞춤 거래는 |Δpos| = 0 이라 수수료가 안 붙는다 — 이 공짜는 두 줄 쪽에만 붙는 가정이다.
정직 : 신호 다음 봉 체결(룩어헤드 없음) · 편도 0.1% · 헤드라인은 항상 두 시장 중 나쁜 쪽
기준일(ASOF): 2026-09-12T20:00:00 — 이 날짜를 바꾸면 영상과 다른 숫자가 나옵니다.
"""
import datetime
import os
import sys

import numpy as np
from numpy.lib.stride_tricks import sliding_window_view as swv

sys.stdout.reconfigure(encoding="utf-8", errors="replace")
HERE = os.path.dirname(os.path.abspath(__file__))
FEE, WARM, BPD, VAL = 0.001, 400, 6, "2024-01-01"      # 4시간봉 = 하루 6봉
FILT_DAYS, WEIGHT = 30, 0.6
PASS_CAGR, PASS_MDD = 0.30, -0.35
SEED = 1000.0                                           # 만원
FILES = {"업비트": os.path.join(HERE, "data_ai2", "KRW-ETH_4h.csv"),
         "바이낸스": os.path.join(HERE, "data_ai2", "ETHUSDT_4h.csv")}


def pc(x):
    return "%.1f%%" % (x * 100)


# ── 지표 수학 ───────────────────────────────────────────────────────
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 _ema(x, n):
    a = 2.0 / (n + 1)
    out = np.empty(len(x))
    out[0] = x[0]
    for i in range(1, len(x)):
        out[i] = a * x[i] + (1 - a) * out[i - 1]
    return out


def _rstd(x, n):
    out = np.full(len(x), np.nan)
    if len(x) >= n:
        out[n - 1:] = swv(x, n).std(axis=1)
    return out


def _atr(h, l, c, n):
    prev = np.r_[c[0], c[:-1]]
    tr = np.maximum(h - l, np.maximum(np.abs(h - prev), np.abs(l - prev)))
    a = np.full(len(tr), np.nan)
    if len(tr) >= n:
        a[n - 1] = tr[:n].mean()
        for i in range(n, len(tr)):
            a[i] = a[i - 1] + (tr[i] - a[i - 1]) / n
    return a


def _ffill(buy, sell, n_warm, n):
    """buy=1 / sell=0 이벤트를 상태로 전개 (이벤트 없는 봉은 직전 상태 유지)"""
    s = np.full(n, np.nan)
    s[sell] = 0.0
    s[buy] = 1.0
    s[:n_warm] = 0.0
    has = ~np.isnan(s)
    idx = np.arange(n)
    out = s[np.maximum.accumulate(np.where(has, idx, 0))]
    out[np.isnan(out)] = 0.0
    return out


def ma_cross(o, h, l, c, fast=50, slow=200):
    f, s = _sma(c, fast), _sma(c, slow)
    out = np.zeros(len(c))
    m = ~(np.isnan(f) | np.isnan(s))
    out[m] = (f[m] > s[m]).astype(float)
    return out


def macd_trend(o, h, l, c, fast=12, slow=26, sig=9):
    m = _ema(c, fast) - _ema(c, slow)
    return (m > _ema(m, sig)).astype(float)


def rsi_meanrev(o, h, l, c, n=14, lo=30, hi=70):
    d = np.diff(c, prepend=c[0])
    au = _ema(np.where(d > 0, d, 0.0), n)
    ad = _ema(np.where(d < 0, -d, 0.0), n)
    rsi = 100 - 100 / (1 + au / np.where(ad == 0, 1e-9, ad))
    return _ffill(rsi < lo, rsi > hi, n + 1, len(c))


def ema_cross(o, h, l, c, fast=12, slow=26):
    f, s = _ema(c, fast), _ema(c, slow)
    out = (f > s).astype(float)
    out[:slow] = 0.0
    return out


def boll_rev(o, h, l, c, n=20, k=2.0):
    ma, sd = _sma(c, n), _rstd(c, n)
    return _ffill((c < ma - k * sd) & ~np.isnan(sd),
                  (c > ma + k * sd) & ~np.isnan(sd), n + 1, len(c))


def boll_break(o, h, l, c, n=20, k=2.0):
    ma, sd = _sma(c, n), _rstd(c, n)
    return _ffill((c > ma + k * sd) & ~np.isnan(sd),
                  (c < ma) & ~np.isnan(ma), n + 1, len(c))


def volbreak(o, h, l, c, k=0.5):
    rng = np.r_[np.nan, h[:-1] - l[:-1]]
    sig = np.zeros(len(c))
    m = ~np.isnan(rng)
    sig[m] = (c[m] > o[m] + k * rng[m]).astype(float)
    return sig


def stoch(o, h, l, c, n=14, d=3, lo=20, hi=80):
    hh = np.full(len(c), np.nan)
    ll = np.full(len(c), np.nan)
    if len(c) >= n:
        hh[n - 1:] = swv(h, n).max(axis=1)
        ll[n - 1:] = swv(l, n).min(axis=1)
    rng = hh - ll
    k = 100 * (c - ll) / np.where((rng == 0) | np.isnan(rng), np.nan, rng)
    kd = _sma(np.where(np.isnan(k), 50.0, k), d)
    kd[:n + d] = 50.0
    return _ffill(kd < lo, kd > hi, n + d, len(c))


def donchian(o, h, l, c, n=20):
    hh = np.full(len(c), np.nan)
    ll = np.full(len(c), np.nan)
    if len(c) > n:
        hh[n:] = swv(h, n).max(axis=1)[:-1]
        ll[n:] = swv(l, n).min(axis=1)[:-1]
    return _ffill(c > hh, c < ll, n + 1, len(c))


def cci(o, h, l, c, n=20, lo=-100, hi=100):
    tp = (h + l + c) / 3
    ma = _sma(tp, n)
    md = np.full(len(c), np.nan)
    if len(c) >= n:
        md[n - 1:] = np.abs(swv(tp, n) - ma[n - 1:, None]).mean(axis=1)
    v = (tp - ma) / np.where((md == 0) | np.isnan(md), np.nan, 0.015 * md)
    return _ffill(np.where(np.isnan(v), False, v < lo),
                  np.where(np.isnan(v), False, v > hi), n + 1, len(c))


def supertrend(o, h, l, c, n=10, mult=3.0):
    a = _atr(h, l, c, n)
    hl2 = (h + l) / 2
    up, dn = hl2 - mult * a, hl2 + mult * a
    d = np.ones(len(c))
    for i in range(1, len(c)):
        if np.isnan(a[i]):
            continue
        if c[i - 1] > up[i - 1]:
            up[i] = max(up[i], up[i - 1])
        if c[i - 1] < dn[i - 1]:
            dn[i] = min(dn[i], dn[i - 1])
        d[i] = 1 if c[i] > dn[i - 1] else (-1 if c[i] < up[i - 1] else d[i - 1])
    return (d > 0).astype(float)


AI12 = [("RSI 14 · 30/70 반등", rsi_meanrev, {}, "평균회귀"),
        ("골든크로스 5/20", ma_cross, {"fast": 5, "slow": 20}, "추세"),
        ("골든크로스 50/200", ma_cross, {"fast": 50, "slow": 200}, "추세"),
        ("MACD 12/26/9", macd_trend, {}, "추세"),
        ("EMA 12/26 크로스", ema_cross, {}, "추세"),
        ("볼린저 20·2 역추세", boll_rev, {}, "평균회귀"),
        ("볼린저 20·2 돌파", boll_break, {}, "추세"),
        ("변동성 돌파 k=0.5", volbreak, {}, "돌파"),
        ("스토캐스틱 14/3", stoch, {}, "평균회귀"),
        ("돈치안 20 돌파", donchian, {}, "돌파"),
        ("CCI 20 ±100", cci, {}, "평균회귀"),
        ("슈퍼트렌드 10/3", supertrend, {}, "추세")]


# ── 데이터·엔진 ─────────────────────────────────────────────────────
def load(path):
    d, cols = [], [[], [], [], []]
    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])
            for i in range(4):
                cols[i].append(float(p[i + 1]))
    return d, np.asarray(cols[0]), np.asarray(cols[1]), np.asarray(cols[2]), np.asarray(cols[3])


def series(path, start):
    d, o, h, l, c = load(path)
    i0 = int(np.searchsorted(np.asarray(d), start))
    d, o, h, l, c = d[i0:], o[i0:], h[i0:], l[i0:], c[i0:]
    n = len(c)
    ret = np.zeros(n)
    ret[1:] = c[1:] / c[:-1] - 1
    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 / yrs, warm=WARM, yrs=yrs)


def grow(S, sig, w=1.0):
    pos = np.r_[0.0, sig[:-1]] * w
    return pos, (1 + pos * S["ret"]) * (1 - FEE * np.abs(np.diff(np.r_[0.0, pos])))


def stats(S, g):
    eq = np.cumprod(g[S["warm"]:S["n"]])
    yrs = len(eq) / S["ppy"]
    return dict(cagr=float(eq[-1] ** (1 / yrs) - 1) if eq[-1] > 0 else -1.0,
                mdd=float((eq / np.maximum.accumulate(eq) - 1).min()))


def filt(S, days):
    sma = _sma(S["c"], days * BPD)
    f = np.zeros(S["n"])
    m = ~np.isnan(sma)
    f[m] = (S["c"][m] > sma[m]).astype(float)
    return f


def path_money(S, sig, w):
    pos, g = grow(S, sig, w=w)
    eq = SEED * np.cumprod(g[S["warm"]:S["n"]])
    d = np.asarray(S["d"][S["warm"]:S["n"]])
    peak = np.maximum.accumulate(eq)
    dd = eq / peak - 1
    i = int(np.argmin(dd))
    j = int(np.argmax(peak[:i + 1] == peak[i]))
    return dict(최종=float(eq[-1]), 고점=float(peak[i]), 바닥=float(eq[i]), 낙폭=float(dd[i]),
                고점날=d[j][:10], 바닥날=d[i][:10], 평균비중=float(pos[S["warm"]:].mean()))


def main():
    first = {}
    for k, v in FILES.items():
        with open(v, encoding="utf-8") as f:
            next(f)
            first[k] = f.readline().split(",")[0][:10]
    start = max(first.values())
    S = {k: series(v, start) for k, v in FILES.items()}
    A = S["바이낸스"]
    print("■ 이더리움 4시간봉 · %s ~ %s (%.2f년) · 편도 %g%% · 다음 봉 체결 · 워밍업 %d봉"
          % (start, A["d"][-1][:10], A["yrs"], FEE * 100, WARM))
    print("  잣대: 두 시장 나쁜 쪽 기준 연 %g%% 이상 · 최대낙폭 %g%% 이내\n"
          % (PASS_CAGR * 100, PASS_MDD * 100))

    print("■ AI 기본값 봇 12종 — 그대로 / 두 줄을 넣고  (연수익 / 최대낙폭, 두 시장 나쁜 쪽)")
    n_raw = n_two = 0
    raw_mdds = []
    for name, fn, prm, fam in AI12:
        r = {}
        for tag, w, use in (("그대로", 1.0, False), ("두줄", WEIGHT, True)):
            cs, ms = [], []
            for mk in ("업비트", "바이낸스"):
                s = S[mk]
                sg = np.clip(np.nan_to_num(fn(s["o"], s["h"], s["l"], s["c"], **prm)), 0, 1)
                if use:
                    sg = sg * filt(s, FILT_DAYS)
                st = stats(s, grow(s, sg, w=w)[1])
                cs.append(st["cagr"])
                ms.append(st["mdd"])
            r[tag] = (min(cs), min(ms))
        ok_raw = r["그대로"][0] >= PASS_CAGR and r["그대로"][1] >= PASS_MDD
        ok_two = r["두줄"][0] >= PASS_CAGR and r["두줄"][1] >= PASS_MDD
        n_raw += ok_raw
        n_two += ok_two
        raw_mdds.append(r["그대로"][1])
        print("  %-20s(%-4s) %7s/%7s  →  %7s/%7s  %s"
              % (name, fam, pc(r["그대로"][0]), pc(r["그대로"][1]),
                 pc(r["두줄"][0]), pc(r["두줄"][1]), "★통과" if ok_two else ""))
    print("\n  ▸ 잣대 통과: 그대로 %d/12  →  두 줄 %d/12" % (n_raw, n_two))
    print("  ▸ 그대로 12종의 최대낙폭 — 가장 얕은 것도 %s. 12종 전부 반 토막 넘게 꺼졌다"
          % pc(max(raw_mdds)))

    # 대표 조합 — 골든크로스 5/20, 나쁜 쪽(바이낸스) 계좌의 실제 금액 경로
    sg = np.clip(np.nan_to_num(ma_cross(A["o"], A["h"], A["l"], A["c"], 5, 20)), 0, 1)
    f = filt(A, FILT_DAYS)
    two = path_money(A, sg * f, WEIGHT)
    raw = path_money(A, sg, 1.0)
    hold = path_money(A, np.ones(A["n"]), 1.0)
    match = path_money(A, np.ones(A["n"]), two["평균비중"])
    # ── 기준선: 지표를 아예 지우고 두 줄만 남기면? ──────────────────────
    print("")
    print("■ 기준선 — 지표를 지우고 '두 줄'만 남긴 계좌 (30일치 평균선 위에서만 · 비중 %.0f%%)"
          % (WEIGHT * 100))
    cs, ms = [], []
    for mk in ("업비트", "바이낸스"):
        s2 = S[mk]
        st2 = stats(s2, grow(s2, filt(s2, FILT_DAYS), w=WEIGHT)[1])
        cs.append(st2["cagr"]); ms.append(st2["mdd"])
    base_c, base_m = min(cs), min(ms)
    print("  두 줄만 (지표 없음)   %7s / %7s   %s"
          % (pc(base_c), pc(base_m), "*잣대 통과" if (base_c >= PASS_CAGR and base_m >= PASS_MDD) else "탈락"))
    win_c = win_m = 0
    for name, fn, prm, fam in AI12:
        cs2, ms2 = [], []
        for mk in ("업비트", "바이낸스"):
            s2 = S[mk]
            sg2 = np.clip(np.nan_to_num(fn(s2["o"], s2["h"], s2["l"], s2["c"], **prm)), 0, 1) * filt(s2, FILT_DAYS)
            st2 = stats(s2, grow(s2, sg2, w=WEIGHT)[1])
            cs2.append(st2["cagr"]); ms2.append(st2["mdd"])
        if min(cs2) >= PASS_CAGR and min(ms2) >= PASS_MDD:
            win_c += min(cs2) > base_c
            win_m += min(ms2) > base_m
    print("  * 잣대를 넘은 봇 여섯 가운데 이 기준선보다 더 번 것 %d개 · 덜 꺼진 것 %d개" % (win_c, win_m))
    print("  * 즉 0개 -> 6개는 '봇이 고쳐졌다'가 아니다. 두 줄만으로도 잣대를 넘고,")
    print("    지표가 더한 건 돈이 아니라 조금 더 얕은 낙폭이며 그것도 절반뿐이다.")

    print("\n■ 1,000만원 계좌의 실제 경로 — 골든크로스 5/20 · 바이낸스(나쁜 쪽)")
    rows = (("그냥 보유", hold), ("AI 봇 그대로", raw), ("두 줄을 넣고", two),
            ("같은 노출 고정비중(%.2f)" % two["평균비중"], match))
    for tag, p in rows:
        print("  %-24s 최종 %9s만원 · 고점 %9s(%s) → 바닥 %8s(%s)  낙폭 %s"
              % (tag, format(p["최종"], ",.0f"), format(p["고점"], ",.0f"), p["고점날"],
                 format(p["바닥"], ",.0f"), p["바닥날"], pc(p["낙폭"])))
    print("\n  ▸ 고점을 1억으로 놓고 옮기면 — 그대로 %s만원 / 두 줄 %s만원  (영상의 3,200 대 7,600)"
          % (format(1e4 * (1 + raw["낙폭"]), ",.0f"), format(1e4 * (1 + two["낙폭"]), ",.0f")))
    print("  ▸ 돈은 두 줄이 덜 번다: 그냥 보유 %s > AI 봇 그대로 %s > 두 줄 %s (만원)"
          % (format(hold["최종"], ",.0f"), format(raw["최종"], ",.0f"), format(two["최종"], ",.0f")))
    print("  ▸ '그냥 덜 넣어서'가 아니다 — 같은 노출 고정비중과 견주면 돈 %.2f배 · "
          "낙폭도 두 줄이 %s 대 %s 로 얕다"
          % (two["최종"] / match["최종"], pc(two["낙폭"]), pc(match["낙폭"])))

    # 다만 그건 이 조합의 이야기다. 12종 전부에 같은 견줌을 붙여 본다.
    print("")
    print("■ 자가반박 전수 — 12종 각각을 '같은 노출 고정비중' 계좌와 견주면 (낙폭만)")
    win = 0
    for name, fn, prm, fam in AI12:
        tm, mm = [], []
        for mk in ("업비트", "바이낸스"):
            s2 = S[mk]
            sg2 = np.clip(np.nan_to_num(fn(s2["o"], s2["h"], s2["l"], s2["c"], **prm)), 0, 1)
            sg2 = sg2 * filt(s2, FILT_DAYS)
            pos2, gg = grow(s2, sg2, w=WEIGHT)
            tm.append(stats(s2, gg)["mdd"])
            ex2 = float(pos2[s2["warm"]:].mean())
            mm.append(stats(s2, grow(s2, np.ones(s2["n"]), w=ex2)[1])["mdd"])
        t_, m_ = min(tm), min(mm)
        ok = t_ > m_
        win += ok
        print("  %-20s 두 줄 %7s  ·  같은 노출 %7s   %s"
              % (name, pc(t_), pc(m_), "두 줄이 얕다" if ok else "고정비중이 얕다"))
    print("  * 두 줄이 더 얕은 것은 %d/12 뿐이다. 격자 전체를 두고 "
          "'두 줄이 낙폭을 줄인다'고 말하면 안 된다." % win)

    # 2022년 — 첫 줄이 한 일
    d = np.asarray(A["d"])
    a = int(np.searchsorted(d, "2022-04-11"))
    b = int(np.searchsorted(d, "2022-07-07"))
    days = (datetime.date.fromisoformat(d[b][:10]) - datetime.date.fromisoformat(d[a][:10])).days
    pos2, g2 = grow(A, sg * f, w=WEIGHT)
    _, graw = grow(A, sg, w=1.0)
    print("\n■ 2022-04-11 ~ 2022-07-07 (%d일) — 이더리움이 30일 평균 아래에 있던 구간" % days)
    print("  * 먼저 판 것은 첫 줄이 아니다 — 2022-04-11 00:00 봉에서 지표(골든크로스)가 먼저 꺼졌고,")
    print("    30일 평균이 깨진 것은 8시간 뒤 08:00 봉이다.")
    print("    첫 줄이 한 일은 파는 게 아니라 '안 돌아오는 것'이다 — 그 %d봉 동안 지표 매수 신호가 "
          "%d봉 켜졌는데 실제로 산 봉은 %d봉이다."
          % (b - a + 1, int(sg[a:b + 1].sum()), int((sg * f)[a:b + 1].sum())))
    print("  이더리움 가격    %s → %s  (%+.1f%%)"
          % (format(A["c"][a], ",.0f"), format(A["c"][b], ",.0f"), (A["c"][b] / A["c"][a] - 1) * 100))
    print("  그냥 보유        1,000만원 → %s만원" % format(1000 * A["c"][b] / A["c"][a], ",.0f"))
    print("  AI 봇 그대로     1,000만원 → %s만원" % format(1000 * np.prod(graw[a:b + 1]), ",.0f"))
    print("  두 줄을 넣고     1,000만원 → %s만원  (그 구간 평균 보유 비중 %.2f — 한 주도 사지 않았다)"
          % (format(1000 * np.prod(g2[a:b + 1]), ",.0f"), pos2[a:b + 1].mean()))

    print("\n■ 한계 — 영상에서 말한 그대로")
    print("  · 12종 전부 매수만 한다(롱 온리). 내리는 쪽에 거는 기능은 없다")
    print("  · 지금 두 시장에 상장돼 있는 이더리움 하나만 봤다(폐지 종목 미포함)")
    print("  · 두 줄을 넣어도 12종 중 6종은 여전히 잣대를 못 넘는다")
    print("  · 개발에 쓰지 않은 15개 코인에 같은 두 줄을 걸면 180조합 중 잣대를 넘은 건"
          " 1조합(SAND · MACD 12/26/9 · 연 30.4% / -34.2%)뿐이었다 (이 파일은 이더리움만 재현한다)")
    print("  · 이 구간은 이더리움이 큰 추세를 두 번 낸 6년 8개월이다. 앞으로도 같다는 보장은 없다")


if __name__ == "__main__":
    main()
