# -*- coding: utf-8 -*-
"""비트코인 봇에 10% 익절을 걸까, 봇이 팔 때까지 둘까 — 재현 코드 (알고랩 · 2026-10-29 영상)

파이썬 표준 라이브러리만 씁니다. 같은 폴더의 data/ 에 가격 자료(4시간봉 CSV)가 있어야 합니다.
    python run_takeprofit.py

봇 규칙
  · 4시간봉 슈퍼트렌드(트레이딩뷰 기본값 ATR 10 · 배수 3)가 상승(초록)이고
  · 4시간봉 종가가 '어제까지 끝난' 일봉(UTC) 30일 평균보다 높을 때 산다(계좌 전부 · 현물).
  · 둘 중 하나라도 꺼지면 판다. 봉 마감에 판정하고 다음 봉 시가에 체결. 편도 수수료 0.1%.
익절 계좌는 여기에 하나를 더한다
  · 산 값(신호 봉 종가)보다 4시간봉 종가가 p% 이상이면 다음 봉 시가에 판다(전부 또는 절반).
  · 전부 판 뒤에는 신호가 꺼졌다 다시 켜질 때까지 쉰다.
기준일 고정(ASOF): 2026-10-28 16:00 UTC 봉까지. 바이낸스 금액은 달러 가격의 배수를 1,000만원에 곱한 값."""
import csv, datetime, math, os

HERE = os.path.dirname(os.path.abspath(__file__))
ASOF = "2026-10-28T16:00:00"
COST = 0.001
WARM = 400
SEED = 1000  # 만원
NaN = float("nan")


def load(fn):
    d, o, h, l, c = [], [], [], [], []
    with open(os.path.join(HERE, "data", fn), encoding="utf-8") as f:
        r = csv.reader(f); next(r)
        for row in r:
            if row[0] > ASOF:
                break
            d.append(row[0]); o.append(float(row[1])); h.append(float(row[2])); l.append(float(row[3])); c.append(float(row[4]))
    return dict(d=d, o=o, h=h, l=l, c=c, n=len(c))


def sma(x, n):
    """누적합 차이로 계산한다(연구 코드 numpy 판과 같은 반올림)."""
    cs = [0.0]
    for v in x:
        cs.append(cs[-1] + v)
    return [NaN if i < n - 1 else (cs[i + 1] - cs[i + 1 - n]) / n for i in range(len(x))]


def ema(x, n):
    a = 2.0 / (n + 1); out = [x[0]]
    for v in x[1:]:
        out.append(a * v + (1 - a) * out[-1])
    return out


def atr(h, l, c, n):
    tr = [max(h[i] - l[i], abs(h[i] - (c[i - 1] if i else c[0])), abs(l[i] - (c[i - 1] if i else c[0]))) for i in range(len(c))]
    a = [NaN] * len(c)
    if len(c) >= n:
        a[n - 1] = sum(tr[:n]) / n
        for i in range(n, len(c)):
            a[i] = a[i - 1] + (tr[i] - a[i - 1]) / n
    return a


def supertrend(S, n=10, mult=3.0):
    h, l, c = S["h"], S["l"], S["c"]
    a = atr(h, l, c, n)
    up = [(h[i] + l[i]) / 2 - mult * a[i] for i in range(S["n"])]
    dn = [(h[i] + l[i]) / 2 + mult * a[i] for i in range(S["n"])]
    d = [1.0] * S["n"]
    for i in range(1, S["n"]):
        if math.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])
        if c[i] > dn[i - 1]:
            d[i] = 1.0
        elif c[i] < up[i - 1]:
            d[i] = -1.0
        else:
            d[i] = d[i - 1]
    return [1.0 if x > 0 else 0.0 for x in d]


def ma_cross(S, fast=50, slow=200):
    f, s = sma(S["c"], fast), sma(S["c"], slow)
    return [1.0 if (not math.isnan(f[i]) and not math.isnan(s[i]) and f[i] > s[i]) else 0.0 for i in range(S["n"])]


def keltner(S, n=20, k=1.5):
    c = S["c"]; e = ema(c, n); a = atr(S["h"], S["l"], c, n)
    out, st = [], 0.0
    for i in range(S["n"]):
        buy = (not math.isnan(a[i])) and c[i] > e[i] + k * a[i]
        if i < n + 1:
            st = 0.0
        elif buy:
            st = 1.0
        elif c[i] < e[i]:
            st = 0.0
        out.append(st)
    return out


def daily_filter(S, n=30):
    days = [x[:10] for x in S["d"]]
    uniq, last = [], []
    for i, dd in enumerate(days):
        if not uniq or uniq[-1] != dd:
            uniq.append(dd); last.append(i)
        else:
            last[-1] = i
    dsma = sma([S["c"][i] for i in last], n)
    f, j = [], -1
    for i, dd in enumerate(days):
        if j < 0 or uniq[j] != dd:
            j += 1
        y = j - 1
        f.append(1.0 if (y >= 0 and not math.isnan(dsma[y]) and S["c"][i] > dsma[y]) else 0.0)
    return f


