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:
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
市场状态识别 — 牛市/熊市判定方法对比 + 自适应策略回测
|
||||
|
||||
方法:
|
||||
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())
|
||||
Reference in New Issue
Block a user