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,563 @@
|
||||
"""
|
||||
日内策略探索 — 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())
|
||||
Reference in New Issue
Block a user