""" 日内策略探索 — 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())