def take_profit(S, base, levels):
    """levels = ((수익률, 그때부터의 비중), …). 비중 0 = 전부 판다(신호가 꺼질 때까지 쉼)."""
    c = S["c"]; out = [0.0] * S["n"]; holding, armed, e, w = False, True, 0.0, 1.0
    for t in range(S["n"]):
        on = base[t] > 0
        if not holding:
            if not on:
                armed = True
            elif armed:
                holding, e, w = True, c[t], 1.0
        if holding:
            if not on:
                holding = False; out[t] = 0.0; continue
            for p, lw in levels:
                if c[t] / e - 1 >= p:
                    w = min(w, lw)
            out[t] = w
    return out


def run(S, w, want_pos=False):
    """수량 고정 엔진: 0→x 면 계좌 x 만큼 수량을 산다 · x→y 면 수량 × y/x · →0 이면 전부 판다. 다음 봉 시가 체결."""
    o, c, n = S["o"], S["c"], S["n"]
    cash, qty, eq, pos = 1.0, 0.0, [1.0] * n, [0.0] * n
    for t in range(1, n):
        wn, wp = w[t - 1], (w[t - 2] if t >= 2 else 0.0)
        if wn != wp:
            px = o[t]
            if wp == 0 and wn > 0:
                q = wn * (cash + qty * c[t - 1]) / c[t - 1]
            elif wn == 0:
                q = 0.0
            else:
                q = qty * wn / wp
            dq = q - qty
            cash -= dq * px + COST * abs(dq) * px
            qty = q
        eq[t] = cash + qty * c[t]
        pos[t] = qty * c[t] / eq[t] if eq[t] > 0 else 0.0
    return (eq, pos) if want_pos else eq


def window(series):
    day = lambda s: datetime.date.fromisoformat(s[:10])
    a = max(day(S["d"][WARM]) for S in series)
    b = min(day(S["d"][-1]) for S in series) + datetime.timedelta(days=1)
    return a, b


def stat(S, eq, a, b):
    ds = [x[:10] for x in S["d"]]
    i = next(k for k, x in enumerate(ds) if x >= a.isoformat())
    j = next((k for k, x in enumerate(ds) if x >= b.isoformat()), S["n"])
    e = [v / eq[i] for v in eq[i:j]]
    peak, mdd = e[0], 0.0
    for v in e:
        peak = max(peak, v); mdd = min(mdd, v / peak - 1)
    yrs = (datetime.date.fromisoformat(S["d"][j - 1][:10]) - datetime.date.fromisoformat(S["d"][i][:10])).days / 365.25
    return SEED * e[-1], e[-1] ** (1 / yrs) - 1, mdd, i, j


def exposure(S, w, i, j):
    """평균 보유 비중 — 봉 끝마다 (코인 평가액 ÷ 계좌) 의 평균."""
    _, pos = run(S, w, True)
    return sum(pos[i:j]) / (j - i)


def match_k(S, base, target, i, j):
    """익절 없는 봇의 살 때 비중 k 를 줄여 평균 보유 비중을 target 에 맞춘다(이분법 14번)."""
    lo, hi = 0.05, 1.0
    for _ in range(14):
        k = (lo + hi) / 2
        if exposure(S, [x * k for x in base], i, j) < target:
            lo = k
        else:
            hi = k
    return (lo + hi) / 2


def won(x):
    x = round(x)
    return f"{x // 10000}억 {x % 10000:,}만원" if x >= 10000 else f"{x:,}만원"


VARS = [("익절 없음", None), ("10% 전부 익절", ((0.10, 0.0),)), ("25% 전부 익절", ((0.25, 0.0),)),
        ("50% 전부 익절", ((0.50, 0.0),)), ("25%에서 절반", ((0.25, 0.5),)), ("50%에서 절반", ((0.50, 0.5),))]
RULES = [("슈퍼트렌드(10·3)", supertrend), ("이동평균 50/200 교차", ma_cross), ("켈트너 돌파(20·1.5)", keltner)]

