#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 沃社区 · 量化工具箱 —— 单文件策略源码(可直接运行)
import json, urllib.request

def fetch(symbol="ETHUSDT", interval="4h", limit=1000):
    url = f"https://api.binance.com/api/v3/klines?symbol={symbol}&interval={interval}&limit={limit}"
    rows = json.loads(urllib.request.urlopen(url, timeout=30).read())
    return [{"t": r[0], "o": float(r[1]), "h": float(r[2]), "l": float(r[3]),
             "c": float(r[4]), "v": float(r[5])} for r in rows]

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""统一回测引擎 v2 —— 保证金记账 + ATR止损/追踪止损 + 手续费

记账方式(合约/永续):
    equity = cash + qty * (price - entry)
    开/加仓: 只扣手续费; 平仓: cash += 平掉数量 * (exit - entry) - 手续费
不含未来函数: 第 i 根收盘产生的信号, 在第 i+1 根开盘执行。
"""
import math

# atr 见下


def backtest(bars, positions, fee_rate=0.0003, capital=10000.0, sizing=1.0,
             allow_short=True, atr_n=14, stop_atr=0.0, trail_atr=0.0, min_hold=0,
             risk_per_trade=0.0, max_leverage=3.0):
    n = len(bars)
    if n < 50:
        return None
    a = atr(bars, atr_n)

    cash, qty, entry, atr_e, peak = capital, 0.0, 0.0, 0.0, 0.0
    trades, equity, exposure = [], [], 0
    cur_want = 0.0
    fees_total = 0.0
    bars_since = 10 ** 9   # 距离上次调仓的K线数(防抖动)

    def eq_now(price):
        return cash + qty * (price - entry)

    for i in range(1, n):
        b = bars[i]
        op = b["o"]

        # ---------- 1) 止损 / 追踪止损 ----------
        if qty != 0 and stop_atr and atr_e > 0:
            if qty > 0:
                stop = entry - stop_atr * atr_e
                if trail_atr:
                    peak = max(peak, b["h"])
                    stop = max(stop, peak - trail_atr * atr_e)
                if b["l"] <= stop:
                    px = op if op < stop else stop
                    pnl = qty * (px - entry) - qty * px * fee_rate
                    cash += qty * (px - entry) - qty * px * fee_rate
                    fees_total += qty * px * fee_rate
                    trades.append({"side": "多", "entry": entry, "exit": px, "pnl": pnl,
                                   "pnl_pct": (px - entry) / entry, "reason": "止损/追踪", "t": b["t"]})
                    qty, entry, cur_want = 0.0, 0.0, 0.0
            else:
                stop = entry + stop_atr * atr_e
                if trail_atr:
                    peak = min(peak, b["l"]) if peak else b["l"]
                    stop = min(stop, peak + trail_atr * atr_e)
                if b["h"] >= stop:
                    px = op if op > stop else stop
                    pnl = qty * (px - entry) - abs(qty) * px * fee_rate
                    cash += qty * (px - entry) - abs(qty) * px * fee_rate
                    fees_total += abs(qty) * px * fee_rate
                    trades.append({"side": "空", "entry": entry, "exit": px, "pnl": pnl,
                                   "pnl_pct": (entry - px) / entry, "reason": "止损/追踪", "t": b["t"]})
                    qty, entry, cur_want = 0.0, 0.0, 0.0

        if qty != 0:
            exposure += 1

        # ---------- 2) 目标仓位调整 ----------
        target = positions[i - 1]
        want = sizing * max(-1.0, min(1.0, float(target)))
        if not allow_short and want < 0:
            want = 0.0

        equity_here = max(eq_now(op), 1e-9)
        # 只在“目标仓位明显变化”时调仓, 避免每根K线因权益漂移而反复交易(手续费黑洞)
        if (abs(want - cur_want) >= 0.05 or (want == 0.0) != (cur_want == 0.0)) and bars_since >= min_hold:
            if risk_per_trade and stop_atr and a[i - 1] > 0:
                # 风险定额仓位: 每次止损亏损≈equity*risk_per_trade
                unit = equity_here * risk_per_trade / (stop_atr * a[i - 1])
                target_qty = (1.0 if want > 0 else -1.0) * abs(want) * min(unit, equity_here * max_leverage / op)
            else:
                target_qty = want * equity_here / op
            delta = target_qty - qty
        else:
            delta = 0.0
        if abs(delta) * op > max(1.0, equity_here * 1e-4):
            # 先结算被平掉的部分
            if qty * delta < 0:
                closed = min(abs(delta), abs(qty)) * (1 if qty > 0 else -1)
                pnl = closed * (op - entry) - abs(closed) * op * fee_rate
                cash += closed * (op - entry) - abs(closed) * op * fee_rate
                fees_total += abs(closed) * op * fee_rate
                trades.append({"side": "多" if qty > 0 else "空", "entry": entry, "exit": op,
                               "pnl": pnl, "pnl_pct": (op - entry) / entry if qty > 0 else (entry - op) / entry,
                               "reason": "信号平仓", "t": b["t"]})
                qty -= closed
                delta = target_qty - qty
            # 再开/加仓
            if abs(delta) * op > max(1.0, equity_here * 1e-4):
                cash -= abs(delta) * op * fee_rate
                fees_total += abs(delta) * op * fee_rate
                if qty == 0 or (qty > 0) == (delta > 0):
                    entry = (entry * abs(qty) + op * abs(delta)) / (abs(qty) + abs(delta))
                else:
                    entry = op
                qty += delta
                atr_e = a[i - 1]
                peak = b["h"] if qty > 0 else b["l"]
            cur_want = want
            bars_since = 0

        bars_since += 1
        equity.append(cash + qty * (b["c"] - entry))

    # 收盘平掉剩余仓位
    if qty != 0:
        px = bars[-1]["c"]
        pnl = qty * (px - entry) - abs(qty) * px * fee_rate
        cash += qty * (px - entry) - abs(qty) * px * fee_rate
        trades.append({"side": "多" if qty > 0 else "空", "entry": entry, "exit": px, "pnl": pnl,
                       "pnl_pct": (px - entry) / entry if qty > 0 else (entry - px) / entry,
                       "reason": "回测结束", "t": bars[-1]["t"]})
        qty = 0.0

    eq = [capital] + equity
    peak_eq, mdd = eq[0], 0.0
    for v in eq:
        peak_eq = max(peak_eq, v)
        if peak_eq > 0:
            mdd = max(mdd, (peak_eq - v) / peak_eq)

    rets = [eq[i] / eq[i - 1] - 1 for i in range(1, len(eq)) if eq[i - 1] > 0]
    mean = sum(rets) / len(rets) if rets else 0.0
    var = sum((x - mean) ** 2 for x in rets) / len(rets) if rets else 0.0
    sd = var ** 0.5
    bars_per_year = len(eq) / max((bars[-1]["t"] - bars[0]["t"]) / 86400000.0, 1) * 365
    sharpe = (mean / sd * math.sqrt(bars_per_year)) if sd > 0 else 0.0

    wins = [t for t in trades if t["pnl"] > 0]
    losses = [t for t in trades if t["pnl"] <= 0]
    gp = sum(t["pnl"] for t in wins)
    gl = -sum(t["pnl"] for t in losses)
    days = max((bars[-1]["t"] - bars[0]["t"]) / 86400000.0, 1)

    return {
        "final_equity": eq[-1],
        "total_return": eq[-1] / capital - 1,
        "max_drawdown": -mdd,
        "sharpe": sharpe,
        "trades": len(trades),
        "win_rate": len(wins) / len(trades) if trades else 0.0,
        "profit_factor": (gp / gl) if gl > 0 else (99.0 if gp > 0 else 0.0),
        "avg_win": (gp / len(wins)) if wins else 0.0,
        "avg_loss": (gl / len(losses)) if losses else 0.0,
        "exposure": exposure / max(len(eq), 1),
        "days": days,
        "return_per_year": (eq[-1] / capital) ** (365.0 / days) - 1 if days > 1 and eq[-1] > 0 else -1.0,
        "fees_total": fees_total,
        "equity_sample": eq[:: max(1, len(eq) // 300)],
        "trade_sample": trades[-150:],
    }


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""策略库 —— 6 套自动化量化策略 (统一下单接口, 纯Python无依赖)

约定:
  每个策略实现 positions(bars, p) -> list[int]
  返回每根K线的"目标仓位"(1=满多, 0=空仓, -1=满空), 该值在K线收盘时确定, 下一根开盘执行。
  止损/止盈/追踪止损由回测引擎统一处理, 策略本身只管方向。

所有策略都带明确参数与风控, 参数集中在 params() 里, 便于调参。
"""


