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 等分析脚本
702 lines
28 KiB
Python
702 lines
28 KiB
Python
"""
|
||
网络验证策略探索 — 5 个业界知名策略 × 全币种 × 1h
|
||
|
||
1. 海龟交易 (Turtle) — Donchian 20/10 通道突破 + 2N ATR 止损
|
||
2. 超级趋势 (SuperTrend) — ATR(10)×3 动态跟踪止损
|
||
3. MACD金叉死叉 — MACD(12,26,9) 零轴交叉 + ATR 止损
|
||
4. 布林收缩爆发 (BBSqueeze) — BB 收缩至 KC 内部后扩张突破
|
||
5. 三均线排列 (TripleEMA) — EMA(10,30,60) 多头/空头排列 + ATR 追踪
|
||
|
||
用法:
|
||
source .venv/bin/activate && python example/web_strategies.py
|
||
"""
|
||
|
||
import asyncio
|
||
import json
|
||
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
|
||
|
||
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
||
INTERVAL = "1h"
|
||
INITIAL = 10_000.0
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 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):
|
||
"""海龟交易 — Donchian 通道突破 + ATR 动态止损"""
|
||
|
||
strategy_type = "turtle"
|
||
|
||
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_entry: float = 0.0
|
||
self._lowest_since_entry: 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
|
||
|
||
# Donchian 通道应排除当前 bar(用前 N 根 bar 计算)
|
||
donchian_high = max(self._highs[-(self.cfg.entry_period + 1):-1])
|
||
donchian_low = min(self._lows[-(self.cfg.entry_period + 1):-1])
|
||
donchian_exit_high = max(self._highs[-(self.cfg.exit_period + 1):-1])
|
||
donchian_exit_low = min(self._lows[-(self.cfg.exit_period + 1):-1])
|
||
|
||
# ── 持仓管理 ──
|
||
if self._side == "long":
|
||
self._highest_since_entry = max(self._highest_since_entry, k.high)
|
||
stop = self._entry_price - self.cfg.atr_stop * ca
|
||
trail_stop = self._highest_since_entry - self.cfg.atr_stop * ca * 0.5
|
||
if k.close < donchian_exit_low or k.close < max(stop, trail_stop):
|
||
self._side = ""
|
||
reason = "跌破10日低点" if k.close < donchian_exit_low else "ATR止损"
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time, confidence=0.25)
|
||
|
||
elif self._side == "short":
|
||
self._lowest_since_entry = min(self._lowest_since_entry, k.low)
|
||
stop = self._entry_price + self.cfg.atr_stop * ca
|
||
trail_stop = self._lowest_since_entry + self.cfg.atr_stop * ca * 0.5
|
||
if k.close > donchian_exit_high or k.close > min(stop, trail_stop):
|
||
self._side = ""
|
||
reason = "突破10日高点" if k.close > donchian_exit_high else "ATR止损"
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time, confidence=0.25)
|
||
|
||
# ── 入场:仅在有明显突破幅度时入场 ──
|
||
else:
|
||
breakout_margin = 0.002 # 需突破通道 0.2% 以上
|
||
if k.close > donchian_high * (1 + breakout_margin):
|
||
self._side = "long"
|
||
self._entry_price = k.close
|
||
self._highest_since_entry = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason="突破20日高点", timestamp=k.open_time, confidence=0.25)
|
||
elif k.close < donchian_low * (1 - breakout_margin):
|
||
self._side = "short"
|
||
self._entry_price = k.close
|
||
self._lowest_since_entry = 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 动态跟踪止损,趋势翻转即反转
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
|
||
class SuperTrendConfig(StrategyConfig):
|
||
atr_period: int = 10
|
||
multiplier: float = 3.0
|
||
|
||
|
||
class SuperTrendStrategy(BaseStrategy):
|
||
"""超级趋势 — ATR 动态跟踪止损,趋势跟踪"""
|
||
|
||
strategy_type = "supertrend"
|
||
|
||
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 # 1=多, -1=空
|
||
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 prev_trend == prev_trend: # always true, just need placeholder
|
||
pass
|
||
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):
|
||
"""MACD 金叉死叉 — 零轴以上只做多,零轴以下只做空"""
|
||
|
||
strategy_type = "macd_cross"
|
||
|
||
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] = [] # MACD 线值
|
||
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
|
||
|
||
# 信号线 = EMA of MACD,简化:用列表算
|
||
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 = self._macd_vals[-1]
|
||
cur_s = self._signal_vals[-1]
|
||
prev_m = self._macd_vals[-2]
|
||
prev_s = 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
|
||
reason = "ATR止损" if k.close < stop else "MACD死叉"
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, 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
|
||
reason = "ATR止损" if k.close > stop else "MACD金叉"
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, 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):
|
||
"""布林收缩爆发 — BB 收缩到极限后扩张,顺势入场"""
|
||
|
||
strategy_type = "bb_squeeze"
|
||
|
||
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) # Keltner 中轨
|
||
self._atr_kc = AtrInc(c.kc_period) # Keltner 宽度的 ATR
|
||
self._atr_stop = AtrInc(14) # 止损 ATR
|
||
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)
|
||
|
||
# BB 在 KC 内部 = 收缩
|
||
is_squeezed = bb_u < kc_u and bb_l > kc_l
|
||
# BB 宽度处于近期最低水平
|
||
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
|
||
|
||
# 收缩释放信号:之前收缩,现在 BB 扩张出 KC
|
||
was_squeezed = self._was_squeezed
|
||
fired = False
|
||
if is_squeezed:
|
||
self._was_squeezed = True
|
||
self._squeeze_bars += 1
|
||
elif self._was_squeezed:
|
||
# BB 不再在 KC 内部 → 收缩释放
|
||
self._was_squeezed = False
|
||
self._squeeze_bars = 0
|
||
fired = True
|
||
|
||
# 方向判断:用价格与 BB 中轨关系 + EMA(5) 动量
|
||
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="ATR止损或转弱", 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="ATR止损或转强", 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 = "triple_ema"
|
||
|
||
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_entry: float = 0.0
|
||
self._lowest_since_entry: 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 = self._ema_fast[-1]
|
||
em = self._ema_mid[-1]
|
||
es = self._ema_slow[-1]
|
||
pf = self._ema_fast[-2]
|
||
pm = 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_entry = max(self._highest_since_entry, k.high)
|
||
trail = self._highest_since_entry - self.cfg.atr_stop * ca
|
||
if fast_cross_mid_down or k.close < trail:
|
||
self._side = ""
|
||
reason = "快线下穿中线" if fast_cross_mid_down else "ATR追踪止损"
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time, confidence=0.25)
|
||
|
||
# ── 空头持仓 ──
|
||
elif self._side == "short":
|
||
self._lowest_since_entry = min(self._lowest_since_entry, k.low)
|
||
trail = self._lowest_since_entry + self.cfg.atr_stop * ca
|
||
if fast_cross_mid_up or k.close > trail:
|
||
self._side = ""
|
||
reason = "快线上穿中线" if fast_cross_mid_up else "ATR追踪止损"
|
||
return Signal(symbol=self.cfg.symbol, side="BUY", reason=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_entry = 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_entry = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason="三线空头排列+快线死叉", timestamp=k.open_time, confidence=0.25)
|
||
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 执行
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
STRATEGIES = {
|
||
"1.海龟交易(Turtle)": (TurtleConfig, TurtleStrategy),
|
||
"2.超级趋势(SuperTrend)": (SuperTrendConfig, SuperTrendStrategy),
|
||
"3.MACD金叉死叉": (MacdCrossConfig, MacdCrossStrategy),
|
||
"4.布林收缩爆发(BBSqueeze)": (BBSqueezeConfig, BBSqueezeStrategy),
|
||
"5.三均线排列(TripleEMA)": (TripleEmaConfig, TripleEmaStrategy),
|
||
}
|
||
|
||
|
||
def make_config(config_cls, symbol):
|
||
"""根据策略类型创建默认参数配置"""
|
||
if config_cls == TurtleConfig:
|
||
return config_cls(symbol=symbol, entry_period=20, exit_period=10, atr_period=20, atr_stop=2.0)
|
||
elif config_cls == SuperTrendConfig:
|
||
return config_cls(symbol=symbol, atr_period=10, multiplier=3.0)
|
||
elif config_cls == MacdCrossConfig:
|
||
return config_cls(symbol=symbol, fast=12, slow=26, signal=9, atr_period=14, atr_stop=2.0)
|
||
elif config_cls == BBSqueezeConfig:
|
||
return config_cls(symbol=symbol, bb_period=20, bb_std=2.0, kc_period=20, kc_mult=1.5, squeeze_lookback=30, atr_stop=2.0)
|
||
elif config_cls == TripleEmaConfig:
|
||
return config_cls(symbol=symbol, fast=10, mid=30, slow=60, atr_period=14, atr_stop=2.0)
|
||
else:
|
||
raise ValueError(f"未知策略: {config_cls}")
|
||
|
||
|
||
async def run_one(config_cls, strategy_cls, symbol, start, end):
|
||
sc = make_config(config_cls, symbol)
|
||
bt = BacktestConfig(symbol=symbol, interval=INTERVAL, start_time=start, end_time=end, initial_capital=INITIAL)
|
||
engine = LongShortEngine(bt, db_config=config.db)
|
||
r = await engine.run(strategy_cls, sc)
|
||
return r
|
||
|
||
|
||
async def main():
|
||
ds = DataService(config.db)
|
||
await ds.connect()
|
||
|
||
# 获取数据范围
|
||
ranges: dict[str, tuple] = {}
|
||
for symbol in SYMBOLS:
|
||
try:
|
||
s, e = await ds.fetch_symbol_date_range(symbol, INTERVAL)
|
||
ranges[symbol] = (s, e)
|
||
print(f" {symbol}: {s.date()} ~ {e.date()}")
|
||
except Exception as ex:
|
||
print(f" {symbol}: 获取范围失败 {ex}")
|
||
|
||
await ds.close()
|
||
|
||
# 汇总数据
|
||
all_results: list[dict] = []
|
||
detail_results: dict[str, dict[str, dict]] = {} # 用于保存详细结果
|
||
|
||
print()
|
||
print("═" * 140)
|
||
print(" 5 策略 × 4 币种 × 1h — 网络验证策略扫描")
|
||
print("═" * 140)
|
||
print()
|
||
|
||
for strategy_name, (config_cls, strategy_cls) in STRATEGIES.items():
|
||
print(f" ■ {strategy_name}")
|
||
print(f" {'币种':<10} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>6} {'耗时s':>7}")
|
||
print(" " + "─" * 120)
|
||
|
||
detail_results[strategy_name] = {}
|
||
|
||
for symbol in SYMBOLS:
|
||
if symbol not in ranges:
|
||
continue
|
||
start, end = ranges[symbol]
|
||
|
||
t0 = time.time()
|
||
try:
|
||
r = await run_one(config_cls, strategy_cls, symbol, start, end)
|
||
elapsed = time.time() - t0
|
||
except Exception as ex:
|
||
print(f" {symbol:<10} {'错误: ' + str(ex)[:50]}")
|
||
continue
|
||
|
||
m = r.metrics
|
||
final = m.final_equity
|
||
print(f" {symbol:<10} {INITIAL:>7.0f} {final:>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.win_rate*100:>6.1f}% {m.profit_factor:>7.2f} {m.total_trades:>6} {elapsed:>6.1f}s")
|
||
|
||
all_results.append({
|
||
"strategy": strategy_name,
|
||
"symbol": symbol,
|
||
"interval": INTERVAL,
|
||
"initial": INITIAL,
|
||
"final": final,
|
||
"total_return": m.total_return_pct,
|
||
"annual_return": m.annual_return_pct,
|
||
"sharpe": m.sharpe_ratio,
|
||
"drawdown": m.max_drawdown_pct,
|
||
"win_rate": m.win_rate * 100,
|
||
"profit_factor": m.profit_factor,
|
||
"trades": m.total_trades,
|
||
"best": m.best_trade_pnl,
|
||
"worst": m.worst_trade_pnl,
|
||
"avg": m.avg_trade_pnl,
|
||
"calmar": m.calmar_ratio,
|
||
"start_date": str(start.date()),
|
||
"end_date": str(end.date()),
|
||
})
|
||
|
||
# 详细交易记录
|
||
detail_results[strategy_name][symbol] = {
|
||
"config": {
|
||
"symbol": symbol,
|
||
"interval": INTERVAL,
|
||
"start": str(start.date()),
|
||
"end": str(end.date()),
|
||
"initial_capital": INITIAL,
|
||
},
|
||
"metrics": {
|
||
"total_return_pct": m.total_return_pct,
|
||
"annual_return_pct": m.annual_return_pct,
|
||
"sharpe_ratio": m.sharpe_ratio,
|
||
"max_drawdown_pct": m.max_drawdown_pct,
|
||
"win_rate": m.win_rate * 100,
|
||
"profit_factor": m.profit_factor,
|
||
"total_trades": m.total_trades,
|
||
"avg_trade_pnl": m.avg_trade_pnl,
|
||
"best_trade_pnl": m.best_trade_pnl,
|
||
"worst_trade_pnl": m.worst_trade_pnl,
|
||
"final_equity": m.final_equity,
|
||
"calmar_ratio": m.calmar_ratio,
|
||
},
|
||
"trades": [
|
||
{
|
||
"side": t.side,
|
||
"price": t.price,
|
||
"quantity": t.quantity,
|
||
"pnl": t.pnl,
|
||
"reason": t.reason,
|
||
"timestamp": t.timestamp,
|
||
"timestamp_str": datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M"),
|
||
}
|
||
for t in r.trades
|
||
],
|
||
}
|
||
print()
|
||
|
||
# ── 汇总:每种策略的最佳/最差 ──
|
||
print("═" * 140)
|
||
print(" ■ 各策略汇总 (按年化收益排序)")
|
||
print(f" {'策略':<28} {'币种':<10} {'本金':>7} {'终值':>9} {'总收益%':>8} {'年化%':>8} {'夏普':>7} {'回撤%':>8} {'胜率%':>7} {'盈亏比':>7} {'交易':>6}")
|
||
print(" " + "─" * 120)
|
||
|
||
for sn in STRATEGIES:
|
||
candidates = [r for r in all_results if r["strategy"] == sn]
|
||
if not candidates:
|
||
continue
|
||
# 按年化排序,显示所有币种
|
||
candidates.sort(key=lambda x: x["annual_return"], reverse=True)
|
||
for c in candidates:
|
||
marker = " ★" if c == candidates[0] else " "
|
||
print(f" {sn:<26}{marker} {c['symbol']:<10} {c['initial']:>7.0f} {c['final']:>9.0f} {c['total_return']:>7.1f}% {c['annual_return']:>7.1f}% {c['sharpe']:>7.2f} {c['drawdown']:>7.1f}% {c['win_rate']:>6.1f}% {c['profit_factor']:>7.2f} {c['trades']:>6}")
|
||
|
||
print("\n═" * 140)
|
||
|
||
# ── 保存结果到 JSON ──
|
||
output_file = _project_root / "engine" / "example" / "web_strategies_result.json"
|
||
with open(output_file, "w", encoding="utf-8") as f:
|
||
json.dump({
|
||
"summary": all_results,
|
||
"detail": detail_results,
|
||
"run_time": datetime.now(timezone.utc).isoformat(),
|
||
}, f, ensure_ascii=False, indent=2, default=str)
|
||
print(f"\n 详细结果已保存至: {output_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|