""" 最佳牛熊判定 — 全币种全周期回测 方法:EMA200斜率 + 价格vs EMA200 + ATH回撤,3选2投票 策略:牛市只做多 / 熊市只做空 / 震荡空仓 币种:BTC / ETH / BNB / SOL,各自最早有数据到2026 用法: source .venv/bin/activate && python example/regime_all.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.data import DataService from engine.example.long_short import LongShortEngine # ════════════════════════════════════════════════════════ # 3法判定器(增量 EMA200,O(1) per bar) # ════════════════════════════════════════════════════════ class RegimeDetector3: """牛熊判定器,内部维护增量 EMA(200),避免每次从头重算""" def __init__(self): self._ath = 0.0 self._e200 = EmaInc(200) def update(self, price: float): """每根 bar 调一次:更新 ATH + EMA(200)""" if price > self._ath: self._ath = price self._e200.update(price) def _ema200_slope(self, idx: int) -> str: if idx < 220: return "unknown" e200 = self._e200 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, price: float, idx: int) -> str: if idx < 210: return "unknown" e = self._e200[idx] if e == 0: return "unknown" return "bull" if price > e else "bear" def _ath_drawdown(self, price: float) -> str: if self._ath == 0: return "unknown" dd = (price - self._ath) / self._ath if dd > -0.15: return "bull" if dd < -0.35: return "bear" return "sideways" def detect(self, price: float, idx: int) -> str: r1 = self._ema200_slope(idx) r2 = self._price_vs_ema200(price, idx) r3 = self._ath_drawdown(price) 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 RegimeEmaConfig(StrategyConfig): fast: int = 10; slow: int = 50; atr_stop: float = 2.5 class RegimeEmaStrategy(BaseStrategy): """按市场状态自适应做多/做空 — 全部指标增量计算,O(1) per bar""" strategy_type = "regime_ema" def __init__(self, c: RegimeEmaConfig): super().__init__(c) self.cfg = c self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = [] self._detector = RegimeDetector3() 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._c.append(k.close); self._h.append(k.high); self._l.append(k.low) # 增量更新所有指标(O(1) each) self._detector.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._c) if n < 220: return None regime = self._detector.detect(k.close, n - 1) 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 regime == "bear": self._side = "" reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊") 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 regime == "bull": self._side = "" reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛") return Signal(symbol=self.cfg.symbol, side="BUY", reason=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=f"牛市金叉", 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=f"熊市死叉", 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(2017, 1, 1) DATE_END = datetime(2026, 1, 1) async def get_actual_range(symbol: str) -> tuple[datetime, datetime]: """获取币种实际数据范围""" ds = DataService(config.db) await ds.connect() try: start, end = await ds.fetch_symbol_date_range(symbol, "4h") return start, end except: return DATE_START, DATE_END finally: await ds.close() async def main(): print() print("═" * 125) print(" 牛熊自适应策略 — 全币种全周期 | 牛市只多/熊市只空/震荡空仓") print("═" * 125) print(f"\n ■ 全周期汇总") print(f" {'币种':<10} {'数据范围':<22} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}") print(" " + "─" * 115) for symbol in SYMBOLS: fast, slow = PARAMS[symbol] sc = RegimeEmaConfig(symbol=symbol, fast=fast, slow=slow) # 获取实际数据范围 try: act_start, act_end = await get_actual_range(symbol) range_str = f"{act_start.date()}~{act_end.date()}" except: act_start, act_end = DATE_START, DATE_END range_str = "2017-2026" bt = BacktestConfig(symbol=symbol, interval="4h", start_time=act_start, end_time=act_end, initial_capital=10_000.0) engine = LongShortEngine(bt, db_config=config.db) r = await engine.run(RegimeEmaStrategy, sc) m = r.metrics long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"] short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"] long_pnl = sum(t.pnl for t in long_t) if long_t else 0 short_pnl = sum(t.pnl for t in short_t) if short_t else 0 print(f" {symbol:<10} {range_str:<22} {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} {long_pnl:>+9.0f} {short_pnl:>+9.0f}") # ── BTC 分段 ── PERIODS = [ ("2017 牛市", datetime(2017,1,1), datetime(2018,1,1)), ("2018 熊市", datetime(2018,1,1), datetime(2019,1,1)), ("2019 反弹", datetime(2019,1,1), datetime(2020,1,1)), ("2020 牛初", datetime(2020,1,1), datetime(2021,1,1)), ("2021 牛市", datetime(2021,1,1), datetime(2022,1,1)), ("2022 熊市", datetime(2022,1,1), datetime(2023,1,1)), ("2023 震荡", datetime(2023,1,1), datetime(2024,1,1)), ("2024-25牛", datetime(2024,1,1), datetime(2026,1,1)), ] print(f"\n ■ BTC 分段表现") print(f" {'阶段':<16} {'总收益%':>7} {'夏普':>6} {'多头P&L':>9} {'空头P&L':>9}") print(" " + "─" * 65) for name, s, e in PERIODS: try: sc = RegimeEmaConfig(symbol="BTCUSDT", fast=10, slow=50) bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=s, end_time=e, initial_capital=10_000.0) eng = LongShortEngine(bt, db_config=config.db) r = await eng.run(RegimeEmaStrategy, sc) lt = [t for t in r.trades if t.pnl is not None and t.side == "SELL"] st = [t for t in r.trades if t.pnl is not None and t.side == "BUY"] print(f" {name:<16} {r.metrics.total_return_pct:>+6.1f}% {r.metrics.sharpe_ratio:>6.2f} {sum(t.pnl for t in lt) if lt else 0:>+8.0f} {sum(t.pnl for t in st) if st else 0:>+8.0f}") except Exception as ex: print(f" {name:<16} 数据不足") print("\n═" * 125) if __name__ == "__main__": asyncio.run(main())