Files
trade/engine/example/three_ema.py
T
Rekey 515e61c517 feat(engine): 添加策略示例集(18 个 Demo)
- backtest_demo.py: 回测基础演示
- strategy_simple.py / three_ema.py / long_short.py: 基础策略(双均线/三均线/多空)
- strategy_optimize*.py (3 版本): 参数优化示例(网格搜索/贝叶斯/遗传算法)
- multi_tf_*.py (4 版本): 多时间框架策略(EMA200/多周期共振/混合信号)
- regime_*.py (4 版本): 市场状态检测(趋势/震荡/波动率区间/全状态)
- cross_section.py: 截面多品种策略
- factor_demo.py: 多因子模型演示
- strategy_battle.py / strategy_more.py: 策略对比与组合
- full_cycle.py: 全流程演示(数据→回测→分析)
- data.py: 数据读取示例
2026-06-12 10:27:04 +08:00

158 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
同周期三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())