# -*- coding: utf-8 -*-
"""AlgoLab 261102 — 비트코인 슈퍼트렌드 봇: 신상(GARCH 슈퍼트렌드)으로 바꿀까, 원조 그대로 둘까.

같은 폴더의 data/ (바이낸스 BTCUSDT · 업비트 KRW-BTC 4시간봉, 2017-10-01 ~ 2026-10-18 16:00 UTC 얼림)만 읽는다.
필요한 것: Python 3.9+ · numpy.   돌리는 법:  python run_stnew.py

두 봇에 똑같이 다는 두 규칙
  ① 어제까지 끝난 일봉(UTC) 종가가 일봉 50일 이동평균보다 높을 때만 산다
  ② 살 때 한 번만 비중을 정한다: 0.5 ÷ 최근 4시간봉 42개 연율 변동성(최대 100%), 팔 때까지 수량 그대로
판정은 봉 마감, 체결은 그 봉 종가 · 편도 수수료 0.1% · 처음 400봉은 지표 준비(평가는 2017-12-06 부터)

원조 = 트레이딩뷰 ta.supertrend(ATR 14, 배수 2)
신상 = GARCH 슈퍼트렌드(같은 생각으로 저희가 짠 판 — 트레이딩뷰에 올라온 특정 스크립트 그대로가 아니다):
       기준선 = 지연을 줄인 지수평균(ZLEMA, 길이 n) · 선의 거리 = 배수 m × ATR 14 × 출렁임 배수
       출렁임 배수 = sqrt(GARCH(1,1) 조건부 분산 ÷ 장기 분산), α 0.10 · β 0.85, 0.5~2 로 자름
"""
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__))
FILES = [("바이낸스", "바이낸스_BTCUSDT_4h.csv"), ("업비트", "업비트_KRW-BTC_4h.csv")]
WARM, FEE, W0 = 400, 0.001, 1000.0   # W0 = 1,000만원


def load(fn):
    d, rows = [], []
    with open(os.path.join(HERE, "data", fn), encoding="utf-8") as f:
        next(f)
        for line in f:
            p = line.rstrip("\n").split(",")
            d.append(p[0]); rows.append([float(x) for x in p[1:5]])
    a = np.asarray(rows)
    o, h, l, c = a[:, 0], a[:, 1], a[:, 2], a[:, 3]
    yrs = (datetime.date.fromisoformat(d[-1][:10]) - datetime.date.fromisoformat(d[0][:10])).days / 365.25
    ret = np.zeros(len(c)); ret[1:] = c[1:] / c[:-1] - 1
    return dict(d=d, o=o, h=h, l=l, c=c, ret=ret, ppy=len(c) / yrs)


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 atr(h, l, c, n):
    pc = np.r_[c[0], c[:-1]]
    tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc)))
    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 supertrend_original(h, l, c, n=14, factor=2.0):
    """트레이딩뷰 ta.supertrend 그대로 — 1 = 상승(보유)."""
    at = atr(h, l, c, n); src = (h + l) / 2
    up = src + factor * at; lo = src - factor * at
    N = len(c); d = np.zeros(N); upper = np.ones(N, dtype=bool)
    for i in range(1, N):
        if np.isnan(at[i]):
            continue
        pl = lo[i - 1] if not np.isnan(lo[i - 1]) else 0.0
        pu = up[i - 1] if not np.isnan(up[i - 1]) else 0.0
        lo[i] = lo[i] if (lo[i] > pl or c[i - 1] < pl) else pl
        up[i] = up[i] if (up[i] < pu or c[i - 1] > pu) else pu
        if np.isnan(at[i - 1]):
            dirn = 1
        elif upper[i - 1]:
            dirn = -1 if c[i] > up[i] else 1
        else:
            dirn = 1 if c[i] < lo[i] else -1
        upper[i] = dirn == 1
        d[i] = 1.0 if dirn == -1 else 0.0
    return d


def supertrend_new(h, l, c, n=14, m=2.0, garch=True, a=0.10, b=0.85):
    """신상 GARCH 슈퍼트렌드 — garch=False 면 출렁임 계산을 끈 판(기준선만 ZLEMA)."""
    lag = (n - 1) // 2
    src = c + (c - np.r_[np.full(lag, c[0]), c[:-lag]]) if lag > 0 else c
    base = ema(src, n); at = atr(h, l, c, 14); N = len(c)
    reg = np.ones(N)
    if garch:
        r = np.zeros(N); r[1:] = np.log(c[1:] / c[:-1]); cum = np.cumsum(r * r)
        s2 = np.empty(N); s2[0] = r[1] ** 2
        for t in range(1, N):
            lr = cum[t - 1] / t
            s2[t] = (1 - a - b) * lr + a * r[t - 1] ** 2 + b * s2[t - 1]
            reg[t] = np.sqrt(s2[t] / lr) if lr > 0 else 1.0
        reg = np.clip(reg, 0.5, 2.0)
    upb = base - m * at * reg; dnb = base + m * at * reg
    d = np.ones(N)
    for i in range(1, N):
        if np.isnan(at[i]) or np.isnan(at[i - 1]):
            continue
        if c[i - 1] > upb[i - 1]:
            upb[i] = max(upb[i], upb[i - 1])
        if c[i - 1] < dnb[i - 1]:
            dnb[i] = min(dnb[i], dnb[i - 1])
        d[i] = 1 if c[i] > dnb[i - 1] else (-1 if c[i] < upb[i - 1] else d[i - 1])
    return (d > 0).astype(float)


