edc50e8809
- 数据层: 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 等分析脚本
564 lines
23 KiB
Python
564 lines
23 KiB
Python
"""
|
|
日内策略探索 — 4 种思路 (15m / 30m / 1h 全币种)
|
|
|
|
1. 均值回归:RSI超买超卖 + 布林带触碰,震荡市中做回归
|
|
2. 多时间框架:4h 牛熊判定方向过滤 + 1h EMA交叉入场
|
|
3. 波动率突破:ATR 收缩后扩张,顺势突破
|
|
4. 成交量:OBV 背离 + VWAP 回归
|
|
|
|
用法:
|
|
source .venv/bin/activate && python example/intraday_explore.py
|
|
"""
|
|
|
|
import asyncio
|
|
import sys
|
|
import time
|
|
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.data import DataService
|
|
from engine.indicators.incremental import EmaInc, AtrInc, RsiInc, BbInc
|
|
from engine.example.long_short import LongShortEngine
|
|
from engine.example.regime_all import RegimeDetector3, RegimeEmaConfig, RegimeEmaStrategy
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
# 策略 1:均值回归 — RSI + 布林带
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
|
|
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):
|
|
"""RSI 极端 + 布林带确认 → 均值回归,ATR 止损"""
|
|
|
|
strategy_type = "mean_rev"
|
|
|
|
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 = "" # "long" / "short"
|
|
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
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
# 策略 2:多时间框架 — 4h 方向 + 1h 入场
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
|
|
class MultiTFConfig(StrategyConfig):
|
|
fast: int = 20
|
|
slow: int = 100
|
|
atr_stop: float = 2.0
|
|
# 4h 数据由策略内部自动加载
|
|
|
|
|
|
class MultiTFStrategy(BaseStrategy):
|
|
"""4h 牛熊判定方向过滤,1h EMA 交叉入场,只顺大势"""
|
|
|
|
strategy_type = "multi_tf"
|
|
|
|
def __init__(self, c: MultiTFConfig):
|
|
super().__init__(c)
|
|
self.cfg = c
|
|
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")
|
|
# 4h 牛熊判定 — 在 on_start 中加载
|
|
self._regime_map: dict[int, str] = {} # timestamp_hour -> regime
|
|
self._4h_loaded = False
|
|
|
|
async def on_start(self) -> None:
|
|
"""加载 4h 数据并预计算牛熊判定"""
|
|
await super().on_start()
|
|
if self._4h_loaded:
|
|
return
|
|
try:
|
|
ds = DataService(config.db)
|
|
await ds.connect()
|
|
try:
|
|
klines_4h = await ds.fetch_klines(
|
|
symbol=self.cfg.symbol, interval="4h",
|
|
start_time=datetime(2017, 1, 1),
|
|
end_time=datetime(2026, 12, 31),
|
|
limit=1_000_000,
|
|
)
|
|
detector = RegimeDetector3()
|
|
for k in klines_4h:
|
|
detector.update(k.close)
|
|
idx = len(detector._e200) - 1
|
|
if idx >= 220:
|
|
regime = detector.detect(k.close, idx)
|
|
# 4h bar 覆盖的时间窗口(按小时取整)
|
|
hour_key = int(k.open_time / 3_600_000)
|
|
for h in range(4):
|
|
self._regime_map[hour_key + h] = regime
|
|
self._4h_loaded = True
|
|
finally:
|
|
await ds.close()
|
|
except Exception:
|
|
pass # 加载失败则不做过滤
|
|
|
|
def _get_regime(self, ts_ms: float) -> str:
|
|
"""根据 1h bar 时间戳查找对应 4h 牛熊状态"""
|
|
hour_key = int(ts_ms / 3_600_000)
|
|
return self._regime_map.get(hour_key, "sideways")
|
|
|
|
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
|
self._ema_fast.update(k.close)
|
|
self._ema_slow.update(k.close)
|
|
self._atr.update(k.high, k.low, k.close)
|
|
|
|
n = len(self._ema_fast)
|
|
if n < self.cfg.slow + 5:
|
|
return None
|
|
|
|
cf, cs = self._ema_fast[-1], self._ema_slow[-1]
|
|
pf, ps = self._ema_fast[-2], self._ema_slow[-2]
|
|
ca = self._atr[-1]
|
|
if cf == 0 or cs == 0 or ca == 0:
|
|
return None
|
|
|
|
golden = pf <= ps and cf > cs
|
|
death = pf >= ps and cf < cs
|
|
regime = self._get_regime(k.open_time)
|
|
|
|
# ── 多头持仓 ──
|
|
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:
|
|
self._side = ""
|
|
reason = "死叉" if death else "ATR止损"
|
|
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:
|
|
self._side = ""
|
|
reason = "金叉" if golden else "ATR止损"
|
|
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
|
|
|
|
# ── 入场:必须顺4h方向 ──
|
|
else:
|
|
if golden and regime == "bull":
|
|
self._side = "long"
|
|
self._hp = k.close
|
|
return Signal(symbol=self.cfg.symbol, side="BUY", reason="4h牛+金叉", timestamp=k.open_time)
|
|
elif death and regime == "bear":
|
|
self._side = "short"
|
|
self._lp = k.close
|
|
return Signal(symbol=self.cfg.symbol, side="SELL", reason="4h熊+死叉", timestamp=k.open_time)
|
|
|
|
return None
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
# 策略 3:波动率突破 — ATR 收缩扩张
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
|
|
class VolBreakConfig(StrategyConfig):
|
|
atr_period: int = 14
|
|
squeeze_period: int = 20 # ATR 收缩回看窗口
|
|
squeeze_ratio: float = 0.7 # 当前 ATR < 最低 ATR * ratio 时视为收缩
|
|
atr_stop: float = 2.0
|
|
|
|
|
|
class VolBreakStrategy(BaseStrategy):
|
|
"""ATR 收缩到极致后扩张 → 顺势突破,ATR 止损"""
|
|
|
|
strategy_type = "vol_break"
|
|
|
|
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 收缩检测:当前 ATR 是否处于 squeeze_period 内的最低水平
|
|
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 扩张信号
|
|
atr_expanding = atr_now > atr_prev * 1.05 if atr_prev > 0 else False
|
|
|
|
# 趋势方向
|
|
cf = self._ema_fast[-1]
|
|
cs = 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
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
# 策略 4:成交量 — OBV 背离 + VWAP 回归
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
|
|
class VolumeConfig(StrategyConfig):
|
|
obv_lookback: int = 20 # OBV 背离检测窗口
|
|
vwap_std: float = 2.0 # VWAP 偏离标准差倍数
|
|
atr_stop: float = 2.0
|
|
|
|
|
|
class VolumeStrategy(BaseStrategy):
|
|
"""OBV 背离(价格新低但 OBV 未新低→看涨)+ VWAP 偏离回归"""
|
|
|
|
strategy_type = "volume"
|
|
|
|
def __init__(self, c: VolumeConfig):
|
|
super().__init__(c)
|
|
self.cfg = c
|
|
self._closes: list[float] = []
|
|
self._highs: list[float] = []
|
|
self._lows: list[float] = []
|
|
self._volumes: list[float] = []
|
|
self._obv: list[float] = [] # 增量 OBV
|
|
self._obv_val: float = 0.0
|
|
self._atr = AtrInc(14)
|
|
self._cum_pv: float = 0.0 # 累积 price*volume
|
|
self._cum_vol: float = 0.0 # 累积 volume
|
|
self._side: str = ""
|
|
self._entry_price: float = 0.0
|
|
|
|
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._volumes.append(k.volume)
|
|
self._atr.update(k.high, k.low, k.close)
|
|
|
|
# 增量 OBV
|
|
n = len(self._closes)
|
|
if n == 1:
|
|
self._obv_val = k.volume
|
|
else:
|
|
if k.close > self._closes[-2]:
|
|
self._obv_val += k.volume
|
|
elif k.close < self._closes[-2]:
|
|
self._obv_val -= k.volume
|
|
self._obv.append(self._obv_val)
|
|
|
|
# 增量 VWAP
|
|
typical = (k.high + k.low + k.close) / 3.0
|
|
self._cum_pv += typical * k.volume
|
|
self._cum_vol += k.volume
|
|
vwap = self._cum_pv / self._cum_vol if self._cum_vol > 0 else k.close
|
|
|
|
if n < self.cfg.obv_lookback + 5:
|
|
return None
|
|
|
|
ca = self._atr[-1]
|
|
if ca == 0:
|
|
return None
|
|
|
|
# OBV 背离检测:价格新低但 OBV 未新低 → 潜在反转
|
|
lookback = self.cfg.obv_lookback
|
|
price_window = self._closes[-lookback:]
|
|
obv_window = self._obv[-lookback:]
|
|
price_made_new_low = min(price_window) == price_window[-1]
|
|
obv_not_new_low = min(obv_window) < obv_window[-1]
|
|
obv_bull_div = price_made_new_low and obv_not_new_low
|
|
|
|
# OBV 负背离:价格新高但 OBV 未新高
|
|
price_made_new_high = max(price_window) == price_window[-1]
|
|
obv_not_new_high = max(obv_window) > obv_window[-1]
|
|
obv_bear_div = price_made_new_high and obv_not_new_high
|
|
|
|
# VWAP 偏离度
|
|
vwap_dev = (k.close - vwap) / vwap if vwap > 0 else 0
|
|
|
|
# ── 持仓管理 ──
|
|
if self._side == "long":
|
|
stop = self._entry_price - self.cfg.atr_stop * ca
|
|
if k.close < stop or vwap_dev < -0.01: # 回到 VWAP 下方
|
|
self._side = ""
|
|
return Signal(symbol=self.cfg.symbol, side="SELL", reason="止损或回VWAP", timestamp=k.open_time)
|
|
|
|
elif self._side == "short":
|
|
stop = self._entry_price + self.cfg.atr_stop * ca
|
|
if k.close > stop or vwap_dev > 0.01:
|
|
self._side = ""
|
|
return Signal(symbol=self.cfg.symbol, side="BUY", reason="止损或回VWAP", timestamp=k.open_time)
|
|
|
|
# ── 入场 ──
|
|
else:
|
|
if obv_bull_div and vwap_dev < -self.cfg.vwap_std * 0.02: # 价格显著低于 VWAP
|
|
self._side = "long"
|
|
self._entry_price = k.close
|
|
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"OBV底背离+VWAP下方", timestamp=k.open_time)
|
|
elif obv_bear_div and vwap_dev > self.cfg.vwap_std * 0.02:
|
|
self._side = "short"
|
|
self._entry_price = k.close
|
|
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"OBV顶背离+VWAP上方", timestamp=k.open_time)
|
|
|
|
return None
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
# 执行
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
|
INTERVALS = ["15m", "30m", "1h"]
|
|
|
|
STRATEGIES = {
|
|
"1.均值回归": (MeanRevConfig, MeanRevStrategy),
|
|
"2.多TF(4h+1h)": (MultiTFConfig, MultiTFStrategy),
|
|
"3.波动突破": (VolBreakConfig, VolBreakStrategy),
|
|
"4.成交量": (VolumeConfig, VolumeStrategy),
|
|
}
|
|
|
|
# 均值回归参数
|
|
MEAN_REV_PARAMS = {
|
|
"BTCUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
|
|
"ETHUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
|
|
"BNBUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
|
|
"SOLUSDT": {"rsi_period": 14, "rsi_oversold": 25, "rsi_overbought": 75, "bb_period": 20, "bb_std": 2.0, "atr_stop": 1.5},
|
|
}
|
|
|
|
# 多TF参数
|
|
MULTI_TF_PARAMS = {
|
|
"BTCUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
|
|
"ETHUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
|
|
"BNBUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
|
|
"SOLUSDT": {"fast": 20, "slow": 100, "atr_stop": 2.0},
|
|
}
|
|
|
|
# 波动突破参数
|
|
VOL_BREAK_PARAMS = {
|
|
"BTCUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
|
"ETHUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
|
"BNBUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
|
"SOLUSDT": {"atr_period": 14, "squeeze_period": 20, "squeeze_ratio": 0.7, "atr_stop": 2.0},
|
|
}
|
|
|
|
# 成交量参数
|
|
VOLUME_PARAMS = {
|
|
"BTCUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
|
|
"ETHUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
|
|
"BNBUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
|
|
"SOLUSDT": {"obv_lookback": 20, "vwap_std": 2.0, "atr_stop": 2.0},
|
|
}
|
|
|
|
|
|
async def run_one(engine_factory, strategy_cls, config_cls, params, symbol, interval, start, end):
|
|
"""运行单组回测"""
|
|
INITIAL = 10_000.0
|
|
sc = config_cls(symbol=symbol, **params)
|
|
bt = BacktestConfig(symbol=symbol, interval=interval, start_time=start, end_time=end, initial_capital=INITIAL)
|
|
engine = engine_factory(bt)
|
|
r = await engine.run(strategy_cls, sc)
|
|
m = r.metrics
|
|
return m, INITIAL, m.final_equity
|
|
|
|
|
|
async def main():
|
|
ds = DataService(config.db)
|
|
await ds.connect()
|
|
|
|
# 预加载所有数据范围
|
|
ranges: dict[str, dict[str, tuple[datetime, datetime]]] = {}
|
|
for interval in INTERVALS:
|
|
ranges[interval] = {}
|
|
for symbol in SYMBOLS:
|
|
try:
|
|
s, e = await ds.fetch_symbol_date_range(symbol, interval)
|
|
ranges[interval][symbol] = (s, e)
|
|
except Exception:
|
|
pass
|
|
await ds.close()
|
|
|
|
all_results: list[dict] = []
|
|
|
|
print()
|
|
print("═" * 135)
|
|
print(" 日内策略探索 — 4思路 × 4币种 × 3周期")
|
|
print("═" * 135)
|
|
|
|
for strategy_name, (config_cls, strategy_cls) in STRATEGIES.items():
|
|
print(f"\n ■ {strategy_name}")
|
|
print(f" {'币种':<10} {'周期':<6} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'交易':>6} {'耗时s':>7}")
|
|
print(" " + "─" * 115)
|
|
|
|
for interval in INTERVALS:
|
|
for symbol in SYMBOLS:
|
|
if symbol not in ranges.get(interval, {}):
|
|
continue
|
|
start, end = ranges[interval][symbol]
|
|
|
|
# 选择参数
|
|
if strategy_name == "1.均值回归":
|
|
params = MEAN_REV_PARAMS[symbol]
|
|
elif strategy_name == "2.多TF(4h+1h)":
|
|
params = MULTI_TF_PARAMS[symbol]
|
|
elif strategy_name == "3.波动突破":
|
|
params = VOL_BREAK_PARAMS[symbol]
|
|
else:
|
|
params = VOLUME_PARAMS[symbol]
|
|
|
|
t0 = time.time()
|
|
try:
|
|
m, initial, final_equity = await run_one(
|
|
lambda bt: LongShortEngine(bt, db_config=config.db),
|
|
strategy_cls, config_cls, params,
|
|
symbol, interval, start, end,
|
|
)
|
|
elapsed = time.time() - t0
|
|
except Exception as ex:
|
|
print(f" {symbol:<10} {interval:<6} {'错误: ' + str(ex)[:40]}")
|
|
continue
|
|
|
|
print(f" {symbol:<10} {interval:<6} {initial:>7.0f} {final_equity:>9.0f} {m.total_return_pct:>7.1f}% {m.annual_return_pct:>7.1f}% {m.sharpe_ratio:>7.2f} {m.max_drawdown_pct:>7.1f}% {m.total_trades:>6} {elapsed:>6.1f}s")
|
|
|
|
all_results.append({
|
|
"strategy": strategy_name, "interval": interval, "symbol": symbol,
|
|
"return": m.total_return_pct, "annual": m.annual_return_pct,
|
|
"sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct,
|
|
"trades": m.total_trades, "initial": initial, "final": final_equity,
|
|
})
|
|
|
|
# ── 汇总:每种策略的最佳组合 ──
|
|
print(f"\n\n ■ 各策略最佳组合 (按夏普排名)")
|
|
print(f" {'策略':<18} {'级别':<6} {'币种':<10} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'交易':>6}")
|
|
print(" " + "─" * 115)
|
|
|
|
# 每种策略取最佳
|
|
for sn in STRATEGIES:
|
|
candidates = [r for r in all_results if r["strategy"] == sn]
|
|
if not candidates:
|
|
continue
|
|
best = max(candidates, key=lambda x: x["sharpe"])
|
|
print(f" {sn:<18} {best['interval']:<6} {best['symbol']:<10} {best['initial']:>7.0f} {best['final']:>9.0f} {best['return']:>7.1f}% {best['annual']:>7.1f}% {best['sharpe']:>7.2f} {best['dd']:>7.1f}% {best['trades']:>6}")
|
|
|
|
print("\n═" * 135)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|