feat: 新增2h/6h时间框架支持,策略重构为增量指标计算
- 数据层: build_aggregates_sql 新增 2h/6h 聚合视图,默认起始时间调整为 2017-05 - 模型层: KlineInterval 类型扩展 2h/6h,DataService 新增对应表名和毫秒映射 - 指标层: 新增 incremental.py 增量指标模块 (EmaInc/AtrInc/RsiInc/BbInc),O(1) per bar - 策略重构: long_short.py 和 regime_all.py 从批量 ema/atr 迁移至增量指标,避免每 bar 重复全量计算 - regime 探测器: RegimeDetector3 改为增量 EMA200,detect() 接口简化 - 回测扩展: regime_timeframe_comparison 从 4h/1d 扩展至 2h/4h/6h/1d - 新增示例: multi_strategy_report, vol_break_compare/periods, intraday_explore, top3_trades 等分析脚本
This commit is contained in:
@@ -0,0 +1,974 @@
|
||||
"""
|
||||
全维度策略对比回测 — 8策略 × 4币种 × 5时间级别 × 4数据量
|
||||
|
||||
策略均来自 Investopedia / BabyPips / TradingView 等知名交易社区,覆盖四大类:
|
||||
- 趋势跟踪:海龟交易、超级趋势、三均线排列、EMA双均线多空
|
||||
- 动量:MACD金叉死叉
|
||||
- 波动率突破:布林收缩爆发、ATR波动率突破
|
||||
- 均值回归:RSI+布林带回归
|
||||
|
||||
用法:
|
||||
source .venv/bin/activate && python example/full_comparison.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 = ["15m", "30m", "1h", "4h", "1d"]
|
||||
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)
|
||||
# Richard Dennis & William Eckhardt, 1983
|
||||
# 20日高点突破入场,10日低点突破出场,2N ATR 止损
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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)
|
||||
# Olivier Seban,广泛用于加密货币和商品
|
||||
# ATR(10)×3 动态跟踪止损,趋势翻转即反转
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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 金叉死叉
|
||||
# Gerald Appel, 1970s
|
||||
# MACD(12,26,9) 零轴附近金叉/死叉 + ATR 止损
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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)
|
||||
# John Bollinger, 2002
|
||||
# BB在KC内部收缩→扩张突破入场
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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)
|
||||
# 经典三线开花 — EMA(10,30,60) 多头/空头排列 + ATR 追踪止损
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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)
|
||||
# 经典指标 — RSI(14)超买超卖 + 布林带确认 → 逆向交易
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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)
|
||||
# 经典波动率策略 — ATR收缩至极低后扩张 → 顺势突破
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 策略注册表
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 参数用于表格的简洁呈现
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
|
||||
# ════════════════════════════════════════════════════════
|
||||
# 执行
|
||||
# ════════════════════════════════════════════════════════
|
||||
|
||||
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):
|
||||
"""安全取值,避免 None"""
|
||||
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] = {} # (symbol, interval) -> (start_dt, end_dt, bar_count_estimate)
|
||||
for symbol in SYMBOLS:
|
||||
for tf in TIMEFRAMES:
|
||||
try:
|
||||
s, e = await ds.fetch_symbol_date_range(symbol, tf)
|
||||
# 粗略估计 bar 数量
|
||||
bar_ms = {"15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "4h": 14_400_000, "1d": 86_400_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 / {
|
||||
"15m": 900_000, "30m": 1_800_000, "1h": 3_600_000, "4h": 14_400_000, "1d": 86_400_000
|
||||
}[tf])
|
||||
|
||||
if actual_bars < min_bars:
|
||||
continue
|
||||
|
||||
# 跳过日线+近半年(bar太少)
|
||||
if tf == "1d" and period_label == "近半年":
|
||||
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} 组回测任务 (8策略×4币种×5时间×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(" 全维度策略对比回测结果")
|
||||
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" / "full_comparison_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())
|
||||
Reference in New Issue
Block a user