Files
trade/engine/example/strategy_battle.py
T
Rekey 626acb20d3 feat: 全链路新增 type 字段支持 + exchange.ts 超时退出优化
- TS: exit 函数统一管理进程退出与 DB 连接关闭;10s 超时 + 异常路径 clearTimeout
- Python: PairType(spot/um/cm) 贯穿 Kline 模型、策略配置、数据查询
- 回测脚本升级: 9策略 × 4币种 × 6时间级别 × 2交易类型
- 新增 generate_report.py 回测报告生成工具
2026-06-17 10:01:52 +08:00

267 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
策略对比回测 — 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
# ════════════════════════════════════════════════════════
# 策略 1MACD 金叉死叉
# ════════════════════════════════════════════════════════
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
# ════════════════════════════════════════════════════════
# 策略 2EMA 双均线
# ════════════════════════════════════════════════════════
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
# ════════════════════════════════════════════════════════
# 策略 3RSI 超卖反弹
# ════════════════════════════════════════════════════════
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}<M={md:.0f}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 注册策略
# ════════════════════════════════════════════════════════
STRATEGIES = [
("MACD金叉死叉", MacdStrategy, MacdConfig()),
("EMA双均线", EmaCrossStrategy, EmaCrossConfig()),
("RSI超卖反弹", RsiStrategy, RsiConfig()),
("布林突破", BollStrategy, BollConfig()),
]
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
async def run_one(
symbol: str,
strategy_name: str,
strategy_cls,
strategy_config: StrategyConfig,
) -> 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.exchange = bt.exchange
strategy_config.type = bt.type
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())