# -*- coding: utf-8 -*-
"""AI가 짜 준 자동매매 봇 5개 — 업비트 4시간봉 백테스트 재현 코드
영상: 「같은 AI가 짜준 자동매매 봇인데 5.8억과 1.2만원」 (AlgoLab, 2026-09)

필요한 것: Python 3.10+ · requests · numpy   (pip install requests numpy)
실행:      python ai_bot_backtest.py
           업비트 공개 API에서 4시간봉을 내려받아(약 2분) 표를 출력합니다.

ASOF: 데이터를 2026-08-31 까지로 자릅니다 — 영상과 같은 숫자가 나오게 하기 위한
      기준일입니다. 최신까지 보려면 ASOF = None 으로 바꾸세요(숫자는 영상과 달라집니다).
      업비트 캔들 페이지 경계 때문에 '그냥 보유' 등 일부 값이 1% 미만으로 다를 수 있습니다.

규칙(영상과 동일):
 · 전략 5개는 AI(클로드)가 초보자 프롬프트에 준 것 그대로, 기본 파라미터 그대로.
 · 신호는 봉 종가에서 확정 → 다음 봉부터 반영(미래참조 방지). 숏 없음, 현금 무수익.
 · 비용: 포지션이 바뀔 때마다 변화분 × 편도 0.1%.
 · v2 = 같은 신호 × 추세 필터(종가 > 1200봉 이동평균 ≈ 200일선) — AI가 준 개선안.
한계: 과거 데이터 백테스트라 미래를 보장하지 않습니다. 슬리피지·세금 미반영.
"""
import datetime
import time

import numpy as np
import requests

ASOF = datetime.date(2026, 8, 31)          # 영상 기준일. None 이면 오늘까지
COST = 0.001                               # 편도 0.1%
FILTER_N = 1200                            # 4시간봉 1200개 = 200일
START = 1000                               # 만원


# ── 업비트 4시간봉 내려받기 (공개 API, 키 불필요) ───────────────────────────
def fetch_upbit_4h(market):
    url = "https://api.upbit.com/v1/candles/minutes/240"
    to = None
    rows = []
    while True:
        params = {"market": market, "count": 200}
        if to:
            params["to"] = to
        r = requests.get(url, params=params, timeout=10)
        r.raise_for_status()
        batch = r.json()
        if not batch:
            break
        rows += batch
        to = batch[-1]["candle_date_time_utc"] + "Z"
        time.sleep(0.11)                    # 공개 API 예의
        if len(batch) < 200:
            break
    rows.sort(key=lambda x: x["candle_date_time_kst"])
    o = np.array([x["opening_price"] for x in rows], float)
    h = np.array([x["high_price"] for x in rows], float)
    l = np.array([x["low_price"] for x in rows], float)
    c = np.array([x["trade_price"] for x in rows], float)
    d = [x["candle_date_time_kst"][:10] for x in rows]
    if ASOF:                                # 캐시 유무와 무관하게 항상 같은 기준으로 자른다
        keep = [i for i, x in enumerate(d) if x <= ASOF.isoformat()]
        o, h, l, c = o[keep], h[keep], l[keep], c[keep]
        d = [d[i] for i in keep]
    return d, o, h, l, c


# ── 지표 ────────────────────────────────────────────────────────────────────
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:
        from numpy.lib.stride_tricks import sliding_window_view as swv
        out[n - 1:] = swv(x, n).std(axis=1)
    return out


def ffill_signal(buy, sell, warm, n):
    s = np.full(n, np.nan)
    s[sell] = 0.0
    s[buy] = 1.0
    s[:warm] = 0.0
    idx = np.arange(n)
    has = ~np.isnan(s)
    last = np.maximum.accumulate(np.where(has, idx, 0))
    out = s[last]
    out[np.isnan(out)] = 0.0
    return out


# ── AI가 준 전략 5개 (기본값 그대로) ────────────────────────────────────────
def ai_rsi(o, h, l, c, n=14, lo=30, hi=70):
    d = np.diff(c, prepend=c[0])
    up = np.where(d > 0, d, 0.0)
    dn = np.where(d < 0, -d, 0.0)
    au, ad = ema(up, n), ema(dn, n)
    rsi = 100 - 100 / (1 + au / np.where(ad == 0, 1e-9, ad))
    return ffill_signal(rsi < lo, rsi > hi, n + 1, len(c))


def ai_goldencross(o, h, l, c, fast=5, slow=20):
    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 ai_macd(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 ai_bollinger(o, h, l, c, n=20, k=2.0):
    ma, sd = sma(c, n), rstd(c, n)
    buy = (c < ma - k * sd) & ~np.isnan(sd)
    sell = (c > ma + k * sd) & ~np.isnan(sd)
    return ffill_signal(buy, sell, n + 1, len(c))


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


STRATS = {"RSI 반등": ai_rsi, "골든크로스": ai_goldencross, "MACD": ai_macd,
          "볼린저 역추세": ai_bollinger, "변동성 돌파": ai_volbreak}


# ── 평가 ────────────────────────────────────────────────────────────────────
def evaluate(sig, ret, d, ppy, warm):
    pos = np.r_[0.0, sig[:-1]]
    g = (1 + pos * ret) * (1 - COST * np.abs(np.diff(np.r_[0.0, pos])))
    gg = g[warm:]
    eq = np.cumprod(gg)
    yrs = len(gg) / ppy
    cagr = eq[-1] ** (1 / yrs) - 1 if eq[-1] > 0 else -1.0
    mdd = float((eq / np.maximum.accumulate(eq) - 1).min())
    return eq[-1], cagr, mdd


def main():
    for market, label in [("KRW-BTC", "비트코인"), ("KRW-ETH", "이더리움")]:
        print(f"\n{label} ({market}) 4시간봉 내려받는 중...")
        d, o, h, l, c = fetch_upbit_4h(market)
        n = len(c)
        ret = np.zeros(n)
        ret[1:] = c[1:] / c[:-1] - 1
        yrs_all = (datetime.date.fromisoformat(d[-1]) -
                   datetime.date.fromisoformat(d[0])).days / 365.25
        ppy = n / yrs_all
        warm = FILTER_N + 1
        filt = np.zeros(n)
        ma = sma(c, FILTER_N)
        m = ~np.isnan(ma)
        filt[m] = (c[m] > ma[m]).astype(float)

        print(f"  {n:,}봉 · {d[warm]} ~ {d[-1]}")
        print(f"  {'전략':<14}{'최종(만원)':>12}{'연복리':>9}{'최대낙폭':>9}")
        mult, cagr, mdd = evaluate(np.ones(n), ret, d, ppy, warm)
        print(f"  {'그냥 보유':<14}{START*mult:>12,.0f}{cagr:>9.1%}{mdd:>9.1%}")
        for name, fn in STRATS.items():
            sig = fn(o, h, l, c)
            for tag, s in [("v1 " + name, sig), ("v2 " + name + "+필터", sig * filt)]:
                mult, cagr, mdd = evaluate(s, ret, d, ppy, warm)
                print(f"  {tag:<14}{START*mult:>12,.1f}{cagr:>9.1%}{mdd:>9.1%}")


if __name__ == "__main__":
    main()
