0cd2cbbb79
- comparison_2h_6h: 9 策略 × 4 币种 × 2 周期 × 4 数据量 = 288 次回测
- 包含海龟、超级趋势、MACD、布林收缩、三均线、RSI 回归、
ATR 波动率突破、EMA 多空、牛熊自适应
- 结论:6h 夏普显著优于 2h(69% 组合),ATR 策略霸榜
- 自动生成 Markdown 回测报告
- vol_break_1h_6h: ATR 波动率突破 × 1h/2h/4h/6h 近半年对比
1054 lines
45 KiB
Python
1054 lines
45 KiB
Python
"""
|
||
2h/6h 策略全维度对比回测 — 9策略 × 4币种 × 2时间级别 × 4数据量
|
||
|
||
8个网络知名策略 + 牛熊自适应策略(RegimeDetector3投票)
|
||
在 2h 和 6h 两个新时间级别上的表现对比。
|
||
|
||
用法:
|
||
source .venv/bin/activate && python example/comparison_2h_6h.py
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
import statistics
|
||
import sys
|
||
import time
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
from typing import Optional, Type
|
||
|
||
_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.models import BacktestConfig, BacktestResult
|
||
from engine.data import DataService
|
||
from engine.indicators.incremental import EmaInc, AtrInc, RsiInc, BbInc
|
||
from engine.example.long_short import LongShortEngine
|
||
|
||
# ── 全局常量 ──
|
||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||
TIMEFRAMES = ["2h", "6h"]
|
||
INITIAL = 10_000.0
|
||
WARMUP = 150
|
||
MAX_CONCURRENCY = 6
|
||
|
||
NOW = datetime.now(timezone.utc)
|
||
PERIODS = {
|
||
"全量": (None, None),
|
||
"近两年": (NOW - timedelta(days=730), NOW),
|
||
"近一年": (NOW - timedelta(days=365), NOW),
|
||
"近半年": (NOW - timedelta(days=182), NOW),
|
||
}
|
||
|
||
MIN_BARS_FOR_PERIOD = {
|
||
"全量": 500,
|
||
"近两年": 200,
|
||
"近一年": 100,
|
||
"近半年": 50,
|
||
}
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 1:海龟交易 (Turtle Trading)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class TurtleConfig(StrategyConfig):
|
||
entry_period: int = 20
|
||
exit_period: int = 10
|
||
atr_period: int = 20
|
||
atr_stop: float = 2.0
|
||
|
||
|
||
class TurtleStrategy(BaseStrategy):
|
||
strategy_type = "趋势跟踪"
|
||
strategy_desc = "Donchian 20/10通道突破 + 2N ATR止损,多空双向"
|
||
|
||
def __init__(self, c: TurtleConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._highs: list[float] = []
|
||
self._lows: list[float] = []
|
||
self._closes: list[float] = []
|
||
self._atr = AtrInc(c.atr_period)
|
||
self._side: str = ""
|
||
self._entry_price: float = 0.0
|
||
self._highest_since: float = 0.0
|
||
self._lowest_since: float = float("inf")
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._highs.append(k.high)
|
||
self._lows.append(k.low)
|
||
self._closes.append(k.close)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
n = len(self._closes)
|
||
min_bars = max(self.cfg.entry_period, self.cfg.atr_period) + 5
|
||
if n < min_bars:
|
||
return None
|
||
ca = self._atr[-1]
|
||
if ca == 0:
|
||
return None
|
||
d_high = max(self._highs[-(self.cfg.entry_period + 1):-1])
|
||
d_low = min(self._lows[-(self.cfg.entry_period + 1):-1])
|
||
d_exit_high = max(self._highs[-(self.cfg.exit_period + 1):-1])
|
||
d_exit_low = min(self._lows[-(self.cfg.exit_period + 1):-1])
|
||
|
||
if self._side == "long":
|
||
self._highest_since = max(self._highest_since, k.high)
|
||
stop = self._entry_price - self.cfg.atr_stop * ca
|
||
trail = self._highest_since - self.cfg.atr_stop * ca * 0.5
|
||
if k.close < d_exit_low or k.close < max(stop, trail):
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="海龟退出", timestamp=k.open_time, confidence=0.25)
|
||
elif self._side == "short":
|
||
self._lowest_since = min(self._lowest_since, k.low)
|
||
stop = self._entry_price + self.cfg.atr_stop * ca
|
||
trail = self._lowest_since + self.cfg.atr_stop * ca * 0.5
|
||
if k.close > d_exit_high or k.close > min(stop, trail):
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="海龟退出", timestamp=k.open_time, confidence=0.25)
|
||
else:
|
||
margin = 0.002
|
||
if k.close > d_high * (1 + margin):
|
||
self._side = "long"
|
||
self._entry_price = k.close
|
||
self._highest_since = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="突破20日高", timestamp=k.open_time, confidence=0.25)
|
||
elif k.close < d_low * (1 - margin):
|
||
self._side = "short"
|
||
self._entry_price = k.close
|
||
self._lowest_since = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="跌破20日低", timestamp=k.open_time, confidence=0.25)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 2:超级趋势 (SuperTrend)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class SuperTrendConfig(StrategyConfig):
|
||
atr_period: int = 10
|
||
multiplier: float = 3.0
|
||
|
||
|
||
class SuperTrendStrategy(BaseStrategy):
|
||
strategy_type = "趋势跟踪"
|
||
strategy_desc = "ATR(10)×3倍动态跟踪止损带,趋势翻转即反转"
|
||
|
||
def __init__(self, c: SuperTrendConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._atr = AtrInc(c.atr_period)
|
||
self._highs: list[float] = []
|
||
self._lows: list[float] = []
|
||
self._closes: list[float] = []
|
||
self._trend: int = 0
|
||
self._final_upper: float = 0.0
|
||
self._final_lower: float = 0.0
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._highs.append(k.high)
|
||
self._lows.append(k.low)
|
||
self._closes.append(k.close)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
n = len(self._closes)
|
||
if n < self.cfg.atr_period + 5:
|
||
return None
|
||
ca = self._atr[-1]
|
||
if ca == 0:
|
||
return None
|
||
hl2 = (k.high + k.low) / 2.0
|
||
upper = hl2 + self.cfg.multiplier * ca
|
||
lower = hl2 - self.cfg.multiplier * ca
|
||
prev_upper = self._final_upper
|
||
prev_lower = self._final_lower
|
||
prev_trend = self._trend
|
||
|
||
if k.close > prev_upper and prev_upper > 0:
|
||
self._trend = 1
|
||
elif k.close < prev_lower and prev_lower > 0:
|
||
self._trend = -1
|
||
|
||
if self._trend == 1:
|
||
self._final_lower = max(lower, prev_lower) if prev_lower > 0 else lower
|
||
self._final_upper = float("inf")
|
||
elif self._trend == -1:
|
||
self._final_upper = min(upper, prev_upper) if prev_upper > 0 else upper
|
||
self._final_lower = float("-inf")
|
||
else:
|
||
self._final_upper = upper
|
||
self._final_lower = lower
|
||
|
||
if prev_trend == self._trend:
|
||
return None
|
||
if self._trend == 1:
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="SuperTrend转多", timestamp=k.open_time, confidence=0.25)
|
||
elif self._trend == -1:
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="SuperTrend转空", timestamp=k.open_time, confidence=0.25)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 3:MACD 金叉死叉
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class MacdCrossConfig(StrategyConfig):
|
||
fast: int = 12
|
||
slow: int = 26
|
||
signal: int = 9
|
||
atr_period: int = 14
|
||
atr_stop: float = 2.0
|
||
|
||
|
||
class MacdCrossStrategy(BaseStrategy):
|
||
strategy_type = "动量"
|
||
strategy_desc = "MACD(12,26,9)零轴上金叉做多/零轴下死叉做空+ATR止损"
|
||
|
||
def __init__(self, c: MacdCrossConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._ema_fast = EmaInc(c.fast)
|
||
self._ema_slow = EmaInc(c.slow)
|
||
self._atr = AtrInc(c.atr_period)
|
||
self._macd_vals: list[float] = []
|
||
self._signal_vals: list[float] = []
|
||
self._side: str = ""
|
||
self._entry_price: float = 0.0
|
||
self._bars_held: int = 0
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
fe = self._ema_fast.update(k.close)
|
||
se = self._ema_slow.update(k.close)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
n = len(self._ema_fast)
|
||
min_bars = max(self.cfg.slow, self.cfg.signal) + 10
|
||
if n < min_bars:
|
||
return None
|
||
macd = fe - se
|
||
self._macd_vals.append(macd)
|
||
if len(self._macd_vals) < self.cfg.signal + 2:
|
||
self._signal_vals.append(0.0)
|
||
return None
|
||
if len(self._signal_vals) < self.cfg.signal:
|
||
self._signal_vals.append(0.0)
|
||
if len(self._signal_vals) == self.cfg.signal:
|
||
self._signal_vals[-1] = sum(self._macd_vals[-self.cfg.signal:]) / self.cfg.signal
|
||
return None
|
||
k_sig = 2.0 / (self.cfg.signal + 1)
|
||
sig_val = macd * k_sig + self._signal_vals[-1] * (1 - k_sig)
|
||
self._signal_vals.append(sig_val)
|
||
if len(self._signal_vals) < 3:
|
||
return None
|
||
cur_m, cur_s = self._macd_vals[-1], self._signal_vals[-1]
|
||
prev_m, prev_s = self._macd_vals[-2], self._signal_vals[-2]
|
||
ca = self._atr[-1]
|
||
if ca == 0:
|
||
return None
|
||
golden = prev_m <= prev_s and cur_m > cur_s
|
||
death = prev_m >= prev_s and cur_m < cur_s
|
||
|
||
if self._side == "long":
|
||
self._bars_held += 1
|
||
stop = self._entry_price - self.cfg.atr_stop * ca
|
||
if k.close < stop or (death and self._bars_held > 3):
|
||
self._side = ""; self._bars_held = 0
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD退出", timestamp=k.open_time, confidence=0.25)
|
||
elif self._side == "short":
|
||
self._bars_held += 1
|
||
stop = self._entry_price + self.cfg.atr_stop * ca
|
||
if k.close > stop or (golden and self._bars_held > 3):
|
||
self._side = ""; self._bars_held = 0
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD退出", timestamp=k.open_time, confidence=0.25)
|
||
else:
|
||
if golden and cur_m > 0:
|
||
self._side = "long"; self._entry_price = k.close; self._bars_held = 0
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD零轴上金叉", timestamp=k.open_time, confidence=0.25)
|
||
elif death and cur_m < 0:
|
||
self._side = "short"; self._entry_price = k.close; self._bars_held = 0
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD零轴下死叉", timestamp=k.open_time, confidence=0.25)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 4:布林收缩爆发 (Bollinger Squeeze)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class BBSqueezeConfig(StrategyConfig):
|
||
bb_period: int = 20
|
||
bb_std: float = 2.0
|
||
kc_period: int = 20
|
||
kc_mult: float = 1.5
|
||
squeeze_lookback: int = 30
|
||
atr_stop: float = 2.0
|
||
|
||
|
||
class BBSqueezeStrategy(BaseStrategy):
|
||
strategy_type = "波动率突破"
|
||
strategy_desc = "BB收缩至KC内部后扩张爆发,顺势入场 + ATR止损"
|
||
|
||
def __init__(self, c: BBSqueezeConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._bb = BbInc(c.bb_period, c.bb_std)
|
||
self._ema = EmaInc(c.kc_period)
|
||
self._atr_kc = AtrInc(c.kc_period)
|
||
self._atr_stop = AtrInc(14)
|
||
self._closes: list[float] = []
|
||
self._side: str = ""
|
||
self._entry_price: float = 0.0
|
||
self._bb_widths: list[float] = []
|
||
self._kc_widths: list[float] = []
|
||
self._was_squeezed: bool = False
|
||
self._squeeze_bars: int = 0
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._closes.append(k.close)
|
||
bb_u, bb_m, bb_l = self._bb.update(k.close)
|
||
typical = (k.high + k.low + k.close) / 3.0
|
||
kc_mid = self._ema.update(typical)
|
||
self._atr_kc.update(k.high, k.low, k.close)
|
||
self._atr_stop.update(k.high, k.low, k.close)
|
||
n = len(self._closes)
|
||
min_bars = max(self.cfg.bb_period, self.cfg.kc_period, self.cfg.squeeze_lookback) + 5
|
||
if n < min_bars:
|
||
return None
|
||
atr_kc = self._atr_kc[-1]
|
||
ca = self._atr_stop[-1]
|
||
if atr_kc == 0 or ca == 0 or bb_u == 0:
|
||
return None
|
||
kc_u = kc_mid + self.cfg.kc_mult * atr_kc
|
||
kc_l = kc_mid - self.cfg.kc_mult * atr_kc
|
||
bb_width = bb_u - bb_l
|
||
kc_width = kc_u - kc_l
|
||
self._bb_widths.append(bb_width)
|
||
self._kc_widths.append(kc_width)
|
||
is_squeezed = bb_u < kc_u and bb_l > kc_l
|
||
lookback = min(self.cfg.squeeze_lookback, len(self._bb_widths))
|
||
recent_bb_w = self._bb_widths[-lookback:]
|
||
min_bb_w = min(recent_bb_w)
|
||
width_squeeze = bb_width < min_bb_w * 1.2
|
||
was_squeezed = self._was_squeezed
|
||
fired = False
|
||
if is_squeezed:
|
||
self._was_squeezed = True
|
||
self._squeeze_bars += 1
|
||
elif self._was_squeezed:
|
||
self._was_squeezed = False
|
||
self._squeeze_bars = 0
|
||
fired = True
|
||
ema5 = sum(self._closes[-5:]) / 5.0 if n >= 5 else k.close
|
||
up_momentum = k.close > bb_m and k.close > ema5
|
||
down_momentum = k.close < bb_m and k.close < ema5
|
||
|
||
if self._side == "long":
|
||
stop = self._entry_price - self.cfg.atr_stop * ca
|
||
if k.close < stop or (down_momentum and not is_squeezed):
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="BB退出", timestamp=k.open_time, confidence=0.25)
|
||
elif self._side == "short":
|
||
stop = self._entry_price + self.cfg.atr_stop * ca
|
||
if k.close > stop or (up_momentum and not is_squeezed):
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="BB退出", timestamp=k.open_time, confidence=0.25)
|
||
else:
|
||
if was_squeezed and fired and width_squeeze:
|
||
if up_momentum:
|
||
self._side = "long"; self._entry_price = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="BB收缩爆发做多", timestamp=k.open_time, confidence=0.25)
|
||
elif down_momentum:
|
||
self._side = "short"; self._entry_price = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="BB收缩爆发做空", timestamp=k.open_time, confidence=0.25)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 5:三均线排列 (Triple EMA)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class TripleEmaConfig(StrategyConfig):
|
||
fast: int = 10
|
||
mid: int = 30
|
||
slow: int = 60
|
||
atr_period: int = 14
|
||
atr_stop: float = 2.0
|
||
|
||
|
||
class TripleEmaStrategy(BaseStrategy):
|
||
strategy_type = "趋势跟踪"
|
||
strategy_desc = "EMA(10,30,60)多头/空头排列,快线金叉入场+ATR追踪止损"
|
||
|
||
def __init__(self, c: TripleEmaConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._ema_fast = EmaInc(c.fast)
|
||
self._ema_mid = EmaInc(c.mid)
|
||
self._ema_slow = EmaInc(c.slow)
|
||
self._atr = AtrInc(c.atr_period)
|
||
self._side: str = ""
|
||
self._entry_price: float = 0.0
|
||
self._highest_since: float = 0.0
|
||
self._lowest_since: float = float("inf")
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._ema_fast.update(k.close)
|
||
self._ema_mid.update(k.close)
|
||
self._ema_slow.update(k.close)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
n = len(self._ema_slow)
|
||
if n < self.cfg.slow + 10:
|
||
return None
|
||
ef, em, es = self._ema_fast[-1], self._ema_mid[-1], self._ema_slow[-1]
|
||
pf, pm = self._ema_fast[-2], self._ema_mid[-2]
|
||
ca = self._atr[-1]
|
||
if ef == 0 or em == 0 or es == 0 or ca == 0:
|
||
return None
|
||
bull_align = ef > em > es
|
||
bear_align = ef < em < es
|
||
fast_cross_mid_up = pf <= pm and ef > em
|
||
fast_cross_mid_down = pf >= pm and ef < em
|
||
|
||
if self._side == "long":
|
||
self._highest_since = max(self._highest_since, k.high)
|
||
trail = self._highest_since - self.cfg.atr_stop * ca
|
||
if fast_cross_mid_down or k.close < trail:
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="三均线退出", timestamp=k.open_time, confidence=0.25)
|
||
elif self._side == "short":
|
||
self._lowest_since = min(self._lowest_since, k.low)
|
||
trail = self._lowest_since + self.cfg.atr_stop * ca
|
||
if fast_cross_mid_up or k.close > trail:
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="三均线退出", timestamp=k.open_time, confidence=0.25)
|
||
else:
|
||
if fast_cross_mid_up and bull_align:
|
||
self._side = "long"; self._entry_price = k.close; self._highest_since = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="三线多头排列", timestamp=k.open_time, confidence=0.25)
|
||
elif fast_cross_mid_down and bear_align:
|
||
self._side = "short"; self._entry_price = k.close; self._lowest_since = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="三线空头排列", timestamp=k.open_time, confidence=0.25)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 6:RSI均值回归 (RSI Mean Reversion)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class MeanRevConfig(StrategyConfig):
|
||
rsi_period: int = 14
|
||
rsi_oversold: float = 25.0
|
||
rsi_overbought: float = 75.0
|
||
bb_period: int = 20
|
||
bb_std: float = 2.0
|
||
atr_stop: float = 1.5
|
||
require_bb_touch: bool = True
|
||
|
||
|
||
class MeanRevStrategy(BaseStrategy):
|
||
strategy_type = "均值回归"
|
||
strategy_desc = "RSI(14)超卖25/超买75 + 布林带触碰确认 → 逆向回归"
|
||
|
||
def __init__(self, c: MeanRevConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._rsi = RsiInc(c.rsi_period)
|
||
self._bb = BbInc(c.bb_period, c.bb_std)
|
||
self._atr = AtrInc(14)
|
||
self._side: str = ""
|
||
self._entry_price: float = 0.0
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
r = self._rsi.update(k.close)
|
||
up, mid, lo = self._bb.update(k.close)
|
||
atr_v = self._atr.update(k.high, k.low, k.close)
|
||
if r == 0 or up == 0 or atr_v == 0:
|
||
return None
|
||
below_bb = k.close < lo if self.cfg.require_bb_touch else True
|
||
above_bb = k.close > up if self.cfg.require_bb_touch else True
|
||
|
||
if self._side == "long":
|
||
stop = self._entry_price - self.cfg.atr_stop * atr_v
|
||
take = self._entry_price + self.cfg.atr_stop * atr_v * 1.5
|
||
if k.close <= stop or k.close >= take or r > 55:
|
||
self._side = ""
|
||
reason = "止损" if k.close <= stop else ("止盈" if k.close >= take else "RSI回归中轨")
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
|
||
elif self._side == "short":
|
||
stop = self._entry_price + self.cfg.atr_stop * atr_v
|
||
take = self._entry_price - self.cfg.atr_stop * atr_v * 1.5
|
||
if k.close >= stop or k.close <= take or r < 45:
|
||
self._side = ""
|
||
reason = "止损" if k.close >= stop else ("止盈" if k.close <= take else "RSI回归中轨")
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
|
||
else:
|
||
if r < self.cfg.rsi_oversold and below_bb:
|
||
self._side = "long"; self._entry_price = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"RSI超卖{r:.0f}", timestamp=k.open_time)
|
||
elif r > self.cfg.rsi_overbought and above_bb:
|
||
self._side = "short"; self._entry_price = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买{r:.0f}", timestamp=k.open_time)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 7:ATR波动率突破 (Volatility Breakout)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class VolBreakConfig(StrategyConfig):
|
||
atr_period: int = 14
|
||
squeeze_period: int = 20
|
||
squeeze_ratio: float = 0.7
|
||
atr_stop: float = 2.0
|
||
|
||
|
||
class VolBreakStrategy(BaseStrategy):
|
||
strategy_type = "波动率突破"
|
||
strategy_desc = "ATR(14)收缩至极低后扩张突破 + EMA(10/30)方向确认"
|
||
|
||
def __init__(self, c: VolBreakConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._atr = AtrInc(c.atr_period)
|
||
self._ema_fast = EmaInc(10)
|
||
self._ema_slow = EmaInc(30)
|
||
self._closes: list[float] = []
|
||
self._highs: list[float] = []
|
||
self._lows: list[float] = []
|
||
self._side: str = ""
|
||
self._entry_price: float = 0.0
|
||
self._was_squeezed = False
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._closes.append(k.close)
|
||
self._highs.append(k.high)
|
||
self._lows.append(k.low)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
self._ema_fast.update(k.close)
|
||
self._ema_slow.update(k.close)
|
||
n = len(self._closes)
|
||
if n < self.cfg.atr_period + self.cfg.squeeze_period:
|
||
return None
|
||
atr_now = self._atr[-1]
|
||
atr_prev = self._atr[-2] if n >= 2 else 0
|
||
ca = atr_now
|
||
if ca == 0:
|
||
return None
|
||
atr_window = [self._atr[i] for i in range(max(0, n - self.cfg.squeeze_period), n) if self._atr[i] > 0]
|
||
if not atr_window:
|
||
return None
|
||
min_atr = min(atr_window)
|
||
is_squeezed = atr_now < min_atr * (1 + (1 - self.cfg.squeeze_ratio))
|
||
atr_expanding = atr_now > atr_prev * 1.05 if atr_prev > 0 else False
|
||
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
|
||
trend_up = cf > cs
|
||
|
||
if self._side == "long":
|
||
self._was_squeezed = False
|
||
stop = self._entry_price - self.cfg.atr_stop * ca
|
||
if k.close < stop or (cf < cs and not is_squeezed):
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR退出", timestamp=k.open_time)
|
||
elif self._side == "short":
|
||
self._was_squeezed = False
|
||
stop = self._entry_price + self.cfg.atr_stop * ca
|
||
if k.close > stop or (cf > cs and not is_squeezed):
|
||
self._side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR退出", timestamp=k.open_time)
|
||
else:
|
||
if is_squeezed:
|
||
self._was_squeezed = True
|
||
elif self._was_squeezed and atr_expanding:
|
||
self._was_squeezed = False
|
||
if trend_up:
|
||
self._side = "long"; self._entry_price = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR扩张突破做多", timestamp=k.open_time)
|
||
else:
|
||
self._side = "short"; self._entry_price = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR扩张突破做空", timestamp=k.open_time)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 8:EMA双均线多空 (EMA Crossover)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class EmaCrossConfig(StrategyConfig):
|
||
fast: int = 10
|
||
slow: int = 50
|
||
atr_stop: float = 2.5
|
||
|
||
|
||
class EmaCrossStrategy(BaseStrategy):
|
||
strategy_type = "趋势跟踪"
|
||
strategy_desc = "EMA(10,50)金叉做多死叉做空 + ATR追踪止损,始终在场"
|
||
|
||
def __init__(self, c: EmaCrossConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._ema_fast = EmaInc(c.fast)
|
||
self._ema_slow = EmaInc(c.slow)
|
||
self._atr = AtrInc(14)
|
||
self._closes: list[float] = []
|
||
self._highs: list[float] = []
|
||
self._lows: list[float] = []
|
||
self._highest: float = 0.0
|
||
self._lowest: float = float('inf')
|
||
self._position_side: str = ""
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._closes.append(k.close)
|
||
self._highs.append(k.high)
|
||
self._lows.append(k.low)
|
||
self._ema_fast.update(k.close)
|
||
self._ema_slow.update(k.close)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
n = len(self._closes)
|
||
if n < self.cfg.slow + 5:
|
||
return None
|
||
cur_f, cur_s = self._ema_fast[-1], self._ema_slow[-1]
|
||
cur_atr = self._atr[-1]
|
||
prev_f, prev_s = self._ema_fast[-2], self._ema_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:
|
||
self._position_side = "short"
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉→做空", timestamp=k.open_time)
|
||
if k.close < stop:
|
||
self._position_side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损→空仓", 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:
|
||
self._position_side = "long"
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉→做多", timestamp=k.open_time)
|
||
if k.close > stop:
|
||
self._position_side = ""
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR止损→空仓", timestamp=k.open_time)
|
||
else:
|
||
if golden:
|
||
self._position_side = "long"; self._highest = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="金叉→做多", timestamp=k.open_time)
|
||
elif death:
|
||
self._position_side = "short"; self._lowest = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="死叉→做空", timestamp=k.open_time)
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 9:牛熊自适应 (Regime Adaptive) — 3法投票
|
||
# EMA200斜率 + 价格vs EMA200 + ATH回撤 → 牛市只多/熊市只空/震荡空仓
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
class RegimeDetector3:
|
||
"""牛熊判定器,内部维护增量 EMA(200),避免每次从头重算"""
|
||
|
||
def __init__(self):
|
||
self._ath = 0.0
|
||
self._e200 = EmaInc(200)
|
||
|
||
def update(self, price: float):
|
||
if price > self._ath:
|
||
self._ath = price
|
||
self._e200.update(price)
|
||
|
||
def _ema200_slope(self, idx: int) -> str:
|
||
if idx < 220: return "unknown"
|
||
e200 = self._e200
|
||
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, price: float, idx: int) -> str:
|
||
if idx < 210: return "unknown"
|
||
e = self._e200[idx]
|
||
if e == 0: return "unknown"
|
||
return "bull" if price > e else "bear"
|
||
|
||
def _ath_drawdown(self, price: float) -> str:
|
||
if self._ath == 0: return "unknown"
|
||
dd = (price - self._ath) / self._ath
|
||
if dd > -0.15: return "bull"
|
||
if dd < -0.35: return "bear"
|
||
return "sideways"
|
||
|
||
def detect(self, price: float, idx: int) -> str:
|
||
r1 = self._ema200_slope(idx)
|
||
r2 = self._price_vs_ema200(price, idx)
|
||
r3 = self._ath_drawdown(price)
|
||
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):
|
||
"""按市场状态自适应做多/做空 — 全部指标增量计算,O(1) per bar"""
|
||
strategy_type = "牛熊自适应"
|
||
strategy_desc = "EMA200斜率+价格vsEMA200+ATH回撤3选2投票,牛市只多/熊市只空"
|
||
|
||
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._ema_fast = EmaInc(c.fast)
|
||
self._ema_slow = EmaInc(c.slow)
|
||
self._atr = AtrInc(14)
|
||
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(k.close)
|
||
self._ema_fast.update(k.close)
|
||
self._ema_slow.update(k.close)
|
||
self._atr.update(k.high, k.low, k.close)
|
||
n = len(self._c)
|
||
if n < 220: return None
|
||
regime = self._detector.detect(k.close, n - 1)
|
||
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
|
||
ca = self._atr[-1]
|
||
pf, ps = self._ema_fast[-2], self._ema_slow[-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
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略注册表
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
STRATEGY_REGISTRY = {
|
||
"1.海龟交易": {
|
||
"config_cls": TurtleConfig,
|
||
"strategy_cls": TurtleStrategy,
|
||
"make_config": lambda s: TurtleConfig(symbol=s, entry_period=20, exit_period=10, atr_period=20, atr_stop=2.0),
|
||
},
|
||
"2.超级趋势": {
|
||
"config_cls": SuperTrendConfig,
|
||
"strategy_cls": SuperTrendStrategy,
|
||
"make_config": lambda s: SuperTrendConfig(symbol=s, atr_period=10, multiplier=3.0),
|
||
},
|
||
"3.MACD金叉死叉": {
|
||
"config_cls": MacdCrossConfig,
|
||
"strategy_cls": MacdCrossStrategy,
|
||
"make_config": lambda s: MacdCrossConfig(symbol=s, fast=12, slow=26, signal=9, atr_period=14, atr_stop=2.0),
|
||
},
|
||
"4.布林收缩爆发": {
|
||
"config_cls": BBSqueezeConfig,
|
||
"strategy_cls": BBSqueezeStrategy,
|
||
"make_config": lambda s: BBSqueezeConfig(symbol=s, bb_period=20, bb_std=2.0, kc_period=20, kc_mult=1.5, squeeze_lookback=30, atr_stop=2.0),
|
||
},
|
||
"5.三均线排列": {
|
||
"config_cls": TripleEmaConfig,
|
||
"strategy_cls": TripleEmaStrategy,
|
||
"make_config": lambda s: TripleEmaConfig(symbol=s, fast=10, mid=30, slow=60, atr_period=14, atr_stop=2.0),
|
||
},
|
||
"6.RSI均值回归": {
|
||
"config_cls": MeanRevConfig,
|
||
"strategy_cls": MeanRevStrategy,
|
||
"make_config": lambda s: MeanRevConfig(symbol=s, rsi_period=14, rsi_oversold=25, rsi_overbought=75, bb_period=20, bb_std=2.0, atr_stop=1.5),
|
||
},
|
||
"7.ATR波动率突破": {
|
||
"config_cls": VolBreakConfig,
|
||
"strategy_cls": VolBreakStrategy,
|
||
"make_config": lambda s: VolBreakConfig(symbol=s, atr_period=14, squeeze_period=20, squeeze_ratio=0.7, atr_stop=2.0),
|
||
},
|
||
"8.EMA双均线多空": {
|
||
"config_cls": EmaCrossConfig,
|
||
"strategy_cls": EmaCrossStrategy,
|
||
"make_config": lambda s: EmaCrossConfig(symbol=s, fast=10, slow=50, atr_stop=2.5),
|
||
},
|
||
"9.牛熊自适应": {
|
||
"config_cls": RegimeEmaConfig,
|
||
"strategy_cls": RegimeEmaStrategy,
|
||
"make_config": lambda s: RegimeEmaConfig(symbol=s, fast=10, slow=50, atr_stop=2.5),
|
||
},
|
||
}
|
||
|
||
STRATEGY_PARAMS_STR = {
|
||
"1.海龟交易": "entry=20/exit=10/ATR(20)x2.0",
|
||
"2.超级趋势": "ATR(10)x3.0",
|
||
"3.MACD金叉死叉": "MACD(12,26,9)/ATR(14)x2.0",
|
||
"4.布林收缩爆发": "BB(20,2.0)/KC(20,1.5)/squeeze=30",
|
||
"5.三均线排列": "EMA(10,30,60)/ATR(14)x2.0",
|
||
"6.RSI均值回归": "RSI(14)25/75+BB(20,2.0)/ATR(14)x1.5",
|
||
"7.ATR波动率突破": "ATR(14)/squeeze=20x0.7/EMA(10,30)",
|
||
"8.EMA双均线多空": "EMA(10,50)/ATR(14)x2.5",
|
||
"9.牛熊自适应": "EMA200投票(斜率+价格+ATH)牛多熊空",
|
||
}
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 执行
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
async def run_one(entry, symbol, interval, period_label, start, end):
|
||
make_config = entry["make_config"]
|
||
strategy_cls = entry["strategy_cls"]
|
||
sc = make_config(symbol)
|
||
bt = BacktestConfig(
|
||
symbol=symbol, interval=interval,
|
||
start_time=start, end_time=end,
|
||
initial_capital=INITIAL, warmup_bars=WARMUP,
|
||
)
|
||
engine = LongShortEngine(bt, db_config=config.db)
|
||
t0 = time.time()
|
||
try:
|
||
r = await engine.run(strategy_cls, sc)
|
||
elapsed = time.time() - t0
|
||
return r, elapsed, None
|
||
except Exception as ex:
|
||
elapsed = time.time() - t0
|
||
return None, elapsed, str(ex)
|
||
|
||
|
||
def safe(val, default=0):
|
||
return default if val is None else val
|
||
|
||
|
||
async def main():
|
||
ds = DataService(config.db)
|
||
await ds.connect()
|
||
|
||
print("正在获取数据范围...")
|
||
date_ranges: dict[tuple[str, str], tuple] = {}
|
||
for symbol in SYMBOLS:
|
||
for tf in TIMEFRAMES:
|
||
try:
|
||
s, e = await ds.fetch_symbol_date_range(symbol, tf)
|
||
bar_ms = {"2h": 7_200_000, "6h": 21_600_000}
|
||
estimated_bars = int((e - s).total_seconds() * 1000 / bar_ms[tf])
|
||
date_ranges[(symbol, tf)] = (s, e, estimated_bars)
|
||
print(f" {symbol} {tf:<4}: {s.date()} ~ {e.date()} (约{estimated_bars:,}根)")
|
||
except Exception as ex:
|
||
print(f" {symbol} {tf:<4}: 获取失败 — {ex}")
|
||
|
||
await ds.close()
|
||
|
||
sem = asyncio.Semaphore(MAX_CONCURRENCY)
|
||
tasks_info: list[dict] = []
|
||
|
||
for strat_name, entry in STRATEGY_REGISTRY.items():
|
||
for symbol in SYMBOLS:
|
||
for tf in TIMEFRAMES:
|
||
key = (symbol, tf)
|
||
if key not in date_ranges:
|
||
continue
|
||
fs, fe, est_bars = date_ranges[key]
|
||
|
||
for period_label, (period_start, period_end) in PERIODS.items():
|
||
actual_start = period_start or fs
|
||
actual_end = period_end or fe
|
||
if actual_start >= actual_end:
|
||
continue
|
||
|
||
min_bars = MIN_BARS_FOR_PERIOD.get(period_label, 50)
|
||
actual_bars = est_bars
|
||
if period_label != "全量":
|
||
actual_bars = int((actual_end - actual_start).total_seconds() * 1000 / {
|
||
"2h": 7_200_000, "6h": 21_600_000
|
||
}[tf])
|
||
|
||
if actual_bars < min_bars:
|
||
continue
|
||
|
||
tasks_info.append({
|
||
"strat_name": strat_name,
|
||
"entry": entry,
|
||
"symbol": symbol,
|
||
"tf": tf,
|
||
"period_label": period_label,
|
||
"start": actual_start,
|
||
"end": actual_end,
|
||
})
|
||
|
||
total = len(tasks_info)
|
||
print(f"\n共 {total} 组回测任务 (9策略×4币种×2时间×4数据量 - 跳过数据不足)")
|
||
|
||
results: list[dict] = []
|
||
completed = 0
|
||
errors = 0
|
||
|
||
async def run_one_safe(info):
|
||
nonlocal completed, errors
|
||
async with sem:
|
||
r, elapsed, err = await run_one(
|
||
info["entry"], info["symbol"], info["tf"],
|
||
info["period_label"], info["start"], info["end"],
|
||
)
|
||
completed += 1
|
||
if err:
|
||
errors += 1
|
||
status = f"✗ {err[:40]}"
|
||
elif r is None:
|
||
errors += 1
|
||
status = "✗ 无结果"
|
||
else:
|
||
m = r.metrics
|
||
status = f"✓ {m.annual_return_pct:+.1f}%/yr"
|
||
print(f" [{completed}/{total}] {info['strat_name']} {info['symbol']} {info['tf']} {info['period_label']} ({elapsed:.1f}s) {status}", flush=True)
|
||
|
||
row = {
|
||
"策略名": info["strat_name"],
|
||
"币种": info["symbol"],
|
||
"时间级别": info["tf"],
|
||
"数据量": info["period_label"],
|
||
"策略类型": info["entry"]["strategy_cls"].strategy_type if r else "",
|
||
"策略参数": STRATEGY_PARAMS_STR.get(info["strat_name"], ""),
|
||
"策略描述": info["entry"]["strategy_cls"].strategy_desc if r else "",
|
||
"日期范围": f"{info['start'].date()}~{info['end'].date()}",
|
||
}
|
||
|
||
if r is not None:
|
||
m = r.metrics
|
||
row.update({
|
||
"初始资金": INITIAL,
|
||
"最终权益": round(m.final_equity, 2),
|
||
"总收益%": round(m.total_return_pct, 2),
|
||
"年化收益%": round(m.annual_return_pct, 2),
|
||
"夏普比率": round(m.sharpe_ratio, 2),
|
||
"最大回撤%": round(m.max_drawdown_pct, 2),
|
||
"胜率%": round(m.win_rate * 100, 2),
|
||
"盈亏比": round(m.profit_factor, 2),
|
||
"交易次数": m.total_trades,
|
||
"平均盈亏": round(m.avg_trade_pnl, 2),
|
||
"最佳盈亏": round(m.best_trade_pnl, 2),
|
||
"最差盈亏": round(m.worst_trade_pnl, 2),
|
||
"卡尔玛比率": round(m.calmar_ratio, 2),
|
||
"耗时s": round(elapsed, 1),
|
||
})
|
||
else:
|
||
row.update({
|
||
"初始资金": INITIAL,
|
||
"最终权益": 0,
|
||
"总收益%": 0,
|
||
"年化收益%": 0,
|
||
"夏普比率": 0,
|
||
"最大回撤%": 0,
|
||
"胜率%": 0,
|
||
"盈亏比": 0,
|
||
"交易次数": 0,
|
||
"平均盈亏": 0,
|
||
"最佳盈亏": 0,
|
||
"最差盈亏": 0,
|
||
"卡尔玛比率": 0,
|
||
"耗时s": round(elapsed, 1),
|
||
"错误": err or "未知错误",
|
||
})
|
||
|
||
results.append(row)
|
||
return row
|
||
|
||
t_total = time.time()
|
||
await asyncio.gather(*[run_one_safe(info) for info in tasks_info])
|
||
total_elapsed = time.time() - t_total
|
||
|
||
print(f"\n全部完成!成功 {total - errors}/{total},错误 {errors},总耗时 {total_elapsed:.0f}s")
|
||
|
||
# ── 打印完整表格 ──
|
||
print()
|
||
print("═" * 195)
|
||
print(" 2h / 6h 全维度策略对比回测结果(9策略 × 4币种 × 2时间 × 4数据量)")
|
||
print("═" * 195)
|
||
print()
|
||
|
||
for strat_name in STRATEGY_REGISTRY:
|
||
strat_results = [r for r in results if r["策略名"] == strat_name]
|
||
if not strat_results:
|
||
continue
|
||
first = strat_results[0]
|
||
print(f"■ {strat_name} | 类型: {first['策略类型']} | {first['策略描述']}")
|
||
print(f" 参数: {first['策略参数']}")
|
||
print(f" {'币种':<10} {'时间':<5} {'数据量':<6} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'日期范围':<24}")
|
||
print(" " + "─" * 185)
|
||
|
||
strat_results.sort(key=lambda x: (SYMBOLS.index(x["币种"]), TIMEFRAMES.index(x["时间级别"]), list(PERIODS.keys()).index(x["数据量"])))
|
||
|
||
for r in strat_results:
|
||
print(f" {r['币种']:<10} {r['时间级别']:<5} {r['数据量']:<6} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6} {r['日期范围']:<24}")
|
||
print()
|
||
|
||
# ── 终极汇总 ──
|
||
print("═" * 195)
|
||
print(" ■ 终极汇总:每组(时间级别+数据量)下各币种最佳策略(按年化收益)")
|
||
print("═" * 195)
|
||
print()
|
||
|
||
for tf in TIMEFRAMES:
|
||
for period_label in PERIODS:
|
||
subset = [r for r in results if r["时间级别"] == tf and r["数据量"] == period_label and r.get("总收益%", 0) != 0]
|
||
if not subset:
|
||
continue
|
||
subset.sort(key=lambda x: x.get("年化收益%", -9999), reverse=True)
|
||
|
||
print(f" ▲ {tf} | {period_label}")
|
||
print(f" {'排名':<5} {'策略名':<22} {'币种':<10} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>7} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
|
||
print(" " + "─" * 130)
|
||
for i, r in enumerate(subset[:5]):
|
||
marker = ["🥇", "🥈", "🥉", " 4", " 5"][i]
|
||
print(f" {marker:<5} {r['策略名']:<22} {r['币种']:<10} {r['总收益%']:>7.1f}% {r['年化收益%']:>7.1f}% {r['夏普比率']:>7.2f} {r['最大回撤%']:>7.1f}% {r['胜率%']:>6.1f}% {r['盈亏比']:>7.2f} {r['交易次数']:>6}")
|
||
print()
|
||
|
||
# ── 保存 JSON ──
|
||
output_file = _project_root / "engine" / "example" / "comparison_2h_6h_result.json"
|
||
with open(output_file, "w", encoding="utf-8") as f:
|
||
json.dump({
|
||
"config": {
|
||
"symbols": SYMBOLS,
|
||
"timeframes": TIMEFRAMES,
|
||
"periods": list(PERIODS.keys()),
|
||
"initial_capital": INITIAL,
|
||
"warmup_bars": WARMUP,
|
||
"total_tasks": total,
|
||
"total_errors": errors,
|
||
"elapsed_seconds": total_elapsed,
|
||
"run_time": datetime.now(timezone.utc).isoformat(),
|
||
},
|
||
"results": results,
|
||
}, f, ensure_ascii=False, indent=2, default=str)
|
||
print(f" 详细结果已保存至: {output_file}")
|
||
print()
|
||
print("═" * 195)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|