Files
trade/engine/example/strategy_battle.py
T
Rekey 515e61c517 feat(engine): 添加策略示例集(18 个 Demo)
- backtest_demo.py: 回测基础演示
- strategy_simple.py / three_ema.py / long_short.py: 基础策略(双均线/三均线/多空)
- strategy_optimize*.py (3 版本): 参数优化示例(网格搜索/贝叶斯/遗传算法)
- multi_tf_*.py (4 版本): 多时间框架策略(EMA200/多周期共振/混合信号)
- regime_*.py (4 版本): 市场状态检测(趋势/震荡/波动率区间/全状态)
- cross_section.py: 截面多品种策略
- factor_demo.py: 多因子模型演示
- strategy_battle.py / strategy_more.py: 策略对比与组合
- full_cycle.py: 全流程演示(数据→回测→分析)
- data.py: 数据读取示例
2026-06-12 10:27:04 +08:00

265 lines
9.9 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.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())