# -*- coding: utf-8 -*-
"""비트코인 선물 봇 — 팔라는 신호에 숏으로 뒤집을까, 현금으로 쉴까 (AlgoLab 재현 코드)

  pip install numpy
  python run_longshort.py

같은 봇 신호를 두 방식으로 돌린다.
  롱만(쉬기) : 사라는 신호에 롱, 팔라는 신호에 롱을 정리하고 현금
  숏도(뒤집기): 팔라는 신호에 같은 크기로 숏
  하락장만    : 팔라는 신호이고 일봉 종가가 200일 평균선 아래(전날 판정)일 때만 숏, 아니면 현금

봇 = strat_pool.py 의 13종 111설정 × 일봉 필터(없음 / 4시간봉 1,200개 평균 ≈ 200일선 위에서만) × BTC·ETH = 444개.
계좌 = 실제 선물처럼 **수량 고정 1배**: 진입 때 계좌 전부로 수량을 정하고 신호가 바뀔 때까지 수량을 안 바꾼다.
  숏 손익 = 수량 × (진입가 − 지금가). 유지증거금 0.4% 아래면 강제청산(봉 고가·저가로 판정) → 계좌 0.
  펀딩비 8시간 실이력(수량 × 가격 × 펀딩률, 롱이 내고 숏이 받는다) · 수수료 편도 0.1%.
  신호는 4시간봉 종가까지의 정보로 정하고 그 종가에 바로 체결한 것으로 계산한다(호가 밀림 미반영).
데이터 = data_longshort/ 의 바이낸스 USDT-M 선물 4시간봉·펀딩비(ASOF 에서 잘라 얼림).

ASOF 를 바꾸면 영상과 숫자가 달라진다 — 영상은 2026-10-04 16:00(UTC) 봉까지다.
"""
import csv
import datetime
import os

import numpy as np

import strat_pool as P

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "data_longshort")
ASOF = "2026-10-04T16:00:00"
START = "2020-02-01"
WARM = 400
COST = 0.001
MAINT = 0.004
ASSETS = [("BTC", "BTCUSDT_4h.csv", "BTCUSDT_funding.csv"),
          ("ETH", "ETHUSDT_4h.csv", "ETHUSDT_funding.csv")]


def ts(x):
    return datetime.datetime.fromisoformat(x if "T" in x else x + "T00:00:00")


def load(csvp, fp):
    d, o, h, l, c = [], [], [], [], []
    with open(os.path.join(DATA, csvp), encoding="utf-8") as f:
        next(f)
        for line in f:
            p = line.rstrip("\n").split(",")
            if ts(p[0]) > ts(ASOF):
                break
            d.append(p[0]); o.append(float(p[1])); h.append(float(p[2])); l.append(float(p[3])); c.append(float(p[4]))
    S = dict(d=d, o=np.array(o), h=np.array(h), l=np.array(l), c=np.array(c), n=len(c))
    S["ret"] = np.r_[0.0, S["c"][1:] / S["c"][:-1] - 1]
    T = [ts(x) for x in d]
    gaps = sum(1 for i in range(1, len(T)) if (T[i] - T[i - 1]).total_seconds() != 14400)
    ev = {}
    with open(os.path.join(DATA, fp), encoding="utf-8") as f:
        for r in csv.DictReader(f):
            ev[ts(r["date"]).isoformat()[:13]] = float(r["funding"])
    fund = np.zeros(S["n"])
    for i, t in enumerate(T):                      # 봉이 끝나는 시각의 정산
        v = ev.get((t + datetime.timedelta(hours=4)).isoformat()[:13])
        if v is not None:
            fund[i] = v
    a = max(WARM, int(np.searchsorted(np.asarray(d), START)))
    return S, fund, a, gaps


def make_sig(S, fn, prm, filt_days):
    base = fn(S["o"], S["h"], S["l"], S["c"], **prm)
    if filt_days:
        sma = P._sma(S["c"], filt_days * 6)        # 4시간봉 6개 = 하루
        f = np.zeros(S["n"])
        m = ~np.isnan(sma)
        f[m] = (S["c"][m] > sma[m]).astype(float)
        base = base * f
    return base


def daily_bear(S):
    """일봉 종가 < 200일 평균 — 하루가 끝난 뒤에야 안다(다음 날 봉부터)."""
    days, last = [], {}
    for i, x in enumerate(S["d"]):
        k = x[:10]
        if k not in last:
            days.append(k)
        last[k] = i
    dc = np.array([S["c"][last[k]] for k in days])
    cs = np.cumsum(np.r_[0.0, dc])
    sma = np.full(len(dc), np.nan)
    sma[199:] = (cs[200:] - cs[:-200]) / 200
    idx = {k: j for j, k in enumerate(days)}
    bear = np.zeros(S["n"], dtype=bool)
    for i, x in enumerate(S["d"]):
        j = idx[x[:10]] - 1
        if j >= 199:
            bear[i] = not (dc[j] > sma[j])
    return bear


