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,227 @@
|
||||
"""
|
||||
策略深度优化 — 成交量确认 + 时间止损 + 参数扫描
|
||||
|
||||
优化点 v3:
|
||||
1. 成交量确认:金叉当日成交量 > 前20根均量 × 1.3,过滤缩量假突破
|
||||
2. 时间止损:持仓超过48根K线自动平仓,避免死扛
|
||||
3. 参数扫描:对 EMA(fast, slow) 组合做网格搜索,每个币种找最优参数
|
||||
|
||||
用法:
|
||||
source .venv/bin/activate && python example/strategy_optimize2.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 ema, atr
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# EMA v3: 趋势 + ATR止损 + 时间止损 + 成交量确认
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class EmaV3Config(StrategyConfig):
|
||||
fast: int = 20
|
||||
slow: int = 50
|
||||
atr_period: int = 14
|
||||
atr_stop_mult: float = 3.0
|
||||
time_stop_bars: int = 48 # 时间止损(30m下48根=1天,4h下48根=8天)
|
||||
vol_factor: float = 1.3 # 成交量确认倍数
|
||||
|
||||
|
||||
class EmaV3Strategy(BaseStrategy):
|
||||
"""EMA v3: 金叉+放量买入,ATR止损+时间止损出场"""
|
||||
|
||||
strategy_type = "ema_v3"
|
||||
|
||||
def __init__(self, c: EmaV3Config):
|
||||
super().__init__(c)
|
||||
self.cfg = c
|
||||
self._closes: list[float] = []
|
||||
self._highs: list[float] = []
|
||||
self._lows: list[float] = []
|
||||
self._volumes: list[float] = []
|
||||
self._highest_since_entry: float = 0.0
|
||||
self._bars_held: int = 0
|
||||
self._in_position = False
|
||||
|
||||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||||
self._closes.append(k.close)
|
||||
self._highs.append(k.high)
|
||||
self._lows.append(k.low)
|
||||
self._volumes.append(k.volume)
|
||||
n = len(self._closes)
|
||||
if n < self.cfg.slow + 20:
|
||||
return None
|
||||
|
||||
fast = ema(self._closes, self.cfg.fast)
|
||||
slow = ema(self._closes, self.cfg.slow)
|
||||
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
|
||||
cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1]
|
||||
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
|
||||
return None
|
||||
|
||||
# 成交量确认:最近bar成交量 > 前20根均量 × factor
|
||||
avg_vol = sum(self._volumes[-21:-1]) / 20 if n > 21 else 0
|
||||
vol_confirmed = k.volume > avg_vol * self.cfg.vol_factor if avg_vol > 0 else True
|
||||
|
||||
if self._in_position:
|
||||
self._bars_held += 1
|
||||
self._highest_since_entry = max(self._highest_since_entry, k.high)
|
||||
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
|
||||
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
|
||||
time_up = self._bars_held >= self.cfg.time_stop_bars
|
||||
|
||||
if k.close < stop_price:
|
||||
self._in_position = False
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"ATR止损", timestamp=k.open_time)
|
||||
if death_cross:
|
||||
self._in_position = False
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"EMA死叉", timestamp=k.open_time)
|
||||
if time_up:
|
||||
self._in_position = False
|
||||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"时间止损({self._bars_held}bar)", timestamp=k.open_time)
|
||||
|
||||
if not self._in_position:
|
||||
golden = fast[-2] <= slow[-2] and cur_f > cur_s
|
||||
if golden and vol_confirmed:
|
||||
self._in_position = True
|
||||
self._entry_price = k.close
|
||||
self._highest_since_entry = k.close
|
||||
self._bars_held = 0
|
||||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||||
reason=f"金叉+放量{'✓' if vol_confirmed else ''} V={k.volume:.0f}",
|
||||
timestamp=k.open_time)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 参数扫描 + 对比
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||||
|
||||
PARAM_GRID = [
|
||||
# (fast, slow)
|
||||
(10, 40), (10, 50), (10, 75),
|
||||
(20, 40), (20, 50), (20, 75),
|
||||
(30, 40), (30, 50), (30, 75),
|
||||
]
|
||||
|
||||
# 原始 v1 基线
|
||||
BASELINE = {
|
||||
"BTCUSDT": (20, 50, 45.5, 0.74, -31.6),
|
||||
"ETHUSDT": (20, 50, 24.4, 0.47, -54.8),
|
||||
"BNBUSDT": (20, 50, 52.0, 0.71, -39.8),
|
||||
"SOLUSDT": (20, 50, 27.8, 0.49, -39.5),
|
||||
}
|
||||
|
||||
|
||||
async def scan_one(symbol: str, fast: int, slow: int) -> dict:
|
||||
"""单次参数扫描"""
|
||||
bt = BacktestConfig(
|
||||
symbol=symbol, interval="4h",
|
||||
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
|
||||
initial_capital=10_000.0,
|
||||
)
|
||||
sc = EmaV3Config(symbol=symbol, fast=fast, slow=slow)
|
||||
engine = BacktestEngine(bt, db_config=config.db)
|
||||
r = await engine.run(EmaV3Strategy, sc)
|
||||
m = r.metrics
|
||||
return {"fast": fast, "slow": slow, "return": m.total_return_pct,
|
||||
"sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct,
|
||||
"trades": m.total_trades, "wr": m.win_rate}
|
||||
|
||||
|
||||
async def main():
|
||||
print()
|
||||
print("═" * 105)
|
||||
print(" EMA v3 深度优化 — 成交量+时间止损+参数扫描 | 4h 周期 | 2024-2026")
|
||||
print("═" * 105)
|
||||
|
||||
all_results: dict[str, list[dict]] = {}
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
print(f"\n ▸ {symbol} 参数扫描 (9 组)...")
|
||||
symbol_results = []
|
||||
for fast, slow in PARAM_GRID:
|
||||
r = await scan_one(symbol, fast, slow)
|
||||
symbol_results.append(r)
|
||||
print(f" EMA({fast},{slow}) 收益={r['return']:>+6.1f}% 夏普={r['sharpe']:>6.2f} 回撤={r['dd']:>6.1f}% 交易={r['trades']:>3} 胜率={r['wr']*100:>5.1f}%")
|
||||
all_results[symbol] = symbol_results
|
||||
|
||||
# ── 每个币种最佳参数 ──
|
||||
print("\n" + "═" * 105)
|
||||
print(" ■ 各币种最优参数:")
|
||||
print(f" {'币种':<10} {'最优参数':<16} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} vs 基线")
|
||||
print("─" * 105)
|
||||
|
||||
best_params = {}
|
||||
for symbol in SYMBOLS:
|
||||
# 按夏普排序
|
||||
sorted_r = sorted(all_results[symbol], key=lambda x: x["sharpe"], reverse=True)
|
||||
best = sorted_r[0]
|
||||
best_params[symbol] = (best["fast"], best["slow"])
|
||||
base = BASELINE[symbol]
|
||||
delta = best["return"] - base[2]
|
||||
print(f" {symbol:<10} EMA({best['fast']},{best['slow']}){'':>8} {best['return']:>6.1f}% {best['sharpe']:>6.2f} {best['dd']:>6.1f}% {best['trades']:>5} {best['wr']*100:>5.1f}% Δ={delta:+.1f}%")
|
||||
|
||||
# ── 汇总对比 ──
|
||||
print("\n" + "═" * 105)
|
||||
print(" ■ 三层对比:基线(v1) → 止损(v2) → 成交量+时间止损+最优参数(v3)")
|
||||
print(f" {'币种':<10} {'版本':<20} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6}")
|
||||
print("─" * 105)
|
||||
|
||||
# v2 数据(从上次运行)
|
||||
V2 = {
|
||||
"BTCUSDT": (20, 50, 7.0, 0.27, -27.8),
|
||||
"ETHUSDT": (20, 50, 41.3, 0.76, -29.6),
|
||||
"BNBUSDT": (20, 50, 43.6, 0.81, -26.1),
|
||||
"SOLUSDT": (20, 50, 48.1, 0.70, -26.6),
|
||||
}
|
||||
|
||||
for symbol in SYMBOLS:
|
||||
# 基线
|
||||
b = BASELINE[symbol]
|
||||
print(f" {symbol:<10} {'v1 原始 EMA双均线':<20} {b[2]:>6.1f}% {b[3]:>6.2f} {b[4]:>6.1f}%")
|
||||
# v2
|
||||
v2 = V2[symbol]
|
||||
print(f" {symbol:<10} {'v2 +ATR止损':<20} {v2[2]:>6.1f}% {v2[3]:>6.2f} {v2[4]:>6.1f}%")
|
||||
# v3 最佳
|
||||
best = [r for r in all_results[symbol] if r["fast"] == best_params[symbol][0] and r["slow"] == best_params[symbol][1]][0]
|
||||
print(f" {symbol:<10} {'v3 全优化+最优参数':<20} {best['return']:>6.1f}% {best['sharpe']:>6.2f} {best['dd']:>6.1f}%")
|
||||
print()
|
||||
|
||||
# ── 组合收益 ──
|
||||
print("─" * 105)
|
||||
print(" ■ 等权组合(4币种各投入2500 USDT):")
|
||||
total_baseline = sum(BASELINE[s][2] for s in SYMBOLS) / 4 * 100
|
||||
total_v2 = sum(V2[s][2] for s in SYMBOLS) / 4 * 100
|
||||
total_v3 = sum(
|
||||
sorted(all_results[s], key=lambda x: x["sharpe"], reverse=True)[0]["return"]
|
||||
for s in SYMBOLS
|
||||
) / 4 * 100
|
||||
print(f" v1 基线组合: {total_baseline:+.0f} USDT")
|
||||
print(f" v2 止损组合: {total_v2:+.0f} USDT")
|
||||
print(f" v3 全优化组合: {total_v3:+.0f} USDT")
|
||||
|
||||
print("\n═" * 105)
|
||||
print(" 扫描完成。")
|
||||
print("═" * 105)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user