""" 多时间框架 v3 — 1d 定趋势 / 4h 找买点 策略: 1d EMA(20,50) → 日线金叉=多头趋势,死叉=空头 4h 回调入场 → 日线多头中,4h 价格回调到 EMA20 附近 + 收阳 → 买入 4h EMA死叉 或 日线转空 → 卖出 ATR 动态止损 币种:BTC/ETH/BNB/SOL | 2024-2026 用法: source .venv/bin/activate && python example/multi_tf_v3.py """ import asyncio import sys 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 BacktestEngine, BacktestConfig, BacktestResult from engine.data import DataService from engine.indicators import ema, atr class DailyTrendConfig(StrategyConfig): """1d趋势+4h交易 策略配置""" # 1d 趋势参数 trend_fast: int = 20 trend_slow: int = 50 # 4h 交易参数 entry_ema: int = 20 # 4h 回调到该均线附近买入 atr_period: int = 14 atr_stop_mult: float = 2.5 # 数据范围 data_start: Optional[datetime] = None data_end: Optional[datetime] = None class DailyTrendStrategy(BaseStrategy): """日线定大势,4h 抓回调 ┌──────────────────────────────────────────────┐ │ 1d EMA(20,50) │ │ ├─ 金叉 → 多头区间 │ │ └─ 死叉 → 空仓等待 │ │ │ │ 4h K线(引擎推送) │ │ ├─ 多头区间 + 回调到 EMA20 + 收阳 → 买入 │ │ ├─ EMA死叉 → 卖出 │ │ └─ ATR 动态止损 │ └──────────────────────────────────────────────┘ """ strategy_type = "daily_trend" def __init__(self, c: DailyTrendConfig): super().__init__(c) self.cfg = c # 1d 预加载 self._klines_1d: list[Kline] = [] self._ema_fast_1d: list[float] = [] self._ema_slow_1d: list[float] = [] # 4h 积累 self._closes: list[float] = [] self._highs: list[float] = [] self._lows: list[float] = [] self._highest: float = 0.0 self._in_position = False async def on_start(self): from engine.common.config import config as app_config ds = DataService(app_config.db) await ds.connect() try: self._klines_1d = await ds.fetch_klines( symbol=self.cfg.symbol, interval="1d", start_time=self.cfg.data_start, end_time=self.cfg.data_end, limit=1_000_000, ) closes_1d = [k.close for k in self._klines_1d] self._ema_fast_1d = ema(closes_1d, self.cfg.trend_fast) self._ema_slow_1d = ema(closes_1d, self.cfg.trend_slow) finally: await ds.close() await super().on_start() def _get_1d_state(self, ts: float) -> tuple[bool, bool, float, float]: """获取日线趋势状态(仅已完成K线) Returns: (is_bull, is_golden_cross, price, ema_fast) """ if not self._klines_1d: return False, False, 0.0, 0.0 for i in range(len(self._klines_1d) - 1, -1, -1): if self._klines_1d[i].close_time <= ts: ef = self._ema_fast_1d[i] es = self._ema_slow_1d[i] if ef == 0 or es == 0: return False, False, self._klines_1d[i].close, ef # 金叉 = EMA20 > EMA50 golden = ef > es # 趋势向上 = 价格在EMA20上方(额外确认) price_above = self._klines_1d[i].close > ef return golden and price_above, golden, self._klines_1d[i].close, ef return False, False, 0.0, 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) n = len(self._closes) if n < self.cfg.entry_ema + 20: return None ema_4h = ema(self._closes, self.cfg.entry_ema) atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period) cur_ema, cur_atr = ema_4h[-1], atr_vals[-1] if cur_ema == 0 or cur_atr == 0: return None is_bull, is_golden, price_1d, ema_1d = self._get_1d_state(k.open_time) # ── 出场 ── if self._in_position: self._highest = max(self._highest, k.high) stop = self._highest - self.cfg.atr_stop_mult * cur_atr # 4h EMA死叉(用EMA9代替,更快反应) ema9_vals = ema(self._closes, 9) ema20_vals = ema(self._closes, 20) if len(ema9_vals) > 2 and ema9_vals[-2] >= ema20_vals[-2] and ema9_vals[-1] < ema20_vals[-1]: self._in_position = False return Signal(symbol=self.cfg.symbol, side="SELL", reason="4h EMA死叉", timestamp=k.open_time) if k.close < stop: self._in_position = False return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"ATR止损", timestamp=k.open_time) # ── 入场 ── if not self._in_position and is_bull: # 4h 价格回调到 EMA20 附近(偏离不超过 1.5 倍 ATR) distance = abs(k.close - cur_ema) / cur_atr near_ema = distance < 1.5 # K线收阳 bullish_bar = k.close > k.open # EMA20 走平或向上 ema_rising = n >= 4 and ema_4h[-1] >= ema_4h[-3] if near_ema and bullish_bar and ema_rising: self._in_position = True self._highest = k.close return Signal( symbol=self.cfg.symbol, side="BUY", confidence=0.8, reason=f"1d多头+4h回调 P={k.close:.0f}≈EMA={cur_ema:.0f} dist={distance:.1f}σ", timestamp=k.open_time, ) return None # ════════════════════════════════════════════════════════ # 运行 # ════════════════════════════════════════════════════════ SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"] DATE_START = datetime(2024, 1, 1) DATE_END = datetime(2026, 1, 1) # v3 EMA最优结果(用于对比) EMA_V3 = { "BTCUSDT": (39.9, 1.03, -11.5, 20, 55.0), "ETHUSDT": (53.6, 1.04, -15.3, 18, 38.9), "BNBUSDT": (26.0, 0.64, -23.4, 23, 34.8), "SOLUSDT": (73.6, 1.18, -25.7, 13, 46.2), } async def main(): print() print("═" * 110) print(" 多时间框架 v3 — 1d 定趋势 / 4h 找回调买点 | 2024-2026") print("═" * 110) print(f" {'币种':<10} {'版本':<22} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}") print("─" * 110) for symbol in SYMBOLS: sc = DailyTrendConfig( symbol=symbol, data_start=DATE_START, data_end=DATE_END, ) bt = BacktestConfig(symbol=symbol, interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0) engine = BacktestEngine(bt, db_config=config.db) r = await engine.run(DailyTrendStrategy, sc) m = r.metrics # v3 对比 v3 = EMA_V3[symbol] print(f" {symbol:<10} {'1d趋势+4h回调':<22} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}% {m.profit_factor:>6.2f}") print(f" {'':<10} {'(对比) EMA v3 最优':<22} {v3[0]:>6.1f}% {v3[1]:>6.2f} {v3[2]:>6.1f}% {v3[3]:>5} {v3[4]:>5.1f}%") # 打印最近几笔出场 sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None] if sells: recent = sells[-3:] for t in recent: dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M") print(f" {'':<10} └ {dt} {t.pnl:>+8.2f} USDT {t.reason}") print() print("═" * 110) if __name__ == "__main__": asyncio.run(main())