print(__doc__.split("\n")[0]); print("기준일:", ASOF, "UTC 봉까지\n")
less = {v[0]: 0 for v in VARS[1:]}; total = 0; match_win = 0
for coin in ("BTC", "ETH"):
    mk = {"바이낸스": load(f"{coin}USDT_4h.csv"), "업비트": load(f"KRW-{coin}_4h.csv")}
    a, b = window(mk.values())
    filt = {m: daily_filter(S) for m, S in mk.items()}
    for rn, fn in RULES:
        print(f"■ {coin} · {rn} · {a} ~ {b - datetime.timedelta(days=1)} · 1,000만원")
        res = {}
        for m, S in mk.items():
            base = [x * y for x, y in zip(fn(S), filt[m])]
            for vn, lv in VARS:
                w = base if lv is None else take_profit(S, base, lv)
                res[(m, vn)] = stat(S, run(S, w), a, b)
        for vn, _ in VARS:
            print(f"  {vn:10s} " + " | ".join(f"{m} {won(res[(m, vn)][0])} (가장 깊은 하락 {res[(m, vn)][2] * 100:.1f}%)" for m in mk))
        mt = []
        for m, S in mk.items():
            base = [x * y for x, y in zip(fn(S), filt[m])]
            i, j = res[(m, "익절 없음")][3:5]
            tgt = exposure(S, take_profit(S, base, ((0.10, 0.0),)), i, j)
            k = match_k(S, base, tgt, i, j)
            mm = stat(S, run(S, [x * k for x in base]), a, b)[0]
            match_win += mm > res[(m, "10% 전부 익절")][0]
            mt.append(f"{m} 살 때 비중 {k:.3f} → {won(mm)} (10% 익절 {won(res[(m, '10% 전부 익절')][0])} · 평균 보유 비중 {tgt * 100:.1f}%)")
        print("  익절 없이 보유 비중만 10% 익절 계좌와 똑같이 맞추면: " + " | ".join(mt))
        for m in mk:
            total += 1
            for vn, _ in VARS[1:]:
                less[vn] += res[(m, vn)][0] < res[(m, "익절 없음")][0]
        if coin == "BTC" and rn.startswith("슈퍼트렌드"):
            keep = (mk, filt, fn)
        print()
print(f"■ 익절 없음보다 돈이 적게 남은 경우 ({total}개 = 코인 2 × 봇 규칙 3 × 거래소 2)")
for vn, k in less.items():
    print(f"  {vn}: {k}/{total}")
print(f"■ 10% 익절 계좌와 평균 보유 비중을 똑같이 맞춘 익절 없는 봇이 더 남긴 경우: {match_win}/{total}")

# 거래 하나하나 — 비트코인 슈퍼트렌드 봇(익절 없음)
mk, filt, fn = keep
print("\n■ 비트코인 슈퍼트렌드 봇(익절 없음) 거래 하나하나 — 2017-12-02 이후 진입")
for m, S in mk.items():
    base = [x * y for x, y in zip(fn(S), filt[m])]
    c, o, d, n = S["c"], S["o"], S["d"], S["n"]
    tr, t = [], 1
    while t < n:
        if base[t] > 0 and base[t - 1] == 0:
            e, u = c[t], t
            while u + 1 < n and base[u + 1] > 0:
                u += 1
            ein = o[t + 1] if t + 1 < n else c[t]
            xout = o[u + 1] if u + 1 < n else c[-1]
            best = max(c[t:u + 1]) / e - 1
            hit = next((k for k in range(t, u + 1) if c[k] / e - 1 >= 0.10), None)
            # 익절 계좌가 실제로 판 값: 종가가 +10% 를 처음 넘은 봉의 다음 봉 시가
            tp = o[hit + 1] / ein - 1 if (hit is not None and hit + 1 < n) else None
            tr.append(dict(d=d[t], end=xout / ein - 1, hit=hit is not None, hitd=d[hit] if hit is not None else "", tp=tp))
            t = u + 1
        else:
            t += 1
    tr = [x for x in tr if x["d"] >= "2017-12-02"]
    hit = [x for x in tr if x["hit"]]
    up = sum(x["end"] >= 0.10 for x in hit)
    print(f"  {m}: 거래 {len(tr)}번 · 번 거래 {sum(x['end'] > 0 for x in tr)}번 · 잃은 거래 {sum(x['end'] <= 0 for x in tr)}번")
    print(f"     10% 를 넘긴 거래 {len(hit)}번 — 봇이 10% 보다 높은 값에 판 거래 {up}번 · 10% 아래로 판 거래 {len(hit) - up}번 · 손실로 끝난 거래 {sum(x['end'] < 0 for x in hit)}번")
    more = [x for x in hit if x["end"] > x["tp"]]
    less_ = [x for x in hit if x["end"] < x["tp"]]
    gain = sum(x["end"] - x["tp"] for x in more) * 100
    loss = sum(x["tp"] - x["end"] for x in less_) * 100
    print(f"     익절 계좌가 실제로 판 값과 견주면 — 익절 없는 봇이 더 받은 거래 {len(more)}번 · 익절 계좌가 더 받은 거래 {len(less_)}번"
          f" · 더 받은 몫을 모으면 봇 쪽 {gain:.1f}%포인트 대 익절 쪽 {loss:.1f}%포인트")
    big = max(tr, key=lambda x: x["end"])
    print(f"     가장 크게 번 거래: {big['d'][:10]} 진입 · {big['end'] * 100:.1f}%")
    if m == "바이낸스":
        hs = sorted(hit, key=lambda x: x["end"]); mid = hs[len(hs) // 2]
        dd = (datetime.date.fromisoformat(mid["hitd"][:10]) - datetime.date.fromisoformat(mid["d"][:10])).days
        print(f"     10% 를 넘긴 거래 중 가운데: {mid['d'][:16]} 진입 → {dd}일 뒤 {mid['hitd'][:10]} 10% 넘음 → 봇은 {mid['end'] * 100:.1f}% 에 팜")
print("\n끝.")
