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: 数据读取示例
This commit is contained in:
Rekey
2026-06-12 10:27:04 +08:00
parent 4da520c14b
commit 515e61c517
21 changed files with 5194 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
"""
策略对比回测 — 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())