""" 市场状态识别 — 牛市/熊市判定方法对比 + 自适应策略回测 方法: 1. EMA200 斜率 — EMA200 向上=牛,向下=熊 2. 价格 vs EMA200 — Price > EMA200 = 牛 3. ATH 回撤 — 距历史高点 < 20% = 牛,> 20% = 熊 4. 综合投票 — 三选二 根据识别结果自动偏多/偏空: 牛市:只做多(金叉买入,死叉平仓) 熊市:只做空(死叉做空,金叉平仓) 震荡(票数2:1或无共识):空仓等待 BTC 2017-2026 全周期测试 用法: source .venv/bin/activate && python example/regime_detect.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 import ema, atr from engine.example.long_short import LongShortEngine # ════════════════════════════════════════════════════════ # 市场状态识别器 # ════════════════════════════════════════════════════════ class RegimeDetector: """市场状态识别:牛/熊/震荡""" def __init__(self, closes: list[float]): self._c = closes self._ath = 0.0 self._ath_tracking = [] # 追踪历史高点序列 def update_ath(self, price: float): if price > self._ath: self._ath = price self._ath_tracking.append(self._ath) def ema200_slope(self, idx: int) -> str: """EMA200 斜率判定""" if idx < 202: return "unknown" e200 = ema(self._c, 200) # 最近5根EMA200的斜率 if e200[idx] == 0 or e200[max(0, idx - 5)] == 0: return "unknown" slope = (e200[idx] - e200[max(0, idx - 5)]) / e200[max(0, idx - 5)] if slope > 0.001: return "bull" elif slope < -0.001: return "bear" return "sideways" def price_vs_ema200(self, idx: int) -> str: """价格 vs EMA200""" if idx < 202: return "unknown" e200 = ema(self._c, 200) if e200[idx] == 0: return "unknown" return "bull" if self._c[idx] > e200[idx] else "bear" def ath_drawdown(self, idx: int) -> str: """ATH 回撤判定(经典加密牛熊指标)""" if not self._ath_tracking or idx >= len(self._ath_tracking): return "unknown" curr_ath = self._ath_tracking[idx] if curr_ath == 0: return "unknown" dd = (self._c[idx] - curr_ath) / curr_ath if dd > -0.20: return "bull" elif dd < -0.40: return "bear" return "sideways" def combined(self, idx: int) -> tuple[str, str, str, str]: """综合判定""" r1 = self.ema200_slope(idx) r2 = self.price_vs_ema200(idx) r3 = self.ath_drawdown(idx) votes = {"bull": 0, "bear": 0} for r in [r1, r2, r3]: if r in votes: votes[r] += 1 if votes["bull"] >= 2: return "bull", r1, r2, r3 elif votes["bear"] >= 2: return "bear", r1, r2, r3 return "sideways", r1, r2, r3 # ════════════════════════════════════════════════════════ # 自适应策略(根据市场状态偏多/偏空) # ════════════════════════════════════════════════════════ class AdaptiveEmaConfig(StrategyConfig): fast: int = 10 slow: int = 50 atr_stop: float = 2.5 class AdaptiveEmaStrategy(BaseStrategy): """牛市只做多、熊市只做空、震荡空仓""" strategy_type = "adaptive_ema" def __init__(self, c: AdaptiveEmaConfig): super().__init__(c) self.cfg = c self._closes: list[float] = [] self._highs: list[float] = [] self._lows: list[float] = [] self._detector: Optional[RegimeDetector] = None self._position_side: str = "" # "long" / "short" self._highest: float = 0.0 self._lowest: float = float('inf') async def on_kline(self, k: Kline) -> Optional[Signal]: self._closes.append(k.close) self._highs.append(k.high) self._lows.append(k.low) if self._detector is None: self._detector = RegimeDetector(self._closes) self._detector.update_ath(k.close) n = len(self._closes) if n < 210: return None regime, r1, r2, r3 = self._detector.combined(n - 1) fast = ema(self._closes, self.cfg.fast) slow = ema(self._closes, self.cfg.slow) atr_vals = atr(self._highs, self._lows, self._closes, 14) cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1] prev_f, prev_s = fast[-2], slow[-2] if cur_f == 0 or cur_s == 0 or cur_atr == 0: return None golden = prev_f <= prev_s and cur_f > cur_s death = prev_f >= prev_s and cur_f < cur_s # ── 多头持仓 ── if self._position_side == "long": self._highest = max(self._highest, k.high) stop = self._highest - self.cfg.atr_stop * cur_atr if death or k.close < stop or regime == "bear": self._position_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._position_side == "short": self._lowest = min(self._lowest, k.low) stop = self._lowest + self.cfg.atr_stop * cur_atr if golden or k.close > stop or regime == "bull": self._position_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._position_side = "long" self._highest = k.close return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"牛市金叉 ({r1}/{r2}/{r3})", timestamp=k.open_time) elif regime == "bear" and death: self._position_side = "short" self._lowest = k.close return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"熊市死叉 ({r1}/{r2}/{r3})", timestamp=k.open_time) # 震荡市:不开仓 return None # ════════════════════════════════════════════════════════ # 对比测试 # ════════════════════════════════════════════════════════ DATE_START = datetime(2017, 1, 1) DATE_END = datetime(2026, 1, 1) async def main(): print() print("═" * 120) print(" 市场状态自适应策略 — 牛市只做多 / 熊市只做空 / 震荡空仓") print("═" * 120) # ── BTC 自适应 vs 多空 vs 只做多 ── print("\n ■ BTC 全周期 2017-2026 对比") print(f" {'策略':<18} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}") print(" " + "─" * 105) # 自适应 sc = AdaptiveEmaConfig(symbol="BTCUSDT") bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0) engine = LongShortEngine(bt, db_config=config.db) r = await engine.run(AdaptiveEmaStrategy, 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"] print(f" {'自适应(牛多熊空)':<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} {sum(t.pnl for t in long_t) if long_t else 0:>+9.0f} {sum(t.pnl for t in short_t) if short_t else 0:>+9.0f}") # 始终多空(之前结果) from engine.example.long_short import LongShortEmaStrategy, LongShortEmaConfig as LSCfg sc2 = LSCfg(symbol="BTCUSDT", fast=10, slow=50) r2 = await engine.run(LongShortEmaStrategy, sc2) m2 = r2.metrics print(f" {'始终多空':<18} {m2.total_return_pct:>6.1f}% {m2.annual_return_pct:>6.1f}% {m2.sharpe_ratio:>6.2f} {m2.max_drawdown_pct:>6.1f}% {m2.total_trades:>5}") # 只做多 from engine.backtest import BacktestEngine as OrigEngine class LongOnlyS(BaseStrategy): strategy_type = "lo"; _in = False; _hp = 0.0 def __init__(self, c): super().__init__(c); self._c = []; self._h = []; self._l = [] async def on_start(self): await super().on_start() async def on_kline(self, k): self._c.append(k.close); self._h.append(k.high); self._l.append(k.low) n = len(self._c) if n < 55: return None f = ema(self._c, 10); s = ema(self._c, 50); 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 if self._in: self._hp = max(self._hp, k.high); stop = self._hp - 2.5 * ca if (pf >= ps and cf < cs) or k.close < stop: self._in = False return Signal(symbol="BTCUSDT", side="SELL", reason="死叉" if pf>=ps else "ATR止损", timestamp=k.open_time) else: if pf <= ps and cf > cs: self._in = True; self._hp = k.close return Signal(symbol="BTCUSDT", side="BUY", reason="金叉", timestamp=k.open_time) return None from engine.common.base import StrategyConfig as SC lo_bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0) lo_e = OrigEngine(lo_bt, db_config=config.db) lo_r = await lo_e.run(LongOnlyS, SC(symbol="BTCUSDT")) lo_m = lo_r.metrics long_only_t = [t for t in lo_r.trades if t.pnl is not None] print(f" {'只做多':<18} {lo_m.total_return_pct:>6.1f}% {lo_m.annual_return_pct:>6.1f}% {lo_m.sharpe_ratio:>6.2f} {lo_m.max_drawdown_pct:>6.1f}% {lo_m.total_trades:>5}") # ── 分段对比 ── 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 分段:自适应 vs 始终多空") print(f" {'阶段':<16} {'自适应':>8} {'始终多空':>8} {'只做多':>8}") print(" " + "─" * 50) for name, s, e in PERIODS: try: bt_p = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=s, end_time=e, initial_capital=10_000.0) # 自适应 sc_p = AdaptiveEmaConfig(symbol="BTCUSDT") e_p = LongShortEngine(bt_p, db_config=config.db) r_p = await e_p.run(AdaptiveEmaStrategy, sc_p) # 始终多空 sc_p2 = LSCfg(symbol="BTCUSDT", fast=10, slow=50) r_p2 = await e_p.run(LongShortEmaStrategy, sc_p2) # 只做多 lo_e2 = OrigEngine(bt_p, db_config=config.db) lo_r2 = await lo_e2.run(LongOnlyS, SC(symbol="BTCUSDT")) print(f" {name:<16} {r_p.metrics.total_return_pct:>+7.1f}% {r_p2.metrics.total_return_pct:>+7.1f}% {lo_r2.metrics.total_return_pct:>+7.1f}%") except Exception as ex: print(f" {name:<16} 错误: {ex}") print("\n═" * 120) if __name__ == "__main__": asyncio.run(main())