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: 数据读取示例
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
同周期三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())
|
||||
Reference in New Issue
Block a user