# ---------------- 指标 (纯python) ----------------
def ema(vals, n):
    k = 2.0 / (n + 1)
    out, prev = [], None
    for v in vals:
        prev = v if prev is None else v * k + prev * (1 - k)
        out.append(prev)
    return out


def sma(vals, n):
    out, s = [], 0.0
    for i, v in enumerate(vals):
        s += v
        if i >= n:
            s -= vals[i - n]
        out.append(s / min(i + 1, n))
    return out


def rsi(vals, n=14):
    out, gain, loss = [50.0], 0.0, 0.0
    for i in range(1, len(vals)):
        ch = vals[i] - vals[i - 1]
        g, l = max(ch, 0.0), max(-ch, 0.0)
        if i <= n:
            gain = (gain * (i - 1) + g) / i
            loss = (loss * (i - 1) + l) / i
        else:
            gain = (gain * (n - 1) + g) / n
            loss = (loss * (n - 1) + l) / n
        out.append(100.0 if loss == 0 else 100 - 100 / (1 + gain / loss))
    return out


def atr(bars, n=14):
    trs = []
    for i, b in enumerate(bars):
        if i == 0:
            trs.append(b["h"] - b["l"])
        else:
            pc = bars[i - 1]["c"]
            trs.append(max(b["h"] - b["l"], abs(b["h"] - pc), abs(b["l"] - pc)))
    return sma(trs, n)


