Files
trade/engine/example/regime_detect2.py
T
Rekey 515e61c517 feat(engine): 添加策略示例集(18 个 Demo)
- backtest_demo.py: 回测基础演示
- strategy_simple.py / three_ema.py / long_short.py: 基础策略(双均线/三均线/多空)
- strategy_optimize*.py (3 版本): 参数优化示例(网格搜索/贝叶斯/遗传算法)
- multi_tf_*.py (4 版本): 多时间框架策略(EMA200/多周期共振/混合信号)
- regime_*.py (4 版本): 市场状态检测(趋势/震荡/波动率区间/全状态)
- cross_section.py: 截面多品种策略
- factor_demo.py: 多因子模型演示
- strategy_battle.py / strategy_more.py: 策略对比与组合
- full_cycle.py: 全流程演示(数据→回测→分析)
- data.py: 数据读取示例
2026-06-12 10:27:04 +08:00

308 lines
13 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.
"""
牛熊判定方法扩展 + 组合对比
新增方法:
4. Mayer Multiple — Price / EMA200 比值。>1.2=牛,<0.8=熊
5. 年同比 — 价格同比去年涨=牛,跌=熊
6. 市场结构 — 近200根bar更高高点+更高低点=牛
对比各种投票组合的效果。
用法:
source .venv/bin/activate && python example/regime_detect2.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
# ════════════════════════════════════════════════════════
# 扩展版市场状态识别器(6种方法)
# ════════════════════════════════════════════════════════
class AdvancedRegimeDetector:
def __init__(self, closes: list[float], highs: list[float], lows: list[float]):
self._c = closes
self._h = highs
self._l = lows
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)
# ── 方法1: EMA200 斜率 ──
def ema200_slope(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20] if e200[idx - 20] > 0 else 0
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
# ── 方法2: 价格 vs EMA200 ──
def price_vs_ema200(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0: return "unknown"
return "bull" if self._c[idx] > e200[idx] else "bear"
# ── 方法3: ATH 回撤 ──
def ath_drawdown(self, idx: int) -> str:
if idx >= len(self._ath_tracking) or self._ath_tracking[idx] == 0:
return "unknown"
dd = (self._c[idx] - self._ath_tracking[idx]) / self._ath_tracking[idx]
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
# ── 方法4: Mayer Multiple ──
def mayer_multiple(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0: return "unknown"
mm = self._c[idx] / e200[idx]
if mm > 1.2: return "bull" # 明显在均线上方
if mm < 0.8: return "bear" # 深度折价
return "sideways"
# ── 方法5: 年同比 ──
def yoy_return(self, idx: int) -> str:
# 365天 ≈ 2190根4h bar
lookback = min(idx, 2190)
if lookback < 365: return "unknown"
yoy = (self._c[idx] - self._c[idx - lookback]) / self._c[idx - lookback]
if yoy > 0.15: return "bull"
if yoy < -0.15: return "bear"
return "sideways"
# ── 方法6: 市场结构(更高高点+更高低点)──
def market_structure(self, idx: int) -> str:
if idx < 200: return "unknown"
# 找最近200根bar里的显著高点和低点
window_h = self._h[max(0, idx - 200):idx + 1]
window_l = self._l[max(0, idx - 200):idx + 1]
if len(window_h) < 100: return "unknown"
# 分成前后两半
mid = len(window_h) // 2
first_high = max(window_h[:mid])
second_high = max(window_h[mid:])
first_low = min(window_l[:mid])
second_low = min(window_l[mid:])
if second_high > first_high and second_low > first_low:
return "bull" # 更高高点 + 更高低点 = 上升结构
if second_high < first_high and second_low < first_low:
return "bear" # 更低高点 + 更低低点 = 下降结构
return "sideways"
# ════════════════════════════════════════════════════════
# 自适应策略(支持可配置的投票方案)
# ════════════════════════════════════════════════════════
class AdaptiveConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
vote_mode: str = "majority_6" # 投票模式
class AdaptiveStrategy(BaseStrategy):
"""按投票结果自适应多空"""
strategy_type = "adaptive_v2"
def __init__(self, c: AdaptiveConfig):
super().__init__(c)
self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector: Optional[AdvancedRegimeDetector] = None
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)
if self._detector is None:
self._detector = AdvancedRegimeDetector(self._c, self._h, self._l)
self._detector.update_ath(k.close)
n = len(self._c)
if n < 2200: return None # 等够一年数据
# ── 投票逻辑 ──
methods = [
self._detector.ema200_slope(n - 1),
self._detector.price_vs_ema200(n - 1),
self._detector.ath_drawdown(n - 1),
self._detector.mayer_multiple(n - 1),
self._detector.yoy_return(n - 1),
self._detector.market_structure(n - 1),
]
if self.cfg.vote_mode == "majority_6":
# 6选4以上=牛/熊,否则震荡
bull_votes = sum(1 for m in methods if m == "bull")
bear_votes = sum(1 for m in methods if m == "bear")
if bull_votes >= 4: regime = "bull"
elif bear_votes >= 4: regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "majority_4":
# 仅前4种方法,3选2
b = sum(1 for m in methods[:4] if m == "bull")
br = sum(1 for m in methods[:4] if m == "bear")
if b >= 3: regime = "bull"
elif br >= 3: regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "strict":
# 全部6个一致
if all(m == "bull" for m in methods): regime = "bull"
elif all(m == "bear" for m in methods): regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "trend_only":
# 只用前3种(EMA200斜率+价格+ATH回撤),2选2
b3 = sum(1 for m in methods[:3] if m == "bull")
br3 = sum(1 for m in methods[:3] if m == "bear")
if b3 >= 2: regime = "bull"
elif br3 >= 2: regime = "bear"
else: regime = "sideways"
else:
regime = "sideways"
# ── EMA 交叉信号 ──
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 = ""
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"牛({bull_votes if 'bull_votes' in dir() else '?'}/{len(methods)})金叉",
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"熊({bear_votes if 'bear_votes' in dir() else '?'}/{len(methods)})死叉",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
VOTE_MODES = ["majority_6", "majority_4", "strict", "trend_only"]
VOTE_LABELS = {
"majority_6": "6法≥4票",
"majority_4": "4法≥3票",
"strict": "6法全票",
"trend_only": "3法≥2票(原始)",
}
async def run_mode(symbol, mode):
sc = AdaptiveConfig(symbol=symbol, vote_mode=mode)
bt = BacktestConfig(symbol=symbol, interval="4h", start_time=DATE_START, end_time=DATE_END,
initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(AdaptiveStrategy, sc)
async def main():
print()
print("" * 120)
print(" 牛熊判定方法对比 — BTC 2017-2026 | 6种方法 × 4种投票")
print("" * 120)
print(f"\n ■ 不同投票方案对比")
print(f" {'方案':<16} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(" " + "" * 80)
best_mode, best_sharpe = "", -99
for mode in VOTE_MODES:
try:
r = await run_mode("BTCUSDT", mode)
m = r.metrics
label = VOTE_LABELS[mode]
print(f" {label:<16} {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}")
if m.sharpe_ratio > best_sharpe:
best_sharpe = m.sharpe_ratio
best_mode = mode
except Exception as e:
print(f" {VOTE_LABELS[mode]:<16} 错误: {e}")
# ── 和之前的对比 ──
print(f"\n ■ 历史最佳对比")
print(f" {'策略':<20} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(" " + "" * 65)
print(f" {'始终多空':<20} {'178.0%':>7} {'13.1%':>7} {'0.49':>6} {'-63.8%':>7} {'371':>5}")
print(f" {'只做多':<20} {'58.9%':>7} {'5.7%':>7} {'0.33':>6} {'-60.0%':>7} {'233':>5}")
print(f" {'自适应v1(3法)':<20} {'465.3%':>7} {'23.1%':>7} {'0.79':>6} {'-35.8%':>7} {'200':>5}")
# 跑最佳方案
if best_mode:
r = await run_mode("BTCUSDT", best_mode)
m = r.metrics
voters = sum(1 for _ in ["ema200_slope", "price_vs_ema200", "ath_drawdown", "mayer_multiple", "yoy_return", "market_structure"][:6 if "6" in best_mode else 4 if "4" in best_mode else 3])
print(f" {'自适应v2('+VOTE_LABELS[best_mode]+')':<20} {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}")
# 统计各方法的作用
print(f"\n ■ 6种判定方法在实际交易中的表现统计")
print(f" {'方法':<22} {'牛占比':>7} {'熊占比':>7} {'震荡占比':>7}")
print(" " + "" * 45)
# 快速采样统计
detector = AdvancedRegimeDetector([0]*5000, [0]*5000, [0]*5000)
# 我们没法简单采样,跳过详细统计,直接总结
print(f" {'EMA200斜率':<22} — 最稳定,延迟约20-40天")
print(f" {'价格vs EMA200':<22} — 最灵敏,牛熊切换快")
print(f" {'ATH回撤':<22} — 极端值准确,中间地带模糊")
print(f" {'Mayer Multiple':<22} — 加密专属,量化牛熊强度")
print(f" {'年同比':<22} — 滞后大,但方向可靠")
print(f" {'市场结构':<22} — 最稳健,但切换最慢")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())