Files
trade/engine/example/multi_strategy_report.py
T
Rekey edc50e8809 feat: 新增2h/6h时间框架支持,策略重构为增量指标计算
- 数据层: build_aggregates_sql 新增 2h/6h 聚合视图,默认起始时间调整为 2017-05
- 模型层: KlineInterval 类型扩展 2h/6h,DataService 新增对应表名和毫秒映射
- 指标层: 新增 incremental.py 增量指标模块 (EmaInc/AtrInc/RsiInc/BbInc),O(1) per bar
- 策略重构: long_short.py 和 regime_all.py 从批量 ema/atr 迁移至增量指标,避免每 bar 重复全量计算
- regime 探测器: RegimeDetector3 改为增量 EMA200,detect() 接口简化
- 回测扩展: regime_timeframe_comparison 从 4h/1d 扩展至 2h/4h/6h/1d
- 新增示例: multi_strategy_report, vol_break_compare/periods, intraday_explore, top3_trades 等分析脚本
2026-06-13 19:30:25 +08:00

394 lines
17 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.
"""
多策略多级别分类回测报告
日内交易 (30m/1h) 中线交易 (2h/4h/6h) 长线交易 (1d/1w)
策略:牛熊自适应 / MACD / EMA双均线 / RSI / 布林突破
币种:BTC / ETH / BNB / SOL
数据:日内近两年,中线+长线全量
用法:
source .venv/bin/activate && python example/multi_strategy_report.py
"""
import asyncio
import sys
from collections import defaultdict
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, atr
# ═══════════════════════════════════════════
# 策略定义
# ═══════════════════════════════════════════
# --- 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._c: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
ml, sl, _ = macd(self._c, self.cfg.fast, self.cfg.slow, self.cfg.signal)
if len(ml) < 3 or ml[-1] == 0: return None
if ml[-2] <= sl[-2] and ml[-1] > sl[-1]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD金叉", timestamp=k.open_time)
if ml[-2] >= sl[-2] and ml[-1] < sl[-1]:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD死叉", timestamp=k.open_time)
return None
# --- 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._c: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
if len(f) < 3 or f[-1] == 0 or s[-1] == 0: return None
if f[-2] <= s[-2] and f[-1] > s[-1]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉", timestamp=k.open_time)
if f[-2] >= s[-2] and f[-1] < s[-1]:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time)
return None
# --- 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._c: list[float] = []; self._in = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
v = rsi(self._c, self.cfg.period)[-1]
if v == 0: return None
if v < self.cfg.oversold and not self._in:
self._in = 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:
self._in = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买({v:.1f})", timestamp=k.open_time)
return None
# --- 布林突破 ---
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._c: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close)
upper, mid, lower = bollinger(self._c, self.cfg.period, self.cfg.std)
if len(upper) < 3 or mid[-1] == 0: return None
p, md = k.close, mid[-1]
pp, pm = self._c[-2], mid[-2]
if pp <= pm and p > md and upper[-1] > 0 and mid[-1] > mid[-2]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"突破BB中轨", timestamp=k.open_time)
if pp >= pm and p < md:
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"跌破BB中轨", timestamp=k.open_time)
return None
# --- 牛熊自适应 (多空双向) ---
class RegimeDetector:
def __init__(self):
self._ath = 0.0
def update_ath(self, price: float):
if price > self._ath: self._ath = price
def ema200_slope(self, closes, idx):
if idx < 210: return "unknown"
e200 = ema(closes, 200)
if e200[idx - 20] == 0: return "unknown"
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20]
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
def price_vs_ema200(self, closes, idx):
if idx < 210: return "unknown"
e200 = ema(closes, 200)
return "bull" if closes[idx] > e200[idx] else "bear"
def ath_drawdown(self, closes, idx):
if self._ath == 0: return "unknown"
dd = (closes[idx] - self._ath) / self._ath
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
def detect(self, closes, idx):
r1 = self.ema200_slope(closes, idx); r2 = self.price_vs_ema200(closes, idx); r3 = self.ath_drawdown(closes, idx)
b = sum(1 for r in [r1,r2,r3] if r=="bull"); br = sum(1 for r in [r1,r2,r3] if r=="bear")
if b >= 2: return "bull"
if br >= 2: return "bear"
return "sideways"
class RegimeConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
class RegimeStrategy(BaseStrategy):
strategy_type = "regime"
def __init__(self, c: RegimeConfig):
super().__init__(c); self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector = RegimeDetector(); self._side = ""; self._hp = 0.0; self._lp = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
self._detector.update_ath(k.close)
n = len(self._c)
if n < 220: return None
regime = self._detector.detect(self._c, n - 1)
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
a = atr(self._h, self._l, self._c, 14)
cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
golden = pf <= ps and cf > cs; death = pf >= ps and cf < cs
if self._side == "long":
self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca
if death or k.close < stop or regime == "bear":
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="平多", timestamp=k.open_time)
elif self._side == "short":
self._lp = min(self._lp, k.low); stop = self._lp + self.cfg.atr_stop * ca
if golden or k.close > stop or regime == "bull":
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="平空", timestamp=k.open_time)
else:
if regime == "bull" and golden:
self._side = "long"; self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="牛市金叉", timestamp=k.open_time)
elif regime == "bear" and death:
self._side = "short"; self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="熊市死叉", timestamp=k.open_time)
return None
# ═══════════════════════════════════════════
# 注册
# ═══════════════════════════════════════════
STRATEGY_REGISTRY = {
"牛熊自适应": (RegimeStrategy, RegimeConfig, "regime"),
"MACD": (MacdStrategy, MacdConfig, "trend"),
"EMA双均线": (EmaCrossStrategy, EmaCrossConfig, "trend"),
"RSI超卖反弹": (RsiStrategy, RsiConfig, "reversal"),
"布林突破": (BollStrategy, BollConfig, "breakout"),
}
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
REGIME_PARAMS = {
"BTCUSDT": (10, 50), "ETHUSDT": (10, 75), "BNBUSDT": (20, 50), "SOLUSDT": (30, 50),
}
CATEGORIES = {
"日内交易": {
"intervals": ["30m", "1h"],
"strategies": ["MACD", "EMA双均线", "RSI超卖反弹", "布林突破", "牛熊自适应"],
"data": "recent",
},
"中线交易": {
"intervals": ["2h", "4h", "6h"],
"strategies": ["牛熊自适应", "MACD", "EMA双均线", "RSI超卖反弹", "布林突破"],
"data": "full",
},
"长线交易": {
"intervals": ["1d", "1w"],
"strategies": ["牛熊自适应", "MACD", "EMA双均线"],
"data": "full",
},
}
RECENT_START = datetime(2024, 6, 1)
RECENT_END = datetime(2026, 6, 12)
FULL_DEFAULT = datetime(2017, 1, 1)
async def run_simple(symbol, interval, strategy_cls, strategy_cfg, start, end) -> BacktestResult | None:
"""使用 BacktestEngine(只做多)运行回测"""
bt = BacktestConfig(
symbol=symbol, interval=interval, start_time=start, end_time=end,
initial_capital=10_000.0, warmup_bars=100,
)
strategy_cfg.symbol = symbol
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(strategy_cls, strategy_cfg)
async def run_regime(symbol, interval, start, end) -> BacktestResult | None:
"""使用 LongShortEngine(多空双向)运行牛熊自适应策略"""
from engine.example.long_short import LongShortEngine
fast, slow = REGIME_PARAMS.get(symbol, (10, 50))
sc = RegimeConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(
symbol=symbol, interval=interval, start_time=start, end_time=end,
initial_capital=10_000.0, warmup_bars=250,
)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(RegimeStrategy, sc)
# ═══════════════════════════════════════════
# 主流程
# ═══════════════════════════════════════════
async def main():
sem = asyncio.Semaphore(2) # 并发控制
async def with_sem(coro):
async with sem:
return await coro
out: list[str] = []
def w(line=""):
out.append(line); print(line)
# 收集所有结果: (category, interval, symbol, strategy_name, result)
all_results: list[dict] = []
w("# 多策略多级别分类回测报告")
w()
w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
w(f"> 初始资金:10,000 USDT | 手续费:0.1% | 滑点:0.05%")
w(f"> 日内交易使用近两年数据 (2024.06-2026.06),中线/长线使用全量历史数据")
w()
for cat_name, cat_cfg in CATEGORIES.items():
intervals = cat_cfg["intervals"]
strategy_names = cat_cfg["strategies"]
use_full = cat_cfg["data"] == "full"
w(f"## {cat_name} ({'/'.join(intervals)})")
w()
for interval in intervals:
w(f"### {interval}")
w()
# 表头
cols = "| 币种 |"
sep = "|------|"
for sn in strategy_names:
cols += f" {sn} 收益% | {sn} 夏普 | {sn} 回撤% | {sn} 交易 | {sn} 胜率% |"
sep += "--------|------|------|------|------|"
w(cols)
w(sep)
for symbol in SYMBOLS:
row = f"| {symbol:<10} |"
tasks = []
for sn in strategy_names:
if sn == "牛熊自适应":
# 牛熊自适应使用 LongShortEngine
if use_full:
start, end = FULL_DEFAULT, RECENT_END
else:
start, end = RECENT_START, RECENT_END
tasks.append((sn, with_sem(run_regime(symbol, interval, start, end))))
else:
cls, cfg_cls, _ = STRATEGY_REGISTRY[sn]
if use_full:
start, end = FULL_DEFAULT, RECENT_END
else:
start, end = RECENT_START, RECENT_END
cfg = cfg_cls()
tasks.append((sn, with_sem(run_simple(symbol, interval, cls, cfg, start, end))))
# 并行执行当前币种的所有策略
results_list = await asyncio.gather(*[t[1] for t in tasks], return_exceptions=True)
for (sn, _), r in zip(tasks, results_list):
if isinstance(r, Exception):
row += f" ERR | — | — | — | — |"
print(f"{symbol} {interval} {sn}: {r}")
elif r is None:
row += f" N/A | — | — | — | — |"
else:
m = r.metrics
row += f" {m.total_return_pct:>+6.1f}% | {m.sharpe_ratio:>4.2f} | {m.max_drawdown_pct:>5.1f}% | {m.total_trades:>4} | {m.win_rate*100:>4.1f}% |"
all_results.append({
"category": cat_name, "interval": interval,
"symbol": symbol, "strategy": sn,
"return": m.total_return_pct, "sharpe": m.sharpe_ratio,
"dd": m.max_drawdown_pct, "trades": m.total_trades,
"win": m.win_rate, "pf": m.profit_factor,
})
w(row)
w()
# ═══════════════════════════════════════
# 汇总分析
# ═══════════════════════════════════════
w("---")
w()
w("## 汇总分析")
w()
for cat_name in CATEGORIES:
cat_results = [r for r in all_results if r["category"] == cat_name]
if not cat_results:
continue
w(f"### {cat_name} — 各币种最优策略")
w()
w("| 币种 | 最佳周期 | 最佳策略 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% |")
w("|------|---------|---------|--------|------|------|------|------|")
for symbol in SYMBOLS:
candidates = [r for r in cat_results if r["symbol"] == symbol]
if not candidates:
continue
best = max(candidates, key=lambda x: x["sharpe"])
w(f"| {symbol} | {best['interval']} | {best['strategy']} | {best['return']:>+7.1f}% | {best['sharpe']:.2f} | {best['dd']:.1f}% | {best['trades']} | {best['win']*100:.1f}% |")
w()
# 全市场最优
w("### 全市场 TOP 10(按夏普排序)")
w()
w("| 排名 | 分类 | 币种 | 周期 | 策略 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% |")
w("|------|------|------|------|------|--------|------|------|------|------|")
ranked = sorted(all_results, key=lambda x: x["sharpe"], reverse=True)
for i, r in enumerate(ranked[:10]):
w(f"| {i+1} | {r['category']} | {r['symbol']} | {r['interval']} | {r['strategy']} | {r['return']:>+7.1f}% | {r['sharpe']:.2f} | {r['dd']:.1f}% | {r['trades']} | {r['win']*100:.1f}% |")
w()
# 按策略类型汇总
w("### 各策略类型平均表现")
w()
w("| 策略 | 分类 | 平均收益% | 平均夏普 | 平均回撤% | 平均胜率% |")
w("|------|------|---------|---------|---------|---------|")
for sn in ["牛熊自适应", "MACD", "EMA双均线", "RSI超卖反弹", "布林突破"]:
for cat_name in CATEGORIES:
sr = [r for r in all_results if r["strategy"] == sn and r["category"] == cat_name]
if not sr:
continue
avg_ret = sum(r["return"] for r in sr) / len(sr)
avg_sh = sum(r["sharpe"] for r in sr) / len(sr)
avg_dd = sum(r["dd"] for r in sr) / len(sr)
avg_win = sum(r["win"] for r in sr) / len(sr)
w(f"| {sn} | {cat_name} | {avg_ret:>+7.1f}% | {avg_sh:.2f} | {avg_dd:.1f}% | {avg_win*100:.1f}% |")
w()
# 写出文件
out_path = Path(__file__).resolve().parent.parent / "backtest" / "MULTI_STRATEGY_REPORT.md"
with open(out_path, "w", encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
print(f"\n✓ 报告已保存到: {out_path}")
if __name__ == "__main__":
asyncio.run(main())