From 4da520c14bb84be930f8b32b9bad5b0b255d6501 Mon Sep 17 00:00:00 2001 From: Rekey Date: Fri, 12 Jun 2026 10:26:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(engine):=20=E6=B7=BB=E5=8A=A0=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E9=A9=B1=E5=8A=A8=E5=9B=9E=E6=B5=8B=E5=BC=95=E6=93=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backtest/engine.py: 事件驱动回测引擎核心,支持 K 线推进/订单撮合/权益曲线 - backtest/models.py: 回测数据模型(订单/成交/持仓/账户快照) - backtest/README.md: 回测模块使用说明 - backtest/STRATEGY.md: 策略开发指南与最佳实践 - backtest/TIMEFRAME_COMPARISON*.md: 多周期回测对比分析报告 --- engine/backtest/README.md | 184 ++++++++ engine/backtest/STRATEGY.md | 178 ++++++++ engine/backtest/TIMEFRAME_COMPARISON.md | 84 ++++ engine/backtest/TIMEFRAME_COMPARISON_2Y.md | 84 ++++ engine/backtest/__init__.py | 21 + engine/backtest/engine.py | 478 +++++++++++++++++++++ engine/backtest/models.py | 146 +++++++ 7 files changed, 1175 insertions(+) create mode 100644 engine/backtest/README.md create mode 100644 engine/backtest/STRATEGY.md create mode 100644 engine/backtest/TIMEFRAME_COMPARISON.md create mode 100644 engine/backtest/TIMEFRAME_COMPARISON_2Y.md create mode 100644 engine/backtest/__init__.py create mode 100644 engine/backtest/engine.py create mode 100644 engine/backtest/models.py diff --git a/engine/backtest/README.md b/engine/backtest/README.md new file mode 100644 index 0000000..91a7a6d --- /dev/null +++ b/engine/backtest/README.md @@ -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 +``` diff --git a/engine/backtest/STRATEGY.md b/engine/backtest/STRATEGY.md new file mode 100644 index 0000000..4b55c9e --- /dev/null +++ b/engine/backtest/STRATEGY.md @@ -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) diff --git a/engine/backtest/TIMEFRAME_COMPARISON.md b/engine/backtest/TIMEFRAME_COMPARISON.md new file mode 100644 index 0000000..cb53c13 --- /dev/null +++ b/engine/backtest/TIMEFRAME_COMPARISON.md @@ -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,适合低频策略 + diff --git a/engine/backtest/TIMEFRAME_COMPARISON_2Y.md b/engine/backtest/TIMEFRAME_COMPARISON_2Y.md new file mode 100644 index 0000000..e045f54 --- /dev/null +++ b/engine/backtest/TIMEFRAME_COMPARISON_2Y.md @@ -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,适合低频策略 + diff --git a/engine/backtest/__init__.py b/engine/backtest/__init__.py new file mode 100644 index 0000000..50d3774 --- /dev/null +++ b/engine/backtest/__init__.py @@ -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", +] diff --git a/engine/backtest/engine.py b/engine/backtest/engine.py new file mode 100644 index 0000000..6b398e4 --- /dev/null +++ b/engine/backtest/engine.py @@ -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 diff --git a/engine/backtest/models.py b/engine/backtest/models.py new file mode 100644 index 0000000..df4ed22 --- /dev/null +++ b/engine/backtest/models.py @@ -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)