Files
easy_tdx_max/docs/superpowers/specs/2026-06-09-backtest-engine-design.md
T
2026-06-09 16:05:17 +08:00

425 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Backtest Engine Design — easy-tdx
**Date:** 2026-06-09
**Status:** Approved
**Module:** `easy_tdx.backtest`
**Priority:** P0 — 量化工具链全栈的第一块拼图
---
## 1. 目标
为 easy-tdx 新增自建回测引擎模块,让用户能基于 easy-tdx 获取的 K 线数据执行策略回测、查看绩效报告。
### 核心约束
- 纯计算模块,与 `chanlun` 同级,零网络依赖
- 仅依赖 `pandas`/`numpy`(项目已有),不引入第三方回测库
- 接收 easy-tdx 标准 DataFrame`datetime, open, close, high, low, vol, amount`
- 双模式策略定义:Python 类继承 + DSL 公式语法
- v1 只实现向量化执行路径(日级策略),架构预留事件驱动扩展点
---
## 2. 架构
### 2.1 文件结构
```
src/easy_tdx/backtest/
├── __init__.py # 公开 API 导出
├── strategy.py # Strategy 基类 + StrategyDataProxy + IndicatorProvider
├── dsl.py # DSL 解析器 + @dsl_strategy 装饰器 + 字符串 DSL 编译
├── engine.py # BacktestEngine(向量化执行路径)
├── orders.py # OrderSimulator(撮合规则)
├── portfolio.py # PortfolioTracker(持仓/资金曲线)
├── performance.py # PerformanceAnalyzer(绩效指标计算)
├── types.py # Trade / Position / Signal / BacktestResult 数据类
└── cli.py # CLI 集成(easy-tdx backtest ...
```
### 2.2 模块交互流
```
easy_tdx MacClient.get_stock_kline() → DataFrame
BacktestEngine(strategy, cash=100000)
┌───────────┼───────────┐
▼ ▼ ▼
Strategy DSL Parser OrderSimulator
(Python类) (公式语法) (撮合规则)
│ │ │
└─────┬─────┘ │
▼ │
Signal (bool mask) │
│ │
▼ ▼
PortfolioTracker ←────┘
PerformanceAnalyzer
BacktestResult
(绩效指标 + 资金曲线 + 交易记录)
```
---
## 3. 核心数据类型
### 3.1 Signal
```python
@dataclass
class Signal:
datetime: int
direction: Literal["BUY", "SELL"]
size: float # 0 = 全仓/清仓
price: float | None # None = 市价
stop_loss: float | None
take_profit: float | None
```
### 3.2 Trade
```python
@dataclass
class Trade:
datetime: int
direction: Literal["BUY", "SELL"]
size: float
price: float
commission: float
slippage: float
pnl: float # 仅平仓时计算
```
### 3.3 Position
```python
@dataclass
class Position:
datetime: int
size: float # 正=多头,负=空头,0=空仓
avg_price: float
market_value: float
unrealized_pnl: float
```
### 3.4 BacktestResult
```python
@dataclass
class BacktestResult:
performance: dict[str, float]
equity_curve: pd.DataFrame # datetime, cash, position_value, total, drawdown, drawdown_pct
trades: pd.DataFrame # datetime, direction, size, price, commission, pnl
positions: pd.DataFrame # datetime, size, avg_price, market_value, unrealized_pnl
config: dict
```
方法:
- `to_json() → str`
- `to_dict() → dict`
- `summary() → None`(打印概要)
---
## 4. Strategy 基类
### 4.1 接口定义
```python
class Strategy(ABC):
def init(self) -> None:
"""注册指标。策略初始化时调用一次。"""
pass
def next(self) -> None:
"""每根 K 线调用。在此生成买卖信号。"""
pass
def I(self, func: Callable, *args, **kwargs) -> np.ndarray:
"""注册指标函数。init() 后一次性计算,返回完整数组。"""
...
def buy(self, size: float = 0, price: float | None = None,
stop_loss: float | None = None, take_profit: float | None = None) -> None:
"""买入。size=0 全仓。"""
...
def sell(self, size: float = 0, price: float | None = None,
stop_loss: float | None = None, take_profit: float | None = None) -> None:
"""卖出。size=0 清仓。"""
...
@property
def data(self) -> StrategyDataProxy: ...
@property
def position(self) -> Position: ...
```
### 4.2 StrategyDataProxy
```python
class StrategyDataProxy:
"""K 线数据代理。支持 .close[0](当前)、.close[-1](前一根)。"""
@property
def open(self) -> _SeriesAccessor: ...
@property
def close(self) -> _SeriesAccessor: ...
@property
def high(self) -> _SeriesAccessor: ...
@property
def low(self) -> _SeriesAccessor: ...
@property
def vol(self) -> _SeriesAccessor: ...
@property
def amount(self) -> _SeriesAccessor: ...
class _SeriesAccessor:
"""[0] 当前值、[-1] 前一根、切片。"""
def __getitem__(self, key: int) -> float: ...
def __len__(self) -> int: ...
```
### 4.3 Python 类策略示例
```python
class MACrossStrategy(Strategy):
def init(self):
self.ma5 = self.I(MA, self.data.close, 5)
self.ma20 = self.I(MA, self.data.close, 20)
def next(self):
if crossover(self.ma5, self.ma20):
self.buy(size=100)
elif crossover(self.ma20, self.ma5):
self.sell(size=100)
engine = BacktestEngine(strategy=MACrossStrategy, cash=100000)
result = engine.run(df)
```
---
## 5. DSL 策略定义
### 5.1 设计边界
| 能做 | 不做 |
|------|------|
| 指标交叉、比较、逻辑组合 | 循环、变量赋值、函数定义 |
| 内置常用函数(CROSS, ABOVE, BELOW, BETWEEN | 自定义控制流 |
| 参数化(可调窗口期) | 图灵完备 |
超出 DSL 能力的——直接用 Python 类。
### 5.2 两种 DSL 模式
**Python 装饰器模式**
```python
from easy_tdx.backtest import dsl_strategy
@dsl_strategy
def dual_ma(df):
buy = CROSS(MA(df.close, 5), MA(df.close, 20))
sell = CROSS(MA(df.close, 20), MA(df.close, 5))
return buy, sell
```
**字符串模式**CLI 用):
```bash
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --cash 100000 --table
```
### 5.3 内置 DSL 函数
复用 `MyTT.py` 已有实现:
| 函数 | 签名 | 含义 |
|------|------|------|
| `MA(series, n)` | `(ndarray, int) → ndarray` | 简单移动平均 |
| `EMA(series, n)` | `(ndarray, int) → ndarray` | 指数移动平均 |
| `RSI(series, n)` | `(ndarray, int) → ndarray` | 相对强弱 |
| `BOLL(series, n, k)` | `(ndarray, int, float) → tuple` | 布林带 |
| `MACD(series, fast, slow, signal)` | `(ndarray, ...) → tuple` | MACD |
| `CROSS(a, b)` | `(ndarray, ndarray) → ndarray[bool]` | 上穿检测 |
| `REF(series, n)` | `(ndarray, int) → ndarray` | 前 n 期值 |
| `HHV(series, n)` | `(ndarray, int) → ndarray` | n 期最高 |
| `LLV(series, n)` | `(ndarray, int) → ndarray` | n 期最低 |
| `BETWEEN(x, a, b)` | `(ndarray, ...) → ndarray[bool]` | 区间判断 |
| `COUNT(cond, n)` | `(ndarray[bool], int) → ndarray` | n 期满足条件次数 |
### 5.4 DSL 编译器
`DSLCompiler.compile(func)` 流程:
1. 调用 `func(mock_df)` 捕获 DSL 函数调用
2. 记录 `(buy_mask, sell_mask)` 信号生成规则
3. 动态生成 Strategy 子类
引擎侧优化:DSL 策略不逐 Bar 调用 `next()`,直接用 bool mask 一次性生成全部 Signal。
---
## 6. 引擎执行流
### 6.1 BacktestEngine 构造参数
```python
class BacktestEngine:
def __init__(
self,
strategy: type[Strategy] | Strategy,
cash: float = 100000.0,
commission: float = 0.0003,
min_commission: float = 5.0,
stamp_tax: float = 0.001,
slippage: float = 0.0,
execution: str = "next_open", # "next_open" | "next_close" | "this_close" | "worst" | "best"
position_mode: str = "full", # "full" | "fixed" | "percent" | "signal_only"
benchmark: pd.DataFrame | None = None,
):
...
```
### 6.2 四步执行管道
1. **信号生成**DSL → bool maskPython 类 → trace next() 生成 mask
2. **信号→订单**OrderSimulator):根据 execution 规则确定成交价,根据仓位模式确定量
3. **持仓追踪**PortfolioTracker):逐 Bar 更新现金/持仓/市值/回撤
4. **绩效分析**PerformanceAnalyzer):从资金曲线计算全部指标
Step 2 是唯一需要逐行处理的步骤(仓位依赖前一 Bar 状态)。其余步骤全向量化。
### 6.3 OrderSimulator 成交价规则
| 模式 | 说明 |
|------|------|
| `next_open`(默认) | 下一根 K 线开盘价成交,最真实 |
| `next_close` | 下一根 K 线收盘价成交 |
| `this_close` | 当根 K 线收盘价成交(有未来函数风险,标注警告) |
| `worst` | 对投资者最差价格(买入取 high,卖出取 low) |
| `best` | 对投资者最优价格(买入取 low,卖出取 high) |
### 6.4 仓位管理规则
| 模式 | 说明 |
|------|------|
| `full`(默认) | 买入用全部现金,卖出清仓 |
| `fixed` | 每次固定股数 |
| `percent` | 每次用总资产的 N% |
| `signal_only` | 只生成信号,不模拟仓位 |
### 6.5 费用模型
- 佣金:`max(size * price * commission_rate, min_commission)`,买卖双向
- 印花税:`size * price * stamp_tax_rate`,仅卖出
- 滑点:`size * slippage_per_share`
### 6.6 多策略批量回测
```python
# 多只股票
results = engine.run_many({
"SH600519": df_519,
"SZ000858": df_858,
})
# → dict[str, BacktestResult]
# 参数扫描
results = engine.run_grid(df, params={
"short": [5, 10, 15],
"long": [20, 30, 60],
})
# → list[GridResult],支持 .sort_by("sharpe").to_table()
```
---
## 7. 绩效指标
| 指标 | key | 算法 |
|------|-----|------|
| 总收益率 | `total_return` | `(total[-1] / total[0]) - 1` |
| 年化收益率 | `annual_return` | `(1 + r) ** (252/n) - 1` |
| 最大回撤 | `max_drawdown` | `max((peak - total) / peak)` |
| 最大回撤天数 | `max_dd_duration` | 首次新高 - 回撤起点 |
| 夏普比率 | `sharpe` | `(mean(ret) - rf/252) / std(ret) * sqrt(252)` |
| 索提诺比率 | `sortino` | 分母只用负收益标准差 |
| 卡玛比率 | `calmar` | `annual_return / max_drawdown` |
| 总交易次数 | `total_trades` | `len(trades)` |
| 盈利/亏损次数 | `win_trades` / `lose_trades` | `trade_pnl > 0 / <= 0` |
| 胜率 | `win_rate` | `win_trades / total_trades` |
| 盈亏比 | `profit_factor` | `sum(win_pnl) / abs(sum(lose_pnl))` |
| 平均盈利/亏损 | `avg_win` / `avg_loss` | 盈利/亏损交易均值 |
| 最大单笔盈亏 | `max_win` / `max_loss` | 单笔极值 |
| 平均持仓天数 | `avg_holding_days` | 买入到卖出的 Bar 数均值 |
| 收益波动率 | `volatility` | `std(daily_ret) * sqrt(252)` |
| 基准超额收益 | `alpha` | 策略收益 - 基准收益(需 benchmark |
| 信息比率 | `information_ratio` | 超额收益均值 / 跟踪误差(需 benchmark |
---
## 8. CLI 集成
### 8.1 命令
```bash
# DSL 字符串模式
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --cash 100000 --table
# DSL 文件模式
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(5),MA(20))" --period 5MIN --adjust QFQ
# 参数扫描
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
# 输出 CSV
easy-tdx backtest SH 600519 --strategy "CROSS(MA(5),MA(20))" --output csv
```
### 8.2 输出格式
默认 JSON`--table` 切换表格,`--output csv` 输出 CSV。与现有 CLI 行为一致。
---
## 9. 测试计划
```
tests/unit/test_backtest_strategy.py # Strategy 基类 + 指标注入
tests/unit/test_backtest_dsl.py # DSL 解析 + 编译
tests/unit/test_backtest_engine.py # 引擎核心(信号→成交→持仓)
tests/unit/test_backtest_orders.py # 撮合规则(5 种 execution 模式)
tests/unit/test_backtest_portfolio.py # 持仓追踪 + 资金曲线
tests/unit/test_backtest_performance.py # 绩效计算(手工验证已知结果)
tests/unit/test_backtest_cli.py # CLI 命令(click test runner
```
全部离线测试,使用手工构造的 DataFrame fixture,零网络依赖。
---
## 10. 未来扩展点(v1 不实现,架构不堵死)
- 事件驱动执行路径(支持日内策略、逐 tick 推演)
- 多品种组合回测(Portfolio 级别,同时持有多只股票)
- 风控模块(最大回撤止损、单笔止损、仓位上限)
- 实时模拟交易(Strategy 基类接口可直接迁移)
- 与缠论模块深度集成(策略可直接引用笔/中枢/买卖点信号)
- 可视化(K 线 + 买卖点标注 + 资金曲线)