Files
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

231 lines
8.7 KiB
Python
Raw Permalink 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.
"""
多时间框架 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())