""" 同周期三EMA策略 — 4h EMA50>200 定方向 + EMA20金叉EMA50入场 所有信号在同一周期(4h)上,不跨级。 """ import asyncio import sys from datetime import datetime, timezone from pathlib import Path _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 from engine.indicators import ema, atr class ThreeEMAConfig(StrategyConfig): ema_entry: int = 20 # 入场均线(金叉慢线时入场) ema_trend: int = 50 # 趋势均线(在200上方=多头) ema_filter: int = 200 # 长期过滤(50必须在其上) atr_stop: float = 2.5 class ThreeEMAStrategy(BaseStrategy): """三EMA同周期策略 EMA200 长期方向 → EMA50>200 才做多 EMA20 金叉 EMA50 → 入场 EMA20 死叉 EMA50 或 EMA50<200 → 出场 ATR 动态止损 """ strategy_type = "three_ema" def __init__(self, c: ThreeEMAConfig): super().__init__(c) self.cfg = c self._closes: list[float] = [] self._highs: list[float] = [] self._lows: list[float] = [] self._highest: float = 0.0 self._in_position = 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) n = len(self._closes) if n < self.cfg.ema_filter + 10: return None # 三条EMA e20 = ema(self._closes, self.cfg.ema_entry) e50 = ema(self._closes, self.cfg.ema_trend) e200 = ema(self._closes, self.cfg.ema_filter) atr_vals = atr(self._highs, self._lows, self._closes, 14) # 当前值和前值 c20, p20 = e20[-1], e20[-2] c50, p50 = e50[-1], e50[-2] c200 = e200[-1] cur_atr = atr_vals[-1] if c20 == 0 or c50 == 0 or c200 == 0 or cur_atr == 0: return None is_bull = c50 > c200 # EMA50在200上方=多头市场 golden = p20 <= p50 and c20 > p50 # 金叉 death = p20 >= p50 and c20 < p50 # 死叉 # ── 出场 ── if self._in_position: self._highest = max(self._highest, k.high) stop = self._highest - self.cfg.atr_stop * cur_atr if not is_bull: self._in_position = False return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA50<200转空", timestamp=k.open_time) if k.close < stop: self._in_position = False return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损", timestamp=k.open_time) if death: self._in_position = False return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA20死叉50", timestamp=k.open_time) # ── 入场 ── if not self._in_position and is_bull and golden: self._in_position = True self._highest = k.close return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"EMA20金叉50 多头确认", timestamp=k.open_time) return None # ═══════════════════════════════════════════════ SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"] DATE_START = datetime(2024, 1, 1) DATE_END = datetime(2026, 1, 1) # 之前最优对比 BEST = { "BTCUSDT": ("EMA v3(10,50)", 39.9, 1.03, -11.5, 20), "ETHUSDT": ("EMA v3(10,75)", 53.6, 1.04, -15.3, 18), "BNBUSDT": ("EMA v1(20,50)", 52.0, 0.71, -39.8, 41), "SOLUSDT": ("EMA v3(30,50)", 73.6, 1.18, -25.7, 13), } async def main(): print() print("═" * 105) print(" 三EMA同周期 — 4h EMA200定势 / EMA20×50交易 | 2024-2026") print("═" * 105) print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'vs最优':>8}") print("─" * 105) results = {} for symbol in SYMBOLS: sc = ThreeEMAConfig(symbol=symbol) 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(ThreeEMAStrategy, sc) m = r.metrics results[symbol] = m _, best_ret, best_sh, _, _ = BEST[symbol] delta = m.total_return_pct - best_ret tag = " ← 新最佳!" if m.sharpe_ratio > best_sh else "" print(f" {symbol:<10} {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} {delta:>+7.1f}%{tag}") sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None] for t in sells[-2:]: dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M") print(f" {'':<10} └ {dt} {t.pnl:>+8.2f} {t.reason}") print("─" * 105) print(f"\n {'币种':<10} {'之前最优':<20} {'收益%':>7} {'夏普':>6} → {'三EMA收益%':>9} {'三EMA夏普':>8}") for symbol in SYMBOLS: name, ret, sh, _, _ = BEST[symbol] m = results[symbol] print(f" {symbol:<10} {name:<20} {ret:>6.1f}% {sh:>6.2f} → {m.total_return_pct:>8.1f}% {m.sharpe_ratio:>7.2f}") print("\n═" * 105) if __name__ == "__main__": asyncio.run(main())