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:
Rekey
2026-06-12 10:27:04 +08:00
parent 4da520c14b
commit 515e61c517
21 changed files with 5194 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
"""
回测引擎使用示例 — 双策略演示
用法:
source .venv/bin/activate && python example/backtest_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
from engine.backtest import BacktestEngine, BacktestConfig
from engine.indicators import sma, rsi
# ============================================================
# 策略 1:双均线交叉
# ============================================================
class MACrossConfig(StrategyConfig):
fast_period: int = 7
slow_period: int = 25
class MACrossStrategy(BaseStrategy):
"""双均线交叉策略 — 使用 engine.indicators.sma 计算均线"""
strategy_type = "ma_cross"
def __init__(self, config: MACrossConfig):
super().__init__(config)
self.config: MACrossConfig = config
self._closes: list[float] = []
self._last_signal: Optional[str] = None
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes.append(kline.close)
# 使用指标库计算 SMA
fast_ma = sma(self._closes, self.config.fast_period)
slow_ma = sma(self._closes, self.config.slow_period)
fast = fast_ma[-1]
slow = slow_ma[-1]
if fast == 0.0 or slow == 0.0:
return None
if fast > slow and self._last_signal != "BUY":
self._last_signal = "BUY"
return Signal(
symbol=self.config.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.8,
reason=f"金叉 MA{self.config.fast_period}>{self.config.slow_period}",
timestamp=kline.open_time,
)
if fast < slow and self._last_signal != "SELL":
self._last_signal = "SELL"
return Signal(
symbol=self.config.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.8,
reason=f"死叉 MA{self.config.fast_period}<{self.config.slow_period}",
timestamp=kline.open_time,
)
return None
# ============================================================
# 策略 2RSI 超买超卖
# ============================================================
class RSIStrategyConfig(StrategyConfig):
period: int = 14
oversold: float = 30.0 # 超卖阈值
overbought: float = 70.0 # 超买阈值
class RSIStrategy(BaseStrategy):
"""RSI 超买超卖策略 — 使用 engine.indicators.rsi 计算 RSI
RSI 低于超卖线 → 买入;RSI 高于超买线 → 卖出。
"""
strategy_type = "rsi"
def __init__(self, config: RSIStrategyConfig):
super().__init__(config)
self.config: RSIStrategyConfig = config
self._closes: list[float] = []
self._has_position = False
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes.append(kline.close)
# 使用指标库计算 RSI
rsi_vals = rsi(self._closes, self.config.period)
current_rsi = rsi_vals[-1]
if current_rsi == 0.0:
return None
# 超卖 → 买入
if current_rsi < self.config.oversold and not self._has_position:
self._has_position = True
return Signal(
symbol=self.config.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.7,
reason=f"RSI超卖 ({current_rsi:.1f} < {self.config.oversold})",
timestamp=kline.open_time,
)
# 超买 → 卖出
if current_rsi > self.config.overbought and self._has_position:
self._has_position = False
return Signal(
symbol=self.config.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.7,
reason=f"RSI超买 ({current_rsi:.1f} > {self.config.overbought})",
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def run_backtest(
engine: BacktestEngine,
strategy_cls,
strategy_config: StrategyConfig,
label: str,
):
"""运行一次回测并打印结果"""
print(f"\n{'' * 60}")
print(f" {label}")
print(f"{'' * 60}")
result = await engine.run(strategy_cls, strategy_config)
print(result.summary())
# 最近 5 笔交易
if result.trades:
print(f"\n 最近 5 笔交易:")
print(f" {'时间':<22} {'方向':<6} {'价格':>10} {'数量':>10} {'盈亏':>10} 原因")
for t in result.trades[-5:]:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
pnl_str = f"{t.pnl:+.4f}" if t.pnl is not None else ""
print(f" {dt:<22} {t.side:<6} {t.price:>10.4f} {t.quantity:>10.6f} {pnl_str:>10} {t.reason}")
return result
async def main():
# ── 回测配置 ──
bt_config = BacktestConfig(
symbol="ETHUSDT",
interval="4h",
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,
)
print(f"\n回测: {bt_config.symbol} {bt_config.interval}")
print(f"时间: {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print(f"初始资金: {bt_config.initial_capital:.2f} USDT")
engine = BacktestEngine(bt_config, db_config=config.db)
# ── 策略 1:双均线交叉 ──
ma_config = MACrossConfig(
name="ma_cross_eth",
symbol="ETHUSDT",
fast_period=7,
slow_period=25,
)
await run_backtest(engine, MACrossStrategy, ma_config, "策略 1:双均线交叉 (MA7/MA25)")
# ── 策略 2:RSI 超买超卖 ──
rsi_config = RSIStrategyConfig(
name="rsi_eth",
symbol="ETHUSDT",
period=14,
oversold=30.0,
overbought=70.0,
)
await run_backtest(engine, RSIStrategy, rsi_config, "策略 2RSI 超买超卖 (30/70)")
print("\n全部回测完成。")
if __name__ == "__main__":
asyncio.run(main())
+234
View File
@@ -0,0 +1,234 @@
"""
横截面动量 — 选强弃弱 + 趋势/均值回归入场
策略:
1. 每根 4h K 线,计算 4 个币种过去 N 根 K 线的收益率
2. 按收益率排名,只有前 2 名允许做多
3. 趋势入场:EMA(10,50) 金叉 + 排名前2 → 买入
4. 回归入场:RSI < 35 + 排名前2 → 回调买入
5. 出场:排名跌出前2 或 EMA死叉 或 ATR止损
币种:BTC/ETH/BNB/SOL | 4h | 2024-2026
"""
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, rsi as calc_rsi
ALL_SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
class CrossSectionConfig(StrategyConfig):
lookback: int = 20 # 排名回溯周期
rank_threshold: int = 2 # 只做前N名
ema_fast: int = 10
ema_slow: int = 50
rsi_period: int = 14
rsi_entry: float = 35.0
atr_stop: float = 2.5
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class CrossSectionStrategy(BaseStrategy):
"""横截面动量 — 只做强势币种"""
strategy_type = "cross_section"
def __init__(self, c: CrossSectionConfig):
super().__init__(c)
self.cfg = c
# 所有币种的数据 {symbol: [Kline]}
self._all_klines: dict[str, list[Kline]] = {}
self._all_closes: dict[str, list[float]] = {}
# 当前币种的数据
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:
for sym in ALL_SYMBOLS:
klines = await ds.fetch_klines(
symbol=sym, interval="4h",
start_time=self.cfg.data_start, end_time=self.cfg.data_end,
limit=1_000_000,
)
self._all_klines[sym] = klines
self._all_closes[sym] = [k.close for k in klines]
finally:
await ds.close()
await super().on_start()
def _get_rank(self, ts: float) -> dict[str, float]:
"""计算所有币种在指定时间戳的排名收益率,返回 {symbol: return%}"""
scores = {}
for sym in ALL_SYMBOLS:
klines = self._all_klines.get(sym, [])
if not klines:
scores[sym] = -999
continue
# 找到时间戳 <= ts 的最新K线索引
idx = len(klines) - 1
for i in range(len(klines) - 1, -1, -1):
if klines[i].open_time <= ts:
idx = i
break
# 计算过去 lookback 根K线的收益率
start_idx = max(0, idx - self.cfg.lookback)
if start_idx >= idx:
scores[sym] = 0
else:
start_price = self._all_closes[sym][start_idx]
end_price = self._all_closes[sym][idx]
scores[sym] = (end_price / start_price - 1) * 100 if start_price > 0 else 0
return scores
def _my_rank(self, ts: float) -> int:
"""当前币种在全部币种中的排名(1=最强)"""
scores = self._get_rank(ts)
my_score = scores.get(self.cfg.symbol, -999)
# 高于我的分数有几个
better = sum(1 for s in scores.values() if s > my_score)
return better + 1
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.ema_slow + 10:
return None
fast = ema(self._closes, self.cfg.ema_fast)
slow = ema(self._closes, self.cfg.ema_slow)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
rsi_vals = calc_rsi(self._closes, self.cfg.rsi_period)
cur_f, cur_s = fast[-1], slow[-1]
prev_f, prev_s = fast[-2], slow[-2]
cur_atr = atr_vals[-1]
cur_rsi = rsi_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0 or cur_rsi == 0:
return None
rank = self._my_rank(k.open_time)
is_top = rank <= self.cfg.rank_threshold
golden = prev_f <= prev_s and cur_f > cur_s # 趋势入场
death = prev_f >= prev_s and cur_f < cur_s # 趋势出场
oversold = cur_rsi < self.cfg.rsi_entry # 均值回归入场
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if not is_top:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"排名跌出前{self.cfg.rank_threshold}(#{rank})", timestamp=k.open_time)
if death:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", 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 not self._in_position and is_top:
# 趋势信号:金叉
if golden:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"金叉+#{rank}横截面动量", timestamp=k.open_time)
# 回归信号:RSI超卖
if oversold:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
confidence=0.7, # 回归信号稍降仓位
reason=f"RSI超卖+#{rank}横截面动量 RSI={cur_rsi:.0f}",
timestamp=k.open_time)
return None
# ═══════════════════════════════════════════════
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 110)
print(" 横截面动量 — 只做最强 + 趋势/回归双信号 | 4h | 2024-2026")
print("" * 110)
print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
print("" * 110)
results = {}
for symbol in ALL_SYMBOLS:
sc = CrossSectionConfig(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(CrossSectionStrategy, sc)
m = r.metrics
results[symbol] = (m, r)
# 统计排名分布和信号类型
trend_signals = sum(1 for t in r.trades if t.side == "BUY" and "金叉" in t.reason)
meanrev_signals = sum(1 for t in r.trades if t.side == "BUY" and "RSI" in t.reason)
exits_rank = sum(1 for t in r.trades if t.side == "SELL" and "排名" in t.reason)
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}")
print(f" {'':<10} └ 趋势入场:{trend_signals} 回归入场:{meanrev_signals} 排名出场:{exits_rank}")
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
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} {t.reason}")
# ── 对比 ──
print("" * 110)
print("\n ■ 对比:纯趋势跟踪 vs 横截面动量")
TREND = {
"BTCUSDT": ("EMA v3(10,50)", 39.9, 1.03, 20),
"ETHUSDT": ("EMA v3(10,75)", 53.6, 1.04, 18),
"BNBUSDT": ("EMA v1(20,50)", 52.0, 0.71, 41),
"SOLUSDT": ("EMA v3(30,50)", 73.6, 1.18, 13),
}
print(f" {'币种':<10} {'纯趋势':>24}{'横截面动量':>24}")
print(f" {'':<10} {'收益% 夏普 交易':>24}{'收益% 夏普 交易':>24}")
for sym in ALL_SYMBOLS:
t_name, t_ret, t_sh, t_tr = TREND[sym]
m, r = results[sym]
print(f" {sym:<10} {t_ret:>5.1f}% {t_sh:>5.2f} {t_tr:>4}次 → {m.total_return_pct:>5.1f}% {m.sharpe_ratio:>5.2f} {m.total_trades:>4}")
print("\n" * 110)
if __name__ == "__main__":
asyncio.run(main())
+62
View File
@@ -0,0 +1,62 @@
"""
DataService 使用示例 — 读取各周期 K 线并打印
用法:
python example/data.py
"""
import asyncio
import sys
from pathlib import Path
# 确保项目根目录在 sys.path 中,以便使用 engine 包导入
_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.data import DataService
from engine.common.config import config
async def main():
print(config.db)
ds = DataService(config.db)
await ds.connect()
try:
# 1. 查看可用交易对
symbols = await ds.fetch_available_symbols("1m")
print(f"可用交易对: {symbols}")
# 2. 各周期读取最新 3 条 BTCUSDT K 线
target = "BTCUSDT"
for interval in ["1m", "5m", "15m", "30m", "1h", "4h", "1d"]:
klines = await ds.fetch_klines(
symbol=target,
interval=interval,
limit=3,
)
print(f"\n{'' * 70}")
print(f" [{interval}] {target}{len(klines)}")
print(f"{'' * 70}")
for k in klines:
print(
f" {k.open_time:.0f} O={k.open:>12.4f} H={k.high:>12.4f}"
f" L={k.low:>12.4f} C={k.close:>12.4f}"
f" V={k.volume:>10.4f} trades={k.trade_count}"
)
# 3. 日期范围
start, end = await ds.fetch_symbol_date_range(target, "1d")
print(f"\n{target} 1d 数据范围: {start} ~ {end}")
print("\n完成")
finally:
await ds.close()
if __name__ == "__main__":
asyncio.run(main())
+253
View File
@@ -0,0 +1,253 @@
"""
多因子组合回测 — 三重共振策略
随机挑选 3 个技术指标组合成一个策略:
- MACD (趋势因子) — 金叉/死叉判断方向
- RSI (动量因子) — 阈值过滤避免追高抄底
- Bollinger (波动率因子) — 中轨确认趋势强度
用法:
source .venv/bin/activate && python example/factor_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
from engine.backtest import BacktestEngine, BacktestConfig
from engine.indicators import macd, rsi, bollinger, atr
# ============================================================
# 三重共振策略
# ============================================================
class TripleFactorConfig(StrategyConfig):
"""三重因子组合策略配置"""
# MACD
macd_fast: int = 12
macd_slow: int = 26
macd_signal: int = 9
# RSI
rsi_period: int = 14
rsi_oversold: float = 30.0 # 超卖线(入场需在此之上)
rsi_overbought: float = 65.0 # 入场过热线(入场需在此之下)
rsi_exit: float = 75.0 # 卖出线
# Bollinger
bb_period: int = 20
bb_std: float = 2.0
# ATR 动态止损倍数(0 表示不启用)
atr_period: int = 14
atr_stop_mult: float = 2.0
class TripleFactorStrategy(BaseStrategy):
"""三重共振策略
┌─────────────┬──────────────────────────────────────┐
│ 因子 │ 作用 │
├─────────────┼──────────────────────────────────────┤
│ MACD (趋势) │ 金叉=看多入场信号,死叉=看空出场信号 │
│ RSI (动量) │ 30<RSI<65 区间入场,RSI>75 过热出场 │
│ BB (波动) │ 价格>中轨确认多头趋势,跌破下轨出场 │
└─────────────┴──────────────────────────────────────┘
入场(三重共振):
1. MACD 金叉(上穿信号线)
2. RSI 在 [30, 65] 区间(合理动量)
3. 价格 > 布林中轨(趋势向上)
出场(任一触发):
1. MACD 死叉(下穿信号线)
2. RSI > 75(过热)
3. 价格 < 布林下轨(趋势破位)
"""
strategy_type = "triple_factor"
def __init__(self, config: TripleFactorConfig):
super().__init__(config)
self.cfg: TripleFactorConfig = config
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
async def on_kline(self, kline: Kline) -> Optional[Signal]:
self._closes.append(kline.close)
self._highs.append(kline.high)
self._lows.append(kline.low)
n = len(self._closes)
max_period = max(
self.cfg.macd_slow + self.cfg.macd_signal,
self.cfg.rsi_period + 1,
self.cfg.bb_period,
)
if n < max_period:
return None
# ── 全量计算因子(每个 bar 一次)──
macd_line, signal_line, _hist = macd(
self._closes,
fast=self.cfg.macd_fast,
slow=self.cfg.macd_slow,
signal=self.cfg.macd_signal,
)
rsi_vals = rsi(self._closes, period=self.cfg.rsi_period)
_upper, mid, lower = bollinger(
self._closes,
period=self.cfg.bb_period,
std=self.cfg.bb_std,
)
# 当前值和前一根的值
cur_macd = macd_line[-1]
cur_signal = signal_line[-1]
prev_macd = macd_line[-2]
prev_signal = signal_line[-2]
cur_rsi = rsi_vals[-1]
prev_rsi = rsi_vals[-2]
cur_mid = mid[-1]
cur_lower = lower[-1]
cur_price = kline.close
if cur_macd == 0.0 or cur_rsi == 0.0 or cur_mid == 0.0:
return None
# ── 入场:2/3 共振即可 ──
golden_cross = prev_macd <= prev_signal and cur_macd > cur_signal
rsi_ok = self.cfg.rsi_oversold < cur_rsi < self.cfg.rsi_overbought
above_mid = cur_price > cur_mid
score = golden_cross + rsi_ok + above_mid
if score >= 2:
return Signal(
symbol=self.cfg.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.6 + score * 0.1,
reason=(
f"{score}/3共振"
f"{' MACD金叉' if golden_cross else ''}"
f"{' RSI=' + str(round(cur_rsi, 1)) if rsi_ok else ''}"
f"{' Price>BBmid' if above_mid else ''}"
),
timestamp=kline.open_time,
)
# ── 出场:任一强信号 ──
death_cross = prev_macd >= prev_signal and cur_macd < cur_signal
rsi_overheat = cur_rsi > self.cfg.rsi_exit
below_lower = cur_price < cur_lower
if death_cross:
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.8,
reason="MACD死叉",
timestamp=kline.open_time,
)
if rsi_overheat and cur_rsi > prev_rsi:
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.7,
reason=f"RSI过热({cur_rsi:.1f})",
timestamp=kline.open_time,
)
if below_lower and cur_price < cur_mid:
return Signal(
symbol=self.cfg.symbol,
side="SELL",
signal_type="MARKET",
confidence=0.6,
reason=f"跌破BB下轨({cur_lower:.2f})",
timestamp=kline.open_time,
)
return None
# ============================================================
# 主函数
# ============================================================
async def main():
bt_config = BacktestConfig(
symbol="BTCUSDT",
interval="4h",
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 = TripleFactorConfig(
name="triple_factor_btc",
symbol="BTCUSDT",
macd_fast=12,
macd_slow=26,
macd_signal=9,
rsi_period=14,
rsi_oversold=30.0,
rsi_overbought=65.0,
rsi_exit=75.0,
bb_period=20,
bb_std=2.0,
)
print()
print("" + "" * 58 + "")
print("" + " 多因子组合回测 — 三重共振策略".center(52) + "")
print("" + "" * 58 + "")
print(f"{'交易对:':<8} {bt_config.symbol:<12} {'周期:':<6} {bt_config.interval:<10}")
print(f"{'时间:':<8} {bt_config.start_time.date()} ~ {bt_config.end_time.date()}")
print(f"{'初始资金:':<8} {bt_config.initial_capital:>12.2f} USDT ║")
print("" + "" * 58 + "")
print("║ 因子组合: ║")
print(f"║ 1. MACD({strategy_config.macd_fast},{strategy_config.macd_slow},{strategy_config.macd_signal}) — 趋势方向 ║")
print(f"║ 2. RSI({strategy_config.rsi_period}) — 动量过滤 ║")
print(f"║ 3. Bollinger({strategy_config.bb_period},{strategy_config.bb_std}) — 波动率确认 ║")
print("" + "" * 58 + "")
print()
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(TripleFactorStrategy, strategy_config)
print(result.summary())
# 打印全部交易
if result.trades:
print(f"\n全部交易 ({len(result.trades)} 笔):")
print(f"{'时间':<22} {'方向':<6} {'价格':>10} {'数量':>10} {'盈亏':>10} 原因")
print("-" * 100)
for t in result.trades:
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M")
pnl_str = f"{t.pnl:+.4f}" if t.pnl is not None else ""
print(f"{dt:<22} {t.side:<6} {t.price:>10.2f} {t.quantity:>10.6f} {pnl_str:>10} {t.reason}")
print("\n回测完成。")
if __name__ == "__main__":
asyncio.run(main())
+152
View File
@@ -0,0 +1,152 @@
"""
全周期回测 — 2017-2026,覆盖完整牛熊
多空双向 EMA 趋势跟踪,展示牛市/熊市/全周期分段表现。
用法:
source .venv/bin/activate && python example/full_cycle.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 BacktestConfig
from engine.indicators import ema, atr
from engine.example.long_short import LongShortEngine, LongShortEmaConfig, LongShortEmaStrategy
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
# 牛熊分段(以 BTC 为参考)
PERIODS = [
("2017-2018 牛市", datetime(2017, 1, 1), datetime(2018, 1, 1)),
("2018 熊市", datetime(2018, 1, 1), datetime(2019, 1, 1)),
("2019 反弹", datetime(2019, 1, 1), datetime(2020, 1, 1)),
("2020 牛初+312", datetime(2020, 1, 1), datetime(2021, 1, 1)),
("2021 牛市", datetime(2021, 1, 1), datetime(2022, 1, 1)),
("2022 熊市", datetime(2022, 1, 1), datetime(2023, 1, 1)),
("2023 复苏", datetime(2023, 1, 1), datetime(2024, 1, 1)),
("2024-2025 牛市", datetime(2024, 1, 1), datetime(2026, 1, 1)),
]
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def run_backtest(symbol, fast, slow, start, end):
sc = LongShortEmaConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(symbol=symbol, interval="4h", start_time=start, end_time=end,
initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(LongShortEmaStrategy, sc)
async def main():
print()
print("" * 125)
print(" 全周期多空回测 — 2017-2026 完整牛熊 | 4h EMA趋势")
print("" * 125)
# ── 全周期汇总 ──
print(f"\n ■ 全周期 2017-2026 汇总")
print(f" {'币种':<10} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 105)
for symbol in SYMBOLS:
fast, slow = PARAMS[symbol]
r = await run_backtest(symbol, fast, slow, DATE_START, DATE_END)
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
long_pnl = sum(t.pnl for t in long_t) if long_t else 0
short_pnl = sum(t.pnl for t in short_t) if short_t else 0
print(f" {symbol:<10} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {long_pnl:>+9.0f} {short_pnl:>+9.0f}")
# ── BTC 分段 ──
print(f"\n ■ BTC 各阶段表现 (参数 EMA{10},{50})")
print(f" {'阶段':<22} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 105)
for period_name, p_start, p_end in PERIODS:
try:
r = await run_backtest("BTCUSDT", 10, 50, p_start, p_end)
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
long_pnl = sum(t.pnl for t in long_t) if long_t else 0
short_pnl = sum(t.pnl for t in short_t) if short_t else 0
print(f" {period_name:<22} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {long_pnl:>+9.0f} {short_pnl:>+9.0f}")
except Exception as e:
print(f" {period_name:<22} 数据不足或错误: {e}")
# ── 只做多 vs 多空全周期对比 ──
print(f"\n ■ BTC 只做多 vs 多空 (全周期)")
# 只做多需要单跑一次(LongShortEngine 本身就支持只做多:不开空就行)
# 简单做法:用原版 BacktestEngine 跑一次只做多
from engine.backtest import BacktestEngine
from engine.common.base import BaseStrategy as BS, Signal as Sig, StrategyConfig as SC
class LongOnlyEMAConfig(SC):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
class LongOnlyEMAStrategy(BS):
strategy_type = "long_only"
def __init__(self, c): super().__init__(c); self.cfg = c
async def on_start(self): self._c = []; self._h = []; self._l = []; self._hp = 0.0; self._in = False; await super().on_start()
async def on_kline(self, k):
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
n = len(self._c)
if n < self.cfg.slow + 5: return None
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
a = atr(self._h, self._l, self._c, 14)
cf, cs, ca = f[-1], s[-1], a[-1]; pf, ps = f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
if self._in:
self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca
if (pf >= ps and cf < cs) or k.close < stop:
self._in = False
return Sig(symbol=self.cfg.symbol, side="SELL", reason="死叉" if pf >= ps else "ATR止损", timestamp=k.open_time)
else:
if pf <= ps and cf > cs:
self._in = True; self._hp = k.close
return Sig(symbol=self.cfg.symbol, side="BUY", reason="金叉", timestamp=k.open_time)
return None
lo_sc = LongOnlyEMAConfig(symbol="BTCUSDT")
lo_bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
lo_eng = BacktestEngine(lo_bt, db_config=config.db)
lo_r = await lo_eng.run(LongOnlyEMAStrategy, lo_sc)
lo_m = lo_r.metrics
# 多空
ls_r = await run_backtest("BTCUSDT", 10, 50, DATE_START, DATE_END)
ls_m = ls_r.metrics
print(f" {'':<10} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(f" {'只做多':<10} {lo_m.total_return_pct:>6.1f}% {lo_m.annual_return_pct:>6.1f}% {lo_m.sharpe_ratio:>6.2f} {lo_m.max_drawdown_pct:>6.1f}% {lo_m.total_trades:>5}")
print(f" {'多空':<10} {ls_m.total_return_pct:>6.1f}% {ls_m.annual_return_pct:>6.1f}% {ls_m.sharpe_ratio:>6.2f} {ls_m.max_drawdown_pct:>6.1f}% {ls_m.total_trades:>5}")
print("\n" * 125)
if __name__ == "__main__":
asyncio.run(main())
+501
View File
@@ -0,0 +1,501 @@
"""
多空双向回测 — EMA 趋势跟踪(支持做空)
基于表现最好的纯趋势参数,增加做空能力:
- 金叉 → 平空仓 + 做多
- 死叉 → 平多仓 + 做空
- ATR 动态止损(多空双向)
- 始终持仓(非多即空)
- 输出增加年化收益
参数(各币种历史最优):
BTC(10,50) ETH(10,75) BNB(20,50) SOL(30,50)
用法:
source .venv/bin/activate && python example/long_short.py
"""
import asyncio
import statistics
import sys
from collections import defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Type
_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.data import DataService
from engine.indicators import ema, atr
from engine.backtest.models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
# ════════════════════════════════════════════════════════
# 多空回测引擎
# ════════════════════════════════════════════════════════
class LongShortEngine:
"""支持多空双向的事件驱动回测引擎"""
def __init__(self, bt_config: BacktestConfig, db_config=None):
self.config = bt_config
self._db_config = db_config
self._cash: float = bt_config.initial_capital
self._position: float = 0.0 # >0 多头, <0 空头, =0 空仓
self._avg_entry_price: float = 0.0
self._trades: list[BacktestTrade] = []
self._equity: list[dict] = []
self._peak_equity: float = 0.0
self._pending_buy: Optional[Signal] = None
self._pending_sell: Optional[Signal] = None
async def run(self, strategy_cls, strategy_config: StrategyConfig) -> BacktestResult:
from engine.common.config import config as app_config
strategy_config.symbol = self.config.symbol
strategy_config.exchange = self.config.exchange
db_cfg = self._db_config or app_config.db
ds = DataService(db_cfg)
await ds.connect()
try:
klines = await ds.fetch_klines(
symbol=self.config.symbol, interval=self.config.interval,
start_time=self.config.start_time, end_time=self.config.end_time,
limit=1_000_000,
)
if len(klines) < self.config.warmup_bars + 2:
raise ValueError(f"数据不足:需 {self.config.warmup_bars+2},实际 {len(klines)}")
strategy = strategy_cls(strategy_config)
await strategy.on_start()
self._cash = self.config.initial_capital
self._position = 0.0
self._avg_entry_price = 0.0
self._trades = []
self._equity = []
self._pending_buy = None
self._pending_sell = None
warmup_end = self.config.warmup_bars
for i in range(warmup_end):
await strategy.on_kline(klines[i])
for i in range(warmup_end, len(klines)):
kline = klines[i]
# 先执行待执行订单(下一根 bar 开盘价)
if self._pending_buy is not None:
self._execute_buy(self._pending_buy, kline)
self._pending_buy = None
if self._pending_sell is not None:
self._execute_sell(self._pending_sell, kline)
self._pending_sell = None
signal = await strategy.on_kline(kline)
if signal is not None and signal.side == "BUY":
self._pending_buy = signal
elif signal is not None and signal.side == "SELL":
self._pending_sell = signal
self._record_equity(kline)
# 强平
if self._position != 0 and len(klines) > 0:
last_k = klines[-1]
if self._position > 0:
self._execute_sell(Signal(symbol=self.config.symbol, side="SELL",
quantity=abs(self._position),
reason="回测结束—强平多仓", timestamp=last_k.open_time), last_k)
else:
self._execute_buy(Signal(symbol=self.config.symbol, side="BUY",
quantity=abs(self._position),
reason="回测结束—强平空仓", timestamp=last_k.open_time), last_k)
await strategy.on_stop()
metrics = self._compute_metrics()
return BacktestResult(config=self.config, strategy_config=strategy_config.model_dump(),
metrics=metrics, trades=self._trades, equity_curve=self._equity)
finally:
await ds.close()
# ── 交易执行 ──
def _execute_buy(self, signal: Signal, kline: Kline) -> None:
exec_price = kline.open * (1 + self.config.slippage_pct)
qty = signal.quantity
if qty is None:
if self._position < 0:
qty = abs(self._position) # 平空仓
else:
max_notional = self._cash * signal.confidence
qty = max_notional / exec_price
qty = self._round_qty(qty)
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
if self._position < 0:
# 平空仓
cover_qty = min(qty, abs(self._position))
cover_notional = exec_price * cover_qty
cover_comm = cover_notional * self.config.commission_pct
pnl = (self._avg_entry_price - exec_price) * cover_qty - cover_comm
self._cash -= cover_notional + cover_comm
self._position += cover_qty
if abs(self._position) < self.config.min_order_qty:
self._position = 0.0
self._avg_entry_price = 0.0
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="BUY", price=exec_price, quantity=cover_qty,
notional=cover_notional, commission=cover_comm,
slippage=exec_price - kline.open, pnl=pnl,
reason=signal.reason))
# 剩余开多
remaining = qty - cover_qty
if remaining >= self.config.min_order_qty:
self._open_long(remaining, exec_price, kline, signal)
else:
# 开多 / 加仓
self._open_long(qty, exec_price, kline, signal)
def _open_long(self, qty: float, exec_price: float, kline: Kline, signal: Signal):
notional = exec_price * qty
commission = notional * self.config.commission_pct
total_cost = notional + commission
if total_cost > self._cash:
qty = self._round_qty(self._cash / (exec_price * (1 + self.config.commission_pct)))
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
total_cost = notional + commission
if self._position > 0:
total_value = self._avg_entry_price * self._position + notional
self._position += qty
self._avg_entry_price = total_value / self._position
else:
self._position = qty
self._avg_entry_price = exec_price
self._cash -= total_cost
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="BUY", price=exec_price, quantity=qty,
notional=notional, commission=commission,
slippage=exec_price - kline.open, reason=signal.reason))
def _execute_sell(self, signal: Signal, kline: Kline) -> None:
exec_price = kline.close * (1 - self.config.slippage_pct)
qty = signal.quantity
if qty is None:
if self._position > 0:
qty = self._position
else:
max_notional = self._cash * signal.confidence
qty = max_notional / exec_price
qty = self._round_qty(qty)
if qty < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
if self._position > 0:
# 平多仓
close_qty = min(qty, self._position)
close_notional = exec_price * close_qty
close_comm = close_notional * self.config.commission_pct
pnl = (exec_price - self._avg_entry_price) * close_qty - close_comm
self._position -= close_qty
self._cash += close_notional - close_comm
if self._position < self.config.min_order_qty:
self._position = 0.0
self._avg_entry_price = 0.0
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="SELL", price=exec_price, quantity=close_qty,
notional=close_notional, commission=close_comm,
slippage=kline.close - exec_price, pnl=pnl,
reason=signal.reason))
# 剩余开空
remaining = qty - close_qty
if remaining >= self.config.min_order_qty:
self._open_short(remaining, exec_price, kline, signal)
else:
self._open_short(qty, exec_price, kline, signal)
def _open_short(self, qty: float, exec_price: float, kline: Kline, signal: Signal):
notional = exec_price * qty
commission = notional * self.config.commission_pct
if self._position < 0:
total_value = self._avg_entry_price * abs(self._position) + notional
self._position -= qty
self._avg_entry_price = total_value / abs(self._position)
else:
self._position = -qty
self._avg_entry_price = exec_price
self._cash += notional - commission
self._trades.append(BacktestTrade(timestamp=kline.open_time, symbol=self.config.symbol,
side="SELL", price=exec_price, quantity=qty,
notional=notional, commission=commission,
slippage=kline.close - exec_price, reason=signal.reason))
# ── 资金曲线 ──
def _record_equity(self, kline: Kline) -> None:
equity = self._cash + self._position * kline.close
if not self._equity:
self._peak_equity = equity
elif equity > self._peak_equity:
self._peak_equity = equity
dd = (equity - self._peak_equity) / self._peak_equity * 100 if self._peak_equity > 0 else 0.0
self._equity.append({"timestamp": kline.open_time, "equity": equity,
"drawdown": dd, "position": self._position})
# ── 绩效 ──
def _compute_metrics(self) -> BacktestMetrics:
if not self._equity:
return BacktestMetrics()
initial = self.config.initial_capital
final = self._equity[-1]["equity"]
total_return_pct = (final - initial) / initial * 100
first_ts = self._equity[0]["timestamp"]
last_ts = self._equity[-1]["timestamp"]
days = (last_ts - first_ts) / (1000 * 86400)
if days > 0 and final > 0 and initial > 0:
annual_return_pct = ((final / initial) ** (365 / days) - 1) * 100
else:
annual_return_pct = 0.0
daily_returns = self._compute_daily_returns()
if len(daily_returns) > 1:
mean_ret = statistics.mean(daily_returns)
std_ret = statistics.stdev(daily_returns)
sharpe_ratio = (mean_ret / std_ret * (365 ** 0.5)) if std_ret > 0 else 0.0
else:
sharpe_ratio = 0.0
max_dd_pct, max_dd_days = self._compute_max_drawdown()
closed = [t for t in self._trades if t.pnl is not None]
total_trades = len(closed)
if total_trades > 0:
winners = [t for t in closed if t.pnl > 0]
losers = [t for t in closed if t.pnl <= 0]
win_rate = len(winners) / total_trades
gp = sum(t.pnl for t in winners)
gl = abs(sum(t.pnl for t in losers))
profit_factor = gp / gl if gl > 0 else (gp if gp > 0 else 0.0)
avg_pnl = sum(t.pnl for t in closed) / total_trades
best_pnl = max(t.pnl for t in closed)
worst_pnl = min(t.pnl for t in closed)
else:
win_rate = profit_factor = avg_pnl = best_pnl = worst_pnl = 0.0
calmar = annual_return_pct / abs(max_dd_pct) if max_dd_pct < 0 else 0.0
return BacktestMetrics(
total_return_pct=total_return_pct, annual_return_pct=annual_return_pct,
sharpe_ratio=sharpe_ratio, max_drawdown_pct=max_dd_pct,
max_drawdown_duration_days=max_dd_days, win_rate=win_rate,
profit_factor=profit_factor, total_trades=total_trades,
avg_trade_pnl=avg_pnl, best_trade_pnl=best_pnl, worst_trade_pnl=worst_pnl,
calmar_ratio=calmar, final_equity=final,
)
def _compute_daily_returns(self) -> list[float]:
if not self._equity:
return []
daily: dict[str, float] = {}
for point in self._equity:
dt = datetime.fromtimestamp(point["timestamp"] / 1000, tz=timezone.utc)
daily[dt.strftime("%Y-%m-%d")] = point["equity"]
sorted_dates = sorted(daily.keys())
returns = []
for i in range(1, len(sorted_dates)):
prev = daily[sorted_dates[i - 1]]
curr = daily[sorted_dates[i]]
if prev > 0:
returns.append((curr - prev) / prev)
return returns
def _compute_max_drawdown(self) -> tuple[float, int]:
if not self._equity:
return 0.0, 0
peak = self._equity[0]["equity"]
max_dd = 0.0
dd_start_idx = 0
max_dd_days = 0
for i, point in enumerate(self._equity):
equity = point["equity"]
if equity > peak:
peak = equity
dd_start_idx = i
dd = (equity - peak) / peak * 100
if dd < max_dd:
max_dd = dd
peak_ts = self._equity[dd_start_idx]["timestamp"]
dd_days = int((point["timestamp"] - peak_ts) / (1000 * 86400))
if dd_days > max_dd_days:
max_dd_days = dd_days
return max_dd, max_dd_days
@staticmethod
def _round_qty(qty: float, decimals: int = 8) -> float:
factor = 10 ** decimals
return int(qty * factor) / factor
# ════════════════════════════════════════════════════════
# 多空趋势策略
# ════════════════════════════════════════════════════════
class LongShortEmaConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class LongShortEmaStrategy(BaseStrategy):
"""EMA金叉做多、死叉做空,始终在场"""
strategy_type = "long_short_ema"
def __init__(self, c: LongShortEmaConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
self._lowest: float = float('inf')
self._position_side: str = "" # "long" / "short"
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.slow + 5:
return None
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1]
prev_f, prev_s = fast[-2], slow[-2]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
golden = prev_f <= prev_s and cur_f > cur_s
death = prev_f >= prev_s and cur_f < cur_s
# ── 多头持仓 ──
if self._position_side == "long":
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if death:
self._position_side = "short"
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉→做空", timestamp=k.open_time)
if k.close < stop:
self._position_side = ""
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损→空仓", timestamp=k.open_time)
# ── 空头持仓 ──
elif self._position_side == "short":
self._lowest = min(self._lowest, k.low)
stop = self._lowest + self.cfg.atr_stop * cur_atr
if golden:
self._position_side = "long"
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉→做多", timestamp=k.open_time)
if k.close > stop:
self._position_side = ""
return Signal(symbol=self.cfg.symbol, side="BUY", reason="ATR止损→空仓", timestamp=k.open_time)
# ── 空仓等待信号 ──
else:
if golden:
self._position_side = "long"
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason="金叉→做多", timestamp=k.open_time)
elif death:
self._position_side = "short"
self._lowest = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason="死叉→做空", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
# 各币种历史最优参数
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
# 只做多结果(用于对比)
LONG_ONLY = {
"BTCUSDT": (39.9, 1.03, -11.5, 18.3),
"ETHUSDT": (53.6, 1.04, -15.3, 23.9),
"BNBUSDT": (52.0, 0.71, -39.8, 23.3),
"SOLUSDT": (73.6, 1.18, -25.7, 31.7),
}
# (总收益%, 夏普, 回撤%, 年化%)
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 112)
print(" 多空双向 EMA 趋势跟踪 | 4h | 2024-2026")
print("" * 112)
header = f" {'币种':<10} {'方向':<6} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}"
print(header)
print("" * 112)
for symbol in SYMBOLS:
fast, slow = PARAMS[symbol]
sc = LongShortEmaConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(LongShortEmaStrategy, sc)
m = r.metrics
long_trades = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_trades = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
lo = LONG_ONLY[symbol]
long_pnl = sum(t.pnl for t in long_trades) if long_trades else 0
short_pnl = sum(t.pnl for t in short_trades) if short_trades else 0
print(f" {symbol:<10} 多空 {m.total_return_pct:>6.1f}% {m.annual_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} 只做多 {lo[0]:>6.1f}% {lo[3]:>6.1f}% {lo[1]:>6.2f} {lo[2]:>6.1f}%")
if long_trades or short_trades:
print(f" {'':<10} └ 多头P&L {long_pnl:>+7.0f} ({len(long_trades)}笔) 空头P&L {short_pnl:>+7.0f} ({len(short_trades)}笔)")
for t in (r.trades[-2:] if r.trades else []):
if t.pnl is not None:
side_label = "平多" if t.side == "SELL" else "平空"
dt = datetime.fromtimestamp(t.timestamp / 1000, tz=timezone.utc).strftime("%m-%d %H:%M")
print(f" {'':<10}{dt} {side_label} {t.pnl:>+8.2f} {t.reason}")
print("" * 112)
print("\n" * 112)
if __name__ == "__main__":
asyncio.run(main())
+259
View File
@@ -0,0 +1,259 @@
"""
多周期策略回测 — 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())
+235
View File
@@ -0,0 +1,235 @@
"""
多周期策略 v2 — 双周期同指标(EMA)
策略逻辑:
4h 和 30m 使用同一个技术指标 EMA,不同参数:
- 4h EMA50 → 判断主趋势方向
- 30m EMA20 → 寻找入场/出场时机
入场:4h 多头(Price > EMA50+ 30m 价格上穿 EMA20
出场:30m 价格下穿 EMA20 或 4h 趋势转空
用法:
source .venv/bin/activate && python example/multi_tf_demo2.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
from engine.data import DataService
from engine.indicators import ema
class DualEMATFConfig(StrategyConfig):
"""双周期 EMA 策略配置"""
# 4h 主趋势
trend_ema_period: int = 50
# 30m 交易信号
entry_ema_period: int = 20
# 数据范围
data_start: Optional[datetime] = None
data_end: Optional[datetime] = None
class DualEMATFStrategy(BaseStrategy):
"""双周期 EMA 均线策略
┌──────────────────────────────────────────────┐
│ 4h EMA50 → 价格在上=多头趋势 │
│ 30m EMA20 → 价格在上+EMA上行+收阳=买入 │
│ EMA下行+收阴=卖出 │
└──────────────────────────────────────────────┘
"""
strategy_type = "dual_ema_tf"
def __init__(self, config: DualEMATFConfig):
super().__init__(config)
self.cfg: DualEMATFConfig = config
# 4h 预加载数据
self._klines_4h: list[Kline] = []
self._ema_4h: list[float] = []
# 30m 数据积累
self._closes_30m: list[float] = []
self._ema_30m: list[float] = []
# 持仓
self._has_position: bool = False
async def on_start(self) -> None:
"""预加载 4h 数据并计算 EMA50"""
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 趋势判断(仅用已完成 K 线,close_time <= ts"""
if not self._klines_4h:
return False, 0.0, 0.0
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)
self._ema_30m = ema(self._closes_30m, self.cfg.entry_ema_period)
n = len(self._closes_30m)
if n < 2:
return None
cur_ema = self._ema_30m[-1]
prev_ema = self._ema_30m[-2]
if cur_ema == 0.0 or prev_ema == 0.0:
return None
cur_price = kline.close
# 30m K线收阳
is_bullish_bar = kline.close > kline.open
# 30m EMA 斜率(最近3根是否递增)
ema_sloping_up = (
n >= 4
and self._ema_30m[-1] > self._ema_30m[-2]
and self._ema_30m[-2] > self._ema_30m[-3]
)
# 4h 趋势
is_uptrend, price_4h, ema_4h = self._get_4h_trend(kline.open_time)
# ── 出场 ──
if self._has_position:
# 仅 4h 趋势转空时出场(让 30m 波动自然消化)
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转空 P={price_4h:.0f}<EMA{self.cfg.trend_ema_period}={ema_4h:.0f}",
timestamp=kline.open_time,
)
# ── 入场 ──
if not self._has_position:
if not is_uptrend:
return None
# 30m 价格在 EMA 上方 + EMA 上行 + 收阳 → 买入
price_above_ema = cur_price > cur_ema
if price_above_ema and ema_sloping_up and is_bullish_bar:
self._has_position = True
return Signal(
symbol=self.cfg.symbol,
side="BUY",
signal_type="MARKET",
confidence=0.7,
reason=(
f"30m多头确认 | "
f"4hP={price_4h:.0f}>E={ema_4h:.0f}"
),
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 = DualEMATFConfig(
name="dual_ema_eth",
symbol="ETHUSDT",
trend_ema_period=50,
entry_ema_period=20,
data_start=bt_config.start_time,
data_end=bt_config.end_time,
)
print()
print("" + "" * 60 + "")
print("" + " 多周期策略 v2 — 双周期同指标 (EMA)".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 EMA{strategy_config.entry_ema_period} → 价格在上+EMA上行+收阳=买入 ║")
print("" + "" * 60 + "")
print()
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(DualEMATFStrategy, 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("-" * 80)
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())
+193
View File
@@ -0,0 +1,193 @@
"""
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())
+230
View File
@@ -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())
+233
View File
@@ -0,0 +1,233 @@
"""
最佳牛熊判定 — 全币种全周期回测
方法:EMA200斜率 + 价格vs EMA200 + ATH回撤,3选2投票
策略:牛市只做多 / 熊市只做空 / 震荡空仓
币种:BTC / ETH / BNB / SOL,各自最早有数据到2026
用法:
source .venv/bin/activate && python example/regime_all.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 BacktestConfig
from engine.indicators import ema, atr
from engine.data import DataService
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 3法判定器
# ════════════════════════════════════════════════════════
class RegimeDetector3:
def __init__(self):
self._ath = 0.0
def update_ath(self, price: float):
if price > self._ath:
self._ath = price
def ema200_slope(self, closes: list[float], idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(closes, 200)
if e200[idx - 20] == 0: return "unknown"
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20]
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
def price_vs_ema200(self, closes: list[float], idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(closes, 200)
if e200[idx] == 0: return "unknown"
return "bull" if closes[idx] > e200[idx] else "bear"
def ath_drawdown(self, closes: list[float], idx: int) -> str:
if self._ath == 0: return "unknown"
dd = (closes[idx] - self._ath) / self._ath
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
def detect(self, closes: list[float], idx: int) -> str:
r1 = self.ema200_slope(closes, idx)
r2 = self.price_vs_ema200(closes, idx)
r3 = self.ath_drawdown(closes, idx)
b = sum(1 for r in [r1, r2, r3] if r == "bull")
br = sum(1 for r in [r1, r2, r3] if r == "bear")
if b >= 2: return "bull"
if br >= 2: return "bear"
return "sideways"
# ════════════════════════════════════════════════════════
# 自适应策略
# ════════════════════════════════════════════════════════
class RegimeEmaConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
class RegimeEmaStrategy(BaseStrategy):
"""按市场状态自适应做多/做空"""
strategy_type = "regime_ema"
def __init__(self, c: RegimeEmaConfig):
super().__init__(c)
self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector = RegimeDetector3()
self._side: str = ""; self._hp: float = 0.0; self._lp: float = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
self._detector.update_ath(k.close)
n = len(self._c)
if n < 220: return None
regime = self._detector.detect(self._c, n - 1)
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
a = atr(self._h, self._l, self._c, 14)
cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
golden = pf <= ps and cf > cs; death = pf >= ps and cf < cs
# 多头持仓
if self._side == "long":
self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca
if death or k.close < stop or regime == "bear":
self._side = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
# 空头持仓
elif self._side == "short":
self._lp = min(self._lp, k.low); stop = self._lp + self.cfg.atr_stop * ca
if golden or k.close > stop or regime == "bull":
self._side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
# 空仓等信号
else:
if regime == "bull" and golden:
self._side = "long"; self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"牛市金叉", timestamp=k.open_time)
elif regime == "bear" and death:
self._side = "short"; self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"熊市死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def get_actual_range(symbol: str) -> tuple[datetime, datetime]:
"""获取币种实际数据范围"""
ds = DataService(config.db)
await ds.connect()
try:
start, end = await ds.fetch_symbol_date_range(symbol, "4h")
return start, end
except:
return DATE_START, DATE_END
finally:
await ds.close()
async def main():
print()
print("" * 125)
print(" 牛熊自适应策略 — 全币种全周期 | 牛市只多/熊市只空/震荡空仓")
print("" * 125)
print(f"\n ■ 全周期汇总")
print(f" {'币种':<10} {'数据范围':<22} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 115)
for symbol in SYMBOLS:
fast, slow = PARAMS[symbol]
sc = RegimeEmaConfig(symbol=symbol, fast=fast, slow=slow)
# 获取实际数据范围
try:
act_start, act_end = await get_actual_range(symbol)
range_str = f"{act_start.date()}~{act_end.date()}"
except:
act_start, act_end = DATE_START, DATE_END
range_str = "2017-2026"
bt = BacktestConfig(symbol=symbol, interval="4h",
start_time=act_start, end_time=act_end, initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(RegimeEmaStrategy, sc)
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
long_pnl = sum(t.pnl for t in long_t) if long_t else 0
short_pnl = sum(t.pnl for t in short_t) if short_t else 0
print(f" {symbol:<10} {range_str:<22} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {long_pnl:>+9.0f} {short_pnl:>+9.0f}")
# ── BTC 分段 ──
PERIODS = [
("2017 牛市", datetime(2017,1,1), datetime(2018,1,1)),
("2018 熊市", datetime(2018,1,1), datetime(2019,1,1)),
("2019 反弹", datetime(2019,1,1), datetime(2020,1,1)),
("2020 牛初", datetime(2020,1,1), datetime(2021,1,1)),
("2021 牛市", datetime(2021,1,1), datetime(2022,1,1)),
("2022 熊市", datetime(2022,1,1), datetime(2023,1,1)),
("2023 震荡", datetime(2023,1,1), datetime(2024,1,1)),
("2024-25牛", datetime(2024,1,1), datetime(2026,1,1)),
]
print(f"\n ■ BTC 分段表现")
print(f" {'阶段':<16} {'总收益%':>7} {'夏普':>6} {'多头P&L':>9} {'空头P&L':>9}")
print(" " + "" * 65)
for name, s, e in PERIODS:
try:
sc = RegimeEmaConfig(symbol="BTCUSDT", fast=10, slow=50)
bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=s, end_time=e, initial_capital=10_000.0)
eng = LongShortEngine(bt, db_config=config.db)
r = await eng.run(RegimeEmaStrategy, sc)
lt = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
st = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
print(f" {name:<16} {r.metrics.total_return_pct:>+6.1f}% {r.metrics.sharpe_ratio:>6.2f} {sum(t.pnl for t in lt) if lt else 0:>+8.0f} {sum(t.pnl for t in st) if st else 0:>+8.0f}")
except Exception as ex:
print(f" {name:<16} 数据不足")
print("\n" * 125)
if __name__ == "__main__":
asyncio.run(main())
+305
View File
@@ -0,0 +1,305 @@
"""
市场状态识别 — 牛市/熊市判定方法对比 + 自适应策略回测
方法:
1. EMA200 斜率 — EMA200 向上=牛,向下=熊
2. 价格 vs EMA200 — Price > EMA200 = 牛
3. ATH 回撤 — 距历史高点 < 20% = 牛,> 20% = 熊
4. 综合投票 — 三选二
根据识别结果自动偏多/偏空:
牛市:只做多(金叉买入,死叉平仓)
熊市:只做空(死叉做空,金叉平仓)
震荡(票数2:1或无共识):空仓等待
BTC 2017-2026 全周期测试
用法:
source .venv/bin/activate && python example/regime_detect.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 BacktestConfig
from engine.indicators import ema, atr
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 市场状态识别器
# ════════════════════════════════════════════════════════
class RegimeDetector:
"""市场状态识别:牛/熊/震荡"""
def __init__(self, closes: list[float]):
self._c = closes
self._ath = 0.0
self._ath_tracking = [] # 追踪历史高点序列
def update_ath(self, price: float):
if price > self._ath:
self._ath = price
self._ath_tracking.append(self._ath)
def ema200_slope(self, idx: int) -> str:
"""EMA200 斜率判定"""
if idx < 202:
return "unknown"
e200 = ema(self._c, 200)
# 最近5根EMA200的斜率
if e200[idx] == 0 or e200[max(0, idx - 5)] == 0:
return "unknown"
slope = (e200[idx] - e200[max(0, idx - 5)]) / e200[max(0, idx - 5)]
if slope > 0.001:
return "bull"
elif slope < -0.001:
return "bear"
return "sideways"
def price_vs_ema200(self, idx: int) -> str:
"""价格 vs EMA200"""
if idx < 202:
return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0:
return "unknown"
return "bull" if self._c[idx] > e200[idx] else "bear"
def ath_drawdown(self, idx: int) -> str:
"""ATH 回撤判定(经典加密牛熊指标)"""
if not self._ath_tracking or idx >= len(self._ath_tracking):
return "unknown"
curr_ath = self._ath_tracking[idx]
if curr_ath == 0:
return "unknown"
dd = (self._c[idx] - curr_ath) / curr_ath
if dd > -0.20:
return "bull"
elif dd < -0.40:
return "bear"
return "sideways"
def combined(self, idx: int) -> tuple[str, str, str, str]:
"""综合判定"""
r1 = self.ema200_slope(idx)
r2 = self.price_vs_ema200(idx)
r3 = self.ath_drawdown(idx)
votes = {"bull": 0, "bear": 0}
for r in [r1, r2, r3]:
if r in votes:
votes[r] += 1
if votes["bull"] >= 2:
return "bull", r1, r2, r3
elif votes["bear"] >= 2:
return "bear", r1, r2, r3
return "sideways", r1, r2, r3
# ════════════════════════════════════════════════════════
# 自适应策略(根据市场状态偏多/偏空)
# ════════════════════════════════════════════════════════
class AdaptiveEmaConfig(StrategyConfig):
fast: int = 10
slow: int = 50
atr_stop: float = 2.5
class AdaptiveEmaStrategy(BaseStrategy):
"""牛市只做多、熊市只做空、震荡空仓"""
strategy_type = "adaptive_ema"
def __init__(self, c: AdaptiveEmaConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._detector: Optional[RegimeDetector] = None
self._position_side: str = "" # "long" / "short"
self._highest: float = 0.0
self._lowest: float = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
self._highs.append(k.high)
self._lows.append(k.low)
if self._detector is None:
self._detector = RegimeDetector(self._closes)
self._detector.update_ath(k.close)
n = len(self._closes)
if n < 210:
return None
regime, r1, r2, r3 = self._detector.combined(n - 1)
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1]
prev_f, prev_s = fast[-2], slow[-2]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
golden = prev_f <= prev_s and cur_f > cur_s
death = prev_f >= prev_s and cur_f < cur_s
# ── 多头持仓 ──
if self._position_side == "long":
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if death or k.close < stop or regime == "bear":
self._position_side = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊市")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
# ── 空头持仓 ──
elif self._position_side == "short":
self._lowest = min(self._lowest, k.low)
stop = self._lowest + self.cfg.atr_stop * cur_atr
if golden or k.close > stop or regime == "bull":
self._position_side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛市")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
# ── 空仓等待 ──
else:
if regime == "bull" and golden:
self._position_side = "long"
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"牛市金叉 ({r1}/{r2}/{r3})", timestamp=k.open_time)
elif regime == "bear" and death:
self._position_side = "short"
self._lowest = k.close
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"熊市死叉 ({r1}/{r2}/{r3})", timestamp=k.open_time)
# 震荡市:不开仓
return None
# ════════════════════════════════════════════════════════
# 对比测试
# ════════════════════════════════════════════════════════
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 120)
print(" 市场状态自适应策略 — 牛市只做多 / 熊市只做空 / 震荡空仓")
print("" * 120)
# ── BTC 自适应 vs 多空 vs 只做多 ──
print("\n ■ BTC 全周期 2017-2026 对比")
print(f" {'策略':<18} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'多头P&L':>10} {'空头P&L':>10}")
print(" " + "" * 105)
# 自适应
sc = AdaptiveEmaConfig(symbol="BTCUSDT")
bt = BacktestConfig(symbol="BTCUSDT", interval="4h",
start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
r = await engine.run(AdaptiveEmaStrategy, sc)
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
print(f" {'自适应(牛多熊空)':<18} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {sum(t.pnl for t in long_t) if long_t else 0:>+9.0f} {sum(t.pnl for t in short_t) if short_t else 0:>+9.0f}")
# 始终多空(之前结果)
from engine.example.long_short import LongShortEmaStrategy, LongShortEmaConfig as LSCfg
sc2 = LSCfg(symbol="BTCUSDT", fast=10, slow=50)
r2 = await engine.run(LongShortEmaStrategy, sc2)
m2 = r2.metrics
print(f" {'始终多空':<18} {m2.total_return_pct:>6.1f}% {m2.annual_return_pct:>6.1f}% {m2.sharpe_ratio:>6.2f} {m2.max_drawdown_pct:>6.1f}% {m2.total_trades:>5}")
# 只做多
from engine.backtest import BacktestEngine as OrigEngine
class LongOnlyS(BaseStrategy):
strategy_type = "lo"; _in = False; _hp = 0.0
def __init__(self, c): super().__init__(c); self._c = []; self._h = []; self._l = []
async def on_start(self): await super().on_start()
async def on_kline(self, k):
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
n = len(self._c)
if n < 55: return None
f = ema(self._c, 10); s = ema(self._c, 50); a = atr(self._h, self._l, self._c, 14)
cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
if self._in:
self._hp = max(self._hp, k.high); stop = self._hp - 2.5 * ca
if (pf >= ps and cf < cs) or k.close < stop:
self._in = False
return Signal(symbol="BTCUSDT", side="SELL",
reason="死叉" if pf>=ps else "ATR止损", timestamp=k.open_time)
else:
if pf <= ps and cf > cs:
self._in = True; self._hp = k.close
return Signal(symbol="BTCUSDT", side="BUY", reason="金叉", timestamp=k.open_time)
return None
from engine.common.base import StrategyConfig as SC
lo_bt = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=DATE_START, end_time=DATE_END, initial_capital=10_000.0)
lo_e = OrigEngine(lo_bt, db_config=config.db)
lo_r = await lo_e.run(LongOnlyS, SC(symbol="BTCUSDT"))
lo_m = lo_r.metrics
long_only_t = [t for t in lo_r.trades if t.pnl is not None]
print(f" {'只做多':<18} {lo_m.total_return_pct:>6.1f}% {lo_m.annual_return_pct:>6.1f}% {lo_m.sharpe_ratio:>6.2f} {lo_m.max_drawdown_pct:>6.1f}% {lo_m.total_trades:>5}")
# ── 分段对比 ──
PERIODS = [
("2017 牛市", datetime(2017,1,1), datetime(2018,1,1)),
("2018 熊市", datetime(2018,1,1), datetime(2019,1,1)),
("2019 反弹", datetime(2019,1,1), datetime(2020,1,1)),
("2020 牛初", datetime(2020,1,1), datetime(2021,1,1)),
("2021 牛市", datetime(2021,1,1), datetime(2022,1,1)),
("2022 熊市", datetime(2022,1,1), datetime(2023,1,1)),
("2023 震荡", datetime(2023,1,1), datetime(2024,1,1)),
("2024-25 牛", datetime(2024,1,1), datetime(2026,1,1)),
]
print(f"\n ■ BTC 分段:自适应 vs 始终多空")
print(f" {'阶段':<16} {'自适应':>8} {'始终多空':>8} {'只做多':>8}")
print(" " + "" * 50)
for name, s, e in PERIODS:
try:
bt_p = BacktestConfig(symbol="BTCUSDT", interval="4h", start_time=s, end_time=e, initial_capital=10_000.0)
# 自适应
sc_p = AdaptiveEmaConfig(symbol="BTCUSDT")
e_p = LongShortEngine(bt_p, db_config=config.db)
r_p = await e_p.run(AdaptiveEmaStrategy, sc_p)
# 始终多空
sc_p2 = LSCfg(symbol="BTCUSDT", fast=10, slow=50)
r_p2 = await e_p.run(LongShortEmaStrategy, sc_p2)
# 只做多
lo_e2 = OrigEngine(bt_p, db_config=config.db)
lo_r2 = await lo_e2.run(LongOnlyS, SC(symbol="BTCUSDT"))
print(f" {name:<16} {r_p.metrics.total_return_pct:>+7.1f}% {r_p2.metrics.total_return_pct:>+7.1f}% {lo_r2.metrics.total_return_pct:>+7.1f}%")
except Exception as ex:
print(f" {name:<16} 错误: {ex}")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())
+307
View File
@@ -0,0 +1,307 @@
"""
牛熊判定方法扩展 + 组合对比
新增方法:
4. Mayer Multiple — Price / EMA200 比值。>1.2=牛,<0.8=熊
5. 年同比 — 价格同比去年涨=牛,跌=熊
6. 市场结构 — 近200根bar更高高点+更高低点=牛
对比各种投票组合的效果。
用法:
source .venv/bin/activate && python example/regime_detect2.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 BacktestConfig
from engine.indicators import ema, atr
from engine.example.long_short import LongShortEngine
# ════════════════════════════════════════════════════════
# 扩展版市场状态识别器(6种方法)
# ════════════════════════════════════════════════════════
class AdvancedRegimeDetector:
def __init__(self, closes: list[float], highs: list[float], lows: list[float]):
self._c = closes
self._h = highs
self._l = lows
self._ath = 0.0
self._ath_tracking = []
def update_ath(self, price: float):
if price > self._ath:
self._ath = price
self._ath_tracking.append(self._ath)
# ── 方法1: EMA200 斜率 ──
def ema200_slope(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
slope = (e200[idx] - e200[idx - 20]) / e200[idx - 20] if e200[idx - 20] > 0 else 0
if slope > 0.002: return "bull"
if slope < -0.002: return "bear"
return "sideways"
# ── 方法2: 价格 vs EMA200 ──
def price_vs_ema200(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0: return "unknown"
return "bull" if self._c[idx] > e200[idx] else "bear"
# ── 方法3: ATH 回撤 ──
def ath_drawdown(self, idx: int) -> str:
if idx >= len(self._ath_tracking) or self._ath_tracking[idx] == 0:
return "unknown"
dd = (self._c[idx] - self._ath_tracking[idx]) / self._ath_tracking[idx]
if dd > -0.15: return "bull"
if dd < -0.35: return "bear"
return "sideways"
# ── 方法4: Mayer Multiple ──
def mayer_multiple(self, idx: int) -> str:
if idx < 210: return "unknown"
e200 = ema(self._c, 200)
if e200[idx] == 0: return "unknown"
mm = self._c[idx] / e200[idx]
if mm > 1.2: return "bull" # 明显在均线上方
if mm < 0.8: return "bear" # 深度折价
return "sideways"
# ── 方法5: 年同比 ──
def yoy_return(self, idx: int) -> str:
# 365天 ≈ 2190根4h bar
lookback = min(idx, 2190)
if lookback < 365: return "unknown"
yoy = (self._c[idx] - self._c[idx - lookback]) / self._c[idx - lookback]
if yoy > 0.15: return "bull"
if yoy < -0.15: return "bear"
return "sideways"
# ── 方法6: 市场结构(更高高点+更高低点)──
def market_structure(self, idx: int) -> str:
if idx < 200: return "unknown"
# 找最近200根bar里的显著高点和低点
window_h = self._h[max(0, idx - 200):idx + 1]
window_l = self._l[max(0, idx - 200):idx + 1]
if len(window_h) < 100: return "unknown"
# 分成前后两半
mid = len(window_h) // 2
first_high = max(window_h[:mid])
second_high = max(window_h[mid:])
first_low = min(window_l[:mid])
second_low = min(window_l[mid:])
if second_high > first_high and second_low > first_low:
return "bull" # 更高高点 + 更高低点 = 上升结构
if second_high < first_high and second_low < first_low:
return "bear" # 更低高点 + 更低低点 = 下降结构
return "sideways"
# ════════════════════════════════════════════════════════
# 自适应策略(支持可配置的投票方案)
# ════════════════════════════════════════════════════════
class AdaptiveConfig(StrategyConfig):
fast: int = 10; slow: int = 50; atr_stop: float = 2.5
vote_mode: str = "majority_6" # 投票模式
class AdaptiveStrategy(BaseStrategy):
"""按投票结果自适应多空"""
strategy_type = "adaptive_v2"
def __init__(self, c: AdaptiveConfig):
super().__init__(c)
self.cfg = c
self._c: list[float] = []; self._h: list[float] = []; self._l: list[float] = []
self._detector: Optional[AdvancedRegimeDetector] = None
self._side: str = ""; self._hp: float = 0.0; self._lp: float = float('inf')
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._c.append(k.close); self._h.append(k.high); self._l.append(k.low)
if self._detector is None:
self._detector = AdvancedRegimeDetector(self._c, self._h, self._l)
self._detector.update_ath(k.close)
n = len(self._c)
if n < 2200: return None # 等够一年数据
# ── 投票逻辑 ──
methods = [
self._detector.ema200_slope(n - 1),
self._detector.price_vs_ema200(n - 1),
self._detector.ath_drawdown(n - 1),
self._detector.mayer_multiple(n - 1),
self._detector.yoy_return(n - 1),
self._detector.market_structure(n - 1),
]
if self.cfg.vote_mode == "majority_6":
# 6选4以上=牛/熊,否则震荡
bull_votes = sum(1 for m in methods if m == "bull")
bear_votes = sum(1 for m in methods if m == "bear")
if bull_votes >= 4: regime = "bull"
elif bear_votes >= 4: regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "majority_4":
# 仅前4种方法,3选2
b = sum(1 for m in methods[:4] if m == "bull")
br = sum(1 for m in methods[:4] if m == "bear")
if b >= 3: regime = "bull"
elif br >= 3: regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "strict":
# 全部6个一致
if all(m == "bull" for m in methods): regime = "bull"
elif all(m == "bear" for m in methods): regime = "bear"
else: regime = "sideways"
elif self.cfg.vote_mode == "trend_only":
# 只用前3种(EMA200斜率+价格+ATH回撤),2选2
b3 = sum(1 for m in methods[:3] if m == "bull")
br3 = sum(1 for m in methods[:3] if m == "bear")
if b3 >= 2: regime = "bull"
elif br3 >= 2: regime = "bear"
else: regime = "sideways"
else:
regime = "sideways"
# ── EMA 交叉信号 ──
f = ema(self._c, self.cfg.fast); s = ema(self._c, self.cfg.slow)
a = atr(self._h, self._l, self._c, 14)
cf, cs, ca, pf, ps = f[-1], s[-1], a[-1], f[-2], s[-2]
if cf == 0 or cs == 0 or ca == 0: return None
golden = pf <= ps and cf > cs; death = pf >= ps and cf < cs
# ── 持仓管理 ──
if self._side == "long":
self._hp = max(self._hp, k.high); stop = self._hp - self.cfg.atr_stop * ca
if death or k.close < stop or regime == "bear":
self._side = ""
reason = "死叉" if death else ("ATR止损" if k.close < stop else "转熊")
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
elif self._side == "short":
self._lp = min(self._lp, k.low); stop = self._lp + self.cfg.atr_stop * ca
if golden or k.close > stop or regime == "bull":
self._side = ""
reason = "金叉" if golden else ("ATR止损" if k.close > stop else "转牛")
return Signal(symbol=self.cfg.symbol, side="BUY", reason=reason, timestamp=k.open_time)
else:
if regime == "bull" and golden:
self._side = "long"; self._hp = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"牛({bull_votes if 'bull_votes' in dir() else '?'}/{len(methods)})金叉",
timestamp=k.open_time)
elif regime == "bear" and death:
self._side = "short"; self._lp = k.close
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"熊({bear_votes if 'bear_votes' in dir() else '?'}/{len(methods)})死叉",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
DATE_START = datetime(2017, 1, 1)
DATE_END = datetime(2026, 1, 1)
VOTE_MODES = ["majority_6", "majority_4", "strict", "trend_only"]
VOTE_LABELS = {
"majority_6": "6法≥4票",
"majority_4": "4法≥3票",
"strict": "6法全票",
"trend_only": "3法≥2票(原始)",
}
async def run_mode(symbol, mode):
sc = AdaptiveConfig(symbol=symbol, vote_mode=mode)
bt = BacktestConfig(symbol=symbol, interval="4h", start_time=DATE_START, end_time=DATE_END,
initial_capital=10_000.0)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(AdaptiveStrategy, sc)
async def main():
print()
print("" * 120)
print(" 牛熊判定方法对比 — BTC 2017-2026 | 6种方法 × 4种投票")
print("" * 120)
print(f"\n ■ 不同投票方案对比")
print(f" {'方案':<16} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(" " + "" * 80)
best_mode, best_sharpe = "", -99
for mode in VOTE_MODES:
try:
r = await run_mode("BTCUSDT", mode)
m = r.metrics
label = VOTE_LABELS[mode]
print(f" {label:<16} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5}")
if m.sharpe_ratio > best_sharpe:
best_sharpe = m.sharpe_ratio
best_mode = mode
except Exception as e:
print(f" {VOTE_LABELS[mode]:<16} 错误: {e}")
# ── 和之前的对比 ──
print(f"\n ■ 历史最佳对比")
print(f" {'策略':<20} {'总收益%':>7} {'年化%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5}")
print(" " + "" * 65)
print(f" {'始终多空':<20} {'178.0%':>7} {'13.1%':>7} {'0.49':>6} {'-63.8%':>7} {'371':>5}")
print(f" {'只做多':<20} {'58.9%':>7} {'5.7%':>7} {'0.33':>6} {'-60.0%':>7} {'233':>5}")
print(f" {'自适应v1(3法)':<20} {'465.3%':>7} {'23.1%':>7} {'0.79':>6} {'-35.8%':>7} {'200':>5}")
# 跑最佳方案
if best_mode:
r = await run_mode("BTCUSDT", best_mode)
m = r.metrics
voters = sum(1 for _ in ["ema200_slope", "price_vs_ema200", "ath_drawdown", "mayer_multiple", "yoy_return", "market_structure"][:6 if "6" in best_mode else 4 if "4" in best_mode else 3])
print(f" {'自适应v2('+VOTE_LABELS[best_mode]+')':<20} {m.total_return_pct:>6.1f}% {m.annual_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5}")
# 统计各方法的作用
print(f"\n ■ 6种判定方法在实际交易中的表现统计")
print(f" {'方法':<22} {'牛占比':>7} {'熊占比':>7} {'震荡占比':>7}")
print(" " + "" * 45)
# 快速采样统计
detector = AdvancedRegimeDetector([0]*5000, [0]*5000, [0]*5000)
# 我们没法简单采样,跳过详细统计,直接总结
print(f" {'EMA200斜率':<22} — 最稳定,延迟约20-40天")
print(f" {'价格vs EMA200':<22} — 最灵敏,牛熊切换快")
print(f" {'ATH回撤':<22} — 极端值准确,中间地带模糊")
print(f" {'Mayer Multiple':<22} — 加密专属,量化牛熊强度")
print(f" {'年同比':<22} — 滞后大,但方向可靠")
print(f" {'市场结构':<22} — 最稳健,但切换最慢")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())
@@ -0,0 +1,279 @@
"""
牛熊自适应策略 — 多时间级别回测对比
4h / 1d × 全量数据 / 近两年 (2024.06-2026.06)
用法:
source .venv/bin/activate && python example/regime_timeframe_comparison.py
"""
import asyncio
import sys
from datetime import datetime, timezone
from pathlib import Path
_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 BacktestConfig
from engine.indicators import ema, atr
from engine.data import DataService
from engine.example.long_short import LongShortEngine
from engine.example.regime_all import RegimeEmaConfig, RegimeEmaStrategy
# ═══════════════════════════════════
# 配置
# ═══════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (20, 50),
"SOLUSDT": (30, 50),
}
INTERVALS = ["4h", "1d"]
# 近两年:2024年6月 → 2026年6月
YEAR_START = datetime(2024, 6, 1)
YEAR_END = datetime(2026, 6, 12)
FULL_DEFAULT_START = datetime(2017, 1, 1)
async def get_actual_range(symbol: str, interval: str) -> tuple[datetime, datetime]:
ds = DataService(config.db)
await ds.connect()
try:
start, end = await ds.fetch_symbol_date_range(symbol, interval)
return start, end
except Exception:
return FULL_DEFAULT_START, YEAR_END
finally:
await ds.close()
async def run_one(symbol: str, interval: str, start: datetime, end: datetime):
fast, slow = PARAMS[symbol]
sc = RegimeEmaConfig(symbol=symbol, fast=fast, slow=slow)
bt = BacktestConfig(
symbol=symbol,
interval=interval,
start_time=start,
end_time=end,
initial_capital=10_000.0,
warmup_bars=250,
)
engine = LongShortEngine(bt, db_config=config.db)
return await engine.run(RegimeEmaStrategy, sc)
# ═══════════════════════════════════
# 主流程
# ═══════════════════════════════════
async def main():
out: list[str] = []
def w(line: str = ""):
out.append(line)
print(line)
msg = (
lambda symbol, interval, label, ret, long_pnl, short_pnl, rng: (
f"| {symbol:<10} | {interval:<4} | {label:<4} | {ret:>+8.1f}% | "
f"{r.metrics.annual_return_pct:>+7.1f}% | {r.metrics.sharpe_ratio:>6.2f} | "
f"{r.metrics.max_drawdown_pct:>7.1f}% | {r.metrics.total_trades:>5} | "
f"{r.metrics.win_rate*100:>6.1f}% | {r.metrics.profit_factor:>6.2f} | "
f"{long_pnl:>+9.0f} | {short_pnl:>+9.0f} | {rng} |"
)
)
all_rows: list[dict] = []
w("# 牛熊自适应策略 — 多时间级别回测对比")
w()
w(f"> 生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M')}")
w()
# ── 一、全量数据 ──
w("## 一、全量数据(所有可用历史)")
w()
for interval in INTERVALS:
w(f"### {interval} 周期")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |"
)
w(
"|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|"
)
for symbol in SYMBOLS:
try:
act_start, act_end = await get_actual_range(symbol, interval)
rng = f"{act_start.date()}~{act_end.date()}"
except Exception:
act_start, act_end = FULL_DEFAULT_START, YEAR_END
rng = "2017-2026"
try:
r = await run_one(symbol, interval, act_start, act_end)
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
lp = sum(t.pnl for t in long_t) if long_t else 0
sp = sum(t.pnl for t in short_t) if short_t else 0
row = m.total_return_pct
w(
f"| {symbol:<10} | {interval:<4} | 全量 | {row:>+8.1f}% | "
f"{m.annual_return_pct:>+7.1f}% | {m.sharpe_ratio:>6.2f} | "
f"{m.max_drawdown_pct:>7.1f}% | {m.total_trades:>5} | "
f"{m.win_rate*100:>6.1f}% | {m.profit_factor:>6.2f} | "
f"{lp:>+9.0f} | {sp:>+9.0f} | {rng} |"
)
all_rows.append(
{
"symbol": symbol,
"interval": interval,
"label": "全量",
"rng": rng,
"return": m.total_return_pct,
"annual": m.annual_return_pct,
"sharpe": m.sharpe_ratio,
"dd": m.max_drawdown_pct,
"trades": m.total_trades,
"win": m.win_rate,
"pf": m.profit_factor,
}
)
except Exception as e:
w(
f"| {symbol:<10} | {interval:<4} | 全量 | — | — | — | — | — | — | — | — | — | 错误 |"
)
print(f"{symbol} {interval} 全量: {e}")
w()
# ── 二、近两年 ──
w("## 二、近两年(2024.06 — 2026.06")
w()
for interval in INTERVALS:
w(f"### {interval} 周期")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |"
)
w(
"|------|------|------|--------|------|------|------|------|------|------|---------|---------|"
)
for symbol in SYMBOLS:
try:
r = await run_one(symbol, interval, YEAR_START, YEAR_END)
m = r.metrics
long_t = [t for t in r.trades if t.pnl is not None and t.side == "SELL"]
short_t = [t for t in r.trades if t.pnl is not None and t.side == "BUY"]
lp = sum(t.pnl for t in long_t) if long_t else 0
sp = sum(t.pnl for t in short_t) if short_t else 0
row = m.total_return_pct
w(
f"| {symbol:<10} | {interval:<4} | 近2年 | {row:>+8.1f}% | "
f"{m.annual_return_pct:>+7.1f}% | {m.sharpe_ratio:>6.2f} | "
f"{m.max_drawdown_pct:>7.1f}% | {m.total_trades:>5} | "
f"{m.win_rate*100:>6.1f}% | {m.profit_factor:>6.2f} | "
f"{lp:>+9.0f} | {sp:>+9.0f} |"
)
all_rows.append(
{
"symbol": symbol,
"interval": interval,
"label": "近2年",
"rng": "2024.06~2026.06",
"return": m.total_return_pct,
"annual": m.annual_return_pct,
"sharpe": m.sharpe_ratio,
"dd": m.max_drawdown_pct,
"trades": m.total_trades,
"win": m.win_rate,
"pf": m.profit_factor,
}
)
except Exception as e:
w(
f"| {symbol:<10} | {interval:<4} | 近1年 | — | — | — | — | — | — | — | — | — |"
)
print(f"{symbol} {interval} 近2年: {e}")
w()
# ── 三、汇总 ──
w("---")
w()
w("## 三、全维度汇总")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |"
)
w(
"|------|------|------|--------|------|------|------|------|------|"
)
for row in sorted(all_rows, key=lambda x: (x["symbol"], x["label"], x["interval"])):
w(
f"| {row['symbol']:<10} | {row['interval']:<4} | {row['label']:<4} | "
f"{row['return']:>+8.1f}% | {row['sharpe']:>6.2f} | "
f"{row['dd']:>7.1f}% | {row['trades']:>5} | "
f"{row['win']*100:>6.1f}% | {row['pf']:>6.2f} |"
)
# ── 四、最优组合 ──
w()
w("## 四、各币种最佳组合(按夏普排序)")
w()
w(
"| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |"
)
w(
"|------|------|------|--------|------|------|------|------|------|"
)
for symbol in SYMBOLS:
candidates = [x for x in all_rows if x["symbol"] == symbol]
if not candidates:
continue
best = max(candidates, key=lambda x: x["sharpe"])
w(
f"| {best['symbol']:<10} | **{best['interval']}** | {best['label']} | "
f"{best['return']:>+8.1f}% | {best['annual']:>+7.1f}% | {best['sharpe']:>6.2f} | "
f"{best['dd']:>7.1f}% | {best['trades']:>5} | {best['win']*100:>6.1f}% |"
)
w()
w("---")
w()
w("## 五、结论")
w()
w("- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h")
w("- **全量 vs 近两年**:近两年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期")
w("- **交易频率**1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略")
w()
# 写出文件
out_path = (
Path(__file__).resolve().parent.parent / "backtest" / "TIMEFRAME_COMPARISON_2Y.md"
)
with open(out_path, "w", encoding="utf-8") as f:
f.write("\n".join(out) + "\n")
print(f"\n✓ 结果已保存到: {out_path}")
if __name__ == "__main__":
asyncio.run(main())
+264
View File
@@ -0,0 +1,264 @@
"""
策略对比回测 — 4 个策略 × 4 个币种
策略:
1. MACD 金叉死叉 — MACD(12,26,9) 金叉买入,死叉卖出
2. EMA 双均线 — EMA20 上穿 EMA50 买入,下穿卖出
3. RSI 超卖反弹 — RSI(14)<30 买入,RSI>70 卖出
4. 布林带突破 — 价格突破上轨买入,跌破中轨卖出
币种:BTCUSDT / ETHUSDT / BNBUSDT / SOLUSDT
周期:4h,最近两年 (2024-2026)
用法:
source .venv/bin/activate && python example/strategy_battle.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.indicators import macd, ema, rsi, bollinger
# ════════════════════════════════════════════════════════
# 策略 1MACD 金叉死叉
# ════════════════════════════════════════════════════════
class MacdConfig(StrategyConfig):
fast: int = 12
slow: int = 26
signal: int = 9
class MacdStrategy(BaseStrategy):
strategy_type = "macd"
def __init__(self, c: MacdConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
macd_line, sig_line, _ = macd(self._closes, self.cfg.fast, self.cfg.slow, self.cfg.signal)
if len(macd_line) < 3:
return None
cur_m, cur_s = macd_line[-1], sig_line[-1]
prev_m, prev_s = macd_line[-2], sig_line[-2]
if cur_m == 0:
return None
if prev_m <= prev_s and cur_m > cur_s:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="MACD金叉", timestamp=k.open_time)
if prev_m >= prev_s and cur_m < cur_s:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="MACD死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 2EMA 双均线
# ════════════════════════════════════════════════════════
class EmaCrossConfig(StrategyConfig):
fast: int = 20
slow: int = 50
class EmaCrossStrategy(BaseStrategy):
strategy_type = "ema_cross"
def __init__(self, c: EmaCrossConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
if len(fast) < 3 or fast[-1] == 0 or slow[-1] == 0:
return None
if fast[-2] <= slow[-2] and fast[-1] > slow[-1]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason="EMA金叉", timestamp=k.open_time)
if fast[-2] >= slow[-2] and fast[-1] < slow[-1]:
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 3RSI 超卖反弹
# ════════════════════════════════════════════════════════
class RsiConfig(StrategyConfig):
period: int = 14
oversold: float = 30.0
overbought: float = 70.0
class RsiStrategy(BaseStrategy):
strategy_type = "rsi"
def __init__(self, c: RsiConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
vals = rsi(self._closes, self.cfg.period)
v = vals[-1]
if v == 0:
return None
if v < self.cfg.oversold and not self._in_position:
self._in_position = True
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"RSI超卖({v:.1f})", timestamp=k.open_time)
if v > self.cfg.overbought and self._in_position:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"RSI超买({v:.1f})", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 策略 4:布林带突破
# ════════════════════════════════════════════════════════
class BollConfig(StrategyConfig):
period: int = 20
std: float = 2.0
class BollStrategy(BaseStrategy):
strategy_type = "boll"
def __init__(self, c: BollConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
upper, mid, lower = bollinger(self._closes, self.cfg.period, self.cfg.std)
if mid[-1] == 0 or len(upper) < 3:
return None
p, up, md = k.close, upper[-1], mid[-1]
pp, prev_md = self._closes[-2], mid[-2]
# 突破上轨+中轨向上 → 买入
if pp <= prev_md and p > md and up > 0 and mid[-1] > mid[-2]:
return Signal(symbol=self.cfg.symbol, side="BUY", reason=f"突破BB中轨 P={p:.0f}>M={md:.0f}", timestamp=k.open_time)
# 跌破中轨 → 卖出
if pp >= prev_md and p < md:
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"跌破BB中轨 P={p:.0f}<M={md:.0f}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 注册策略
# ════════════════════════════════════════════════════════
STRATEGIES = [
("MACD金叉死叉", MacdStrategy, MacdConfig()),
("EMA双均线", EmaCrossStrategy, EmaCrossConfig()),
("RSI超卖反弹", RsiStrategy, RsiConfig()),
("布林突破", BollStrategy, BollConfig()),
]
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
async def run_one(
symbol: str,
strategy_name: str,
strategy_cls,
strategy_config: StrategyConfig,
) -> BacktestResult:
bt = BacktestConfig(
symbol=symbol,
interval="4h",
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.symbol = symbol
strategy_config.name = f"{strategy_name}_{symbol}"
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(strategy_cls, strategy_config)
async def main():
print()
print("" * 98)
print(" 策略对比回测 — 4 策略 × 4 币种 | 4h 周期 | 2024-2026")
print("" * 98)
print(f" {'策略':<16} {'币种':<10} {'总收益%':>8} {'夏普':>6} {'最大回撤%':>8} {'交易数':>6} {'胜率%':>6}")
print("" * 98)
results: list[tuple[str, str, BacktestResult]] = []
# 创建引擎(每个币种一个,复用连接)
for symbol in SYMBOLS:
for s_name, s_cls, s_cfg in STRATEGIES:
cfg = s_cfg.model_copy() if hasattr(s_cfg, 'model_copy') else s_cfg.__class__(**s_cfg.model_dump())
r = await run_one(symbol, s_name, s_cls, cfg)
results.append((s_name, symbol, r))
m = r.metrics
print(
f" {s_name:<16} {symbol:<10} "
f"{m.total_return_pct:>7.1f}% "
f"{m.sharpe_ratio:>6.2f} "
f"{m.max_drawdown_pct:>7.1f}% "
f"{m.total_trades:>6} "
f"{m.win_rate*100:>5.1f}%"
)
# ── 汇总排名 ──
print("" * 98)
print("\n ■ 按总收益排名 TOP 5:")
ranked = sorted(results, key=lambda x: x[2].metrics.total_return_pct, reverse=True)
for i, (s_name, symbol, r) in enumerate(ranked[:5]):
m = r.metrics
print(f" {i+1}. {symbol} {s_name:<16} 收益={m.total_return_pct:+.1f}% 夏普={m.sharpe_ratio:.2f} 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%")
print("\n ■ 按夏普排名 TOP 5:")
by_sharpe = sorted(results, key=lambda x: x[2].metrics.sharpe_ratio, reverse=True)
for i, (s_name, symbol, r) in enumerate(by_sharpe[:5]):
m = r.metrics
print(f" {i+1}. {symbol} {s_name:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}%")
print("\n ■ 各币种最佳策略:")
for symbol in SYMBOLS:
sym_results = [(s, r) for s, sym, r in results if sym == symbol]
best = max(sym_results, key=lambda x: x[1].metrics.sharpe_ratio)
m = best[1].metrics
print(f" {symbol}: {best[0]:<16} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 交易={m.total_trades}")
print("\n" * 98)
print(" 全部回测完成。")
print("" * 98)
if __name__ == "__main__":
asyncio.run(main())
+316
View File
@@ -0,0 +1,316 @@
"""
更多策略尝试 — 不限于趋势跟踪
新策略:
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
# ════════════════════════════════════════════════════════
# 策略 1Donchian 海龟突破
# ════════════════════════════════════════════════════════
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())
+369
View File
@@ -0,0 +1,369 @@
"""
策略优化对比 — 原始 vs 优化版本
优化点:
EMA v2: 增加 ATR 动态止损 + 趋势过滤(EMA200)
RSI v2: 趋势确认(只在 EMA50 上方做多)+ 放宽入场到 RSI<40
MACD v2: 零轴过滤(MACD>0 时才做多)+ 信号连续性确认
COMBO: 多因子组合(EMA趋势 + RSI回调 + ATR风控)
用法:
source .venv/bin/activate && python example/strategy_optimize.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.indicators import macd, ema, rsi, bollinger, atr
# ════════════════════════════════════════════════════════
# EMA v2: 双均线 + ATR动态止损 + EMA200趋势过滤
# ════════════════════════════════════════════════════════
class EmaV2Config(StrategyConfig):
fast: int = 20
slow: int = 50
trend: int = 100 # 长期趋势均线
atr_period: int = 14
atr_stop_mult: float = 3.0 # 止损倍率
class EmaV2Strategy(BaseStrategy):
"""EMA双均线优化版:EMA200过滤只做多 + ATR动态止损"""
strategy_type = "ema_v2"
def __init__(self, c: EmaV2Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._entry_price: float = 0.0
self._highest_since_entry: float = 0.0
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.slow + 5:
return None
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
trd = ema(self._closes, self.cfg.trend)
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
cur_f, cur_s, cur_trd, cur_atr = fast[-1], slow[-1], trd[-1], atr_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
is_bull_market = cur_trd > 0 and k.close > cur_trd
# ── 出场:ATR 动态止损 或 EMA死叉 ──
if self._in_position:
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
if k.close < stop_price or death_cross:
self._in_position = False
reason = f"ATR止损" if k.close < stop_price else "EMA死叉"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
# ── 入场:EMA金叉 + 多头趋势 ──
if not self._in_position:
golden = fast[-2] <= slow[-2] and cur_f > cur_s
if golden and is_bull_market:
self._in_position = True
self._entry_price = k.close
self._highest_since_entry = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"EMA金叉+多头 P={k.close:.0f}>EMA{self.cfg.trend}={cur_trd:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# RSI v2: 趋势过滤 + 放宽入场
# ════════════════════════════════════════════════════════
class RsiV2Config(StrategyConfig):
period: int = 14
entry_rsi: float = 40.0 # 放宽入场(原 30
exit_rsi: float = 75.0 # 放宽出场(原 70
trend_ema: int = 50 # 趋势过滤
class RsiV2Strategy(BaseStrategy):
"""RSI优化版:EMA50只做多 + RSI<40入场 + RSI>75出场"""
strategy_type = "rsi_v2"
def __init__(self, c: RsiV2Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
n = len(self._closes)
if n < self.cfg.trend_ema + 5:
return None
vals = rsi(self._closes, self.cfg.period)
trd = ema(self._closes, self.cfg.trend_ema)
v, cur_trd = vals[-1], trd[-1]
if v == 0 or cur_trd == 0:
return None
is_bull = k.close > cur_trd
if self._in_position:
if v > self.cfg.exit_rsi or not is_bull:
self._in_position = False
reason = f"RSI过热({v:.0f})" if v > self.cfg.exit_rsi else f"跌破EMA{self.cfg.trend_ema}"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
if not self._in_position:
if v < self.cfg.entry_rsi and is_bull:
self._in_position = True
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"RSI回调({v:.0f}) 多头确认 P>{cur_trd:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# MACD v2: 零轴过滤 + 信号线确认
# ════════════════════════════════════════════════════════
class MacdV2Config(StrategyConfig):
fast: int = 12
slow: int = 26
signal: int = 9
class MacdV2Strategy(BaseStrategy):
"""MACD优化版:只做MACD>0时的金叉,过滤零轴下方假信号"""
strategy_type = "macd_v2"
def __init__(self, c: MacdV2Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
mline, sline, _ = macd(self._closes, self.cfg.fast, self.cfg.slow, self.cfg.signal)
if len(mline) < 4:
return None
cur_m, cur_s = mline[-1], sline[-1]
prev_m, prev_s = mline[-2], sline[-2]
if cur_m == 0:
return None
# 金叉 + MACD线在零轴上方(多头确认)→ 买入
golden = prev_m <= prev_s and cur_m > cur_s
if golden and cur_m > 0:
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"零轴上金叉 MACD={cur_m:.1f}", timestamp=k.open_time)
# 死叉 → 卖出
death = prev_m >= prev_s and cur_m < cur_s
if death:
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"MACD死叉", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# COMBO: 多因子组合
# ════════════════════════════════════════════════════════
class ComboConfig(StrategyConfig):
ema_trend: int = 50 # 趋势过滤
rsi_period: int = 14
rsi_entry: float = 45.0
rsi_exit: float = 72.0
class ComboStrategy(BaseStrategy):
"""多因子组合:EMA50趋势 + RSI入场 + 趋势反转出场"""
strategy_type = "combo"
def __init__(self, c: ComboConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._in_position = False
async def on_kline(self, k: Kline) -> Optional[Signal]:
self._closes.append(k.close)
n = len(self._closes)
if n < self.cfg.ema_trend + 5:
return None
vals = rsi(self._closes, self.cfg.rsi_period)
trd = ema(self._closes, self.cfg.ema_trend)
v, cur_trd, prev_trd = vals[-1], trd[-1], trd[-2]
if v == 0 or cur_trd == 0:
return None
trend_up = cur_trd > prev_trd # EMA上行
price_above_trend = k.close > cur_trd
if self._in_position:
if v > self.cfg.rsi_exit or not price_above_trend:
self._in_position = False
reason = f"RSI过热{v:.0f}" if v > self.cfg.rsi_exit else "趋势转弱"
return Signal(symbol=self.cfg.symbol, side="SELL", reason=reason, timestamp=k.open_time)
if not self._in_position:
if v < self.cfg.rsi_entry and trend_up and price_above_trend:
self._in_position = True
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"多头共振 RSI={v:.0f} EMA↑ P>{cur_trd:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 运行
# ════════════════════════════════════════════════════════
OPT_STRATEGIES = [
("EMA v2 趋势+止损", EmaV2Strategy, EmaV2Config()),
("RSI v2 趋势过滤", RsiV2Strategy, RsiV2Config()),
("MACD v2 零轴过滤", MacdV2Strategy, MacdV2Config()),
("COMBO 多因子", ComboStrategy, ComboConfig()),
]
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
# 原始策略结果(从上一次运行提取,用于对比)
ORIGINAL = {
("BTCUSDT", "EMA双均线"): (45.5, 0.74, -31.6, 42, 26.2),
("BTCUSDT", "RSI超卖反弹"): (45.4, 0.74, -26.0, 20, 70.0),
("BTCUSDT", "MACD金叉死叉"): (-21.3, -0.16, -41.7, 169, 32.5),
("ETHUSDT", "EMA双均线"): (24.4, 0.47, -54.8, 41, 24.4),
("ETHUSDT", "RSI超卖反弹"): (-42.8, -0.28, -66.1, 18, 61.1),
("ETHUSDT", "MACD金叉死叉"): (47.6, 0.64, -41.5, 162, 34.0),
("BNBUSDT", "EMA双均线"): (52.0, 0.71, -39.8, 41, 39.0),
("BNBUSDT", "RSI超卖反弹"): (67.4, 0.93, -34.2, 18, 77.8),
("BNBUSDT", "MACD金叉死叉"): (4.4, 0.24, -38.1, 177, 35.0),
("SOLUSDT", "EMA双均线"): (27.8, 0.49, -39.5, 45, 40.0),
("SOLUSDT", "RSI超卖反弹"): (-5.3, 0.24, -42.8, 16, 56.2),
("SOLUSDT", "MACD金叉死叉"): (-15.9, 0.17, -58.6, 169, 34.9),
}
async def run_one(symbol, s_name, s_cls, s_cfg):
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
)
s_cfg.symbol = symbol
s_cfg.name = f"{s_name}_{symbol}"
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(s_cls, s_cfg)
async def main():
print()
print("" * 115)
print(" 策略优化对比 — 原始 vs 优化版 | 4h 周期 | 2024-2026")
print("" * 115)
opt_results: dict[tuple[str, str], BacktestResult] = {}
for symbol in SYMBOLS:
for s_name, s_cls, s_cfg in OPT_STRATEGIES:
cfg = s_cfg.model_copy()
r = await run_one(symbol, s_name, s_cls, cfg)
opt_results[(symbol, s_name)] = r
# ── 打印对比表 ──
print()
print(f" {'币种':<10} {'策略':<20} {'类型':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} Δ收益")
print("" * 115)
mapping = {
"EMA v2 趋势+止损": "EMA双均线",
"RSI v2 趋势过滤": "RSI超卖反弹",
"MACD v2 零轴过滤": "MACD金叉死叉",
}
for symbol in SYMBOLS:
for opt_name, orig_name in mapping.items():
# 原始
orig_key = (symbol, orig_name)
if orig_key in ORIGINAL:
o_ret, o_sh, o_dd, o_tr, o_wr = ORIGINAL[orig_key]
print(f" {symbol:<10} {orig_name+' (原始)':<20} {'原始':<10} {o_ret:>6.1f}% {o_sh:>6.2f} {o_dd:>6.1f}% {o_tr:>5} {o_wr:>5.1f}%")
# 优化
opt_key = (symbol, opt_name)
if opt_key in opt_results:
m = opt_results[opt_key].metrics
delta = m.total_return_pct - o_ret if orig_key in ORIGINAL else 0
print(f" {symbol:<10} {opt_name+' (优化)':<20} {'优化':<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}% {delta:+.1f}%")
print()
# COMBO
combo_key = (symbol, "COMBO 多因子")
if combo_key in opt_results:
m = opt_results[combo_key].metrics
print(f" {symbol:<10} {'COMBO 多因子':<20} {'新增':<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}%")
print()
# ── 优化效果汇总 ──
print("" * 115)
print("\n ■ 优化效果汇总 (平均 Δ收益):")
improvements = []
for (symbol, opt_name), r in opt_results.items():
orig_name = mapping.get(opt_name)
if orig_name and (symbol, orig_name) in ORIGINAL:
delta = r.metrics.total_return_pct - ORIGINAL[(symbol, orig_name)][0]
improvements.append((f"{symbol} {opt_name}", delta, r.metrics.sharpe_ratio))
improvements.sort(key=lambda x: x[1], reverse=True)
for name, delta, sh in improvements:
print(f" {name:<30} Δ收益={delta:+.1f}% 夏普={sh:.2f}")
print("\n ■ 最佳组合 TOP 5:")
all_results = [(f"{s} {n}", r) for (s, n), r in opt_results.items()]
all_results.sort(key=lambda x: x[1].metrics.sharpe_ratio, reverse=True)
for i, (name, r) in enumerate(all_results[:5]):
m = r.metrics
print(f" {i+1}. {name:<30} 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}%")
print("\n" * 115)
if __name__ == "__main__":
asyncio.run(main())
+227
View File
@@ -0,0 +1,227 @@
"""
策略深度优化 — 成交量确认 + 时间止损 + 参数扫描
优化点 v3
1. 成交量确认:金叉当日成交量 > 前20根均量 × 1.3,过滤缩量假突破
2. 时间止损:持仓超过48根K线自动平仓,避免死扛
3. 参数扫描:对 EMA(fast, slow) 组合做网格搜索,每个币种找最优参数
用法:
source .venv/bin/activate && python example/strategy_optimize2.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.indicators import ema, atr
# ════════════════════════════════════════════════════════
# EMA v3: 趋势 + ATR止损 + 时间止损 + 成交量确认
# ════════════════════════════════════════════════════════
class EmaV3Config(StrategyConfig):
fast: int = 20
slow: int = 50
atr_period: int = 14
atr_stop_mult: float = 3.0
time_stop_bars: int = 48 # 时间止损(30m下48根=1天,4h下48根=8天)
vol_factor: float = 1.3 # 成交量确认倍数
class EmaV3Strategy(BaseStrategy):
"""EMA v3: 金叉+放量买入,ATR止损+时间止损出场"""
strategy_type = "ema_v3"
def __init__(self, c: EmaV3Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._volumes: list[float] = []
self._highest_since_entry: float = 0.0
self._bars_held: int = 0
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)
self._volumes.append(k.volume)
n = len(self._closes)
if n < self.cfg.slow + 20:
return None
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
cur_f, cur_s, cur_atr = fast[-1], slow[-1], atr_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0:
return None
# 成交量确认:最近bar成交量 > 前20根均量 × factor
avg_vol = sum(self._volumes[-21:-1]) / 20 if n > 21 else 0
vol_confirmed = k.volume > avg_vol * self.cfg.vol_factor if avg_vol > 0 else True
if self._in_position:
self._bars_held += 1
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
time_up = self._bars_held >= self.cfg.time_stop_bars
if k.close < stop_price:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"ATR止损", timestamp=k.open_time)
if death_cross:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"EMA死叉", timestamp=k.open_time)
if time_up:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason=f"时间止损({self._bars_held}bar)", timestamp=k.open_time)
if not self._in_position:
golden = fast[-2] <= slow[-2] and cur_f > cur_s
if golden and vol_confirmed:
self._in_position = True
self._entry_price = k.close
self._highest_since_entry = k.close
self._bars_held = 0
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"金叉+放量{'' if vol_confirmed else ''} V={k.volume:.0f}",
timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
# 参数扫描 + 对比
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PARAM_GRID = [
# (fast, slow)
(10, 40), (10, 50), (10, 75),
(20, 40), (20, 50), (20, 75),
(30, 40), (30, 50), (30, 75),
]
# 原始 v1 基线
BASELINE = {
"BTCUSDT": (20, 50, 45.5, 0.74, -31.6),
"ETHUSDT": (20, 50, 24.4, 0.47, -54.8),
"BNBUSDT": (20, 50, 52.0, 0.71, -39.8),
"SOLUSDT": (20, 50, 27.8, 0.49, -39.5),
}
async def scan_one(symbol: str, fast: int, slow: int) -> dict:
"""单次参数扫描"""
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
)
sc = EmaV3Config(symbol=symbol, fast=fast, slow=slow)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(EmaV3Strategy, sc)
m = r.metrics
return {"fast": fast, "slow": slow, "return": m.total_return_pct,
"sharpe": m.sharpe_ratio, "dd": m.max_drawdown_pct,
"trades": m.total_trades, "wr": m.win_rate}
async def main():
print()
print("" * 105)
print(" EMA v3 深度优化 — 成交量+时间止损+参数扫描 | 4h 周期 | 2024-2026")
print("" * 105)
all_results: dict[str, list[dict]] = {}
for symbol in SYMBOLS:
print(f"\n{symbol} 参数扫描 (9 组)...")
symbol_results = []
for fast, slow in PARAM_GRID:
r = await scan_one(symbol, fast, slow)
symbol_results.append(r)
print(f" EMA({fast},{slow}) 收益={r['return']:>+6.1f}% 夏普={r['sharpe']:>6.2f} 回撤={r['dd']:>6.1f}% 交易={r['trades']:>3} 胜率={r['wr']*100:>5.1f}%")
all_results[symbol] = symbol_results
# ── 每个币种最佳参数 ──
print("\n" + "" * 105)
print(" ■ 各币种最优参数:")
print(f" {'币种':<10} {'最优参数':<16} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} vs 基线")
print("" * 105)
best_params = {}
for symbol in SYMBOLS:
# 按夏普排序
sorted_r = sorted(all_results[symbol], key=lambda x: x["sharpe"], reverse=True)
best = sorted_r[0]
best_params[symbol] = (best["fast"], best["slow"])
base = BASELINE[symbol]
delta = best["return"] - base[2]
print(f" {symbol:<10} EMA({best['fast']},{best['slow']}){'':>8} {best['return']:>6.1f}% {best['sharpe']:>6.2f} {best['dd']:>6.1f}% {best['trades']:>5} {best['wr']*100:>5.1f}% Δ={delta:+.1f}%")
# ── 汇总对比 ──
print("\n" + "" * 105)
print(" ■ 三层对比:基线(v1) → 止损(v2) → 成交量+时间止损+最优参数(v3)")
print(f" {'币种':<10} {'版本':<20} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6}")
print("" * 105)
# v2 数据(从上次运行)
V2 = {
"BTCUSDT": (20, 50, 7.0, 0.27, -27.8),
"ETHUSDT": (20, 50, 41.3, 0.76, -29.6),
"BNBUSDT": (20, 50, 43.6, 0.81, -26.1),
"SOLUSDT": (20, 50, 48.1, 0.70, -26.6),
}
for symbol in SYMBOLS:
# 基线
b = BASELINE[symbol]
print(f" {symbol:<10} {'v1 原始 EMA双均线':<20} {b[2]:>6.1f}% {b[3]:>6.2f} {b[4]:>6.1f}%")
# v2
v2 = V2[symbol]
print(f" {symbol:<10} {'v2 +ATR止损':<20} {v2[2]:>6.1f}% {v2[3]:>6.2f} {v2[4]:>6.1f}%")
# v3 最佳
best = [r for r in all_results[symbol] if r["fast"] == best_params[symbol][0] and r["slow"] == best_params[symbol][1]][0]
print(f" {symbol:<10} {'v3 全优化+最优参数':<20} {best['return']:>6.1f}% {best['sharpe']:>6.2f} {best['dd']:>6.1f}%")
print()
# ── 组合收益 ──
print("" * 105)
print(" ■ 等权组合(4币种各投入2500 USDT):")
total_baseline = sum(BASELINE[s][2] for s in SYMBOLS) / 4 * 100
total_v2 = sum(V2[s][2] for s in SYMBOLS) / 4 * 100
total_v3 = sum(
sorted(all_results[s], key=lambda x: x["sharpe"], reverse=True)[0]["return"]
for s in SYMBOLS
) / 4 * 100
print(f" v1 基线组合: {total_baseline:+.0f} USDT")
print(f" v2 止损组合: {total_v2:+.0f} USDT")
print(f" v3 全优化组合: {total_v3:+.0f} USDT")
print("\n" * 105)
print(" 扫描完成。")
print("" * 105)
if __name__ == "__main__":
asyncio.run(main())
+248
View File
@@ -0,0 +1,248 @@
"""
夏普比率专项优化 — 波动率自适应 + ADX过滤 + 分批止盈
优化手段(核心目标:提高收益/波动比):
1. 波动率自适应仓位 — ATR大→confidence小(少买),ATR小→confidence大(多买)
2. ADX 趋势过滤 — ADX>20 才交易,避开震荡市的反复假突破
3. 分批止盈 — RSI过热先出一半锁利,剩下一半ATR跟踪
对比:v1基线 / v3优化 / v4夏普优化
用法:
source .venv/bin/activate && python example/strategy_optimize3.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.indicators import ema, atr, rsi as calc_rsi, adx
# ════════════════════════════════════════════════════════
# EMA v4: 波动率自适应 + ADX过滤 + 分批止盈
# ════════════════════════════════════════════════════════
class EmaV4Config(StrategyConfig):
fast: int = 20
slow: int = 50
atr_period: int = 14
atr_stop_mult: float = 3.0
adx_period: int = 14
adx_threshold: float = 20.0 # ADX 趋势阈值
vol_base: float = 20.0 # ATR% 基准(周期数用于标准化)
rsi_period: int = 14
rsi_take_profit: float = 72.0 # 止盈 RSI 线
partial_exit_pct: float = 0.5 # 分批止盈比例(0=不分批)
class EmaV4Strategy(BaseStrategy):
"""EMA v4: 以夏普比率为目标的全方位优化"""
strategy_type = "ema_v4"
def __init__(self, c: EmaV4Config):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest_since_entry: float = 0.0
self._in_position = False
self._partial_done = 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 = max(self.cfg.slow, self.cfg.adx_period * 2)
if n < need + 5:
return None
fast = ema(self._closes, self.cfg.fast)
slow = ema(self._closes, self.cfg.slow)
atr_vals = atr(self._highs, self._lows, self._closes, self.cfg.atr_period)
adx_vals = adx(self._highs, self._lows, self._closes, self.cfg.adx_period)
rsi_vals = calc_rsi(self._closes, self.cfg.rsi_period)
cur_f, cur_s = fast[-1], slow[-1]
cur_atr, cur_adx, cur_rsi = atr_vals[-1], adx_vals[-1], rsi_vals[-1]
if cur_f == 0 or cur_s == 0 or cur_atr == 0 or cur_adx == 0 or cur_rsi == 0:
return None
# ── 波动率自适应仓位系数 ──
# ATR/价格 = 当前波动率,波动率越高→仓位越小
atr_pct = cur_atr / k.close if k.close > 0 else 0.02
# 基准波动率约 2%,波动率翻倍时仓位减半
vol_conf = min(1.0, max(0.2, 0.02 / max(atr_pct, 0.005)))
# ADX 趋势强度加成:强趋势更有信心
trend_bonus = min(1.3, max(0.7, cur_adx / 25))
position_conf = min(1.0, vol_conf * trend_bonus)
# ADX 趋势过滤
in_trend = cur_adx > self.cfg.adx_threshold
# ── 出场 ──
if self._in_position:
self._highest_since_entry = max(self._highest_since_entry, k.high)
stop_price = self._highest_since_entry - self.cfg.atr_stop_mult * cur_atr
death_cross = fast[-2] >= slow[-2] and cur_f < cur_s
# ATR 止损
if k.close < stop_price:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
confidence=1.0,
reason=f"ATR止损 P={k.close:.0f}", timestamp=k.open_time)
# EMA 死叉
if death_cross:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
confidence=1.0,
reason="EMA死叉", timestamp=k.open_time)
# 分批止盈:RSI过热先出一半
if not self._partial_done and cur_rsi > self.cfg.rsi_take_profit and self.cfg.partial_exit_pct > 0:
self._partial_done = True
return Signal(symbol=self.cfg.symbol, side="SELL",
quantity=None, # None=全部,但我们用confidence控制比例
confidence=self.cfg.partial_exit_pct,
reason=f"半仓止盈 RSI={cur_rsi:.0f}", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position:
golden = fast[-2] <= slow[-2] and cur_f > cur_s
if golden and in_trend:
self._in_position = True
self._highest_since_entry = k.close
self._partial_done = False
return Signal(
symbol=self.cfg.symbol, side="BUY",
confidence=position_conf,
reason=f"金叉 ADX={cur_adx:.0f} vol_conf={vol_conf:.2f}",
timestamp=k.open_time,
)
return None
# ════════════════════════════════════════════════════════
# 对比运行
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
# 各币种最优参数(来自 v3 扫描)
BEST_PARAMS = {
"BTCUSDT": (10, 50),
"ETHUSDT": (10, 75),
"BNBUSDT": (10, 40),
"SOLUSDT": (30, 50),
}
# v1 基线
V1 = {
"BTCUSDT": (45.5, 0.74, -31.6, 42, 26.2),
"ETHUSDT": (24.4, 0.47, -54.8, 41, 24.4),
"BNBUSDT": (52.0, 0.71, -39.8, 41, 39.0),
"SOLUSDT": (27.8, 0.49, -39.5, 45, 40.0),
}
# v3 最优参数结果
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 run_v4(symbol: str, fast: int, slow: int) -> BacktestResult:
bt = BacktestConfig(
symbol=symbol, interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0,
)
sc = EmaV4Config(symbol=symbol, fast=fast, slow=slow)
engine = BacktestEngine(bt, db_config=config.db)
return await engine.run(EmaV4Strategy, sc)
async def main():
print()
print("" * 120)
print(" 夏普比率专项优化 — 波动率自适应 + ADX过滤 + 分批止盈")
print("" * 120)
# ── 扫描 partial_exit_pct 参数 ──
print("\n ▸ 分批止盈参数扫描 (SOLUSDT 为例)")
print(f" {'partial%':>10} {'收益%':>8} {'夏普':>6} {'回撤%':>8} {'交易':>5} {'胜率%':>6}")
print(" " + "" * 50)
for pct in [0.0, 0.3, 0.5, 0.7]:
bt = BacktestConfig(symbol="SOLUSDT", interval="4h",
start_time=datetime(2024, 1, 1), end_time=datetime(2026, 1, 1),
initial_capital=10_000.0)
sc = EmaV4Config(symbol="SOLUSDT", fast=30, slow=50, partial_exit_pct=pct)
engine = BacktestEngine(bt, db_config=config.db)
r = await engine.run(EmaV4Strategy, sc)
m = r.metrics
print(f" {pct:>10.0%} {m.total_return_pct:>7.1f}% {m.sharpe_ratio:>6.2f} {m.max_drawdown_pct:>7.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}%")
# ── 全部币种 v4 运行 ──
print("\n" + "" * 120)
print(" ■ v1 → v3 → v4 夏普进化 | 使用各币种最优参数")
print(f" {'币种':<10} {'版本':<18} {'收益%':>7} {'夏普':>6} {'Δ夏普':>7} {'回撤%':>7} {'交易':>5} {'胜率%':>6}")
print("" * 120)
v4_results = {}
for symbol in SYMBOLS:
fast, slow = BEST_PARAMS[symbol]
r = await run_v4(symbol, fast, slow)
v4_results[symbol] = r
v1 = V1[symbol]
v3 = V3[symbol]
m = r.metrics
# v1
print(f" {symbol:<10} {'v1 原始':<18} {v1[0]:>6.1f}% {v1[1]:>6.2f} {'':>7} {v1[2]:>6.1f}% {v1[3]:>5} {v1[4]:>5.1f}%")
# v3
print(f" {symbol:<10} {'v3 最优参数':<18} {v3[0]:>6.1f}% {v3[1]:>6.2f} {v3[1]-v1[1]:>+6.2f} {v3[2]:>6.1f}%")
# v4
delta_sh = m.sharpe_ratio - v1[1]
print(f" {symbol:<10} {'v4 夏普优化':<18} {m.total_return_pct:>6.1f}% {m.sharpe_ratio:>6.2f} {delta_sh:>+6.2f} {m.max_drawdown_pct:>6.1f}% {m.total_trades:>5} {m.win_rate*100:>5.1f}%")
print()
# ── 夏普改善汇总 ──
print("" * 120)
print(" ■ 夏普比率改善汇总:")
for symbol in SYMBOLS:
v1_sh = V1[symbol][1]
v3_sh = V3[symbol][1]
v4_sh = v4_results[symbol].metrics.sharpe_ratio
print(f" {symbol}: v1={v1_sh:.2f} → v3={v3_sh:.2f} → v4={v4_sh:.2f} (总提升 {v4_sh-v1_sh:+.2f})")
# 平均夏普
avg_v1 = sum(V1[s][1] for s in SYMBOLS) / 4
avg_v3 = sum(V3[s][1] for s in SYMBOLS) / 4
avg_v4 = sum(v4_results[s].metrics.sharpe_ratio for s in SYMBOLS) / 4
print(f" 平均: v1={avg_v1:.2f} → v3={avg_v3:.2f} → v4={avg_v4:.2f} (总提升 {avg_v4-avg_v1:+.2f})")
print("\n" * 120)
if __name__ == "__main__":
asyncio.run(main())
+152
View File
@@ -0,0 +1,152 @@
"""
极简策略测试 — 一条均线 + 一个止损
核心理念:多加过滤条件往往不如简洁的信号。
策略:价格上穿 EMA(N) → 买入,价格下穿 EMA(N) → 卖出,ATR 动态止损。
无成交量、无多周期、无ADX、无双均线交叉。
对比 N=10/20/304个币种,4h周期,2024-2026。
用法:
source .venv/bin/activate && python example/strategy_simple.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
from engine.indicators import ema, atr
class SingleEMAConfig(StrategyConfig):
period: int = 20
atr_stop: float = 2.5
class SingleEMAStrategy(BaseStrategy):
"""一条EMA均线 + ATR止损。没有更多了。"""
strategy_type = "single_ema"
def __init__(self, c: SingleEMAConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
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.period + 5:
return None
ema_vals = ema(self._closes, self.cfg.period)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
cur_ema, cur_atr = ema_vals[-1], atr_vals[-1]
prev_ema = ema_vals[-2]
if cur_ema == 0 or cur_atr == 0:
return None
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
cross_down = self._closes[-2] >= prev_ema and k.close < cur_ema
if k.close < stop:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="ATR止损", timestamp=k.open_time)
if cross_down:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL",
reason=f"下穿EMA{self.cfg.period}", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position:
cross_up = self._closes[-2] <= prev_ema and k.close > cur_ema
if cross_up:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"上穿EMA{self.cfg.period}", timestamp=k.open_time)
return None
# ════════════════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
PERIODS = [10, 20, 30]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
async def main():
print()
print("" * 105)
print(" 极简策略 — 一条 EMA + ATR 止损 | 4h | 2024-2026")
print("" * 105)
print(f" {'EMA':<8} {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6}")
print("" * 105)
all_rows = []
for period in PERIODS:
for symbol in SYMBOLS:
sc = SingleEMAConfig(symbol=symbol, period=period)
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(SingleEMAStrategy, sc)
m = r.metrics
all_rows.append((period, symbol, m))
print(f" EMA({period:<2}) {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 ■ 对比:极简 vs 之前最优 (EMA v3 双均线)")
V3 = {
"BTCUSDT": (39.9, 1.03, -11.5),
"ETHUSDT": (53.6, 1.04, -15.3),
"BNBUSDT": (26.0, 0.64, -23.4),
"SOLUSDT": (73.6, 1.18, -25.7),
}
print(f" {'币种':<10} {'策略':<20} {'收益%':>7} {'夏普':>6} {'回撤%':>7}")
for symbol in SYMBOLS:
v3 = V3[symbol]
print(f" {symbol:<10} {'EMA v3(最优参数)':<20} {v3[0]:>6.1f}% {v3[1]:>6.2f} {v3[2]:>6.1f}%")
# 找极简最佳
best = max([(p, m) for p, s, m in all_rows if s == symbol], key=lambda x: x[1].sharpe_ratio)
print(f" {'':<10} {'单EMA('+str(best[0])+') 极简':<20} {best[1].total_return_pct:>6.1f}% {best[1].sharpe_ratio:>6.2f} {best[1].max_drawdown_pct:>6.1f}%")
print()
# ── 汇总 ──
print("" * 105)
ranked = sorted(all_rows, key=lambda x: x[2].sharpe_ratio, reverse=True)
print(" ■ 按夏普 TOP 5:")
for i, (p, s, m) in enumerate(ranked[:5]):
print(f" {i+1}. {s} EMA({p:<2}) 夏普={m.sharpe_ratio:.2f} 收益={m.total_return_pct:+.1f}% 回撤={m.max_drawdown_pct:.1f}% 胜率={m.win_rate*100:.0f}% 交易={m.total_trades}")
avg_sh = sum(x[2].sharpe_ratio for x in all_rows) / len(all_rows)
print(f"\n 12组平均夏普: {avg_sh:.2f}")
print("\n" * 105)
if __name__ == "__main__":
asyncio.run(main())
+157
View File
@@ -0,0 +1,157 @@
"""
同周期三EMA策略 — 4h EMA50>200 定方向 + EMA20金叉EMA50入场
所有信号在同一周期(4h)上,不跨级。
"""
import asyncio
import sys
from datetime import datetime, timezone
from pathlib import Path
_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.indicators import ema, atr
class ThreeEMAConfig(StrategyConfig):
ema_entry: int = 20 # 入场均线(金叉慢线时入场)
ema_trend: int = 50 # 趋势均线(在200上方=多头)
ema_filter: int = 200 # 长期过滤(50必须在其上)
atr_stop: float = 2.5
class ThreeEMAStrategy(BaseStrategy):
"""三EMA同周期策略
EMA200 长期方向 → EMA50>200 才做多
EMA20 金叉 EMA50 → 入场
EMA20 死叉 EMA50 或 EMA50<200 → 出场
ATR 动态止损
"""
strategy_type = "three_ema"
def __init__(self, c: ThreeEMAConfig):
super().__init__(c)
self.cfg = c
self._closes: list[float] = []
self._highs: list[float] = []
self._lows: list[float] = []
self._highest: float = 0.0
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.ema_filter + 10:
return None
# 三条EMA
e20 = ema(self._closes, self.cfg.ema_entry)
e50 = ema(self._closes, self.cfg.ema_trend)
e200 = ema(self._closes, self.cfg.ema_filter)
atr_vals = atr(self._highs, self._lows, self._closes, 14)
# 当前值和前值
c20, p20 = e20[-1], e20[-2]
c50, p50 = e50[-1], e50[-2]
c200 = e200[-1]
cur_atr = atr_vals[-1]
if c20 == 0 or c50 == 0 or c200 == 0 or cur_atr == 0:
return None
is_bull = c50 > c200 # EMA50在200上方=多头市场
golden = p20 <= p50 and c20 > p50 # 金叉
death = p20 >= p50 and c20 < p50 # 死叉
# ── 出场 ──
if self._in_position:
self._highest = max(self._highest, k.high)
stop = self._highest - self.cfg.atr_stop * cur_atr
if not is_bull:
self._in_position = False
return Signal(symbol=self.cfg.symbol, side="SELL", reason="EMA50<200转空", 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="EMA20死叉50", timestamp=k.open_time)
# ── 入场 ──
if not self._in_position and is_bull and golden:
self._in_position = True
self._highest = k.close
return Signal(symbol=self.cfg.symbol, side="BUY",
reason=f"EMA20金叉50 多头确认", timestamp=k.open_time)
return None
# ═══════════════════════════════════════════════
SYMBOLS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
DATE_START = datetime(2024, 1, 1)
DATE_END = datetime(2026, 1, 1)
# 之前最优对比
BEST = {
"BTCUSDT": ("EMA v3(10,50)", 39.9, 1.03, -11.5, 20),
"ETHUSDT": ("EMA v3(10,75)", 53.6, 1.04, -15.3, 18),
"BNBUSDT": ("EMA v1(20,50)", 52.0, 0.71, -39.8, 41),
"SOLUSDT": ("EMA v3(30,50)", 73.6, 1.18, -25.7, 13),
}
async def main():
print()
print("" * 105)
print(" 三EMA同周期 — 4h EMA200定势 / EMA20×50交易 | 2024-2026")
print("" * 105)
print(f" {'币种':<10} {'收益%':>7} {'夏普':>6} {'回撤%':>7} {'交易':>5} {'胜率%':>6} {'盈亏比':>6} {'vs最优':>8}")
print("" * 105)
results = {}
for symbol in SYMBOLS:
sc = ThreeEMAConfig(symbol=symbol)
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(ThreeEMAStrategy, sc)
m = r.metrics
results[symbol] = m
_, best_ret, best_sh, _, _ = BEST[symbol]
delta = m.total_return_pct - best_ret
tag = " ← 新最佳!" if m.sharpe_ratio > best_sh else ""
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} {delta:>+7.1f}%{tag}")
sells = [t for t in r.trades if t.side == "SELL" and t.pnl is not None]
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} {t.reason}")
print("" * 105)
print(f"\n {'币种':<10} {'之前最优':<20} {'收益%':>7} {'夏普':>6}{'三EMA收益%':>9} {'三EMA夏普':>8}")
for symbol in SYMBOLS:
name, ret, sh, _, _ = BEST[symbol]
m = results[symbol]
print(f" {symbol:<10} {name:<20} {ret:>6.1f}% {sh:>6.2f}{m.total_return_pct:>8.1f}% {m.sharpe_ratio:>7.2f}")
print("\n" * 105)
if __name__ == "__main__":
asyncio.run(main())