def stddev(vals, n):
    out = []
    for i in range(len(vals)):
        w = vals[max(0, i - n + 1):i + 1]
        m = sum(w) / len(w)
        out.append((sum((x - m) ** 2 for x in w) / len(w)) ** 0.5)
    return out


# ---------------- 策略 1: 双均线趋势跟踪 ----------------
class TrendEMA:
    key = "trend_ema"
    name = "趋势跟踪 · 双均线"
    desc = ("EMA50/EMA200 判趋势方向, 价格回踩EMA20且趋势向上做多; "
            "趋势结束或ATR追踪止损离场。适合大波动趋势行情。")
    market = "趋势"
    params = {"fast": 50, "slow": 200, "entry": 20, "stop_atr": 2.5, "trail_atr": 3.0}

    @staticmethod
    def positions(bars, p):
        closes = [b["c"] for b in bars]
        f, s, e = ema(closes, p["fast"]), ema(closes, p["slow"]), ema(closes, p["entry"])
        pos = []
        for i in range(len(bars)):
            if i < 2:
                pos.append(0)
                continue
            if f[i] > s[i] and closes[i] > e[i]:
                pos.append(1)
            elif f[i] < s[i] and closes[i] < e[i]:
                pos.append(-1)
            else:
                pos.append(0)
        return pos


# ---------------- 策略 2: 唐奇安通道突破 ----------------
class DonchianBreakout:
    key = "donchian"
    name = "突破 · 唐奇安通道"
    desc = ("价格突破近N根K线最高价做多、跌破最低价做空, "
            "反向通道中点离场。经典海龟式突破, 抓单边起爆。")
    market = "趋势"
    params = {"entry_n": 55, "exit_n": 20, "stop_atr": 3.0, "trail_atr": 4.0}

    @staticmethod
    def positions(bars, p):
        n_in, n_out = p["entry_n"], p["exit_n"]
        pos, cur = [], 0
        for i in range(len(bars)):
            if i < n_in:
                pos.append(0)
                continue
            hh = max(b["h"] for b in bars[i - n_in:i])
            ll = min(b["l"] for b in bars[i - n_in:i])
            c = bars[i]["c"]
            if cur <= 0 and c > hh:
                cur = 1
            elif cur >= 0 and c < ll:
                cur = -1
            if cur == 1:
                mid = (max(b["h"] for b in bars[i - n_out:i]) + min(b["l"] for b in bars[i - n_out:i])) / 2
                if c < mid:
                    cur = 0
            elif cur == -1:
                mid = (max(b["h"] for b in bars[i - n_out:i]) + min(b["l"] for b in bars[i - n_out:i])) / 2
                if c > mid:
                    cur = 0
            pos.append(cur)
        return pos


# ---------------- 策略 3: 布林带 + RSI 均值回归 ----------------
class MeanRevRSI:
    key = "meanrev"
    name = "均值回归 · 布林带+RSI"
    desc = ("价格触及布林带外轨且RSI超卖/超买时反向入场, "
            "回到中轨离场。适合震荡市, 逆势但带硬止损。")
    market = "震荡"
    params = {"bb_n": 20, "bb_k": 2.0, "rsi_n": 14, "rsi_lo": 30, "rsi_hi": 70,
              "stop_atr": 2.0, "trail_atr": 0}

    @staticmethod
    def positions(bars, p):
        closes = [b["c"] for b in bars]
        mid = sma(closes, p["bb_n"])
        sd = stddev(closes, p["bb_n"])
        r = rsi(closes, p["rsi_n"])
        pos, cur = [], 0
        for i in range(len(bars)):
            if i < p["bb_n"] + 2:
                pos.append(0)
                continue
            up, lo = mid[i] + p["bb_k"] * sd[i], mid[i] - p["bb_k"] * sd[i]
            c = closes[i]
            if cur == 0:
                if c < lo and r[i] < p["rsi_lo"]:
                    cur = 1
                elif c > up and r[i] > p["rsi_hi"]:
                    cur = -1
            elif cur == 1 and c >= mid[i]:
                cur = 0
            elif cur == -1 and c <= mid[i]:
                cur = 0
            pos.append(cur)
        return pos