def rule_50d(dates, c, n=50):
    """규칙 ①: 어제까지 끝난 일봉(UTC) 종가 > 일봉 50일선."""
    days = np.array([x[:10] for x in dates])
    uniq, first = np.unique(days, return_index=True)
    order = np.argsort(first); uniq = uniq[order]
    last_idx = np.r_[np.sort(first)[1:] - 1, len(c) - 1]
    dc = c[last_idx]; s = sma(dc, n)
    up = np.zeros(len(uniq)); mm = ~np.isnan(s); up[mm] = dc[mm] > s[mm]
    pos = {u: k for k, u in enumerate(uniq)}
    out = np.zeros(len(c))
    for i, dd in enumerate(days):
        k = pos[dd] - 1
        if k >= 0:
            out[i] = up[k]
    return out


def rule_weight(c, target=0.5, win=42, ppy=2190.0):
    """규칙 ②의 목표 비중: 0.5 ÷ 최근 42봉 연율 변동성(최대 1)."""
    N = len(c); r = np.zeros(N); r[1:] = c[1:] / c[:-1] - 1
    w = np.ones(N)
    for t in range(win, N):
        v = r[t - win + 1:t + 1].std() * np.sqrt(ppy)
        w[t] = min(1.0, target / v) if v > 1e-9 else 1.0
    return w


def account(c, tw, entry_only=True, fee=FEE):
    """코인 수량으로 드는 계좌. entry_only=True 면 살 때만 비중을 정하고 팔 때까지 수량 그대로."""
    N = len(c); E = np.ones(N); cash, units = 1.0, 0.0; k = 0; held = np.zeros(N)
    for i in range(N):
        eq = cash + units * c[i]
        cur = units * c[i] / eq if eq > 0 else 0.0
        t = tw[i]
        new = 0.0 if t <= 0 else (t if (not entry_only or cur == 0) else cur)
        if abs(new - cur) > 1e-12:
            eq -= fee * abs(new - cur) * eq
            units = new * eq / c[i]; cash = eq - units * c[i]; k += 1
        E[i] = eq; held[i] = new
    return E, held, k


def stats(S, E):
    e = E[WARM:] / E[WARM - 1]
    yrs = len(e) / S["ppy"]
    cagr = e[-1] ** (1 / yrs) - 1
    mdd = float((e / np.maximum.accumulate(e) - 1).min())
    return e[-1] * W0, cagr * 100, mdd * 100


def won(x):
    x = int(round(x))
    return f"{x // 10000}억 {x % 10000:,}만원" if x >= 10000 else f"{x:,}만원"


def show(tag, S, E, k=None):
    end, cg, md = stats(S, E)
    print(f"  {tag:<22} {won(end):>14} (연 {cg:.1f}% · 가장 크게 줄었던 폭 {md:.1f}%)" + (f" · 사고판 횟수 {k}번" if k else ""))
    return int(round(end)), round(md, 1)