def run_held(S, fund, target, a):
    """수량 고정 1배 계좌. target[i] = i번째 봉 종가에 맞출 비중(-1~1). 반환: 곡선, 청산 시각, 펀딩 손익."""
    c, h, l = S["c"], S["h"], S["l"]
    eq, q, ent, base, cur, fsum = 1.0, 0.0, 0.0, 1.0, 0.0, 0.0
    t0 = target[a - 1]
    if t0 != 0.0:
        eq -= COST * abs(t0) * eq
        q, ent, base, cur = t0 * eq / c[a - 1], c[a - 1], eq, t0
    curve = [eq]
    for i in range(a, S["n"]):
        if q != 0.0:
            worst = h[i] if q < 0 else l[i]
            if base + q * (worst - ent) <= MAINT * abs(q) * worst:
                curve.extend([0.0] * (S["n"] - i))
                return np.array(curve), S["d"][i], fsum
            if fund[i] != 0.0:
                pay = q * c[i] * fund[i]
                base -= pay
                fsum -= pay
            eq = base + q * (c[i] - ent)
        tgt = target[i]
        if tgt != cur:
            if q != 0.0:
                eq -= COST * abs(q) * c[i]
            q = 0.0
            if tgt != 0.0:
                eq -= COST * abs(tgt) * eq
                q, ent = tgt * eq / c[i], c[i]
            base, cur = eq, tgt
        curve.append(eq)
    return np.array(curve), None, fsum


def run_reb(S, fund, target, a):
    """민감도 — 봉마다 같은 비중으로 다시 맞추는 계좌(인버스 ETF 꼴)."""
    pos = np.r_[0.0, target[:-1]]
    g = (1 + pos * S["ret"] - pos * fund) * (1 - COST * np.abs(np.diff(np.r_[0.0, pos])))
    return np.r_[1.0, np.cumprod(g[a:])]


def kpi(curve):
    peak = np.maximum.accumulate(curve)
    dd = curve / np.where(peak > 0, peak, 1) - 1
    return float(curve[-1]), float(dd.min())


def 만원(x):
    v = int(round(x * 1000))
    return f"{v // 10000}억 {v % 10000:,}만원" if v >= 10000 else f"{v:,}만원"


