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: 数据读取示例
This commit is contained in:
Rekey
2026-06-12 10:27:04 +08:00
parent 4da520c14b
commit 515e61c517
21 changed files with 5194 additions and 0 deletions
+307
View File
@@ -0,0 +1,307 @@
"""
牛熊判定方法扩展 + 组合对比
新增方法:
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())