""" 多策略多级别分类回测报告 日内交易 (30m/1h) | 中线交易 (2h/4h/6h) | 长线交易 (1d/1w) 策略:牛熊自适应 / MACD / EMA双均线 / RSI / 布林突破 币种:BTC / ETH / BNB / SOL 数据:日内近两年,中线+长线全量 用法: source .venv/bin/activate && python example/multi_strategy_report.py """ import asyncio import sys from collections import defaultdict from datetime import datetime, timezone from pathlib import Path from typing import Optional _project_root = Path(__file__).resolve().parent.parent.parent if str(_project_root) not in sys.path: sys.path.insert(0, str(_project_root)) from engine.common.base import BaseStrategy, Signal, StrategyConfig from engine.common.models import Kline from engine.common.config import config from engine.backtest import BacktestEngine, BacktestConfig, BacktestResult from engine.indicators import macd, ema, rsi, bollinger, atr # ═══════════════════════════════════════════ # 策略定义 # ═══════════════════════════════════════════ # --- MACD --- class MacdConfig(StrategyConfig): fast: int = 12; slow: int = 26; signal: int = 9 class MacdStrategy(BaseStrategy): strategy_type = "macd" def __init__(self, c: MacdConfig): super().__init__(c); self.cfg = c; self._c: list[float] = [] async def on_kline(self, k: Kline) -> Optional[Signal]: self._c.append(k.close) ml, sl, _ = macd(self._c, self.cfg.fast, self.cfg.slow, self.cfg.signal) if len(ml) < 3 or ml[-1] == 0: return None if ml[-2] <= sl[-2] and ml[-1] > sl[-1]: return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD金叉", timestamp=k.open_time) if ml[-2] >= sl[-2] and ml[-1] < sl[-1]: return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD死叉", timestamp=k.open_time) return None # --- EMA双均线 --- class EmaCrossConfig(StrategyConfig): fast: int = 20; slow: int = 50 class EmaCrossStrategy(BaseStrategy): strategy_type = "ema_cross" def __init__(self, c: EmaCrossConfig): super().__init__(c); self.cfg = c; self._c: list[float] = [] async def on_kline(self, k: Kline) -> Optional[Signal]: self._c.append(k.close) f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow) if len(f) < 3 or f[-1] == 0 or s[-1] == 0: return None if f[-2] <= s[-2] and f[-1] > s[-1]: return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉", timestamp=k.open_time) if f[-2] >= s[-2] and f[-1] < s[-1]: return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time) return None # --- RSI --- class RsiConfig(StrategyConfig): period: int = 14; oversold: float = 30.0; overbought: float = 70.0 class RsiStrategy(BaseStrategy): strategy_type = "rsi" def __init__(self, c: RsiConfig): super().__init__(c); self.cfg = c; self._c: list[float] = []; self._in = False async def on_kline(self, k: Kline) -> Optional[Signal]: self._c.append(k.close) v = rsi(self._c, self.cfg.period)[-1] if v == 0: return None if v < self.cfg.oversold and not self._in: self._in = True return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"RSI超卖({v:.1f})", timestamp=k.open_time) if v > self.cfg.overbought and self._in: self._in = False return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买({v:.1f})", timestamp=k.open_time) return None # --- 布林突破 --- class BollConfig(StrategyConfig): period: int = 20; std: float = 2.0 class BollStrategy(BaseStrategy): strategy_type = "boll" def __init__(self, c: BollConfig): super().__init__(c); self.cfg = c; self._c: list[float] = [] async def on_kline(self, k: Kline) -> Optional[Signal]: self._c.append(k.close) upper, mid, lower = bollinger(self._c, self.cfg.period, self.cfg.std) if len(upper) < 3 or mid[-1] == 0: return None p, md = k.close, mid[-1] pp, pm = self._c[-2], mid[-2] if pp <= pm and p > md and upper[-1] > 0 and mid[-1] > mid[-2]: return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"突破BB中轨", timestamp=k.open_time) if pp >= pm and p < md: return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"跌破BB中轨", timestamp=k.open_time) return None # --- 牛熊自适应 (多空双向) --- class RegimeDetector: def __init__(self): self._ath = 0.0 def update_ath(self, price: float): if price > self._ath: self._ath = price def ema200_slope(self, closes, idx): if idx < 210: return "unknown" e200 = ema(closes, 200) if e200[idx - 20] == 0: return "unknown" slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20] if slope > 0.002: return "bull" if slope < -0.002: return "bear" return "sideways" def price_vs_ema200(self, closes, idx): if idx < 210: return "unknown" e200 = ema(closes, 200) return "bull" if closes[idx] > e200[idx] else "bear" def ath_drawdown(self, closes, idx): if self._ath == 0: return "unknown" dd = (closes[idx] - self._ath) / self._ath if dd > -0.15: return "bull" if dd < -0.35: return "bear" return "sideways" def detect(self, closes, idx): r1 = self.ema200_slope(closes, idx); r2 = self.price_vs_ema200(closes, idx); r3 = self.ath_drawdown(closes, idx) b = sum(1 for r in [r1,r2,r3] if r=="bull"); br = sum(1 for r in [r1,r2,r3] if r=="bear") if b >= 2: return "bull" if br >= 2: return "bear" return "sideways" class RegimeConfig(StrategyConfig): fast: int = 10; slow: int = 50; atr_stop: float = 2.5 class RegimeStrategy(BaseStrategy): strategy_type = "regime" def __init__(self, c: RegimeConfig): super().__init__(c); self.cfg = c self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = [] self._detector = RegimeDetector(); self._side = ""; self._hp = 0.0; self._lp = float('inf') async def on_kline(self, k: Kline) -> Optional[Signal]: self._c.append(k.close); self._h.append(k.high); self._l.append(k.low) self._detector.update_ath(k.close) n = len(self._c) if n < 220: return None regime = self._detector.detect(self._c, n - 1) f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow) a = atr(self._h, self._l, self._c, 14) cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2] if cf == 0 or cs == 0 or ca == 0: return None golden = pf <= ps and cf > cs; death = pf >= ps and cf < cs if self._side == "long": self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca if death or k.close < stop or regime == "bear": self._side = "" return Signal(symbol=self.cfg.symbol, side="SELL", reason="平多", timestamp=k.open_time) elif self._side == "short": self._lp = min(self._lp, k.low); stop = self._lp + self.cfg.atr_stop * ca if golden or k.close > stop or regime == "bull": self._side = "" return Signal(symbol=self.cfg.symbol, side="BUY", reason="平空", timestamp=k.open_time) else: if regime == "bull" and golden: self._side = "long"; self._hp = k.close return Signal(symbol=self.cfg.symbol, side="BUY", reason="牛市金叉", timestamp=k.open_time) elif regime == "bear" and death: self._side = "short"; self._lp = k.close return Signal(symbol=self.cfg.symbol, side="SELL", reason="熊市死叉", timestamp=k.open_time) return None # ═══════════════════════════════════════════ # 注册 # ═══════════════════════════════════════════ STRATEGY_REGISTRY = { "牛熊自适应": (RegimeStrategy, RegimeConfig, "regime"), "MACD": (MacdStrategy, MacdConfig, "trend"), "EMA双均线": (EmaCrossStrategy, EmaCrossConfig, "trend"), "RSI超卖反弹": (RsiStrategy, RsiConfig, "reversal"), "布林突破": (BollStrategy, BollConfig, "breakout"), } SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"] REGIME_PARAMS = { "BTCUSDT": (10, 50), "ETHUSDT": (10, 75), "BNBUSDT": (20, 50), "SOLUSDT": (30, 50), } CATEGORIES = { "日内交易": { "intervals": ["30m", "1h"], "strategies": ["MACD", "EMA双均线", "RSI超卖反弹", "布林突破", "牛熊自适应"], "data": "recent", }, "中线交易": { "intervals": ["2h", "4h", "6h"], "strategies": ["牛熊自适应", "MACD", "EMA双均线", "RSI超卖反弹", "布林突破"], "data": "full", }, "长线交易": { "intervals": ["1d", "1w"], "strategies": ["牛熊自适应", "MACD", "EMA双均线"], "data": "full", }, } RECENT_START = datetime(2024, 6, 1) RECENT_END = datetime(2026, 6, 12) FULL_DEFAULT = datetime(2017, 1, 1) async def run_simple(symbol, interval, strategy_cls, strategy_cfg, start, end) -> BacktestResult | None: """使用 BacktestEngine(只做多)运行回测""" bt = BacktestConfig( symbol=symbol, interval=interval, start_time=start, end_time=end, initial_capital=10_000.0, warmup_bars=100, ) strategy_cfg.symbol = symbol engine = BacktestEngine(bt, db_config=config.db) return await engine.run(strategy_cls, strategy_cfg) async def run_regime(symbol, interval, start, end) -> BacktestResult | None: """使用 LongShortEngine(多空双向)运行牛熊自适应策略""" from engine.example.long_short import LongShortEngine fast, slow = REGIME_PARAMS.get(symbol, (10, 50)) sc = RegimeConfig(symbol=symbol, fast=fast, slow=slow) bt = BacktestConfig( symbol=symbol, interval=interval, start_time=start, end_time=end, initial_capital=10_000.0, warmup_bars=250, ) engine = LongShortEngine(bt, db_config=config.db) return await engine.run(RegimeStrategy, sc) # ═══════════════════════════════════════════ # 主流程 # ═══════════════════════════════════════════ async def main(): sem = asyncio.Semaphore(2) # 并发控制 async def with_sem(coro): async with sem: return await coro out: list[str] = [] def w(line=""): out.append(line); print(line) # 收集所有结果: (category, interval, symbol, strategy_name, result) all_results: list[dict] = [] w("# 多策略多级别分类回测报告") w() w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}") w(f"> 初始资金:10,000 USDT | 手续费:0.1% | 滑点:0.05%") w(f"> 日内交易使用近两年数据 (2024.06-2026.06),中线/长线使用全量历史数据") w() for cat_name, cat_cfg in CATEGORIES.items(): intervals = cat_cfg["intervals"] strategy_names = cat_cfg["strategies"] use_full = cat_cfg["data"] == "full" w(f"## {cat_name} ({'/'.join(intervals)})") w() for interval in intervals: w(f"### {interval}") w() # 表头 cols = "| 币种 |" sep = "|------|" for sn in strategy_names: cols += f" {sn} 收益% | {sn} 夏普 | {sn} 回撤% | {sn} 交易 | {sn} 胜率% |" sep += "--------|------|------|------|------|" w(cols) w(sep) for symbol in SYMBOLS: row = f"| {symbol:<10} |" tasks = [] for sn in strategy_names: if sn == "牛熊自适应": # 牛熊自适应使用 LongShortEngine if use_full: start, end = FULL_DEFAULT, RECENT_END else: start, end = RECENT_START, RECENT_END tasks.append((sn, with_sem(run_regime(symbol, interval, start, end)))) else: cls, cfg_cls, _ = STRATEGY_REGISTRY[sn] if use_full: start, end = FULL_DEFAULT, RECENT_END else: start, end = RECENT_START, RECENT_END cfg = cfg_cls() tasks.append((sn, with_sem(run_simple(symbol, interval, cls, cfg, start, end)))) # 并行执行当前币种的所有策略 results_list = await asyncio.gather(*[t[1] for t in tasks], return_exceptions=True) for (sn, _), r in zip(tasks, results_list): if isinstance(r, Exception): row += f" ERR | — | — | — | — |" print(f" ✗ {symbol} {interval} {sn}: {r}") elif r is None: row += f" N/A | — | — | — | — |" else: m = r.metrics row += f" {m.total_return_pct:>+6.1f}% | {m.sharpe_ratio:>4.2f} | {m.max_drawdown_pct:>5.1f}% | {m.total_trades:>4} | {m.win_rate*100:>4.1f}% |" all_results.append({ "category": cat_name, "interval": interval, "symbol": symbol, "strategy": sn, "return": m.total_return_pct, "sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct, "trades": m.total_trades, "win": m.win_rate, "pf": m.profit_factor, }) w(row) w() # ═══════════════════════════════════════ # 汇总分析 # ═══════════════════════════════════════ w("---") w() w("## 汇总分析") w() for cat_name in CATEGORIES: cat_results = [r for r in all_results if r["category"] == cat_name] if not cat_results: continue w(f"### {cat_name} — 各币种最优策略") w() w("| 币种 | 最佳周期 | 最佳策略 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% |") w("|------|---------|---------|--------|------|------|------|------|") for symbol in SYMBOLS: candidates = [r for r in cat_results if r["symbol"] == symbol] if not candidates: continue best = max(candidates, key=lambda x: x["sharpe"]) w(f"| {symbol} | {best['interval']} | {best['strategy']} | {best['return']:>+7.1f}% | {best['sharpe']:.2f} | {best['dd']:.1f}% | {best['trades']} | {best['win']*100:.1f}% |") w() # 全市场最优 w("### 全市场 TOP 10(按夏普排序)") w() w("| 排名 | 分类 | 币种 | 周期 | 策略 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% |") w("|------|------|------|------|------|--------|------|------|------|------|") ranked = sorted(all_results, key=lambda x: x["sharpe"], reverse=True) for i, r in enumerate(ranked[:10]): w(f"| {i+1} | {r['category']} | {r['symbol']} | {r['interval']} | {r['strategy']} | {r['return']:>+7.1f}% | {r['sharpe']:.2f} | {r['dd']:.1f}% | {r['trades']} | {r['win']*100:.1f}% |") w() # 按策略类型汇总 w("### 各策略类型平均表现") w() w("| 策略 | 分类 | 平均收益% | 平均夏普 | 平均回撤% | 平均胜率% |") w("|------|------|---------|---------|---------|---------|") for sn in ["牛熊自适应", "MACD", "EMA双均线", "RSI超卖反弹", "布林突破"]: for cat_name in CATEGORIES: sr = [r for r in all_results if r["strategy"] == sn and r["category"] == cat_name] if not sr: continue avg_ret = sum(r["return"] for r in sr) / len(sr) avg_sh = sum(r["sharpe"] for r in sr) / len(sr) avg_dd = sum(r["dd"] for r in sr) / len(sr) avg_win = sum(r["win"] for r in sr) / len(sr) w(f"| {sn} | {cat_name} | {avg_ret:>+7.1f}% | {avg_sh:.2f} | {avg_dd:.1f}% | {avg_win*100:.1f}% |") w() # 写出文件 out_path = Path(__file__).resolve().parent.parent / "backtest" / "MULTI_STRATEGY_REPORT.md" with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(out) + "\n") print(f"\n✓ 报告已保存到: {out_path}") if __name__ == "__main__": asyncio.run(main())