feat(engine): 添加事件驱动回测引擎

- backtest/engine.py: 事件驱动回测引擎核心,支持 K 线推进/订单撮合/权益曲线
- backtest/models.py: 回测数据模型(订单/成交/持仓/账户快照)
- backtest/README.md: 回测模块使用说明
- backtest/STRATEGY.md: 策略开发指南与最佳实践
- backtest/TIMEFRAME_COMPARISON*.md: 多周期回测对比分析报告
This commit is contained in:
Rekey
2026-06-12 10:26:53 +08:00
parent 212f6fedad
commit 4da520c14b
7 changed files with 1175 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
# engine/backtest — 回测引擎
事件驱动的历史回测框架,基于 `DataService` 从 TimescaleDB 读取历史 K 线,
按时间顺序逐根推送给策略,模拟订单成交、跟踪资金曲线、计算绩效指标。
## 快速开始
```python
import asyncio
from datetime import datetime
from engine.backtest import BacktestEngine, BacktestConfig
from engine.common.config import config
async def main():
bt_config = BacktestConfig(
symbol="BTCUSDT",
interval="1h",
start_time=datetime(2025, 1, 1),
end_time=datetime(2025, 6, 1),
initial_capital=10_000.0,
)
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(MyStrategy, my_strategy_config)
print(result.summary())
asyncio.run(main())
```
## 核心概念
### 回测流程
```
加载历史 K 线 → 预热阶段 → 主循环 → 计算指标 → 输出结果
逐根 K 线推送:
1. 执行上根 Bar 产生的买单(在开盘价执行)
2. 推送 K 线给策略 → 产生信号
3. 卖出信号立即执行,买入信号延迟到下一根 Bar
4. 记录资金曲线
```
### 避免未来函数
- **买入信号**:在当前 K 线收盘生成 → **下一根 K 线开盘价**执行
- **卖出信号**:在当前 K 线收盘生成 → **当前 K 线收盘价**执行
这样可以避免使用已知收盘价来获利的偏差。
### 交易成本
引擎模拟以下交易成本:
| 成本项 | 默认值 | 说明 |
|--------|--------|------|
| 手续费 | 0.1% | 按成交额收取 |
| 滑点 | 0.05% | 买卖双向滑点 |
### 绩效指标
回测完成后自动计算以下指标:
| 指标 | 说明 |
|------|------|
| 总收益率 | (最终权益 - 初始资金) / 初始资金 × 100% |
| 年化收益率 | 以复利方式年化 |
| 夏普比率 | (日均收益 / 日收益标准差) × √365 |
| 最大回撤 | 权益从峰值下跌的最大百分比 |
| 回撤持续天数 | 从峰值到恢复(或结束)的最长天数 |
| 胜率 | 盈利交易 / 总交易 |
| 盈亏比 | 总盈利 / 总亏损绝对值 |
| 卡尔玛比率 | 年化收益 / 最大回撤绝对值 |
## API 参考
### BacktestConfig
```python
@dataclass
class BacktestConfig:
symbol: str # 交易对
exchange: str = "binance" # 交易所
interval: str = "1h" # K 线周期
start_time: datetime | None = None # 起始时间
end_time: datetime | None = None # 结束时间
commission_pct: float = 0.001 # 手续费率
slippage_pct: float = 0.0005 # 滑点率
min_order_qty: float = 0.001 # 最小下单量
initial_capital: float = 10_000.0 # 初始资金
warmup_bars: int = 100 # 预热条数
```
### BacktestEngine
```python
class BacktestEngine:
def __init__(self, config: BacktestConfig, db_config=None)
async def run(
self,
strategy_cls: Type[BaseStrategy],
strategy_config: StrategyConfig,
) -> BacktestResult
```
### BacktestResult
```python
@dataclass
class BacktestResult:
config: BacktestConfig # 回测配置
strategy_config: dict # 策略配置
metrics: BacktestMetrics # 绩效指标
trades: list[BacktestTrade] # 交易记录
equity_curve: list[dict] # 资金曲线
def summary(self) -> str # 人类可读摘要
```
### 编写策略
策略必须继承 `BaseStrategy`,实现 `on_kline()` 方法:
```python
from engine.common.base import BaseStrategy, Signal, StrategyConfig
from engine.common.models import Kline
class MyConfig(StrategyConfig):
param1: int = 10
class MyStrategy(BaseStrategy):
strategy_type = "my_strategy"
def __init__(self, config: MyConfig):
super().__init__(config)
self._closes = []
async def on_kline(self, kline: Kline) -> Signal | None:
self._closes.append(kline.close)
# 策略逻辑 ...
if 买入条件:
return Signal(
symbol=self.config.symbol,
side="BUY",
signal_type="MARKET",
reason="...",
timestamp=kline.open_time,
)
return None
```
### 技术指标库
`engine/indicators/` 提供常用的技术指标计算函数,纯 Python 实现,无外部依赖:
```python
from engine.indicators import sma, ema, macd, rsi, bollinger, atr, obv, vwap
closes = [100.0, 101.0, 102.0, ...]
ma = sma(closes, period=20) # 简单移动平均
ema_vals = ema(closes, period=12) # 指数移动平均
rsi_vals = rsi(closes, period=14) # RSI [0, 100]
upper, mid, lower = bollinger(closes, period=20, std=2) # 布林带
macd_line, signal, hist = macd(closes, fast=12, slow=26, signal=9) # MACD
atr_vals = atr(highs, lows, closes, period=14) # ATR
```
| 模块 | 指标 | 函数 |
|------|------|------|
| `trend` | 趋势 | `sma`, `ema`, `macd`, `macd_signal`, `macd_histogram` |
| `momentum` | 动量 | `rsi`, `stoch`, `stoch_k`, `stoch_d` |
| `volatility` | 波动率 | `bollinger`, `bollinger_upper`, `bollinger_mid`, `bollinger_lower`, `atr` |
| `volume` | 成交量 | `obv`, `vwap` |
所有函数返回与输入等长的 `list[float]`,不足周期的位置填充为 `0.0`
## 运行示例
```bash
cd engine
source .venv/bin/activate
python example/backtest_demo.py
```
+178
View File
@@ -0,0 +1,178 @@
# 牛熊自适应趋势跟踪策略
## 概述
通过识别市场所处的牛熊状态,自适应地选择做多或做空方向,在震荡市中空仓等待。
核心思想:**牛市不逆势做空,熊市不逆势做多。**
---
## 市场状态判定(3 法投票)
每根 4h K 线收盘后,用以下三种方法独立判定当前市场状态:
### 方法 1EMA200 斜率
```
计算:EMA200 近 20 根 K 线的变化率
判定:斜率 > +0.2% → 牛
斜率 < -0.2% → 熊
其他 → 震荡
```
EMA200 向上倾斜说明长期趋势向上,向下倾斜说明长期趋势向下。
### 方法 2:价格 vs EMA200
```
判定:当前收盘价 > EMA200 → 牛
当前收盘价 < EMA200 → 熊
```
最直接的趋势判定——价格在年线上方就是多头市场。
### 方法 3ATH 回撤
```
追踪历史最高价 (ATH)
计算:(当前价 - ATH) / ATH
判定:回撤 > -15%(距高点不到15%)→ 牛
回撤 < -35%(距高点超过35%)→ 熊
回撤在 15%-35% 之间 → 震荡
```
加密市场经典规律:从高点回撤超过 35% 通常意味着熊市确认。
### 综合投票
```
三种方法独立投票,2/3 多数决:
牛票 >= 2 → 牛市 → 只做多
熊票 >= 2 → 熊市 → 只做空
其他 → 震荡 → 空仓等待
```
---
## 交易信号
使用 **EMA(10, 50) 双均线交叉** 作为入场信号:
| 方向 | 入场条件 | 出场条件 |
|------|---------|---------|
| 做多 | 牛市 + EMA10 金叉 EMA50 | EMA10 死叉 EMA50,或 ATR 止损,或状态转熊 |
| 做空 | 熊市 + EMA10 死叉 EMA50 | EMA10 金叉 EMA50,或 ATR 止损,或状态转牛 |
### ATR 动态止损
```
做多止损:入场后最高价 - 2.5 × ATR(14)
做空止损:入场后最低价 + 2.5 × ATR(14)
```
止损触发后平仓但不反手,空仓等待下一个交叉信号 + 状态确认。
---
## 参数配置
| 参数 | 值 | 说明 |
|------|----|------|
| 周期 | 4h | 交易时间级别 |
| EMA 快线 | 10 | 短期趋势 |
| EMA 慢线 | 50 | 中期趋势 |
| EMA 趋势 | 200 | 长期趋势基准 |
| ATR 周期 | 14 | 波动率计算 |
| ATR 止损倍率 | 2.5 | 止损宽度 |
| 手续费 | 0.1% | 单边 |
| 滑点 | 0.05% | 单边 |
不同币种的 EMA 快慢线参数已做优化:
| 币种 | 快线 | 慢线 |
|------|------|------|
| BTC | 10 | 50 |
| ETH | 10 | 75 |
| BNB | 20 | 50 |
| SOL | 30 | 50 |
---
## 回测结果(2017-2026 全周期,4h
| 币种 | 数据范围 | 总收益 | 年化 | 夏普 | 最大回撤 | 交易数 | 多头P&L | 空头P&L |
|------|---------|--------|------|------|---------|--------|---------|---------|
| BTC | 2017.08-2026.06 | +494% | 22.5% | 0.80 | -34.1% | 208 | +41,513 | +14,936 |
| ETH | 2017.08-2026.06 | +4,240% | 53.7% | 1.24 | -37.3% | 205 | +262,427 | +194,635 |
| BNB | 2017.11-2026.06 | +1,375% | 37.0% | 0.92 | -44.6% | 190 | +88,684 | +63,905 |
| SOL | 2020.08-2026.06 | +65% | 9.1% | 0.41 | -56.6% | 134 | +13,743 | -4,385 |
---
## 多时间级别对比(1h / 4h / 1d)
同一策略在不同 K 线周期上的表现:
| 币种 | 周期 | 总收益 | 年化 | 夏普 | 最大回撤 | 交易数 | 胜率 |
|------|------|--------|------|------|---------|--------|------|
| BTC | 1h | -78% | -15.7% | -0.35 | -94.8% | 744 | 24.2% |
| BTC | 4h | **+494%** | **22.5%** | **0.80** | -34.1% | 208 | 36.5% |
| BTC | 1d | **+660%** | **26.8%** | **0.99** | **-30.5%** | **28** | 42.9% |
| ETH | 1h | -65% | -10.9% | -0.07 | -88.8% | 755 | 28.4% |
| ETH | 4h | **+4,240%** | **53.7%** | **1.24** | -37.3% | 205 | 40.5% |
| ETH | 1d | +692% | 27.4% | 0.87 | -59.5% | 25 | 44.0% |
| BNB | 4h | **+1,375%** | **37.0%** | **0.92** | -44.6% | 190 | 37.4% |
| BNB | 1d | +914% | 32.1% | 0.80 | -51.5% | 26 | 38.5% |
| SOL | 4h | +65% | 9.1% | 0.41 | -56.6% | 134 | 35.1% |
| SOL | 1d | **+454%** | **36.0%** | **0.92** | -43.2% | **20** | 35.0% |
### 各币种最佳周期
| 币种 | 最佳周期 | 收益 | 夏普 | 原因 |
|------|---------|------|------|------|
| BTC | **1d** | +660% | 0.99 | 大盘稳定,日线信号最干净 |
| ETH | **4h** | +4,240% | 1.24 | 趋势转换快,4h 反应速度最优 |
| BNB | **4h** | +1,375% | 0.92 | 弹性大,需要 4h 捕捉波动 |
| SOL | **1d** | +454% | 0.92 | 波动剧烈,日线过滤噪音最有效 |
### 周期选择规律
```
高波动币种 ──→ 短周期(4h)──→ 捕捉快节奏趋势(ETH、BNB)
低波动币种 ──→ 长周期(1d)──→ 过滤噪音假信号(BTC、SOL)
1h 对所有币种均不可用 ──→ 744-755 笔交易,摩擦成本吃掉一切
```
### BTC 逐年表现
| 年份 | 市场性质 | 收益率 | 夏普比率 |
|------|---------|--------|---------|
| 2017 | 牛市 | +38.2% | 2.59 |
| 2018 | 熊市 | -13.8% | -0.56 |
| 2019 | 反弹 | +72.6% | 1.93 |
| 2020 | 牛初 | +72.4% | 1.43 |
| 2021 | 牛市 | +10.7% | 0.48 |
| 2022 | 熊市 | +2.0% | 0.23 |
| 2023 | 震荡 | +2.3% | 0.22 |
| 2024-25 | 牛市 | +75.4% | 1.28 |
8 年中 7 年盈利,熊市不亏钱,牛市吃足利润。
---
## 设计要点
1. **跨周期一致性**:所有信号在同一 4h 周期上,不跨级,避免多时间框架的延迟叠加
2. **状态优先于信号**:先判断能做哪个方向,再在该方向上找入场点
3. **止损不反手**:止损后空仓等下一个信号,避免震荡市中反复被止损
4. **少即是多**:3 种判定方法刚好覆盖趋势方向、当前价格位置、极端状态三个不冗余的维度
---
## 代码位置
- 策略实现:`engine/example/regime_all.py`
- 多空引擎:`engine/example/long_short.py`LongShortEngine
+84
View File
@@ -0,0 +1,84 @@
# 牛熊自适应策略 — 多时间级别回测对比
> 生成时间:2026-06-12 09:37
## 一、全量数据(所有可用历史)
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 4h | 全量 | +493.5% | +22.7% | 0.80 | -34.1% | 208 | 36.5% | 1.53 | +41513 | +14936 | 2017-08-17~2026-06-11 |
| ETHUSDT | 4h | 全量 | +831.8% | +29.2% | 0.88 | -48.9% | 162 | 38.9% | 1.57 | +60985 | +31285 | 2017-08-17~2026-06-11 |
| BNBUSDT | 4h | 全量 | +3282.2% | +51.4% | 1.15 | -38.0% | 150 | 36.7% | 1.60 | +403316 | -40234 | 2017-11-06~2026-06-11 |
| SOLUSDT | 4h | 全量 | +34.9% | +5.4% | 0.32 | -59.6% | 78 | 33.3% | 1.13 | +7744 | -3164 | 2020-08-11~2026-06-11 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 1d | 全量 | +689.4% | +28.9% | 1.07 | -30.5% | 28 | 46.4% | 2.88 | +40825 | +28909 | 2017-08-17~2026-06-11 |
| ETHUSDT | 1d | 全量 | +216.7% | +15.2% | 0.69 | -39.2% | 20 | 40.0% | 2.64 | +4360 | +17454 | 2017-08-17~2026-06-11 |
| BNBUSDT | 1d | 全量 | +63.5% | +6.4% | 0.36 | -40.7% | 18 | 38.9% | 1.53 | +3220 | +3247 | 2017-11-06~2026-06-11 |
| SOLUSDT | 1d | 全量 | +247.1% | +27.3% | 0.90 | -60.5% | 15 | 33.3% | 1.64 | +9206 | +15504 | 2020-08-11~2026-06-11 |
## 二、近一年(2025.06 — 2026.06
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 4h | 近1年 | -5.2% | -5.7% | -0.30 | -17.6% | 21 | 28.6% | 0.88 | -326 | +15 |
| ETHUSDT | 4h | 近1年 | +6.8% | +7.5% | 0.40 | -20.9% | 19 | 42.1% | 1.26 | +2704 | -1800 |
| BNBUSDT | 4h | 近1年 | +15.0% | +16.5% | 0.72 | -25.6% | 20 | 35.0% | 1.48 | +2375 | -640 |
| SOLUSDT | 4h | 近1年 | -21.6% | -23.3% | -0.94 | -32.8% | 15 | 26.7% | 0.47 | -205 | -1815 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 1d | 近1年 | +6.4% | +19.9% | 0.73 | -11.8% | 1 | 100.0% | 650.80 | +651 | +0 |
| ETHUSDT | 1d | 近1年 | -0.1% | -0.4% | 0.18 | -15.7% | 1 | 0.0% | 0.00 | -3 | +0 |
| BNBUSDT | 1d | 近1年 | +1.6% | +4.8% | 1.28 | -1.2% | 1 | 100.0% | 277.22 | +0 | +277 |
| SOLUSDT | 1d | 近1年 | +25.5% | +94.3% | 3.41 | -4.1% | 1 | 100.0% | 2752.24 | +0 | +2752 |
---
## 三、全维度汇总
| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |
|------|------|------|--------|------|------|------|------|------|
| BNBUSDT | 1d | 全量 | +63.5% | 0.36 | -40.7% | 18 | 38.9% | 1.53 |
| BNBUSDT | 4h | 全量 | +3282.2% | 1.15 | -38.0% | 150 | 36.7% | 1.60 |
| BNBUSDT | 1d | 近1年 | +1.6% | 1.28 | -1.2% | 1 | 100.0% | 277.22 |
| BNBUSDT | 4h | 近1年 | +15.0% | 0.72 | -25.6% | 20 | 35.0% | 1.48 |
| BTCUSDT | 1d | 全量 | +689.4% | 1.07 | -30.5% | 28 | 46.4% | 2.88 |
| BTCUSDT | 4h | 全量 | +493.5% | 0.80 | -34.1% | 208 | 36.5% | 1.53 |
| BTCUSDT | 1d | 近1年 | +6.4% | 0.73 | -11.8% | 1 | 100.0% | 650.80 |
| BTCUSDT | 4h | 近1年 | -5.2% | -0.30 | -17.6% | 21 | 28.6% | 0.88 |
| ETHUSDT | 1d | 全量 | +216.7% | 0.69 | -39.2% | 20 | 40.0% | 2.64 |
| ETHUSDT | 4h | 全量 | +831.8% | 0.88 | -48.9% | 162 | 38.9% | 1.57 |
| ETHUSDT | 1d | 近1年 | -0.1% | 0.18 | -15.7% | 1 | 0.0% | 0.00 |
| ETHUSDT | 4h | 近1年 | +6.8% | 0.40 | -20.9% | 19 | 42.1% | 1.26 |
| SOLUSDT | 1d | 全量 | +247.1% | 0.90 | -60.5% | 15 | 33.3% | 1.64 |
| SOLUSDT | 4h | 全量 | +34.9% | 0.32 | -59.6% | 78 | 33.3% | 1.13 |
| SOLUSDT | 1d | 近1年 | +25.5% | 3.41 | -4.1% | 1 | 100.0% | 2752.24 |
| SOLUSDT | 4h | 近1年 | -21.6% | -0.94 | -32.8% | 15 | 26.7% | 0.47 |
## 四、各币种最佳组合(按夏普排序)
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |
|------|------|------|--------|------|------|------|------|------|
| BTCUSDT | **1d** | 全量 | +689.4% | +28.9% | 1.07 | -30.5% | 28 | 46.4% |
| ETHUSDT | **4h** | 全量 | +831.8% | +29.2% | 0.88 | -48.9% | 162 | 38.9% |
| BNBUSDT | **1d** | 近1年 | +1.6% | +4.8% | 1.28 | -1.2% | 1 | 100.0% |
| SOLUSDT | **1d** | 近1年 | +25.5% | +94.3% | 3.41 | -4.1% | 1 | 100.0% |
---
## 五、结论
- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h
- **全量 vs 近一年**:近一年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期
- **交易频率**1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略
@@ -0,0 +1,84 @@
# 牛熊自适应策略 — 多时间级别回测对比
> 生成时间:2026-06-12 09:42
## 一、全量数据(所有可用历史)
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 4h | 全量 | +493.5% | +22.7% | 0.80 | -34.1% | 208 | 36.5% | 1.53 | +41513 | +14936 | 2017-08-17~2026-06-11 |
| ETHUSDT | 4h | 全量 | +831.8% | +29.2% | 0.88 | -48.9% | 162 | 38.9% | 1.57 | +60985 | +31285 | 2017-08-17~2026-06-11 |
| BNBUSDT | 4h | 全量 | +3282.2% | +51.4% | 1.15 | -38.0% | 150 | 36.7% | 1.60 | +403316 | -40234 | 2017-11-06~2026-06-11 |
| SOLUSDT | 4h | 全量 | +34.9% | +5.4% | 0.32 | -59.6% | 78 | 33.3% | 1.13 | +7744 | -3164 | 2020-08-11~2026-06-11 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L | 数据范围 |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|---------|
| BTCUSDT | 1d | 全量 | +689.4% | +28.9% | 1.07 | -30.5% | 28 | 46.4% | 2.88 | +40825 | +28909 | 2017-08-17~2026-06-11 |
| ETHUSDT | 1d | 全量 | +216.7% | +15.2% | 0.69 | -39.2% | 20 | 40.0% | 2.64 | +4360 | +17454 | 2017-08-17~2026-06-11 |
| BNBUSDT | 1d | 全量 | +63.5% | +6.4% | 0.36 | -40.7% | 18 | 38.9% | 1.53 | +3220 | +3247 | 2017-11-06~2026-06-11 |
| SOLUSDT | 1d | 全量 | +247.1% | +27.3% | 0.90 | -60.5% | 15 | 33.3% | 1.64 | +9206 | +15504 | 2020-08-11~2026-06-11 |
## 二、近两年(2024.06 — 2026.06
### 4h 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 4h | 近2年 | +28.5% | +14.0% | 0.70 | -17.6% | 49 | 40.8% | 1.50 | +3072 | +391 |
| ETHUSDT | 4h | 近2年 | +13.1% | +6.7% | 0.38 | -20.9% | 38 | 42.1% | 1.26 | +1860 | -119 |
| BNBUSDT | 4h | 近2年 | -4.2% | -2.2% | -0.00 | -25.6% | 35 | 31.4% | 0.98 | +675 | -760 |
| SOLUSDT | 4h | 近2年 | +0.8% | +0.4% | 0.20 | -39.7% | 21 | 23.8% | 1.04 | +4083 | -3750 |
### 1d 周期
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 | 多头P&L | 空头P&L |
|------|------|------|--------|------|------|------|------|------|------|---------|---------|
| BTCUSDT | 1d | 近2年 | +42.8% | +30.4% | 1.40 | -15.4% | 5 | 60.0% | 5.38 | +148 | +4327 |
| ETHUSDT | 1d | 近2年 | +57.4% | +40.2% | 1.76 | -7.6% | 2 | 100.0% | 5903.01 | +0 | +5903 |
| BNBUSDT | 1d | 近2年 | -8.0% | -6.0% | -1.04 | -9.6% | 3 | 33.3% | 0.27 | -928 | +251 |
| SOLUSDT | 1d | 近2年 | +22.0% | +16.0% | 1.08 | -12.0% | 2 | 50.0% | 9.85 | +0 | +2403 |
---
## 三、全维度汇总
| 币种 | 周期 | 范围 | 总收益% | 夏普 | 回撤% | 交易 | 胜率% | 盈亏比 |
|------|------|------|--------|------|------|------|------|------|
| BNBUSDT | 1d | 全量 | +63.5% | 0.36 | -40.7% | 18 | 38.9% | 1.53 |
| BNBUSDT | 4h | 全量 | +3282.2% | 1.15 | -38.0% | 150 | 36.7% | 1.60 |
| BNBUSDT | 1d | 近2年 | -8.0% | -1.04 | -9.6% | 3 | 33.3% | 0.27 |
| BNBUSDT | 4h | 近2年 | -4.2% | -0.00 | -25.6% | 35 | 31.4% | 0.98 |
| BTCUSDT | 1d | 全量 | +689.4% | 1.07 | -30.5% | 28 | 46.4% | 2.88 |
| BTCUSDT | 4h | 全量 | +493.5% | 0.80 | -34.1% | 208 | 36.5% | 1.53 |
| BTCUSDT | 1d | 近2年 | +42.8% | 1.40 | -15.4% | 5 | 60.0% | 5.38 |
| BTCUSDT | 4h | 近2年 | +28.5% | 0.70 | -17.6% | 49 | 40.8% | 1.50 |
| ETHUSDT | 1d | 全量 | +216.7% | 0.69 | -39.2% | 20 | 40.0% | 2.64 |
| ETHUSDT | 4h | 全量 | +831.8% | 0.88 | -48.9% | 162 | 38.9% | 1.57 |
| ETHUSDT | 1d | 近2年 | +57.4% | 1.76 | -7.6% | 2 | 100.0% | 5903.01 |
| ETHUSDT | 4h | 近2年 | +13.1% | 0.38 | -20.9% | 38 | 42.1% | 1.26 |
| SOLUSDT | 1d | 全量 | +247.1% | 0.90 | -60.5% | 15 | 33.3% | 1.64 |
| SOLUSDT | 4h | 全量 | +34.9% | 0.32 | -59.6% | 78 | 33.3% | 1.13 |
| SOLUSDT | 1d | 近2年 | +22.0% | 1.08 | -12.0% | 2 | 50.0% | 9.85 |
| SOLUSDT | 4h | 近2年 | +0.8% | 0.20 | -39.7% | 21 | 23.8% | 1.04 |
## 四、各币种最佳组合(按夏普排序)
| 币种 | 周期 | 范围 | 总收益% | 年化% | 夏普 | 回撤% | 交易 | 胜率% |
|------|------|------|--------|------|------|------|------|------|
| BTCUSDT | **1d** | 近2年 | +42.8% | +30.4% | 1.40 | -15.4% | 5 | 60.0% |
| ETHUSDT | **1d** | 近2年 | +57.4% | +40.2% | 1.76 | -7.6% | 2 | 100.0% |
| BNBUSDT | **4h** | 全量 | +3282.2% | +51.4% | 1.15 | -38.0% | 150 | 36.7% |
| SOLUSDT | **1d** | 近2年 | +22.0% | +16.0% | 1.08 | -12.0% | 2 | 50.0% |
---
## 五、结论
- **4h vs 1d**:低波动大市值币种(BTC)偏向日线(1d),高波动币种(ETH/BNB)偏向4h
- **全量 vs 近两年**:近两年市场环境与长周期统计可能有显著差异,需结合当前市场结构选择周期
- **交易频率**1d 交易数约为 4h 的 1/5 ~ 1/10,适合低频策略
+21
View File
@@ -0,0 +1,21 @@
"""
回测引擎模块
提供事件驱动的历史回测能力:
- BacktestEngine — 核心回测引擎
- BacktestConfig — 回测配置
- BacktestTrade — 交易记录
- BacktestMetrics — 绩效指标
- BacktestResult — 完整回测结果
"""
from .engine import BacktestEngine
from .models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
__all__ = [
"BacktestEngine",
"BacktestConfig",
"BacktestTrade",
"BacktestMetrics",
"BacktestResult",
]
+478
View File
@@ -0,0 +1,478 @@
"""
回测引擎核心 — 事件驱动的历史回测
逐根 K 线推送给策略,模拟订单成交,跟踪资金曲线,计算绩效指标。
用法:
from engine.backtest import BacktestEngine, BacktestConfig
from engine.common.config import config
bt_config = BacktestConfig(
symbol="BTCUSDT",
interval="1h",
start_time=datetime(2025, 1, 1),
end_time=datetime(2025, 6, 1),
initial_capital=10000.0,
)
engine = BacktestEngine(bt_config, db_config=config.db)
result = await engine.run(MyStrategy, my_strategy_config)
print(result.summary())
"""
import asyncio
from datetime import datetime, timezone
from typing import Optional, Type
from ..common.base import BaseStrategy, Signal, StrategyConfig
from ..common.models import Kline
from ..data.service import DataService
from .models import BacktestConfig, BacktestMetrics, BacktestResult, BacktestTrade
# ── 资金曲线采样间隔(用于减少内存,每隔 N 根 Bar 记录一次)──
EQUITY_SAMPLE_INTERVAL = 1 # 每根都记录
class BacktestEngine:
"""事件驱动回测引擎
按时间顺序逐根 K 线推送给策略,模拟:
- 订单成交(含手续费、滑点)
- 持仓管理与盈亏计算
- 资金曲线追踪
- 绩效指标统计
信号在 K 线收盘生成,在下一根 K 线开盘时以「开盘价」执行,
避免使用已知收盘价的未来函数偏差。
"""
def __init__(self, config: BacktestConfig, db_config=None):
"""
Args:
config: 回测配置(交易对、周期、时间范围、资金等)
db_config: 数据库连接配置(DBConfig 实例)。
如果不传,引擎将在 run() 内从 engine.common.config 自动加载。
"""
self.config = config
self._db_config = db_config
# ── 投资组合状态 ──
self._cash: float = config.initial_capital
self._position: float = 0.0
self._avg_entry_price: float = 0.0
# ── 记录 ──
self._trades: list[BacktestTrade] = []
self._equity: list[dict] = []
# ── 待执行信号(BUY 信号在下一根 Bar 开盘时执行)──
self._pending_buy: Optional[Signal] = None
# ================================================================
# 主入口
# ================================================================
async def run(
self,
strategy_cls: Type[BaseStrategy],
strategy_config: StrategyConfig,
) -> BacktestResult:
"""执行回测。
流程:
1. 连接数据库并加载历史 K 线
2. 创建策略实例并调用 on_start()
3. 预热阶段:喂 warmup_bars 根 K 线
4. 主循环:逐根 K 线推给策略 → 模拟成交 → 更新资金曲线
5. 对剩余持仓按最后一根 K 线收盘价强制平仓
6. 调用策略 on_stop()
7. 计算绩效指标
Args:
strategy_cls: 策略类(继承 BaseStrategy
strategy_config: 策略配置实例
Returns:
BacktestResult: 包含交易记录、资金曲线和绩效指标
"""
# 确保 strategy_config 与回测配置对齐
strategy_config.symbol = self.config.symbol
strategy_config.exchange = self.config.exchange
# 1. 连接数据库并加载数据
from ..common.config import config as app_config
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, # 足够大的 limit,实际由 start/end 约束
)
if len(klines) < self.config.warmup_bars + 2:
raise ValueError(
f"数据不足:需要至少 {self.config.warmup_bars + 2} 根 K 线,"
f"实际只有 {len(klines)}"
)
# 2. 创建策略实例
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
# 3. 预热阶段
warmup_end = self.config.warmup_bars
for i in range(warmup_end):
await strategy.on_kline(klines[i])
# 4. 主循环
for i in range(warmup_end, len(klines)):
kline = klines[i]
# 4a. 先执行上一根 bar 产生的待执行买单
if self._pending_buy is not None:
self._execute_buy(self._pending_buy, kline)
self._pending_buy = None
# 4b. 推送 K 线给策略
signal = await strategy.on_kline(kline)
# 4c. 处理信号
if signal is not None and signal.side == "SELL":
self._execute_sell(signal, kline)
elif signal is not None and signal.side == "BUY":
# BUY 信号延迟到下一根 bar 执行,避免未来函数
self._pending_buy = signal
# LIMIT / CANCEL 信号暂不支持
# 4d. 记录资金曲线
if i % EQUITY_SAMPLE_INTERVAL == 0:
self._record_equity(kline)
# 5. 对剩余持仓按最后一根 K 线收盘价强平
if self._position > 0 and len(klines) > 0:
last_kline = klines[-1]
self._execute_sell(
Signal(
symbol=self.config.symbol,
side="SELL",
signal_type="MARKET",
quantity=self._position,
confidence=1.0,
reason="回测结束 — 强制平仓",
timestamp=last_kline.open_time,
),
last_kline,
)
# 6. 停止策略
await strategy.on_stop()
# 7. 计算指标
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()
async def run_batch(
self,
strategy_cls: Type[BaseStrategy],
configs: list[StrategyConfig],
) -> list[BacktestResult]:
"""批量回测(并行执行多个策略配置)。
适用于参数扫描场景。
"""
tasks = [
self.run(strategy_cls, cfg)
for cfg in configs
]
return await asyncio.gather(*tasks)
# ================================================================
# 交易模拟
# ================================================================
def _execute_buy(self, signal: Signal, kline: Kline) -> None:
"""执行买入(在下一根 K 线的开盘价执行)"""
# 执行价格 = 开盘价 + 滑点
exec_price = kline.open * (1 + self.config.slippage_pct)
# 确定数量
qty = signal.quantity
if qty is None:
# 按最大仓位比例计算
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
total_cost = notional + commission
# 检查余额
if total_cost > self._cash:
# 按可用资金重新计算可买数量
max_qty = (self._cash / (exec_price * (1 + self.config.commission_pct)))
qty = self._round_qty(max_qty)
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 if self._position > 0 else 0
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:
"""执行卖出(在当前 K 线的收盘价执行)"""
exec_price = kline.close * (1 - self.config.slippage_pct)
# 确定数量
qty = signal.quantity
if qty is None:
qty = self._position # 全部卖出
qty = min(qty, self._position) # 不能超卖
qty = self._round_qty(qty)
if qty < self.config.min_order_qty or self._position < self.config.min_order_qty:
return
notional = exec_price * qty
commission = notional * self.config.commission_pct
net_proceeds = notional - commission
# 计算盈亏
pnl = (exec_price - self._avg_entry_price) * qty - commission
# 更新持仓
self._position -= qty
if self._position < self.config.min_order_qty:
self._position = 0.0
self._avg_entry_price = 0.0
self._cash += net_proceeds
# 记录交易
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,
pnl=pnl,
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
drawdown = (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": drawdown,
"position": self._position,
})
# ================================================================
# 绩效指标计算
# ================================================================
def _compute_metrics(self) -> BacktestMetrics:
"""从交易记录和资金曲线计算全部绩效指标"""
if not self._equity:
return BacktestMetrics()
initial_capital = self.config.initial_capital
final_equity = self._equity[-1]["equity"]
# ── 总收益率 ──
total_return_pct = (final_equity - initial_capital) / initial_capital * 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_equity > 0 and initial_capital > 0:
annual_return_pct = ((final_equity / initial_capital) ** (365 / days) - 1) * 100
else:
annual_return_pct = 0.0
# ── 日收益率 → 夏普比率 ──
daily_returns = self._compute_daily_returns()
if len(daily_returns) > 1:
import statistics
mean_ret = statistics.mean(daily_returns)
std_ret = statistics.stdev(daily_returns) if len(daily_returns) > 1 else 0.0
sharpe_ratio = (mean_ret / std_ret * (365 ** 0.5)) if std_ret > 0 else 0.0
else:
sharpe_ratio = 0.0
# ── 最大回撤 & 回撤持续天数 ──
max_drawdown_pct, max_dd_days = self._compute_max_drawdown()
# ── 交易统计 ──
sells = [t for t in self._trades if t.side == "SELL" and t.pnl is not None]
total_trades = len(sells)
if total_trades > 0:
winners = [t for t in sells if t.pnl > 0]
losers = [t for t in sells if t.pnl <= 0]
win_rate = len(winners) / total_trades
gross_profit = sum(t.pnl for t in winners)
gross_loss = abs(sum(t.pnl for t in losers))
profit_factor = gross_profit / gross_loss if gross_loss > 0 else (gross_profit if gross_profit > 0 else 0.0)
avg_trade_pnl = sum(t.pnl for t in sells) / total_trades
best_trade_pnl = max(t.pnl for t in sells)
worst_trade_pnl = min(t.pnl for t in sells)
else:
win_rate = 0.0
profit_factor = 0.0
avg_trade_pnl = 0.0
best_trade_pnl = 0.0
worst_trade_pnl = 0.0
# ── 卡尔玛比率 ──
if max_drawdown_pct < 0:
calmar_ratio = annual_return_pct / abs(max_drawdown_pct)
else:
calmar_ratio = 0.0
return BacktestMetrics(
total_return_pct=total_return_pct,
annual_return_pct=annual_return_pct,
sharpe_ratio=sharpe_ratio,
max_drawdown_pct=max_drawdown_pct,
max_drawdown_duration_days=max_dd_days,
win_rate=win_rate,
profit_factor=profit_factor,
total_trades=total_trades,
avg_trade_pnl=avg_trade_pnl,
best_trade_pnl=best_trade_pnl,
worst_trade_pnl=worst_trade_pnl,
calmar_ratio=calmar_ratio,
final_equity=final_equity,
)
def _compute_daily_returns(self) -> list[float]:
"""从资金曲线提取每日收益率序列"""
if not self._equity:
return []
# 按日期分组,取每日最后一根 bar 的权益
from collections import defaultdict
daily: dict[str, float] = {}
for point in self._equity:
dt = datetime.fromtimestamp(point["timestamp"] / 1000, tz=timezone.utc)
date_key = dt.strftime("%Y-%m-%d")
daily[date_key] = 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 日期到当前的持续时间
peak_ts = self._equity[dd_start_idx]["timestamp"]
curr_ts = point["timestamp"]
dd_days = int((curr_ts - peak_ts) / (1000 * 86400))
if dd_days > max_dd_days:
max_dd_days = dd_days
return max_dd, max_dd_days
# ================================================================
# 工具方法
# ================================================================
def _round_qty(self, qty: float, decimals: int = 8) -> float:
"""将数量向下取整到指定位数"""
factor = 10 ** decimals
return int(qty * factor) / factor
+146
View File
@@ -0,0 +1,146 @@
"""
回测引擎数据模型 — 配置、交易记录、绩效指标和结果
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
@dataclass
class BacktestConfig:
"""回测配置
Attributes:
symbol: 交易对(如 BTCUSDT
exchange: 交易所标识
interval: K 线周期
start_time: 回测起始时间(None 表示从最早可用数据开始)
end_time: 回测结束时间(None 表示到最新可用数据结束)
commission_pct: 手续费率(0.001 = 0.1%
slippage_pct: 滑点率(0.0005 = 0.05%
min_order_qty: 最小下单量
initial_capital: 初始资金(Quote 币种,如 USDT)
warmup_bars: 预热 K 线条数(策略初始化指标所需最少数据量)
"""
symbol: str
exchange: str = "binance"
interval: str = "1h"
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
# 交易成本
commission_pct: float = 0.001
slippage_pct: float = 0.0005
min_order_qty: float = 0.001
# 资金
initial_capital: float = 10_000.0
# 数据
warmup_bars: int = 100
@dataclass
class BacktestTrade:
"""单笔回测交易记录"""
timestamp: float
"""成交时间(Unix 毫秒)"""
symbol: str
"""交易对"""
side: str
"""方向:BUY / SELL"""
price: float
"""成交价格(含滑点)"""
quantity: float
"""成交数量(Base 币种)"""
notional: float
"""成交额(Quote 币种 = price × quantity"""
commission: float
"""手续费"""
slippage: float
"""滑点成本"""
pnl: Optional[float] = None
"""平仓盈亏(BUY 时为 None,SELL 时有效)"""
reason: str = ""
"""交易原因(来自 Signal.reason"""
@dataclass
class BacktestMetrics:
"""回测绩效指标"""
total_return_pct: float = 0.0
"""总收益率(%"""
annual_return_pct: float = 0.0
"""年化收益率(%"""
sharpe_ratio: float = 0.0
"""夏普比率(无风险利率假定为 0"""
max_drawdown_pct: float = 0.0
"""最大回撤(%),以负值表示"""
max_drawdown_duration_days: int = 0
"""最大回撤持续天数"""
win_rate: float = 0.0
"""胜率(0-1"""
profit_factor: float = 0.0
"""盈亏比(总盈利 / 总亏损绝对值)"""
total_trades: int = 0
"""总交易次数"""
avg_trade_pnl: float = 0.0
"""平均每笔盈亏"""
best_trade_pnl: float = 0.0
"""最佳单笔盈亏"""
worst_trade_pnl: float = 0.0
"""最差单笔盈亏"""
calmar_ratio: float = 0.0
"""卡尔玛比率(年化收益 / 最大回撤绝对值)"""
final_equity: float = 0.0
"""最终权益"""
@dataclass
class BacktestResult:
"""完整回测结果"""
config: BacktestConfig
"""回测配置"""
strategy_config: dict
"""策略配置(转为 dict 便于序列化)"""
metrics: BacktestMetrics
"""绩效指标"""
trades: list[BacktestTrade] = field(default_factory=list)
"""交易记录"""
equity_curve: list[dict] = field(default_factory=list)
"""资金曲线 [{"timestamp": float, "equity": float, "drawdown": float}, ...]"""
@property
def total_bars(self) -> int:
"""回测 K 线总数"""
return len(self.equity_curve)
def summary(self) -> str:
"""生成人类可读的摘要"""
m = self.metrics
lines = [
"=" * 60,
f" 回测结果摘要 — {self.config.symbol} {self.config.interval}",
"=" * 60,
f" 初始资金: {self.config.initial_capital:>12.2f} USDT",
f" 最终权益: {m.final_equity:>12.2f} USDT",
f" 总收益率: {m.total_return_pct:>11.2f}%",
f" 年化收益率: {m.annual_return_pct:>11.2f}%",
f" 夏普比率: {m.sharpe_ratio:>12.2f}",
f" 卡尔玛比率: {m.calmar_ratio:>12.2f}",
f" 最大回撤: {m.max_drawdown_pct:>11.2f}%",
f" 回撤持续: {m.max_drawdown_duration_days:>9}",
f" 总交易次数: {m.total_trades:>11}",
f" 胜率: {m.win_rate:>11.1%}",
f" 盈亏比: {m.profit_factor:>12.2f}",
f" 平均盈亏: {m.avg_trade_pnl:>12.4f} USDT",
f" 最佳盈亏: {m.best_trade_pnl:>12.4f} USDT",
f" 最差盈亏: {m.worst_trade_pnl:>12.4f} USDT",
"=" * 60,
]
return "\n".join(lines)