4294ec401d
- 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
377 lines
15 KiB
Python
377 lines
15 KiB
Python
"""
|
||
多周期牛熊共识策略 — 四周期协同判定
|
||
|
||
核心思路:
|
||
1w 定宏观方向(只做多 / 只做空)→ 1d 确认中周期 → 4h EMA 金叉/死叉入场
|
||
配合 ATR 动态止损 + 多周期投票确认出场
|
||
|
||
对比基准:单周期 RegimeEmaStrategy(regime_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())
|