feat(engine): 添加事件驱动回测引擎
- backtest/engine.py: 事件驱动回测引擎核心,支持 K 线推进/订单撮合/权益曲线 - backtest/models.py: 回测数据模型(订单/成交/持仓/账户快照) - backtest/README.md: 回测模块使用说明 - backtest/STRATEGY.md: 策略开发指南与最佳实践 - backtest/TIMEFRAME_COMPARISON*.md: 多周期回测对比分析报告
This commit is contained in:
@@ -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
|
||||
```
|
||||
@@ -0,0 +1,178 @@
|
||||
# 牛熊自适应趋势跟踪策略
|
||||
|
||||
## 概述
|
||||
|
||||
通过识别市场所处的牛熊状态,自适应地选择做多或做空方向,在震荡市中空仓等待。
|
||||
|
||||
核心思想:**牛市不逆势做空,熊市不逆势做多。**
|
||||
|
||||
---
|
||||
|
||||
## 市场状态判定(3 法投票)
|
||||
|
||||
每根 4h K 线收盘后,用以下三种方法独立判定当前市场状态:
|
||||
|
||||
### 方法 1:EMA200 斜率
|
||||
|
||||
```
|
||||
计算:EMA200 近 20 根 K 线的变化率
|
||||
判定:斜率 > +0.2% → 牛
|
||||
斜率 < -0.2% → 熊
|
||||
其他 → 震荡
|
||||
```
|
||||
|
||||
EMA200 向上倾斜说明长期趋势向上,向下倾斜说明长期趋势向下。
|
||||
|
||||
### 方法 2:价格 vs EMA200
|
||||
|
||||
```
|
||||
判定:当前收盘价 > EMA200 → 牛
|
||||
当前收盘价 < EMA200 → 熊
|
||||
```
|
||||
|
||||
最直接的趋势判定——价格在年线上方就是多头市场。
|
||||
|
||||
### 方法 3:ATH 回撤
|
||||
|
||||
```
|
||||
追踪历史最高价 (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)
|
||||
@@ -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,适合低频策略
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user