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,230 @@
|
||||
"""
|
||||
多时间框架 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())
|
||||
Reference in New Issue
Block a user