docs: revise backtest spec per feasibility review (rev 2)

- DSL requires explicit column references (close/open/high/low)
- Add order_reject_policy: reduce | skip
- Add future_leak_warning for this_close execution
- Add benchmark datetime alignment rules
- Support pre-computed indicator columns via StrategyDataProxy
- Add chanlun_result injection (P0 interface, v2 auto-integration)
- Adjust priorities: DSL→P1, run_many/run_grid→P1, chanlun→P1

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
GitHub
2026-06-09 16:14:05 +08:00
co-authored by Claude Opus 4.8
parent 8d68e9c094
commit 480e635dcb
@@ -1,7 +1,7 @@
# Backtest Engine Design — easy-tdx # Backtest Engine Design — easy-tdx
**Date:** 2026-06-09 **Date:** 2026-06-09
**Status:** Approved **Status:** Approved (rev 2 — incorporated DeepSeek feasibility review)
**Module:** `easy_tdx.backtest` **Module:** `easy_tdx.backtest`
**Priority:** P0 — 量化工具链全栈的第一块拼图 **Priority:** P0 — 量化工具链全栈的第一块拼图
@@ -15,10 +15,27 @@
- 纯计算模块,与 `chanlun` 同级,零网络依赖 - 纯计算模块,与 `chanlun` 同级,零网络依赖
- 仅依赖 `pandas`/`numpy`(项目已有),不引入第三方回测库 - 仅依赖 `pandas`/`numpy`(项目已有),不引入第三方回测库
- 接收 easy-tdx 标准 DataFrame`datetime, open, close, high, low, vol, amount` - 接收 easy-tdx 标准 DataFrame`datetime, open, close, high, low, vol, amount`及带指标列的扩展 DataFrame
- 双模式策略定义:Python 类继承 + DSL 公式语法 - 双模式策略定义:Python 类继承P0+ DSL 公式语法P1
- v1 只实现向量化执行路径(日级策略),架构预留事件驱动扩展点 - v1 只实现向量化执行路径(日级策略),架构预留事件驱动扩展点
### v1 范围(P0 vs P1
| 优先级 | 模块 | 说明 |
|--------|------|------|
| **P0** | Strategy 基类 | Python 类继承 + `init()`/`next()` 生命周期 |
| **P0** | 向量化引擎 | 信号→撮合→持仓→绩效 四步管道 |
| **P0** | OrderSimulator | 5 种成交价规则 + 订单拒绝策略 |
| **P0** | PortfolioTracker | 持仓追踪 + 资金曲线 |
| **P0** | PerformanceAnalyzer | 18 项绩效指标 |
| **P0** | CLI `easy-tdx backtest` | 自动获取 K 线 + 执行回测 |
| **P0** | 预计算指标列支持 | DataFrame 中的额外列可直接在策略中引用 |
| **P0** | 缠论预留接口 | `self.chanlun` 属性,引擎接受可选 `chanlun_result` 参数 |
| **P1** | DSL 装饰器 | `@dsl_strategy` Python 函数模式 |
| **P1** | DSL 字符串解析 | CLI `--strategy "CROSS(MA(close,5),MA(close,20))"` |
| **P1** | `run_many` 多股票 | 顺序独立回测多只股票 |
| **P1** | `run_grid` 参数扫描 | 参数网格搜索 |
--- ---
## 2. 架构 ## 2. 架构
@@ -29,27 +46,29 @@
src/easy_tdx/backtest/ src/easy_tdx/backtest/
├── __init__.py # 公开 API 导出 ├── __init__.py # 公开 API 导出
├── strategy.py # Strategy 基类 + StrategyDataProxy + IndicatorProvider ├── strategy.py # Strategy 基类 + StrategyDataProxy + IndicatorProvider
├── dsl.py # DSL 解析器 + @dsl_strategy 装饰器 + 字符串 DSL 编译 ├── dsl.py # DSL 解析器 + @dsl_strategy 装饰器 + 字符串 DSL 编译 (P1)
├── engine.py # BacktestEngine(向量化执行路径) ├── engine.py # BacktestEngine(向量化执行路径)
├── orders.py # OrderSimulator(撮合规则) ├── orders.py # OrderSimulator(撮合规则 + 订单拒绝策略
├── portfolio.py # PortfolioTracker(持仓/资金曲线) ├── portfolio.py # PortfolioTracker(持仓/资金曲线)
├── performance.py # PerformanceAnalyzer(绩效指标计算) ├── performance.py # PerformanceAnalyzer(绩效指标计算)
├── types.py # Trade / Position / Signal / BacktestResult 数据类 ├── types.py # Trade / Position / Signal / BacktestResult 数据类
└── cli.py # CLI 集成(easy-tdx backtest ... └── cli.py # CLI 集成(easy-tdx backtest ...,自动获取 K 线
``` ```
### 2.2 模块交互流 ### 2.2 模块交互流
``` ```
easy_tdx MacClient.get_stock_kline() → DataFrame easy_tdx MacClient.get_stock_kline() → DataFrame
或 MacClient.get_stock_kline_with_indicators() → DataFrame (含指标列)
或 ChanlunAnalyser.process_klines() → ChanlunResult (可选注入)
BacktestEngine(strategy, cash=100000) BacktestEngine(strategy, cash=100000, chanlun_result=None)
┌───────────┼───────────┐ ┌───────────┼───────────┐
▼ ▼ ▼ ▼ ▼ ▼
Strategy DSL Parser OrderSimulator Strategy DSL Parser OrderSimulator
(Python类) (公式语法) (撮合规则) (Python类) (公式语法) (撮合+拒绝策略)
│ │ │ │ │ │
└─────┬─────┘ │ └─────┬─────┘ │
▼ │ ▼ │
@@ -95,6 +114,7 @@ class Trade:
commission: float commission: float
slippage: float slippage: float
pnl: float # 仅平仓时计算 pnl: float # 仅平仓时计算
rejected: bool = False # 订单是否被拒绝(资金/持仓不足)
``` ```
### 3.3 Position ### 3.3 Position
@@ -116,9 +136,9 @@ class Position:
class BacktestResult: class BacktestResult:
performance: dict[str, float] performance: dict[str, float]
equity_curve: pd.DataFrame # datetime, cash, position_value, total, drawdown, drawdown_pct equity_curve: pd.DataFrame # datetime, cash, position_value, total, drawdown, drawdown_pct
trades: pd.DataFrame # datetime, direction, size, price, commission, pnl trades: pd.DataFrame # datetime, direction, size, price, commission, pnl, rejected
positions: pd.DataFrame # datetime, size, avg_price, market_value, unrealized_pnl positions: pd.DataFrame # datetime, size, avg_price, market_value, unrealized_pnl
config: dict config: dict # 包含 future_leak_warning 等标记
``` ```
方法: 方法:
@@ -161,13 +181,25 @@ class Strategy(ABC):
@property @property
def position(self) -> Position: ... def position(self) -> Position: ...
@property
def chanlun(self) -> Any | None:
"""缠论分析结果(如果引擎注入了 chanlun_result 参数)。
v1 仅作为数据通道,策略可读取笔/中枢/买卖点信息。
v2 计划实现自动集成。"""
...
``` ```
### 4.2 StrategyDataProxy ### 4.2 StrategyDataProxy
```python ```python
class StrategyDataProxy: class StrategyDataProxy:
"""K 线数据代理。支持 .close[0](当前)、.close[-1](前一根)。""" """K 线数据代理。支持 .close[0](当前)、.close[-1](前一根)。
自动暴露 DataFrame 中的所有列——包括预计算的指标列。
例如 get_stock_kline_with_indicators() 返回的 MACD_DIF 列
可直接通过 self.data.MACD_DIF[0] 访问。
"""
@property @property
def open(self) -> _SeriesAccessor: ... def open(self) -> _SeriesAccessor: ...
@property @property
@@ -180,6 +212,8 @@ class StrategyDataProxy:
def vol(self) -> _SeriesAccessor: ... def vol(self) -> _SeriesAccessor: ...
@property @property
def amount(self) -> _SeriesAccessor: ... def amount(self) -> _SeriesAccessor: ...
# 额外列(预计算指标、缠论结果等)通过 __getattr__ 动态暴露
def __getattr__(self, name: str) -> _SeriesAccessor: ...
class _SeriesAccessor: class _SeriesAccessor:
"""[0] 当前值、[-1] 前一根、切片。""" """[0] 当前值、[-1] 前一根、切片。"""
@@ -189,6 +223,8 @@ class _SeriesAccessor:
### 4.3 Python 类策略示例 ### 4.3 Python 类策略示例
**基本用法:**
```python ```python
class MACrossStrategy(Strategy): class MACrossStrategy(Strategy):
def init(self): def init(self):
@@ -205,9 +241,59 @@ engine = BacktestEngine(strategy=MACrossStrategy, cash=100000)
result = engine.run(df) result = engine.run(df)
``` ```
**使用预计算指标列:**
```python
class BollingerBreakout(Strategy):
def init(self):
pass # 指标已在 DataFrame 中预计算
def next(self):
if self.data.close[0] > self.data.BOLL_UPPER[0]:
self.sell(size=0)
elif self.data.close[0] < self.data.BOLL_LOWER[0]:
self.buy(size=0)
# 先获取带指标的 DataFrame
with MacClient.from_best_host() as c:
df = c.get_stock_kline_with_indicators(
Market.SH, "600519",
indicators=["BOLL"],
count=500,
)
engine = BacktestEngine(strategy=BollingerBreakout, cash=100000)
result = engine.run(df)
```
**使用缠论信号(v1 手动注入):**
```python
class ChanlunStrategy(Strategy):
def init(self):
pass
def next(self):
# self.chanlun 是外部注入的 ChanlunResult
if self.chanlun is None:
return
last_mmd = self.chanlun.mmds[-1] if self.chanlun.mmds else None
if last_mmd and "1buy" in last_mmd.msg:
self.buy(size=0)
# 获取 K 线 + 缠论分析
with MacClient.from_best_host() as c:
df = c.get_stock_kline(Market.SH, "600519", Period.DAILY, count=800)
analyser = ChanlunAnalyser("SH600519", "DAILY")
chanlun_result = analyser.process_klines(df)
engine = BacktestEngine(strategy=ChanlunStrategy, cash=100000)
result = engine.run(df, chanlun_result=chanlun_result)
```
--- ---
## 5. DSL 策略定义 ## 5. DSL 策略定义 (P1)
### 5.1 设计边界 ### 5.1 设计边界
@@ -216,6 +302,7 @@ result = engine.run(df)
| 指标交叉、比较、逻辑组合 | 循环、变量赋值、函数定义 | | 指标交叉、比较、逻辑组合 | 循环、变量赋值、函数定义 |
| 内置常用函数(CROSS, ABOVE, BELOW, BETWEEN | 自定义控制流 | | 内置常用函数(CROSS, ABOVE, BELOW, BETWEEN | 自定义控制流 |
| 参数化(可调窗口期) | 图灵完备 | | 参数化(可调窗口期) | 图灵完备 |
| **显式引用价格序列** (`close`, `open`, `high`, `low`) | 隐式推断(消除歧义) |
超出 DSL 能力的——直接用 Python 类。 超出 DSL 能力的——直接用 Python 类。
@@ -233,12 +320,14 @@ def dual_ma(df):
return buy, sell return buy, sell
``` ```
**字符串模式**CLI 用): **字符串模式**CLI 用,显式引用列名):
```bash ```bash
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --cash 100000 --table easy-tdx backtest SH 600519 --strategy "CROSS(MA(close,5),MA(close,20))" --cash 100000 --table
``` ```
编译器内部将 `close` 映射到 DataFrame 的 `close` 列。支持的列名:`open`, `close`, `high`, `low`, `vol`, `amount`,以及预计算的指标列名(如 `MACD_DIF`, `BOLL_UPPER`)。
### 5.3 内置 DSL 函数 ### 5.3 内置 DSL 函数
复用 `MyTT.py` 已有实现: 复用 `MyTT.py` 已有实现:
@@ -282,17 +371,23 @@ class BacktestEngine:
min_commission: float = 5.0, min_commission: float = 5.0,
stamp_tax: float = 0.001, stamp_tax: float = 0.001,
slippage: float = 0.0, slippage: float = 0.0,
execution: str = "next_open", # "next_open" | "next_close" | "this_close" | "worst" | "best" execution: str = "next_open", # "next_open" | "next_close" | "this_close" | "worst" | "best"
position_mode: str = "full", # "full" | "fixed" | "percent" | "signal_only" position_mode: str = "full", # "full" | "fixed" | "percent" | "signal_only"
order_reject_policy: str = "reduce", # "reduce" | "skip"
benchmark: pd.DataFrame | None = None, benchmark: pd.DataFrame | None = None,
): ):
... ...
def run(self, df: pd.DataFrame, chanlun_result: Any | None = None) -> BacktestResult:
"""执行回测。df 是 easy_tdx 标准 K 线 DataFrame(可含额外指标列)。
chanlun_result 可选注入缠论分析结果,策略通过 self.chanlun 访问。"""
...
``` ```
### 6.2 四步执行管道 ### 6.2 四步执行管道
1. **信号生成**DSL → bool maskPython 类 → trace next() 生成 mask 1. **信号生成**DSL → bool maskPython 类 → trace next() 生成 mask
2. **信号→订单**OrderSimulator):根据 execution 规则确定成交价,根据仓位模式确定量 2. **信号→订单**OrderSimulator):根据 execution 规则确定成交价,根据仓位模式确定量,根据 order_reject_policy 处理资金/持仓不足
3. **持仓追踪**PortfolioTracker):逐 Bar 更新现金/持仓/市值/回撤 3. **持仓追踪**PortfolioTracker):逐 Bar 更新现金/持仓/市值/回撤
4. **绩效分析**PerformanceAnalyzer):从资金曲线计算全部指标 4. **绩效分析**PerformanceAnalyzer):从资金曲线计算全部指标
@@ -304,7 +399,7 @@ Step 2 是唯一需要逐行处理的步骤(仓位依赖前一 Bar 状态)
|------|------| |------|------|
| `next_open`(默认) | 下一根 K 线开盘价成交,最真实 | | `next_open`(默认) | 下一根 K 线开盘价成交,最真实 |
| `next_close` | 下一根 K 线收盘价成交 | | `next_close` | 下一根 K 线收盘价成交 |
| `this_close` | 当根 K 线收盘价成交(未来函数风险,标注警告 | | `this_close` | 当根 K 线收盘价成交(**未来函数风险**,引擎自动在日志输出红色警告,并在 `BacktestResult.config` 中标记 `future_leak_warning=True` |
| `worst` | 对投资者最差价格(买入取 high,卖出取 low) | | `worst` | 对投资者最差价格(买入取 high,卖出取 low) |
| `best` | 对投资者最优价格(买入取 low,卖出取 high) | | `best` | 对投资者最优价格(买入取 low,卖出取 high) |
@@ -317,13 +412,29 @@ Step 2 是唯一需要逐行处理的步骤(仓位依赖前一 Bar 状态)
| `percent` | 每次用总资产的 N% | | `percent` | 每次用总资产的 N% |
| `signal_only` | 只生成信号,不模拟仓位 | | `signal_only` | 只生成信号,不模拟仓位 |
### 6.5 费用模型 ### 6.5 订单拒绝策略
当资金不足(买入)或持仓不足(卖出)时的处理方式:
| 模式 | 说明 |
|------|------|
| `reduce`(默认) | 自动减到最大可买/可卖量,记录成交 |
| `skip` | 跳过该信号,记录一条 `rejected=True` 的 Trade |
### 6.6 费用模型
- 佣金:`max(size * price * commission_rate, min_commission)`,买卖双向 - 佣金:`max(size * price * commission_rate, min_commission)`,买卖双向
- 印花税:`size * price * stamp_tax_rate`,仅卖出 - 印花税:`size * price * stamp_tax_rate`,仅卖出
- 滑点:`size * slippage_per_share` - 滑点:`size * slippage_per_share`
### 6.6 多策略批量回测 ### 6.7 基准对齐规则
`benchmark` 参数传入 `pd.DataFrame`,要求包含 `datetime``close` 列。引擎内部执行:
1.`datetime` 对齐(`merge` on `datetime`
2. 缺失值前向填充(`ffill`
3. 仍缺失的 Bar 不计入 alpha/information_ratio 计算
### 6.8 多策略批量回测 (P1)
```python ```python
# 多只股票 # 多只股票
@@ -356,6 +467,7 @@ results = engine.run_grid(df, params={
| 卡玛比率 | `calmar` | `annual_return / max_drawdown` | | 卡玛比率 | `calmar` | `annual_return / max_drawdown` |
| 总交易次数 | `total_trades` | `len(trades)` | | 总交易次数 | `total_trades` | `len(trades)` |
| 盈利/亏损次数 | `win_trades` / `lose_trades` | `trade_pnl > 0 / <= 0` | | 盈利/亏损次数 | `win_trades` / `lose_trades` | `trade_pnl > 0 / <= 0` |
| 被拒绝订单数 | `rejected_trades` | `trade.rejected == True` 的数量 |
| 胜率 | `win_rate` | `win_trades / total_trades` | | 胜率 | `win_rate` | `win_trades / total_trades` |
| 盈亏比 | `profit_factor` | `sum(win_pnl) / abs(sum(lose_pnl))` | | 盈亏比 | `profit_factor` | `sum(win_pnl) / abs(sum(lose_pnl))` |
| 平均盈利/亏损 | `avg_win` / `avg_loss` | 盈利/亏损交易均值 | | 平均盈利/亏损 | `avg_win` / `avg_loss` | 盈利/亏损交易均值 |
@@ -371,25 +483,32 @@ results = engine.run_grid(df, params={
### 8.1 命令 ### 8.1 命令
```bash CLI 自动获取 K 线数据,用户无需手动构造 DataFrame:
# DSL 字符串模式
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --cash 100000 --table
# DSL 文件模式 ```bash
# 基本用法:自动获取 K 线 + 执行回测
easy-tdx backtest SH 600519 --strategy "CROSS(MA(close,5),MA(close,20))" --cash 100000 --table
# 指定周期/复权/数量
easy-tdx backtest SH 600519 --strategy "CROSS(MA(close,5),MA(close,20))" \
--period DAILY --adjust QFQ --count 500 --table
# Python 类策略文件
easy-tdx backtest SH 600519 --strategy-file my_strategy.py --cash 100000 easy-tdx backtest SH 600519 --strategy-file my_strategy.py --cash 100000
# 参数化 # 参数化
easy-tdx backtest SH 600519 --strategy "CROSS(MA({short}),MA({long}))" --params short=5,long=20 easy-tdx backtest SH 600519 --strategy "CROSS(MA(close,{short}),MA(close,{long}))" \
--params short=5,long=20
# 指定周期/复权 # 参数扫描 (P1)
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --period 5MIN --adjust QFQ easy-tdx backtest SH 600519 --strategy "CROSS(MA(close,{short}),MA(close,{long}))" \
# 参数扫描
easy-tdx backtest SH 600519 --strategy "CROSS(MA({short}),MA({long}))" \
--grid short=5,10,15 --grid long=20,30,60 --sort-by sharpe --table --grid short=5,10,15 --grid long=20,30,60 --sort-by sharpe --table
# 输出 CSV # 输出 CSV
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --output csv easy-tdx backtest SH 600519 --strategy "CROSS(MA(close,5),MA(close,20))" --output csv
# 策略文件中用预计算指标列
easy-tdx backtest SH 600519 --strategy-file boll_strategy.py --indicators BOLL --table
``` ```
### 8.2 输出格式 ### 8.2 输出格式
@@ -401,10 +520,10 @@ easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --output csv
## 9. 测试计划 ## 9. 测试计划
``` ```
tests/unit/test_backtest_strategy.py # Strategy 基类 + 指标注入 tests/unit/test_backtest_strategy.py # Strategy 基类 + 指标注入 + 预计算列 + 缠论注入
tests/unit/test_backtest_dsl.py # DSL 解析 + 编译 tests/unit/test_backtest_dsl.py # DSL 解析 + 编译 (P1)
tests/unit/test_backtest_engine.py # 引擎核心(信号→成交→持仓) tests/unit/test_backtest_engine.py # 引擎核心(信号→成交→持仓)
tests/unit/test_backtest_orders.py # 撮合规则(5 种 execution 模式 tests/unit/test_backtest_orders.py # 撮合规则(5 种 execution + 2 种 reject policy
tests/unit/test_backtest_portfolio.py # 持仓追踪 + 资金曲线 tests/unit/test_backtest_portfolio.py # 持仓追踪 + 资金曲线
tests/unit/test_backtest_performance.py # 绩效计算(手工验证已知结果) tests/unit/test_backtest_performance.py # 绩效计算(手工验证已知结果)
tests/unit/test_backtest_cli.py # CLI 命令(click test runner tests/unit/test_backtest_cli.py # CLI 命令(click test runner
@@ -416,9 +535,10 @@ tests/unit/test_backtest_cli.py # CLI 命令(click test runner
## 10. 未来扩展点(v1 不实现,架构不堵死) ## 10. 未来扩展点(v1 不实现,架构不堵死)
- 事件驱动执行路径(支持日内策略、逐 tick 推演) - **缠论自动集成** (P1→v2):引擎自动调用 `ChanlunAnalyser`,策略通过 `self.chanlun` 直接获取笔/中枢/买卖点,无需手动注入
- 多品种组合回测(Portfolio 级别,同时持有多只股票) - **事件驱动执行路径**:支持日内策略、逐 tick 推演
- 风控模块(最大回撤止损、单笔止损、仓位上限) - **多品种组合回测**:Portfolio 级别,同时持有多只股票的资金分配
- 实时模拟交易(Strategy 基类接口可直接迁移) - **风控模块**:最大回撤止损、单笔止损、仓位上限
- 与缠论模块深度集成(策略可直接引用笔/中枢/买卖点信号) - **实时模拟交易**:Strategy 基类接口可直接迁移
- 可视化K 线 + 买卖点标注 + 资金曲线 - **可视化**K 线 + 买卖点标注 + 资金曲线
- **信号导出**`--save-signals` 将买卖点写入 DataFrame,联动 indicator 命令