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:
@@ -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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略 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())
|
||||
Reference in New Issue
Block a user