""" 策略对比回测 — 4 个策略 × 4 个币种 策略: 1. MACD 金叉死叉 — MACD(12,26,9) 金叉买入,死叉卖出 2. EMA 双均线 — EMA20 上穿 EMA50 买入,下穿卖出 3. RSI 超卖反弹 — RSI(14)<30 买入,RSI>70 卖出 4. 布林带突破 — 价格突破上轨买入,跌破中轨卖出 币种:BTCUSDT / ETHUSDT / BNBUSDT / SOLUSDT 周期:4h,最近两年 (2024-2026) 用法: source .venv/bin/activate && python example/strategy_battle.py """ import asyncio import sys 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 # ════════════════════════════════════════════════════════ # 策略 1: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._closes: list[float] = [] async def on_kline(self, k: Kline) -> Optional[Signal]: self._closes.append(k.close) macd_line, sig_line, _ = macd(self._closes, self.cfg.fast, self.cfg.slow, self.cfg.signal) if len(macd_line) < 3: return None cur_m, cur_s = macd_line[-1], sig_line[-1] prev_m, prev_s = macd_line[-2], sig_line[-2] if cur_m == 0: return None if prev_m <= prev_s and cur_m > cur_s: return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD金叉", timestamp=k.open_time) if prev_m >= prev_s and cur_m < cur_s: return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD死叉", timestamp=k.open_time) return None # ════════════════════════════════════════════════════════ # 策略 2: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._closes: list[float] = [] async def on_kline(self, k: Kline) -> Optional[Signal]: self._closes.append(k.close) fast = ema(self._closes, self.cfg.fast) slow = ema(self._closes, self.cfg.slow) if len(fast) < 3 or fast[-1] == 0 or slow[-1] == 0: return None if fast[-2] <= slow[-2] and fast[-1] > slow[-1]: return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉", timestamp=k.open_time) if fast[-2] >= slow[-2] and fast[-1] < slow[-1]: return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time) return None # ════════════════════════════════════════════════════════ # 策略 3: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._closes: list[float] = [] self._in_position = False async def on_kline(self, k: Kline) -> Optional[Signal]: self._closes.append(k.close) vals = rsi(self._closes, self.cfg.period) v = vals[-1] if v == 0: return None if v < self.cfg.oversold and not self._in_position: self._in_position = 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_position: self._in_position = False return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买({v:.1f})", timestamp=k.open_time) return None # ════════════════════════════════════════════════════════ # 策略 4:布林带突破 # ════════════════════════════════════════════════════════ 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._closes: list[float] = [] async def on_kline(self, k: Kline) -> Optional[Signal]: self._closes.append(k.close) upper, mid, lower = bollinger(self._closes, self.cfg.period, self.cfg.std) if mid[-1] == 0 or len(upper) < 3: return None p, up, md = k.close, upper[-1], mid[-1] pp, prev_md = self._closes[-2], mid[-2] # 突破上轨+中轨向上 → 买入 if pp <= prev_md and p > md and up > 0 and mid[-1] > mid[-2]: return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"突破BB中轨 P={p:.0f}>M={md:.0f}", timestamp=k.open_time) # 跌破中轨 → 卖出 if pp >= prev_md and p < md: return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"跌破BB中轨 P={p:.0f} BacktestResult: bt = BacktestConfig( symbol=symbol, interval="4h", start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1), initial_capital=10_000.0, commission_pct=0.001, slippage_pct=0.0005, warmup_bars=100, ) strategy_config.symbol = symbol strategy_config.name = f"{strategy_name}_{symbol}" engine = BacktestEngine(bt, db_config=config.db) return await engine.run(strategy_cls, strategy_config) async def main(): print() print("═" * 98) print(" 策略对比回测 — 4 策略 × 4 币种 | 4h 周期 | 2024-2026") print("═" * 98) print(f" {'策略':<16} {'币种':<10} {'总收益%':>8} {'夏普':>6} {'最大回撤%':>8} {'交易数':>6} {'胜率%':>6}") print("─" * 98) results: list[tuple[str, str, BacktestResult]] = [] # 创建引擎(每个币种一个,复用连接) for symbol in SYMBOLS: for s_name, s_cls, s_cfg in STRATEGIES: cfg = s_cfg.model_copy() if hasattr(s_cfg, 'model_copy') else s_cfg.__class__(**s_cfg.model_dump()) r = await run_one(symbol, s_name, s_cls, cfg) results.append((s_name, symbol, r)) m = r.metrics print( f" {s_name:<16} {symbol:<10} " f"{m.total_return_pct:>7.1f}% " f"{m.sharpe_ratio:>6.2f} " f"{m.max_drawdown_pct:>7.1f}% " f"{m.total_trades:>6} " f"{m.win_rate*100:>5.1f}%" ) # ── 汇总排名 ── print("─" * 98) print("\n ■ 按总收益排名 TOP 5:") ranked = sorted(results, key=lambda x: x[2].metrics.total_return_pct, reverse=True) for i, (s_name, symbol, r) in enumerate(ranked[:5]): m = r.metrics print(f" {i+1}. {symbol} {s_name:<16} 收益={m.total_return_pct:+.1f}% 夏普={m.sharpe_ratio:.2f} 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%") print("\n ■ 按夏普排名 TOP 5:") by_sharpe = sorted(results, key=lambda x: x[2].metrics.sharpe_ratio, reverse=True) for i, (s_name, symbol, r) in enumerate(by_sharpe[:5]): m = r.metrics print(f" {i+1}. {symbol} {s_name:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}%") print("\n ■ 各币种最佳策略:") for symbol in SYMBOLS: sym_results = [(s, r) for s, sym, r in results if sym == symbol] best = max(sym_results, key=lambda x: x[1].metrics.sharpe_ratio) m = best[1].metrics print(f" {symbol}: {best[0]:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 交易={m.total_trades}") print("\n═" * 98) print(" 全部回测完成。") print("═" * 98) if __name__ == "__main__": asyncio.run(main())