Files
trade/engine/example/regime_mtf_strategy.py
T
Rekey 4294ec401d feat: 多周期牛熊判定模块 — 方案一矩阵展示 + 四法投票 + 多TF策略
- engine/indicators/regime.py: RegimeDetector(四法投票) + MultiTimeframeRegime(多周期并行)
  四法: EMA200斜率 / 价格vsEMA200 / ATH回撤 / 窄幅盘整(<3%振幅)
  全部 O(1)/bar 增量计算,适用于回测和实时
- engine/example/regime_display.py: 多周期牛熊矩阵展示脚本
  独立加载各周期数据 → 运行判定 → 日线对齐矩阵 + 详细拆解 + 统计
  输出 engine/backtest/REGIME_MATRIX_BTCUSDT.md
- engine/example/regime_mtf_strategy.py: 多周期共识策略 + 四策略对比回测
  MTF Consensus: 1w定方向 + 1d确认 + 4h EMA入场
  vs Old Regime(单TF基线) vs Long/Short(无过滤)
- engine/indicators/__init__.py: 导出 RegimeDetector, MultiTimeframeRegime
2026-06-17 11:30:19 +08:00

377 lines
15 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.
"""
多周期牛熊共识策略 — 四周期协同判定
核心思路:
1w 定宏观方向(只做多 / 只做空)→ 1d 确认中周期 → 4h EMA 金叉/死叉入场
配合 ATR 动态止损 + 多周期投票确认出场
对比基准:单周期 RegimeEmaStrategyregime_all.py)、无过滤 LongShortEmaStrategy
用法:
source .venv/bin/activate && python example/regime_mtf_strategy.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 BacktestConfig
from engine.indicators.incremental import EmaInc, AtrInc
from engine.indicators.regime import MultiTimeframeRegime
from engine.example.long_short import LongShortEngine
from engine.example.regime_all import RegimeEmaConfig as OldRegimeCfg
from engine.example.regime_all import RegimeEmaStrategy as OldRegimeS
from engine.example.long_short import LongShortEmaConfig as LSCfg
from engine.example.long_short import LongShortEmaStrategy as LSS
# ════════════════════════════════════════════════════════
# 多周期牛熊共识策略
# ════════════════════════════════════════════════════════
class MTFConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class MTFRegimeStrategy(BaseStrategy):
"""四周期协同牛熊策略
判定层(MultiTimeframeRegime,即时法):
每个 4h bar 的 close 同时更新 1h / 4h / 1d / 1w 四个检测器,
1w 定方向宏调,1d 确认中周期,1h 预警微观背离。
入场:
做多 — 1w bull + 1d not bear + 4h EMA 金叉
做空 — 1w bear + 1d not bull + 4h EMA 死叉
出场:
EMA 交叉反转、ATR 跟踪止损、高周期方向逆转
"""
strategy_type = "mtf_regime"
def __init__(self, c: MTFConfig):
super().__init__(c)
self.cfg = c
self._mtf = MultiTimeframeRegime(["1h", "4h", "1d", "1w"])
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(14)
self._side: str = ""
self._hp: float = 0.0
self._lp: float = float("inf")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._mtf.update(k.close)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._mtf._prices)
if n < 220:
return None
regimes = self._mtf.detect_all()
r1h = regimes.get("1h", "unknown")
r4h = regimes.get("4h", "unknown")
r1d = regimes.get("1d", "unknown")
r1w = regimes.get("1w", "unknown")
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
ca = self._atr[-1]
pf, ps = self._ema_fast[-2], self._ema_slow[-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
exit_reason = None
if death:
exit_reason = "死叉"
elif k.close < stop:
exit_reason = "ATR止损"
elif r1w == "bear":
exit_reason = "1w转熊→平多"
elif r1d == "bear" and r4h == "bear":
exit_reason = "1d+4h双双转熊→平多"
if exit_reason:
self._side = ""
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=exit_reason, timestamp=k.open_time)
# ── 空头持仓 ──
elif self._side == "short":
self._lp = min(self._lp, k.low)
stop = self._lp + self.cfg.atr_stop * ca
exit_reason = None
if golden:
exit_reason = "金叉"
elif k.close > stop:
exit_reason = "ATR止损"
elif r1w == "bull":
exit_reason = "1w转牛→平空"
elif r1d == "bull" and r4h == "bull":
exit_reason = "1d+4h双双转牛→平空"
if exit_reason:
self._side = ""
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=exit_reason, timestamp=k.open_time)
# ── 空仓等信号 ──
else:
# 做多条件:1w牛 + 1d不熊 + 4h金叉
long_ok = r1w == "bull" and r1d != "bear" and golden
# 做空条件:1w熊 + 1d不牛 + 4h死叉
short_ok = r1w == "bear" and r1d != "bull" and death
if long_ok:
self._side = "long"
self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"多周期做多({r1w}/{r1d}/{r4h})",
timestamp=k.open_time)
if short_ok:
self._side = "short"
self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"多周期做空({r1w}/{r1d}/{r4h})",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 对照策略 2:1w 绝对过滤(更强约束)
# ════════════════════════════════════════════════════════
class MTFStrictConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class MTFStrictStrategy(BaseStrategy):
"""严格多周期策略 — 必须 1w + 1d 方向一致才入场
入场:
做多 — 1w bull + 1d bull + 4h 金叉
做空 — 1w bear + 1d bear + 4h 死叉
出场:
4h 死叉/金叉 或 ATR 止损 或 1w 方向逆转
"""
strategy_type = "mtf_strict"
def __init__(self, c: MTFStrictConfig):
super().__init__(c)
self.cfg = c
self._mtf = MultiTimeframeRegime(["1h", "4h", "1d", "1w"])
self._ema_fast = EmaInc(c.fast)
self._ema_slow = EmaInc(c.slow)
self._atr = AtrInc(14)
self._side: str = ""
self._hp: float = 0.0
self._lp: float = float("inf")
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._mtf.update(k.close)
self._ema_fast.update(k.close)
self._ema_slow.update(k.close)
self._atr.update(k.high, k.low, k.close)
n = len(self._mtf._prices)
if n < 220:
return None
regimes = self._mtf.detect_all()
r4h = regimes.get("4h", "unknown")
r1d = regimes.get("1d", "unknown")
r1w = regimes.get("1w", "unknown")
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
ca = self._atr[-1]
pf, ps = self._ema_fast[-2], self._ema_slow[-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 r1w == "bear":
self._side = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "1w转熊")
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=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 r1w == "bull":
self._side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "1w转牛")
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=reason, timestamp=k.open_time)
# 空仓
else:
if r1w == "bull" and r1d == "bull" and golden:
self._side = "long"
self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"严格做多({r1w}+{r1d}+{r4h})",
timestamp=k.open_time)
if r1w == "bear" and r1d == "bear" and death:
self._side = "short"
self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"严格做空({r1w}+{r1d}+{r4h})",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 回测入口
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def run_one(engine, strategy_cls, config_cls, symbol, desc, params=None):
fast, slow = PARAMS[symbol] if params is None else params
sc = config_cls(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END,
initial_capital=10_000.0, warmup_bars=250,
)
eng = engine(bt, db_config=config.db)
r = await eng.run(strategy_cls, sc)
return r
async def main():
print()
print("" * 120)
print(" 多周期牛熊共识策略 — MTF Regime × 4h EMA | 2024-2026")
print("" * 120)
print()
print(" 对比策略:")
print(" • MTF Consensus — 1w定方向 + 1d确认 + 4h金叉/死叉入场(新)")
print(" • MTF Strict — 1w+1d必须同向才入场(新)")
print(" • Old Regime — 单周期(4h)三法投票(regime_all.py 基线)")
print(" • Long/Short — 无牛熊过滤,始终多空(long_short.py 基线)")
print()
for symbol in SYMBOLS:
print(f" ── {symbol} ──")
print(f" {'策略':<18} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'多头P&L':>9} {'空头P&L':>9}")
print(" " + "" * 110)
results = {}
# 1. MTF Consensus (new)
try:
r1 = await run_one(LongShortEngine, MTFRegimeStrategy, MTFConfig, symbol, "MTF Consensus")
m = r1.metrics
long1 = [t for t in r1.trades if t.pnl is not None and t.side == "SELL"]
short1 = [t for t in r1.trades if t.pnl is not None and t.side == "BUY"]
lp1 = sum(t.pnl for t in long1) if long1 else 0
sp1 = sum(t.pnl for t in short1) if short1 else 0
results["mtf"] = m
print(f" {'MTF Consensus':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp1:>+9.0f} {sp1:>+9.0f}")
except Exception as e:
print(f" {'MTF Consensus':<18} 错误: {e}")
# 2. MTF Strict (new)
try:
r2 = await run_one(LongShortEngine, MTFStrictStrategy, MTFStrictConfig, symbol, "MTF Strict")
m = r2.metrics
long2 = [t for t in r2.trades if t.pnl is not None and t.side == "SELL"]
short2 = [t for t in r2.trades if t.pnl is not None and t.side == "BUY"]
lp2 = sum(t.pnl for t in long2) if long2 else 0
sp2 = sum(t.pnl for t in short2) if short2 else 0
results["strict"] = m
print(f" {'MTF Strict':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp2:>+9.0f} {sp2:>+9.0f}")
except Exception as e:
print(f" {'MTF Strict':<18} 错误: {e}")
# 3. Old Regime (baseline)
try:
r3 = await run_one(LongShortEngine, OldRegimeS, OldRegimeCfg, symbol, "Old Regime")
m = r3.metrics
long3 = [t for t in r3.trades if t.pnl is not None and t.side == "SELL"]
short3 = [t for t in r3.trades if t.pnl is not None and t.side == "BUY"]
lp3 = sum(t.pnl for t in long3) if long3 else 0
sp3 = sum(t.pnl for t in short3) if short3 else 0
results["old"] = m
print(f" {'Old Regime':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp3:>+9.0f} {sp3:>+9.0f}")
except Exception as e:
print(f" {'Old Regime':<18} 错误: {e}")
# 4. Long/Short (baseline, no filter)
try:
r4 = await run_one(LongShortEngine, LSS, LSCfg, symbol, "Long/Short")
m = r4.metrics
long4 = [t for t in r4.trades if t.pnl is not None and t.side == "SELL"]
short4 = [t for t in r4.trades if t.pnl is not None and t.side == "BUY"]
lp4 = sum(t.pnl for t in long4) if long4 else 0
sp4 = sum(t.pnl for t in short4) if short4 else 0
results["ls"] = m
print(f" {'Long/Short':<18} {m.total_return_pct:>+6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f} {lp4:>+9.0f} {sp4:>+9.0f}")
except Exception as e:
print(f" {'Long/Short':<18} 错误: {e}")
# 简要对比
if len(results) >= 2:
best_sharpe = max(results.items(), key=lambda x: x[1].sharpe_ratio)
worst_dd = min(results.items(), key=lambda x: x[1].max_drawdown_pct)
print(f" └ 最佳夏普: {best_sharpe[0]} ({best_sharpe[1].sharpe_ratio:.2f}) | "
f"最小回撤: {worst_dd[0]} ({worst_dd[1].max_drawdown_pct:.1f}%)")
print()
print("" * 120)
if __name__ == "__main__":
asyncio.run(main())