# -*- coding: utf-8 -*-
"""비트코인 슈퍼트렌드 봇 — 1시간봉에 걸까, 4시간봉에 걸까 (AlgoLab 재현 코드 · 표준 라이브러리만)

같은 폴더의 data_btc/ 에 얼려 둔 1시간봉 세 개를 읽는다.
  BTCUSDT_1h.csv  바이낸스 현물(2017-08 ~ 2026-10-18)
  KRW-BTC_1h.csv  업비트(2017-09 ~ 2026-10-18)
  BTCUSD_1h.csv   비트스탬프(2013-06 ~ 2017-09) — 다른 거래소·다른 시대 확인용

규칙(트레이딩뷰 btc_supertrend_vol.pine 과 같은 규칙):
  · 1시간봉을 UTC 경계로 묶어 2·4·6·8·12시간봉·일봉을 만든다
  · 슈퍼트렌드(ATR 14 · 배수 2, 파인 ta.supertrend 와 같은 식)가 상승이고
    어제까지 끝난 일봉(UTC) 종가가 일봉 50일 이동평균 위일 때 산다. 둘 중 하나라도 꺼지면 판다
  · 살 때 한 번 비중 = min(1, 0.5 ÷ 최근 7일 연율 변동성) — 팔 때까지 수량 그대로
  · 봉이 끝날 때 판정하고 그 봉 종가에 체결 · 사고팔 때마다 편도 수수료(기본 0.1%)
  python run_supertrend4h.py
"""
import datetime
import math
import os

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data_btc")
START, END = "2017-10-01", "2026-10-18T16:00:00"
SEED = 10_000_000
TFS = (1, 2, 4, 6, 8, 12, 24)


def load(name, a=START, b=END):
    d, o, h, l, c = [], [], [], [], []
    with open(os.path.join(DATA, name), encoding="utf-8") as f:
        next(f)
        for line in f:
            p = line.rstrip("\n").split(",")
            if len(p) < 5:
                continue
            t = p[0] if len(p[0]) > 10 else p[0] + "T00:00:00"   # 자정 봉은 날짜만 적혀 있다
            if t < a or t > b:
                continue
            d.append(t); o.append(float(p[1])); h.append(float(p[2])); l.append(float(p[3])); c.append(float(p[4]))
    return d, o, h, l, c


