Files
trade/engine/example/multi_tf_demo.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

260 lines
9.1 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.
"""
多周期策略回测 — 4h 定趋势,30m 找买点
策略逻辑:
1. 4h EMA20 判断大趋势:价格 > EMA20 = 上升趋势
2. 30m RSI 寻找入场时机:上升趋势中 RSI < 35 = 回调买入
3. 出场:RSI > 70(超买)或 4h 趋势反转向下
用法:
source .venv/bin/activate && python example/multi_tf_demo.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, DBConfig
from engine.backtest import BacktestEngine, BacktestConfig
from engine.data import DataService
from engine.indicators import ema, rsi
# ============================================================
# 多周期趋势回调策略
# ============================================================
class MultiTFConfig(StrategyConfig):
"""多周期策略配置"""
# 4h 趋势参数
trend_ema_period: int = 20
# 30m 入场参数
entry_rsi_period: int = 14
entry_rsi_threshold: float = 35.0 # RSI 低于此值视为回调
# 出场参数
exit_rsi_threshold: float = 70.0 # RSI 高于此值出场
# 数据范围(用于预加载 4h 数据)
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class MultiTimeframeStrategy(BaseStrategy):
"""多周期趋势回调策略
┌─────────────────────────────┐
│ 4h K 线 → EMA20 判断趋势 │
│ Price > EMA20 = 上升趋势 │
└─────────────┬───────────────┘
│ 上升趋势
┌─────────────────────────────┐
│ 30m K 线 → 寻找入场时机 │
│ RSI < 35 = 回调买入 │
└─────────────┬───────────────┘
│ 持仓中
┌─────────────────────────────┐
│ 出场条件 │
│ RSI > 70 或 4h 趋势反转 │
└─────────────────────────────┘
"""
strategy_type = "multi_tf"
def __init__(self, config: MultiTFConfig):
super().__init__(config)
self.cfg: MultiTFConfig = config
# 4h 数据(在 on_start 中加载)
self._klines_4h: list[Kline] = []
self._ema_4h: list[float] = []
# 30m 数据积累
self._closes_30m: list[float] = []
# 持仓状态
self._has_position: bool = False
async def on_start(self) -> None:
"""预加载 4h K 线数据并计算 EMA"""
from engine.common.config import config as app_config
ds = DataService(app_config.db)
await ds.connect()
try:
self._klines_4h = await ds.fetch_klines(
symbol=self.cfg.symbol,
interval="4h",
start_time=self.cfg.data_start,
end_time=self.cfg.data_end,
limit=1_000_000,
)
closes_4h = [k.close for k in self._klines_4h]
self._ema_4h = ema(closes_4h, self.cfg.trend_ema_period)
finally:
await ds.close()
await super().on_start()
def _get_4h_trend(self, ts: float) -> tuple[bool, float, float]:
"""获取指定时间戳对应的 4h 趋势
只使用已完成的 4h K 线(close_time <= ts),避免前视偏差。
Returns:
(is_uptrend, price, ema_value)
"""
if not self._klines_4h:
return False, 0.0, 0.0
# 从后往前找最近已完成的 4h bar
for i in range(len(self._klines_4h) - 1, -1, -1):
if self._klines_4h[i].close_time <= ts:
price = self._klines_4h[i].close
ema_val = self._ema_4h[i]
if ema_val == 0.0:
return False, price, ema_val
return price > ema_val, price, ema_val
return False, 0.0, 0.0
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes_30m.append(kline.close)
# ── 获取 4h 趋势 ──
is_uptrend, price_4h, ema_4h = self._get_4h_trend(kline.open_time)
# ── 计算 30m RSI ──
rsi_vals = rsi(self._closes_30m, self.cfg.entry_rsi_period)
cur_rsi = rsi_vals[-1]
if cur_rsi == 0.0:
return None
# ── 出场逻辑 ──
if self._has_position:
# 4h 趋势反转(价格跌破 EMA)→ 止损出场
if not is_uptrend and price_4h > 0:
self._has_position = False
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.9,
reason=f"4h趋势反转 Price={price_4h:.2f}<EMA={ema_4h:.2f}",
timestamp=kline.open_time,
)
# 30m RSI 过热 → 止盈出场
if cur_rsi > self.cfg.exit_rsi_threshold:
self._has_position = False
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.8,
reason=f"30m RSI过热 {cur_rsi:.1f}>{self.cfg.exit_rsi_threshold}",
timestamp=kline.open_time,
)
# ── 入场逻辑 ──
if not self._has_position:
# 条件14h 上升趋势
if not is_uptrend:
return None
# 条件2:30m RSI 回调到超卖区
if cur_rsi < self.cfg.entry_rsi_threshold:
self._has_position = True
return Signal(
symbol=self.cfg.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.7,
reason=(
f"4h升势回调买入 | "
f"4hPrice={price_4h:.0f}>EMA={ema_4h:.0f} | "
f"30mRSI={cur_rsi:.1f}"
),
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def main():
bt_config = BacktestConfig(
symbol="ETHUSDT",
interval="30m",
start_time=datetime(2024, 1, 1),
end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
commission_pct=0.001,
slippage_pct=0.0005,
warmup_bars=100,
)
strategy_config = MultiTFConfig(
name="multi_tf_eth",
symbol="ETHUSDT",
trend_ema_period=20,
entry_rsi_period=14,
entry_rsi_threshold=35.0,
exit_rsi_threshold=70.0,
data_start=bt_config.start_time,
data_end=bt_config.end_time,
)
print()
print("" + "" * 60 + "")
print("" + " 多周期策略 — 4h 定趋势 / 30m 找买点".center(54) + "")
print("" + "" * 60 + "")
print(f"{'交易对:':<8} {bt_config.symbol:<14} {'周期:':<6} {bt_config.interval:<12}")
print(f"{'时间:':<8} {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print("" + "" * 60 + "")
print("║ 策略逻辑: ║")
print(f"║ 4h EMA{strategy_config.trend_ema_period} → 判断趋势方向 ║")
print(f"║ 30m RSI{strategy_config.entry_rsi_period} < {strategy_config.entry_rsi_threshold} → 回调买入 ║")
print(f"║ 30m RSI{strategy_config.entry_rsi_period} > {strategy_config.exit_rsi_threshold} → 止盈 / 4h趋势反转 → 止损 ║")
print("" + "" * 60 + "")
print()
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(MultiTimeframeStrategy, strategy_config)
print(result.summary())
# 打印最近交易
sells = [t for t in result.trades if t.pnl is not None]
if sells:
print(f"\n最近 10 笔平仓交易:")
print(f"{'时间':<22} {'方向':<6} {'价格':>10} {'盈亏':>10} 原因")
print("-" * 85)
for t in sells[-10:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
print(f"{dt:<22} {t.side:<6} {t.price:>10.2f} {t.pnl:>+10.2f} {t.reason}")
print("\n回测完成。")
if __name__ == "__main__":
asyncio.run(main())