# ---------------- 策略 4: 区间网格 ----------------
class GridRange:
    key = "grid"
    name = "网格 · 区间高抛低吸"
    desc = ("用最近周期的高低点划定区间, 把区间均分为若干格, "
            "跌一格加仓、涨一格减仓。适合横盘震荡, 靠低手续费吃差价。")
    market = "震荡"
    params = {"lookback": 240, "grids": 10, "stop_atr": 5.0, "trail_atr": 0}

    @staticmethod
    def positions(bars, p):
        n, g = p["lookback"], p["grids"]
        pos = []
        for i in range(len(bars)):
            if i < n:
                pos.append(0)
                continue
            w = bars[i - n:i]
            hi = max(b["h"] for b in w)
            lo = min(b["l"] for b in w)
            if hi <= lo:
                pos.append(0)
                continue
            c = bars[i]["c"]
            frac = (c - lo) / (hi - lo)          # 0=底部 1=顶部
            lvl = int(round((1 - frac) * g))     # 越便宜仓位越高
            pos.append(lvl / float(g))
        return pos


# ---------------- 策略 5: 动量突破混合(ATR过滤) ----------------
class MomentumATR:
    key = "momentum"
    name = "动量 · ATR波动过滤"
    desc = ("动量为正(近N根涨幅>0)且波动率处于健康区间时顺势持有, "
            "波动率过热(ATR占比过高)时空仓避险。适合加速行情。")
    market = "趋势"
    params = {"mom_n": 24, "atr_n": 24, "atr_hi": 0.035, "stop_atr": 2.5, "trail_atr": 2.5}

    @staticmethod
    def positions(bars, p):
        closes = [b["c"] for b in bars]
        a = atr(bars, p["atr_n"])
        pos = []
        for i in range(len(bars)):
            if i < max(p["mom_n"], p["atr_n"]) + 1:
                pos.append(0)
                continue
            mom = closes[i] / closes[i - p["mom_n"]] - 1
            vol = a[i] / closes[i]
            if vol > p["atr_hi"]:
                pos.append(0)
            elif mom > 0.005:
                pos.append(1)
            elif mom < -0.005:
                pos.append(-1)
            else:
                pos.append(0)
        return pos


# ---------------- 策略 6: 双时间框架顺势回踩 ----------------
class PullbackTrend:
    key = "pullback"
    name = "回踩入场 · 多时间框架"
    desc = ("1小时以下用EMA20判断短趋势, 4小时级别趋势同向时, "
            "回调到EMA20附近入场。比直接追突破的入场价更好。")
    market = "趋势"
    params = {"fast": 20, "slow": 60, "atr_n": 14, "stop_atr": 2.0, "trail_atr": 3.0}

    @staticmethod
    def positions(bars, p):
        closes = [b["c"] for b in bars]
        f, s = ema(closes, p["fast"]), ema(closes, p["slow"])
        pos = []
        for i in range(len(bars)):
            if i < p["slow"] + 2:
                pos.append(0)
                continue
            c = closes[i]
            near = abs(c - f[i]) / f[i] < 0.01
            if f[i] > s[i] and (near or c > f[i]):
                pos.append(1)
            elif f[i] < s[i] and (near or c < f[i]):
                pos.append(-1)
            else:
                pos.append(0)
        return pos


ALL = [TrendEMA, DonchianBreakout, MeanRevRSI, GridRange, MomentumATR, PullbackTrend]
BY_KEY = {s.key: s for s in ALL}


if __name__ == "__main__":
    bars = fetch()
    p = GridRange.params
    pos = GridRange.positions(bars, p)
    r = backtest(bars, pos, fee_rate=0.0003, stop_atr=p.get("stop_atr", 0),
                 trail_atr=p.get("trail_atr", 0), min_hold=6, risk_per_trade=0.01)
    print("网格 · 区间高抛低吸 回测(最近1000根4H): 收益 %.1f%% | 回撤 %.1f%% | 交易 %d | 胜率 %.0f%%"
          % (r["total_return"] * 100, r["max_drawdown"] * 100, r["trades"], r["win_rate"] * 100))