def resample(d, o, h, l, c, k):
    if k == 1:
        return d, o, h, l, c
    D, O, H, L, C = [], [], [], [], []
    last = None
    for i in range(len(d)):
        key = d[i][:11] + "%02d" % ((int(d[i][11:13]) // k) * k)
        if key != last:
            D.append(key + ":00:00"); O.append(o[i]); H.append(h[i]); L.append(l[i]); C.append(c[i]); last = key
        else:
            H[-1] = max(H[-1], h[i]); L[-1] = min(L[-1], l[i]); C[-1] = c[i]
    return D, O, H, L, C


def series(name, k, a=START, b=END):
    d, o, h, l, c = resample(*load(name, a, b), k)
    yrs = (datetime.datetime.fromisoformat(d[-1]) - datetime.datetime.fromisoformat(d[0])).total_seconds() / (365.25 * 86400)
    return dict(d=d, h=h, l=l, c=c, ppy=len(c) / yrs)


def atr(h, l, c, n):
    tr = [h[0] - l[0]] + [max(h[i] - l[i], abs(h[i] - c[i - 1]), abs(l[i] - c[i - 1])) for i in range(1, len(c))]
    out = [math.nan] * len(c)
    if len(c) >= n:
        out[n - 1] = sum(tr[:n]) / n
        for i in range(n, len(c)):
            out[i] = out[i - 1] + (tr[i] - out[i - 1]) / n
    return out


def supertrend_up(h, l, c, n=14, factor=2.0):
    """1 = 상승(초록) · 0 = 하락(빨강). 파인 ta.supertrend 와 같은 식."""
    a = atr(h, l, c, n)
    N = len(c)
    up = [(h[i] + l[i]) / 2 + factor * a[i] for i in range(N)]
    lo = [(h[i] + l[i]) / 2 - factor * a[i] for i in range(N)]
    is_upper = [True] * N
    res = [0.0] * N
    for i in range(1, N):
        if math.isnan(a[i]):
            continue
        pl = 0.0 if math.isnan(lo[i - 1]) else lo[i - 1]
        pu = 0.0 if math.isnan(up[i - 1]) else up[i - 1]
        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 math.isnan(a[i - 1]):
            dirn = 1
        elif is_upper[i - 1]:
            dirn = -1 if c[i] > up[i] else 1
        else:
            dirn = 1 if c[i] < lo[i] else -1
        is_upper[i] = dirn == 1
        res[i] = 1.0 if dirn == -1 else 0.0
    return res


def above_daily_sma(d, c, n=50):
    """그 봉의 '어제까지 끝난 일봉 종가 > 일봉 50일선' (일봉 종가 = 그날 마지막 봉 종가)."""
    days, dc = [], []
    for i in range(len(d)):
        if not days or d[i][:10] != days[-1]:
            days.append(d[i][:10]); dc.append(c[i])
        else:
            dc[-1] = c[i]
    up = []
    for k in range(len(dc)):
        if k + 1 < n:
            up.append(0.0)
        else:
            up.append(1.0 if dc[k] > sum(dc[k + 1 - n:k + 1]) / n else 0.0)
    pos = {x: k for k, x in enumerate(days)}
    return [up[pos[x[:10]] - 1] if pos[x[:10]] >= 1 else 0.0 for x in d]


def vol_weight(c, win, ann, target=0.5):
    r = [0.0] + [c[i] / c[i - 1] - 1 for i in range(1, len(c))]
    cs = [0.0]; cs2 = [0.0]
    for x in r:
        cs.append(cs[-1] + x); cs2.append(cs2[-1] + x * x)
    w = [1.0] * len(c)
    for t in range(win, len(c)):
        s = cs[t + 1] - cs[t + 1 - win]; s2 = cs2[t + 1] - cs2[t + 1 - win]
        v = math.sqrt(max(s2 / win - (s / win) ** 2, 0.0)) * math.sqrt(ann)
        w[t] = min(1.0, target / v) if v > 1e-9 else 1.0
    return w


def simulate(c, want, fee):
    """want[i] > 0 이면 들고 있고 싶다(값 = 살 때 비중). 산 뒤에는 수량 그대로, 0 이 되면 판다."""
    cash, units, n_tr = 1.0, 0.0, 0
    E, wts = [], []
    for i in range(len(c)):
        eq = cash + units * c[i]
        cur = units * c[i] / eq if eq > 0 else 0.0
        t = want[i]
        new = 0.0 if t <= 0 else (t if 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]; n_tr += 1
        E.append(eq); wts.append(new)
    g = [1.0] + [E[i] / E[i - 1] for i in range(1, len(E))]
    return g, wts, n_tr


def stats(g, ppy):
    eq, peak, mdd = 1.0, 1.0, 0.0
    for x in g:
        eq *= x; peak = max(peak, eq); mdd = min(mdd, eq / peak - 1)
    return eq ** (ppy / len(g)) - 1, mdd, eq


def run(name, k, eval_start, fee=0.001, a=START, b=END, keep=False, atr_n=14, factor=2.0):
    S = series(name, k, a, b)
    d, h, l, c = S["d"], S["h"], S["l"], S["c"]
    bpd = 24 // k
    w = vol_weight(c, 7 * bpd, bpd * 365.0)
    st = supertrend_up(h, l, c, n=atr_n, factor=factor)
    fl = above_daily_sma(d, c)
    g, wts, n_tr = simulate(c, [st[i] * fl[i] * w[i] for i in range(len(c))], fee)
    W = next(i for i, x in enumerate(d) if x >= eval_start)
    cg, mdd, mult = stats(g[W:], S["ppy"])
    ret = [1.0] + [c[i] / c[i - 1] for i in range(1, len(c))]
    hc, hm, hmult = stats(ret[W:], S["ppy"])
    expo = sum(wts[W:]) / len(wts[W:])
    fc, fm, _ = stats([1 + expo * (x - 1) for x in ret[W:]], S["ppy"])
    r = dict(cagr=cg, mdd=mdd, end=mult * SEED, trades=n_tr, hold_cagr=hc, hold_mdd=hm, hold_end=hmult * SEED,
             expo=expo, fix_cagr=fc, fix_mdd=fm, start=d[W], last=d[-1])
    if keep:
        r.update(d=d[W:], g=g[W:], ret=ret[W:], wts=wts[W:], c=c[W:])
    return r


def won(x):
    """원 → '1억 1,066만원' · '1,956만원'"""
    m = int(round(x / 1e4))
    if m >= 10000:
        return "%d억 %s만원" % (m // 10000, format(m % 10000, ",")) if m % 10000 else "%d억원" % (m // 10000)
    return format(m, ",") + "만원"


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


def main():
    eval_start = series("KRW-BTC_1h.csv", 4)["d"][400]      # 4시간봉 400봉 워밍업 뒤부터 잰다
    print("비트코인 슈퍼트렌드 봇 · ATR 14 · 배수 2 · 일봉 50일선 · 살 때 비중 0.5÷7일 변동성 · 편도 0.1%")
    print("기간 %s ~ %s (UTC) · 1,000만원\n" % (eval_start, END))
    print("[1] 차트 시간봉만 바꾼다 — 설정은 그대로")
    res = {}
    for k in TFS:
        for ex, f in (("바이낸스", "BTCUSDT_1h.csv"), ("업비트", "KRW-BTC_1h.csv")):
            res[(k, ex)] = r = run(f, k, eval_start, keep=(k in (1, 4)))
        b, u = res[(k, "바이낸스")], res[(k, "업비트")]
        lab = "%d시간봉" % k if k < 24 else "일봉"
        print("  %-6s 바이낸스 %s (연 %s · 가장 깊은 골짜기 %s · 사고판 횟수 %d번) | 업비트 %s (연 %s · %s · %d번)"
              % (lab, won(b["end"]), pct(b["cagr"]), pct(b["mdd"]), b["trades"], won(u["end"]), pct(u["cagr"]), pct(u["mdd"]), u["trades"]))
    for ex in ("바이낸스", "업비트"):
        r = res[(4, ex)]
        print("  그냥 보유 %s %s (연 %s · 가장 깊은 골짜기 %s)" % (ex, won(r["hold_end"]), pct(r["hold_cagr"]), pct(r["hold_mdd"])))
    b4, b1 = res[(4, "바이낸스")], res[(1, "바이낸스")]
    print("  1시간봉은 4시간봉보다 %.1f배 자주 사고팔았다(바이낸스 %d번 대 %d번)" % (b1["trades"] / b4["trades"], b1["trades"], b4["trades"]))

    print("\n[2] 같은 평균 비중을 늘 들고 있었다면(매 봉 맞춤 · 비용 0 — 이 계좌에 유리한 계산)")
    for k in (1, 4, 6, 8):
        for ex in ("바이낸스", "업비트"):
            r = res[(k, ex)]
            print("  %d시간봉 %s 봇 평균 비중 %.0f%% → 고정 비중 계좌 연 %s · 가장 깊은 골짜기 %s (봇 연 %s · %s)"
                  % (k, ex, r["expo"] * 100, pct(r["fix_cagr"]), pct(r["fix_mdd"]), pct(r["cagr"]), pct(r["mdd"])))

    print("\n[3] 수수료를 바꾸면 (바이낸스)")
    for fee in (0.0, 0.0005, 0.001, 0.0012, 0.0015, 0.0025):
        r1 = run("BTCUSDT_1h.csv", 1, eval_start, fee=fee); r4 = run("BTCUSDT_1h.csv", 4, eval_start, fee=fee)
        print("  편도 %.2f%%  1시간봉 연 %s · 4시간봉 연 %s (%s)" % (fee * 100, pct(r1["cagr"]), pct(r4["cagr"]), won(r4["end"])))

    print("\n[4] 최근 2년만 (2024-10-18 16:00 UTC ~) — 같은 계좌를 그날부터 다시 잰다")
    for ex in ("바이낸스", "업비트"):
        row = []
        for k in (1, 4):
            r = res[(k, ex)]
            i0 = next(i for i, x in enumerate(r["d"]) if x >= "2024-10-18T16:00:00")
            _, m1, e1 = stats(r["g"][i0 + 1:], 1); _, m2, e2 = stats(r["ret"][i0 + 1:], 1)
            row.append("%d시간봉 봇 %s(골짜기 %s)" % (k, won(e1 * SEED), pct(m1)))
        print("  %s %s · 그냥 보유 %s(골짜기 %s)" % (ex, " · ".join(row), won(e2 * SEED), pct(m2)))

    print("\n[5] 바이낸스 4시간봉 봇이 사고판 기록")
    r = res[(4, "바이낸스")]
    tr, i, eq = [], 0, 1.0
    eqs = []
    for x in r["g"]:
        eq *= x; eqs.append(eq)
    w = r["wts"]
    while i < len(w):
        if w[i] > 0 and (i == 0 or w[i - 1] == 0):
            j = i
            while j + 1 < len(w) and w[j + 1] > 0:
                j += 1
            ex = j + 1 if j + 1 < len(w) else j
            tr.append((r["d"][i], r["c"][i], w[i], r["d"][ex], r["c"][ex], eqs[ex] / (eqs[i - 1] if i > 0 else 1.0)))
            i = j + 1
        else:
            i += 1
    best = max(tr, key=lambda x: x[5])
    loss = sorted(x[5] - 1 for x in tr if x[5] <= 1)
    print("  산 뒤 판 것 %d번 · 계좌가 늘어난 것 %d번 · 줄어든 것 %d번(한 번에 가장 크게 줄어든 것 %s)"
          % (len(tr), sum(1 for x in tr if x[5] > 1), len(loss), pct(loss[0])))
    print("  계좌를 가장 많이 늘린 한 번: %s 종가 %s달러에 계좌의 %.0f%%로 사서 → %s 종가 %s달러에 팔았다 · 계좌 %.3f배"
          % (best[0], format(round(best[1]), ","), best[2] * 100, best[3], format(round(best[4]), ","), best[5]))
    print("  해마다(바이낸스 4시간봉 봇 대 그냥 보유):")
    for y in range(2018, 2027):
        idx = [k for k, x in enumerate(r["d"]) if x[:4] == str(y)]
        if not idx:
            continue
        b0 = idx[0] - 1 if idx[0] > 0 else 0
        bot = eqs[idx[-1]] / eqs[b0] - 1
        hold = r["c"][idx[-1]] / r["c"][b0] - 1
        print("    %d  봇 %+.1f%%  그냥 보유 %+.1f%%" % (y, bot * 100, hold * 100))

    print("\n[6] 1시간봉에 맞게 선을 고쳐 주면? — ATR 길이 56(4시간봉 14 × 4)")
    for fa in (2.0, 4.0):
        row = []
        for ex, f in (("바이낸스", "BTCUSDT_1h.csv"), ("업비트", "KRW-BTC_1h.csv")):
            r = run(f, 1, eval_start, atr_n=56, factor=fa)
            row.append("%s %s (연 %s · 골짜기 %s · %d번)" % (ex, won(r["end"]), pct(r["cagr"]), pct(r["mdd"]), r["trades"]))
        print("  1시간봉 ATR 56 · 배수 %d  %s" % (fa, " | ".join(row)))
    print("  (비교: 4시간봉 ATR 14 · 배수 2  바이낸스 %s · 업비트 %s)" % (won(res[(4, "바이낸스")]["end"]), won(res[(4, "업비트")]["end"])))

    print("\n[7] 다른 거래소·다른 시대 — 비트스탬프 2014-01 ~ 2017-09 (편도 0.1%)")
    bs = {}
    for k in TFS:
        bs[k] = r = run("BTCUSD_1h.csv", k, "2014-01-01", a="2013-06-01", b="2017-09-30T23:59:59")
        lab = "%d시간봉" % k if k < 24 else "일봉"
        print("  %-6s 연 %s · 가장 깊은 골짜기 %s · 사고판 횟수 %d번" % (lab, pct(r["cagr"]), pct(r["mdd"]), r["trades"]))
    print("  그냥 보유(4시간봉 기준) 연 %s · 가장 깊은 골짜기 %s" % (pct(bs[4]["hold_cagr"]), pct(bs[4]["hold_mdd"])))
    print("\n끝.")


if __name__ == "__main__":
    main()