print("AlgoLab 261102 — 비트코인 슈퍼트렌드 봇: 신상으로 바꿀까, 원조 그대로 둘까")
for name, fn in FILES:
    S = load(fn); c, h, l = S["c"], S["h"], S["l"]
    f50 = rule_50d(S["d"], c); wv = rule_weight(c)
    print(f"\n== {name} 4시간봉 · 평가 {S['d'][WARM][:10]} ~ {S['d'][-1][:16]} · 1,000만원 · 편도 0.1%")
    hold = np.cumprod(1 + S["ret"]); hold = hold / hold[WARM - 1]
    print(f"  {'그냥 보유':<22} {won(hold[-1] * W0):>14} (가장 크게 줄었던 폭 {float((hold[WARM:] / np.maximum.accumulate(hold[WARM:]) - 1).min()) * 100:.1f}%)")
    so = supertrend_original(h, l, c)
    E, _, k = account(c, so, entry_only=False); o_raw = show("원조 · 규칙 없이", S, E, k)
    E, _, k = account(c, so * f50 * wv); o_dev = show("원조 · 두 규칙", S, E, k); o_dev_k = k
    more, deep35 = 0, 0
    print("  신상 9가지(길이 n · 배수 m)          두 규칙                         규칙 없이")
    for n in (14, 20, 50):
        for m in (2.0, 3.0, 4.0):
            sn = supertrend_new(h, l, c, n, m)
            Ed, _, kd = account(c, sn * f50 * wv); Er, _, kr = account(c, sn, entry_only=False)
            ed, cd, md = stats(S, Ed); er, cr, mr = stats(S, Er)
            more += int(round(ed)) > o_dev[0]; deep35 += md < -35
            print(f"    n{n:<3} m{m:g}   {won(ed):>14} ({md:6.1f}%)        {won(er):>14} ({mr:6.1f}%)")
    print(f"  → 두 규칙을 달았을 때 신상이 원조보다 더 남긴 설정: {more}/9 · 가장 크게 줄었던 폭이 35% 를 넘긴 신상 설정: {deep35}/9")
    _, held14, k14 = account(c, supertrend_new(h, l, c, 14, 2.0) * f50 * wv)
    _, heldo, _ = account(c, so * f50 * wv)
    print(f"  → 사고판 횟수(두 규칙): 신상 길이 14·배수 2 {k14}번 · 원조 {o_dev_k}번")
    print(f"  → 평균하면 계좌의 몇 %를 비트코인으로 들고 있었나(두 규칙): 원조 {heldo[WARM:].mean() * 100:.0f}% · 신상 길이 14·배수 2 {held14[WARM:].mean() * 100:.0f}%")
    lag = lambda x: np.r_[0.0, x[:-1]]
    Eo1, _, _ = account(c, lag(so * f50 * wv)); En1, _, _ = account(c, lag(supertrend_new(h, l, c, 14, 2.0) * f50 * wv))
    eo1, _, mo1 = stats(S, Eo1); en1, _, mn1 = stats(S, En1)
    print(f"  → 한 봉 늦게 체결하면(두 규칙): 원조 {won(eo1)} ({mo1:.1f}%) · 신상 길이 14·배수 2 {won(en1)} ({mn1:.1f}%)")
    zg = 0
    for n in (14, 20, 50):
        for m in (2.0, 3.0, 4.0):
            Eg, _, _ = account(c, supertrend_new(h, l, c, n, m, True) * f50 * wv)
            Ez, _, _ = account(c, supertrend_new(h, l, c, n, m, False) * f50 * wv)
            zg += Eg[-1] > Ez[-1]
    print(f"  → 출렁임 계산(GARCH)을 켠 판이 끈 판(기준선만 ZLEMA)보다 더 남긴 설정: {zg}/9")
    print("  수수료·밀림을 늘리면(사고팔 때마다 편도) — 원조 · 두 규칙 대 신상 길이 14 배수 2 · 두 규칙")
    sn = supertrend_new(h, l, c, 14, 2.0)
    for fee in (0.00075, 0.001, 0.0015, 0.002, 0.0025):
        Eo, _, _ = account(c, so * f50 * wv, fee=fee); En, _, _ = account(c, sn * f50 * wv, fee=fee)
        eo, _, mo = stats(S, Eo); en, _, mn = stats(S, En)
        print(f"    편도 {fee * 100:.3f}%  원조 {won(eo):>14} ({mo:6.1f}%)  신상 {won(en):>14} ({mn:6.1f}%)  {'신상이 더 남김' if en > eo else '원조가 더 남김'}")
    Eo, _, _ = account(c, so * f50 * wv); En, _, _ = account(c, sn * f50 * wv)
    i2 = next(k for k, x in enumerate(S["d"]) if x >= "2024-10-18")
    print(f"  최근 2년(2024-10-18 ~ 끝, 두 규칙 · 그 날 계좌에서 다시 잼): 원조 {(Eo[-1] / Eo[i2 - 1] - 1) * 100:+.1f}% · 신상 {(En[-1] / En[i2 - 1] - 1) * 100:+.1f}%")
    i3 = next(k for k, x in enumerate(S["d"]) if x >= "2023-01-01")
    print(f"  2022년 말부터(2023-01-01 ~ 끝, 두 규칙): 원조 {Eo[-1] / Eo[i3 - 1]:.2f}배 · 신상 {En[-1] / En[i3 - 1]:.2f}배")
    a = next(k for k, x in enumerate(S["d"]) if x >= "2020-12-15"); b = next(k for k, x in enumerate(S["d"]) if x >= "2021-03-15")
    tot = (En[-1] / En[WARM - 1]) / (Eo[-1] / Eo[WARM - 1]); seg = (En[b] / En[a]) / (Eo[b] / Eo[a])
    print(f"  신상 ÷ 원조(끝 잔고): 전체 {tot:.3f} · 2020-12-15~2021-03-15 석 달 {seg:.3f} · 그 석 달을 뺀 나머지 {tot / seg:.3f}")
print("\n끝.")
