515e61c517
- 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: 数据读取示例
194 lines
7.1 KiB
Python
194 lines
7.1 KiB
Python
"""
|
|
4h EMA50>EMA200 定大势 / 30m 找买点
|
|
|
|
策略:
|
|
4h EMA50 > EMA200 → 中长期多头趋势确认,允许做多
|
|
30m 入场 → 4h多头区间中,30m价格上穿EMA20 + 收阳 → 买入
|
|
30m 出场 → 下穿EMA20 或 4h趋势打破(EMA50<EMA200) 或 ATR止损
|
|
"""
|
|
|
|
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
|
|
from engine.data import DataService
|
|
from engine.indicators import ema, atr
|
|
|
|
|
|
class EMA200FilterConfig(StrategyConfig):
|
|
ema30_fast: int = 10 # 30m 快线
|
|
ema30_slow: int = 50 # 30m 慢线
|
|
atr_stop: float = 3.0
|
|
data_start: Optional[datetime] = None
|
|
data_end: Optional[datetime] = None
|
|
|
|
|
|
class EMA200FilterStrategy(BaseStrategy):
|
|
"""4h EMA50>200 多头趋势 + 30m 双EMA金叉
|
|
|
|
30m 用双EMA交叉替代价格穿越,大幅减少假信号。
|
|
"""
|
|
|
|
strategy_type = "ema200_filter"
|
|
|
|
def __init__(self, c: EMA200FilterConfig):
|
|
super().__init__(c)
|
|
self.cfg = c
|
|
self._klines_4h: list[Kline] = []
|
|
|
|
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_4h = await ds.fetch_klines(
|
|
symbol=self.cfg.symbol, interval="4h",
|
|
start_time=datetime(2023, 1, 1),
|
|
end_time=self.cfg.data_end,
|
|
limit=1_000_000,
|
|
)
|
|
finally:
|
|
await ds.close()
|
|
await super().on_start()
|
|
|
|
def _is_4h_bull(self, ts: float) -> bool:
|
|
if len(self._klines_4h) < 201:
|
|
return False
|
|
if not hasattr(self, '_ema50_4h'):
|
|
closes = [k.close for k in self._klines_4h]
|
|
self._ema50_4h = ema(closes, 50)
|
|
self._ema200_4h = ema(closes, 200)
|
|
for i in range(len(self._klines_4h) - 1, -1, -1):
|
|
if self._klines_4h[i].close_time <= ts:
|
|
e50 = self._ema50_4h[i]
|
|
e200 = self._ema200_4h[i]
|
|
return e50 > 0 and e200 > 0 and e50 > e200
|
|
return 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)
|
|
need = self.cfg.ema30_slow + 10
|
|
if n < need:
|
|
return None
|
|
|
|
# 30m 双EMA(金叉/死叉)
|
|
fast = ema(self._closes, self.cfg.ema30_fast)
|
|
slow = ema(self._closes, self.cfg.ema30_slow)
|
|
atr_vals = atr(self._highs, self._lows, self._closes, 14)
|
|
cur_f, cur_s = fast[-1], slow[-1]
|
|
prev_f, prev_s = fast[-2], slow[-2]
|
|
cur_atr = atr_vals[-1]
|
|
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
|
|
return None
|
|
|
|
is_bull = self._is_4h_bull(k.open_time)
|
|
|
|
# ── 出场 ──
|
|
if self._in_position:
|
|
self._highest = max(self._highest, k.high)
|
|
stop = self._highest - self.cfg.atr_stop * cur_atr
|
|
death = prev_f >= prev_s and cur_f < cur_s
|
|
|
|
if not is_bull:
|
|
self._in_position = False
|
|
return Signal(symbol=self.cfg.symbol, side="SELL", reason="4h转空", 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=f"30m EMA死叉", timestamp=k.open_time)
|
|
|
|
# ── 入场 ──
|
|
if not self._in_position and is_bull:
|
|
golden = prev_f <= prev_s and cur_f > cur_s
|
|
if golden and k.close > k.open:
|
|
self._in_position = True
|
|
self._highest = k.close
|
|
return Signal(symbol=self.cfg.symbol, side="BUY",
|
|
reason=f"4h多头+30m金叉", timestamp=k.open_time)
|
|
|
|
return None
|
|
|
|
|
|
# ════════════════════════════════════════════════════════
|
|
|
|
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
|
|
DATE_START = datetime(2024, 1, 1)
|
|
DATE_END = datetime(2026, 1, 1)
|
|
|
|
|
|
async def main():
|
|
print()
|
|
print("═" * 105)
|
|
print(" 4h EMA50>EMA200 定大势 / 30m 双EMA金叉 | 2024-2026")
|
|
print("═" * 105)
|
|
print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'持有%':>6}")
|
|
print("─" * 105)
|
|
|
|
for symbol in SYMBOLS:
|
|
sc = EMA200FilterConfig(symbol=symbol, data_start=DATE_START, data_end=DATE_END)
|
|
bt = BacktestConfig(symbol=symbol, interval="30m",
|
|
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0,
|
|
warmup_bars=50)
|
|
engine = BacktestEngine(bt, db_config=config.db)
|
|
r = await engine.run(EMA200FilterStrategy, sc)
|
|
m = r.metrics
|
|
|
|
# 持仓时间占比
|
|
if r.equity_curve:
|
|
bars_with_position = sum(1 for e in r.equity_curve if e.get("position", 0) > 0)
|
|
position_pct = bars_with_position / len(r.equity_curve) * 100
|
|
else:
|
|
position_pct = 0
|
|
|
|
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} {position_pct:>5.0f}%")
|
|
|
|
# 最近平仓
|
|
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
|
|
if sells:
|
|
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} USDT {t.reason}")
|
|
|
|
print("─" * 105)
|
|
|
|
# 对比之前最优
|
|
print("\n ■ 对比:之前最优策略")
|
|
BEST = {
|
|
"BTCUSDT": ("EMA v3(10,50) 4h", 39.9, 1.03, -11.5, 20),
|
|
"ETHUSDT": ("EMA v3(10,75) 4h", 53.6, 1.04, -15.3, 18),
|
|
"BNBUSDT": ("EMA v1(20,50) 4h", 52.0, 0.71, -39.8, 41),
|
|
"SOLUSDT": ("EMA v3(30,50) 4h", 73.6, 1.18, -25.7, 13),
|
|
}
|
|
print(f" {'币种':<10} {'策略':<22} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
|
|
for symbol in SYMBOLS:
|
|
name, ret, sh, dd, tr = BEST[symbol]
|
|
print(f" {symbol:<10} {name:<22} {ret:>6.1f}% {sh:>6.2f} {dd:>6.1f}% {tr:>5}")
|
|
|
|
print("\n═" * 105)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|