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: 数据读取示例
317 lines
12 KiB
Python
317 lines
12 KiB
Python
"""
|
||
更多策略尝试 — 不限于趋势跟踪
|
||
|
||
新策略:
|
||
1. Donchian 海龟 — 20周期突破买入,10周期跌破卖出,ATR止损
|
||
2. 价格乖离率 — 偏离 MA50 超 2σ 时反向交易(均值回归)
|
||
3. 1h+4h 多TF动量 — 1h MACD 金叉 + 4h EMA 多头共振
|
||
|
||
币种:BTCUSDT / ETHUSDT / BNBUSDT / SOLUSDT | 周期:4h | 2024-2026
|
||
|
||
用法:
|
||
source .venv/bin/activate && python example/strategy_more.py
|
||
"""
|
||
|
||
import asyncio
|
||
import math
|
||
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, macd, sma
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 1:Donchian 海龟突破
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
|
||
class DonchianConfig(StrategyConfig):
|
||
entry_period: int = 20 # 突破周期
|
||
exit_period: int = 10 # 退场周期
|
||
atr_period: int = 14
|
||
atr_stop: float = 2.0 # ATR止损倍数
|
||
|
||
|
||
class DonchianStrategy(BaseStrategy):
|
||
"""海龟交易:突破N日最高买入,跌破M日最低卖出"""
|
||
|
||
strategy_type = "donchian"
|
||
|
||
def __init__(self, c: DonchianConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._highs: list[float] = []
|
||
self._lows: list[float] = []
|
||
self._closes: list[float] = []
|
||
self._highest: float = 0.0
|
||
self._in_position = False
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._highs.append(k.high)
|
||
self._lows.append(k.low)
|
||
self._closes.append(k.close)
|
||
n = len(self._closes)
|
||
if n < self.cfg.entry_period + 5:
|
||
return None
|
||
|
||
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
|
||
cur_atr = atr_vals[-1]
|
||
if cur_atr == 0:
|
||
return None
|
||
|
||
# 通道计算
|
||
entry_high = max(self._highs[-self.cfg.entry_period:-1])
|
||
exit_low = min(self._lows[-self.cfg.exit_period:-1])
|
||
|
||
if self._in_position:
|
||
self._highest = max(self._highest, k.high)
|
||
stop = self._highest - self.cfg.atr_stop * cur_atr
|
||
if k.close < exit_low or k.close < stop:
|
||
self._in_position = False
|
||
reason = "跌破退出通道" if k.close < exit_low else "ATR止损"
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
|
||
|
||
if not self._in_position:
|
||
if k.close > entry_high:
|
||
self._in_position = True
|
||
self._highest = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||
reason=f"突破{self.cfg.entry_period}日高 {k.close:.0f}>{entry_high:.0f}",
|
||
timestamp=k.open_time)
|
||
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 2:价格乖离率(均值回归)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
|
||
class DeviationConfig(StrategyConfig):
|
||
ma_period: int = 50
|
||
entry_dev: float = -2.0 # 偏离 sigma 入场(负数=超跌)
|
||
exit_dev: float = 0.5 # 回归到此附近出场
|
||
atr_period: int = 14
|
||
atr_stop: float = 1.5
|
||
|
||
|
||
class DeviationStrategy(BaseStrategy):
|
||
"""均值回归:价格暴跌偏离均线 → 买入博反弹"""
|
||
|
||
strategy_type = "deviation"
|
||
|
||
def __init__(self, c: DeviationConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._closes: list[float] = []
|
||
self._highs: list[float] = []
|
||
self._lows: list[float] = []
|
||
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.ma_period + 5:
|
||
return None
|
||
|
||
ma = sma(self._closes, self.cfg.ma_period)
|
||
cur_ma = ma[-1]
|
||
if cur_ma == 0:
|
||
return None
|
||
|
||
# 计算标准差
|
||
window = self._closes[-self.cfg.ma_period:]
|
||
mean = sum(window) / len(window)
|
||
variance = sum((x - mean) ** 2 for x in window) / len(window)
|
||
stdev = math.sqrt(variance) if variance > 0 else mean * 0.01
|
||
|
||
# 乖离率(sigma单位)
|
||
deviation = (k.close - cur_ma) / stdev if stdev > 0 else 0
|
||
|
||
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
|
||
|
||
if self._in_position:
|
||
# 回归到exit_dev sigma内 或 ATR止损
|
||
if deviation > self.cfg.exit_dev:
|
||
self._in_position = False
|
||
return Signal(symbol=self.cfg.symbol, side="SELL",
|
||
reason=f"回归均线 dev={deviation:.1f}σ", timestamp=k.open_time)
|
||
|
||
if not self._in_position:
|
||
if deviation < self.cfg.entry_dev:
|
||
self._in_position = True
|
||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||
confidence=0.6, # 逆势交易降低仓位
|
||
reason=f"超跌反弹 dev={deviation:.1f}σ P={k.close:.0f}<MA={cur_ma:.0f}",
|
||
timestamp=k.open_time)
|
||
|
||
return None
|
||
|
||
|
||
# ════════════════════════════════════════════════════════
|
||
# 策略 3:多TF动量共振 (1h MACD + 4h EMA)
|
||
# ════════════════════════════════════════════════════════
|
||
|
||
|
||
class MultiTFConfig(StrategyConfig):
|
||
ema_4h: int = 50
|
||
macd_fast: int = 12
|
||
macd_slow: int = 26
|
||
macd_signal: int = 9
|
||
atr_stop: float = 2.5
|
||
data_start: Optional[datetime] = None
|
||
data_end: Optional[datetime] = None
|
||
|
||
|
||
class MultiTFStrategy(BaseStrategy):
|
||
"""1h MACD金叉 + 4h EMA多头 → 共振做多"""
|
||
|
||
strategy_type = "multi_tf_momentum"
|
||
|
||
def __init__(self, c: MultiTFConfig):
|
||
super().__init__(c)
|
||
self.cfg = c
|
||
self._klines_4h: list[Kline] = []
|
||
self._closes_1h: list[float] = []
|
||
self._highs_1h: list[float] = []
|
||
self._lows_1h: 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=self.cfg.data_start, 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 not self._klines_4h:
|
||
return False
|
||
closes = [k.close for k in self._klines_4h]
|
||
ema_vals = ema(closes, self.cfg.ema_4h)
|
||
for i in range(len(self._klines_4h) - 1, -1, -1):
|
||
if self._klines_4h[i].close_time <= ts:
|
||
return ema_vals[i] > 0 and self._klines_4h[i].close > ema_vals[i]
|
||
return False
|
||
|
||
async def on_kline(self, k: Kline) -> Optional[Signal]:
|
||
self._closes_1h.append(k.close)
|
||
self._highs_1h.append(k.high)
|
||
self._lows_1h.append(k.low)
|
||
n = len(self._closes_1h)
|
||
if n < 40:
|
||
return None
|
||
|
||
mline, sline, _ = macd(self._closes_1h, self.cfg.macd_fast, self.cfg.macd_slow, self.cfg.macd_signal)
|
||
atr_vals = atr(self._highs_1h, self._lows_1h, self._closes_1h, 14)
|
||
cur_m, cur_s, cur_atr = mline[-1], sline[-1], atr_vals[-1]
|
||
prev_m, prev_s = mline[-2], sline[-2]
|
||
if cur_m == 0 or cur_atr == 0:
|
||
return None
|
||
|
||
is_4h_bull = self._is_4h_bull(k.open_time)
|
||
golden = prev_m <= prev_s and cur_m > cur_s
|
||
|
||
if self._in_position:
|
||
self._highest = max(self._highest, k.high)
|
||
death = prev_m >= prev_s and cur_m < cur_s
|
||
stop = self._highest - self.cfg.atr_stop * cur_atr
|
||
if death or k.close < stop or not is_4h_bull:
|
||
self._in_position = False
|
||
reason = "MACD死叉" if death else ("ATR止损" if k.close < stop else "4h转空")
|
||
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
|
||
|
||
if not self._in_position:
|
||
if golden and is_4h_bull and cur_m > 0:
|
||
self._in_position = True
|
||
self._highest = k.close
|
||
return Signal(symbol=self.cfg.symbol, side="BUY",
|
||
reason="1hMACD金叉+4h多头", 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 run(symbol, s_name, s_cls, s_cfg, interval="4h"):
|
||
bt = BacktestConfig(symbol=symbol, interval=interval,
|
||
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
|
||
s_cfg.symbol = symbol
|
||
if hasattr(s_cfg, 'data_start'):
|
||
s_cfg.data_start = DATE_START
|
||
s_cfg.data_end = DATE_END
|
||
engine = BacktestEngine(bt, db_config=config.db)
|
||
return await engine.run(s_cls, s_cfg)
|
||
|
||
|
||
async def main():
|
||
strategies = [
|
||
("Donchian海龟", DonchianStrategy, DonchianConfig(), "4h"),
|
||
("乖离率回归", DeviationStrategy, DeviationConfig(), "4h"),
|
||
("1h+4h动量", MultiTFStrategy, MultiTFConfig(), "1h"),
|
||
]
|
||
|
||
print()
|
||
print("═" * 105)
|
||
print(" 更多策略尝试 | 2024-2026")
|
||
print("═" * 105)
|
||
print(f" {'策略':<16} {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
|
||
print("─" * 105)
|
||
|
||
all_rows = []
|
||
for s_name, s_cls, s_cfg, interval in strategies:
|
||
for symbol in SYMBOLS:
|
||
r = await run(symbol, s_name, s_cls, s_cfg.model_copy(), interval)
|
||
m = r.metrics
|
||
all_rows.append((s_name, symbol, m))
|
||
print(f" {s_name:<16} {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}")
|
||
|
||
# ── 排名 ──
|
||
print("─" * 105)
|
||
print("\n ■ 按夏普 TOP 5:")
|
||
ranked = sorted(all_rows, key=lambda x: x[2].sharpe_ratio, reverse=True)
|
||
for i, (s_name, symbol, m) in enumerate(ranked[:5]):
|
||
print(f" {i+1}. {symbol} {s_name:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%")
|
||
|
||
print("\n ■ 各币种最佳:")
|
||
for symbol in SYMBOLS:
|
||
sym_rows = [(s, m) for s, sym, m in all_rows if sym == symbol]
|
||
best = max(sym_rows, key=lambda x: x[1].sharpe_ratio)
|
||
print(f" {symbol}: {best[0]:<16} 夏普={best[1].sharpe_ratio:.2f} 收益={best[1].total_return_pct:+.1f}%")
|
||
|
||
avg_sh = sum(x[2].sharpe_ratio for x in all_rows) / len(all_rows)
|
||
print(f"\n 平均夏普: {avg_sh:.2f}")
|
||
|
||
print("\n═" * 105)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
asyncio.run(main())
|