515e61c517
- 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: 数据读取示例
265 lines
9.9 KiB
Python
265 lines
9.9 KiB
Python
"""
|
||
策略对比回测 — 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}<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())
|