def main():
    rows, rep = [], None
    for name, csvp, fp in ASSETS:
        S, fund, a, gaps = load(csvp, fp)
        bear = daily_bear(S)
        print(f"[{name}] {S['d'][a - 1]} ~ {S['d'][-1]} (UTC) · 4시간봉 {S['n'] - a + 1:,}개 · 빠진 봉 {gaps}")
        hold = float(S["c"][-1] / S["c"][a - 1])
        fv = [v for v in fund[a:] if v != 0.0]
        print(f"   그냥 들고만 있었다면(1배 롱 · 수수료·펀딩 없이) 1,000만원 → {만원(hold)}"
              f" · 펀딩비가 플러스(롱이 내고 숏이 받음)였던 정산 {sum(v > 0 for v in fv) / len(fv) * 100:.0f}%")
        cs, liq, _ = run_held(S, fund, -np.ones(S["n"]), a)
        print(f"   1배 숏을 {S['d'][a - 1][:10]} 에 잡고 들고만 있었다면: 강제청산 {liq} "
              f"(가격 {S['c'][a - 1]:,.0f} → 그날 고가 {S['h'][S['d'].index(liq)]:,.0f})")
        for sname, (fn, grid, src) in P.POOL.items():
            for prm in grid:
                for filt in (0, 200):
                    sig = make_sig(S, fn, prm, filt)
                    tL = sig.copy()
                    tLS = 2.0 * sig - 1.0
                    tG = np.where(sig > 0, sig, np.where(bear, -(1.0 - sig), 0.0))
                    cL, _, _ = run_held(S, fund, tL, a)
                    cS, lqS, _ = run_held(S, fund, tLS, a)
                    cG, _, _ = run_held(S, fund, tG, a)
                    r = dict(asset=name, bot=sname, prm=prm, filt=filt,
                             L=kpi(cL), LS=kpi(cS), G=kpi(cG), liq=lqS,
                             LSr=run_reb(S, fund, tLS, a)[-1], Lr=run_reb(S, fund, tL, a)[-1])
                    rows.append(r)
                    if name == "BTC" and sname == "슈퍼트렌드" and prm == {"n": 10, "mult": 3.0} and filt == 0:
                        tS = -(1.0 - sig)
                        _, _, fS = run_held(S, fund, tS, a)
                        seg = t = 0
                        up = 0
                        pos = tLS[a - 1:]
                        cc = S["c"][a - 1:]
                        dd_ = S["d"][a - 1:]
                        segs_ = []
                        i = 0
                        while i < len(pos):
                            j = i
                            while j + 1 < len(pos) and pos[j + 1] == pos[i]:
                                j += 1
                            if pos[i] < 0:
                                seg += 1
                                up += int(cc[min(j + 1, len(cc) - 1)] > cc[i])
                                if j + 1 < len(pos):
                                    segs_.append((cc[j + 1] / cc[i] - 1, dd_[i], cc[i], dd_[j + 1], cc[j + 1]))
                            i = j + 1
                        segs_.sort(key=lambda x: x[0])
                        mid_ = segs_[len(segs_) // 2]
                        fills_L = int(np.sum(np.diff(tL[a - 1:]) != 0))
                        rep = dict(r, segs=seg, up=up, mid=mid_, mid_rank=len(segs_) // 2 + 1, nseg=len(segs_), fund_short=fS, fills_L=fills_L, fills_LS=2 * fills_L,
                                   yrs=(ts(S["d"][-1]) - ts(S["d"][a - 1])).days / 365.25)

    n = len(rows)
    more = [r for r in rows if r["LS"][0] > r["L"][0]]
    shal = [r for r in rows if r["LS"][1] > r["L"][1]]
    both = [r for r in rows if r["LS"][0] > r["L"][0] and r["LS"][1] > r["L"][1]]
    g_more = [r for r in rows if r["G"][0] > r["L"][0]]
    g_both = [r for r in rows if r["G"][0] > r["L"][0] and r["G"][1] > r["L"][1]]
    btc_gap = sorted(r["LS"][0] / r["L"][0] for r in rows if r["asset"] == "BTC")
    rg = rep["LS"][0] / rep["L"][0]

    print("\n① 대표 봇 — 트레이딩뷰 슈퍼트렌드 기본값(ATR 10 · 배수 3) · 비트코인 · 필터 없음 · 1,000만원")
    print(f"   기간 {rep['yrs']:.2f}년 (6년 8개월)")
    print(f"   롱만(팔라는 신호에 현금) : {만원(rep['L'][0])} · 고점 대비 가장 깊게 {rep['L'][1] * 100:.1f}%")
    print(f"   숏도(팔라는 신호에 숏)   : {만원(rep['LS'][0])} · 고점 대비 가장 깊게 {rep['LS'][1] * 100:.1f}%")
    print(f"   팔라는 신호 {rep['segs']}번 중 다시 사라는 신호가 날 때 가격이 판 값보다 높았던 것 {rep['up']}번")
    m = rep["mid"]
    print(f"   가격 변화가 가운데인 숏 하나({rep['nseg']}개 중 {rep['mid_rank']}번째): {m[1]} 숏 {m[2]:,.1f} → "
          f"{m[3]} 되삼 {m[4]:,.1f} ({m[0] * 100:+.2f}%)")
    print(f"   숏 구간에서 받은 펀딩비(계좌 대비 누적) {rep['fund_short']:+.3f} — 받는 쪽이었다")
    print(f"   체결 횟수 롱만 {rep['fills_L']}번 · 숏도 {rep['fills_LS']}번")
    print(f"   숏으로 잃은 비율(숏도÷롱만) {rg:.3f} — 비트코인 222개 중 덜 잃은 쪽부터 "
          f"{sum(x <= rg for x in btc_gap) / len(btc_gap) * 100:.0f}% 자리 (가운데 {np.median(btc_gap):.3f})")
    all_gap = sorted(r["LS"][0] / r["L"][0] for r in rows)
    print(f"   444개 전체로 세면 덜 잃은 쪽부터 {sum(x <= rg for x in all_gap) / len(all_gap) * 100:.0f}% 자리")

    print(f"\n② 444개 전부 — 봇 13종 111설정 × 필터 2 × BTC·ETH = {n}개")
    print(f"   숏도가 돈을 더 남긴 것 {len(more)}개 · 롱만이 더 남긴 것 {n - len(more)}개")
    for r in more:
        print(f"     {r['asset']} {r['bot']} {r['prm']} 필터{r['filt']}: 롱만 {r['L'][0]:.2f}배/{r['L'][1] * 100:.1f}%"
              f" · 숏도 {r['LS'][0]:.2f}배/{r['LS'][1] * 100:.1f}%  ← 숏도가 더 깊게 떨어짐")
    print(f"   숏도가 덜 떨어진 것 {len(shal)}개 · 돈도 더 남기고 덜 떨어진 것 {len(both)}개")
    print(f"   숏도에서 강제청산된 설정 {sum(1 for r in rows if r['liq'])}개 "
          f"(전부 {sorted(set(r['bot'] for r in rows if r['liq']))})")
    print(f"   민감도 — 봉마다 다시 맞추는 계좌로 재면 숏도가 더 남긴 것 {sum(1 for r in rows if r['LSr'] > r['Lr'])}개")

    print("\n③ 하락장일 때만 숏 — 일봉 종가가 200일 평균선 아래(전날 판정)인 날만 숏, 아니면 현금")
    print(f"   롱만보다 돈을 더 남긴 것 {len(g_more)}개 · 돈도 더 남기고 덜 떨어진 것 {len(g_both)}개 "
          f"(자산: {sorted(set(r['asset'] for r in g_both))})")
    for nm in ("BTC", "ETH"):
        k = sum(1 for r in rows if r["asset"] == nm)
        print(f"   {nm}: 돈을 더 남긴 것 {sum(1 for r in g_more if r['asset'] == nm)}개 / {k}개 · "
              f"돈도 더 남기고 덜 떨어진 것 {sum(1 for r in g_both if r['asset'] == nm)}개")


if __name__ == "__main__":
    main()
