515e61c517
- 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: 数据读取示例
234 lines
9.3 KiB
Python
234 lines
9.3 KiB
Python
"""
|
|
最佳牛熊判定 — 全币种全周期回测
|
|
|
|
方法: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 import ema, atr
|
|
from engine.data import DataService
|
|
from engine.example.long_short import LongShortEngine
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
# 3法判定器
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
|
|
class RegimeDetector3:
|
|
def __init__(self):
|
|
self._ath = 0.0
|
|
|
|
def update_ath(self, price: float):
|
|
if price > self._ath:
|
|
self._ath = price
|
|
|
|
def ema200_slope(self, closes: list[float], idx: int) -> str:
|
|
if idx < 210: return "unknown"
|
|
e200 = ema(closes, 200)
|
|
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, closes: list[float], idx: int) -> str:
|
|
if idx < 210: return "unknown"
|
|
e200 = ema(closes, 200)
|
|
if e200[idx] == 0: return "unknown"
|
|
return "bull" if closes[idx] > e200[idx] else "bear"
|
|
|
|
def ath_drawdown(self, closes: list[float], idx: int) -> str:
|
|
if self._ath == 0: return "unknown"
|
|
dd = (closes[idx] - self._ath) / self._ath
|
|
if dd > -0.15: return "bull"
|
|
if dd < -0.35: return "bear"
|
|
return "sideways"
|
|
|
|
def detect(self, closes: list[float], idx: int) -> str:
|
|
r1 = self.ema200_slope(closes, idx)
|
|
r2 = self.price_vs_ema200(closes, idx)
|
|
r3 = self.ath_drawdown(closes, idx)
|
|
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):
|
|
"""按市场状态自适应做多/做空"""
|
|
|
|
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._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)
|
|
self._detector.update_ath(k.close)
|
|
n = len(self._c)
|
|
if n < 220: return None
|
|
|
|
regime = self._detector.detect(self._c, n - 1)
|
|
|
|
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"牛市金叉", 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())
|