Files
awayingsandClaude 990b4a7802 docs: 拆分超大文档——教程/参考分离,去过期版本横幅,模型枚举归并
- backtest_usage(742→485):CLI 章移交 cli-backtest.md,示例/注意事项
  抽至 backtest-examples.md,重建目录
- quantitative-guide(628→325):第 5-7 章(滑点/执行仿真/归因/工作流)
  抽至 quantitative-advanced.md 并重编号
- api_reference(704→478):删除过期版本横幅(1.16.2)与快速开始教程段;
  数据模型/枚举与 field_mapping.md 逐表核对后去重(field_mapping 为唯一权威);
  WebSocket 节随 web-api.md 合并移除
- field_mapping:吸收 MAC 协议枚举(Period/Adjust/Category/BoardType/
  SortType/ExMarket),全部文档回到 ≤500 行

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-08 23:21:44 +08:00

486 lines
14 KiB
Markdown
Raw Permalink 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.
# easy-tdx 回测引擎使用手册
`easy_tdx.backtest` 是一个纯计算层的向量化策略回测引擎,零网络依赖,可完全离线运行。
## 目录
- [快速开始](#快速开始)
- [编写策略](#编写策略)
- [策略生命周期](#策略生命周期)
- [访问行情数据](#访问行情数据)
- [注册技术指标](#注册技术指标)
- [金叉检测](#金叉检测)
- [生成交易信号](#生成交易信号)
- [引擎配置](#引擎配置)
- [成交价规则](#成交价规则)
- [仓位模式](#仓位模式)
- [费用模型](#费用模型)
- [订单拒绝策略](#订单拒绝策略)
- [获取回测结果](#获取回测结果)
- [绩效指标一览](#绩效指标一览)
- [资金曲线](#资金曲线)
- [交易记录](#交易记录)
- [序列化输出](#序列化输出)
- [CLI 命令行](#cli-命令行) → 见 [cli-backtest.md](./cli-backtest.md)
- [进阶用法](#进阶用法)
- [预计算指标列](#预计算指标列)
- [缠论结果注入](#缠论结果注入)
- [自定义策略文件](#自定义策略文件)
- [完整示例](#完整示例) → 见 [backtest-examples.md](./backtest-examples.md)
---
## 快速开始
```python
import pandas as pd
from easy_tdx.backtest import BacktestEngine, Strategy, crossover
from easy_tdx import MyTT
# 1. 定义策略
class DualMAStrategy(Strategy):
def init(self):
self.ma5 = self.I(MyTT.MA, self.data.close, 5)
self.ma20 = self.I(MyTT.MA, self.data.close, 20)
self.cross = crossover(self.ma5, self.ma20)
def next(self):
if self.cross[self._bar_index]:
self.buy(size=0) # 全仓买入
elif self.position["size"] > 0:
self.sell(size=0) # 全部卖出
# 2. 准备数据(DataFrame 必须包含 datetime, open, close, high, low 列)
# 通过 TdxClient 获取真实数据:
# from easy_tdx import TdxClient
# client = TdxClient()
# df = client.get_stock_kline("SZ", "000001", period="DAILY", count=500)
# 3. 运行回测
engine = BacktestEngine(DualMAStrategy, cash=100000)
result = engine.run(df)
# 4. 查看结果
print(f"总收益率: {result.performance['total_return']:.2%}")
print(f"夏普比率: {result.performance['sharpe']:.2f}")
print(f"最大回撤: {result.performance['max_drawdown']:.2%}")
print(f"交易次数: {result.performance['total_trades']}")
```
---
## 编写策略
### 策略生命周期
继承 `Strategy` 基类,实现两个方法:
```python
class MyStrategy(Strategy):
def init(self):
"""回测开始前调用一次。注册指标、初始化内部状态。"""
pass
def next(self):
"""每根 K 线调用一次。根据当前行情生成交易信号。"""
pass
```
引擎内部执行顺序:
```
_bind_data(df) → _call_init() → 逐 bar 调用 _set_bar_index(i) + _call_next()
```
### 访问行情数据
通过 `self.data` 代理访问 K 线数据,支持相对索引:
```python
def next(self):
# 当前 bar(索引 0
price = self.data.close[0]
# 前一根 bar(索引 -1
prev_price = self.data.close[-1]
# 前两根 bar(索引 -2
prev2 = self.data.close[-2]
```
> **提示**:回溯索引(如 `[-1]`、`[-2]`)在回测首根 bar 数据不足时返回
> `NaN` 而非报错。如需完全跳过指标预热期,设置 `warmup_bars`(见下文)。
**标准列**`open`, `close`, `high`, `low`, `vol`, `amount`
```python
self.data.open[0] # 开盘价
self.data.close[0] # 收盘价
self.data.high[0] # 最高价
self.data.low[0] # 最低价
self.data.vol[0] # 成交量
self.data.amount[0] # 成交额
```
**自定义列**:如果 DataFrame 包含额外列(如 `MACD_DIF`),通过属性名直接访问:
```python
self.data.MACD_DIF[0] # 自动通过 __getattr__ 查找
```
**获取完整数组**`.raw` 属性返回 numpy 数组,可传入指标函数:
```python
close_array = self.data.close.raw # numpy ndarray
```
### 注册技术指标
使用 `self.I()``init()` 中注册指标。`_SeriesAccessor` 参数会自动解包为 numpy 数组:
```python
from easy_tdx import MyTT
def init(self):
# 均线
self.ma5 = self.I(MyTT.MA, self.data.close, 5)
self.ma20 = self.I(MyTT.MA, self.data.close, 20)
# MACD
self.dif, self.dea, self.macd = self.I(MyTT.MACD, self.data.close)
# 布林带
self.upper, self.mid, self.lower = self.I(MyTT.BOLL, self.data.close, 20)
def next(self):
# 用索引访问指标值
if self.ma5[self._bar_index] > self.ma20[self._bar_index]:
self.buy(size=0)
```
### 金叉检测
`crossover(a, b)` 检测序列 a 从下方穿越 b(金叉):
```python
from easy_tdx.backtest import crossover
def init(self):
self.ma5 = self.I(MyTT.MA, self.data.close, 5)
self.ma20 = self.I(MyTT.MA, self.data.close, 20)
self.golden = crossover(self.ma5, self.ma20) # 金叉:ma5 上穿 ma20
self.death = crossover(self.ma20, self.ma5) # 死叉:ma20 上穿 ma5
def next(self):
if self.golden[self._bar_index]:
self.buy(size=0)
if self.death[self._bar_index]:
self.sell(size=0)
```
### 生成交易信号
`next()` 中调用 `self.buy()``self.sell()`
```python
self.buy(size=100) # 买入 100 股
self.buy(size=0) # 全仓买入(引擎自动计算股数)
self.buy(size=100, price=10.5) # 限价买入
self.buy(size=100, stop_loss=9.0, take_profit=12.0) # 带止损止盈
self.sell(size=100) # 卖出 100 股
self.sell(size=0) # 全部卖出
```
**参数说明**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `size` | float | 0 | 交易数量,0 = 全仓/清仓 |
| `price` | float \| None | None | 限价,None = 市价单 |
| `stop_loss` | float \| None | None | 止损价。当根 bar 的 low 触及止损价时触发平仓信号 |
| `take_profit` | float \| None | None | 止盈价。当根 bar 的 high 触及止盈价时触发平仓信号 |
> **止损/止盈成交时点**:SL/TP 信号触发后,**延迟到下一根 bar 开盘成交**(与普通
> 策略信号一致),而非在信号当根以触发价成交。若下一根跳空,取对持仓者更不利的实际
> 开盘价(卖出取 `min(下一根开盘, 触发价)`)。这避免了"假设能在止损价精确成交"的
> 前视偏差,回测结果更贴近真实滑点与跳空场景。
**查看当前持仓**
```python
def next(self):
pos = self.position # {"size": 100.0}
if pos["size"] > 0:
# 当前持有多头
pass
```
---
## 引擎配置
```python
engine = BacktestEngine(
strategy=MyStrategy,
cash=100000.0, # 初始资金
commission=0.0003, # 佣金率(万三)
min_commission=5.0, # 最低佣金(元)
stamp_tax=0.001, # 印花税率(千一,仅卖出)
slippage=0.0, # 滑点(每股)
execution="next_open", # 成交价规则
position_mode="full", # 仓位模式
reject_policy="reduce", # 拒绝策略
warmup_bars=0, # 指标预热 bar 数(前 N 根不调用 next(),不产生信号)
)
```
### 指标预热(warmup
技术指标(如 MA20、MACD)在前若干根 bar 的值是 `NaN` 或不稳定的。为避免
预热期产生错误信号或访问越界:
- **回溯访问容错**`self.data.close[-1]` / `[-2]` 等负向索引在首根 bar
(数据不足)时返回 `NaN` 而非抛 `IndexError`,策略无需手动加 `bar_index > 0`
守卫。
- **warmup_bars 参数**:设置后引擎在前 `warmup_bars` 根不调用 `next()`、不
产生信号(资金曲线照常推进)。例如用 MA20 策略时可设 `warmup_bars=20`
```python
engine = BacktestEngine(MyStrategy, cash=100000, warmup_bars=20)
```
### 成交价规则
| 模式 | 成交价 | 说明 |
|------|--------|------|
| `next_open` | 下一根 K 线开盘价 | **默认**,最贴近实盘 |
| `next_close` | 下一根 K 线收盘价 | 日内策略常用 |
| `this_close` | 当前 K 线收盘价 | ⚠️ 存在**未来函数**风险,引擎会标记 `future_leak_warning` |
| `worst` | 买入取高价 / 卖出取低价 | 保守估计滑点 |
| `best` | 买入取低价 / 卖出取高价 | 乐观估计 |
信号在 bar N 产生时,`next_*` 模式在 bar N+1 成交,`this_close` 在 bar N 成交。
### 仓位模式
| 模式 | `buy(size=X)` 行为 |
|------|---------------------|
| `full` | `size=0` 时全仓,按 100 股整手计算;`size>0` 时买入指定股数 |
| `fixed` | 严格按 `size` 买入指定股数 |
| `percent` | `size` 表示总资产的百分比(如 0.5 = 50%),按 100 股整手计算 |
### 费用模型
引擎模拟 A 股费用结构:
- **佣金**`max(成交金额 × commission, min_commission)`,双向收取
- **印花税**`成交金额 × stamp_tax`,仅卖出时收取
- **滑点**`成交股数 × slippage`,双向收取
```python
# 免佣回测
engine = BacktestEngine(MyStrategy, commission=0.0, min_commission=0.0, stamp_tax=0.0)
# 模拟实际佣金
engine = BacktestEngine(MyStrategy, commission=0.00025, min_commission=5.0, stamp_tax=0.001)
```
### 订单拒绝策略
当资金不足(买入)或持仓不足(卖出)时:
| 策略 | 行为 |
|------|------|
| `reduce` | 减少到可执行的股数,生成实际成交 |
| `skip` | 拒绝整个订单,标记 `rejected=True` |
---
## 获取回测结果
```python
result = engine.run(df)
```
`result``BacktestResult` 对象,包含:
### 绩效指标一览
```python
perf = result.performance # dict[str, float]
```
| 指标 | Key | 说明 |
|------|-----|------|
| 总收益率 | `total_return` | (期末权益 / 期初资金) - 1 |
| 年化收益率 | `annual_return` | 按年化复利计算 |
| 最大回撤 | `max_drawdown` | 峰值到谷底的最大跌幅比例 |
| 最大回撤持续 | `max_dd_duration` | 最大回撤持续的 bar 数 |
| 夏普比率 | `sharpe` | (日超额收益均值 / 日标准差) × √252 |
| 索提诺比率 | `sortino` | 分母只用负收益标准差 |
| 卡玛比率 | `calmar` | 年化收益 / 最大回撤 |
| 年化波动率 | `volatility` | 日收益率标准差 × √252 |
| 总交易次数 | `total_trades` | 卖出次数(完整闭环) |
| 盈利次数 | `win_trades` | PnL > 0 的卖出 |
| 亏损次数 | `lose_trades` | PnL ≤ 0 的卖出 |
| 被拒绝次数 | `rejected_trades` | 资金/持仓不足被拒绝的总次数 |
| 胜率 | `win_rate` | 盈利次数 / 总交易次数 |
| 盈亏比 | `profit_factor` | 总盈利 / |总亏损| |
| 平均盈利 | `avg_win` | 盈利交易的平均 PnL |
| 平均亏损 | `avg_loss` | 亏损交易的平均 PnL |
| 最大盈利 | `max_win` | 单笔最大盈利 |
| 最大亏损 | `max_loss` | 单笔最大亏损 |
| 平均持仓天数 | `avg_holding_days` | 固定值 5.0(待改进) |
### 资金曲线
```python
equity = result.equity_curve # pd.DataFrame
# 列:datetime, cash, position_value, total, drawdown, drawdown_pct
```
| 列 | 说明 |
|----|------|
| `datetime` | 时间 |
| `cash` | 可用现金 |
| `position_value` | 持仓市值 |
| `total` | 总权益 = cash + position_value |
| `drawdown` | 回撤金额 = 峰值 - 当前总权益 |
| `drawdown_pct` | 回撤比例 |
### 交易记录
```python
trades = result.trades # pd.DataFrame
# 列:datetime, direction, size, price, commission, pnl, rejected
```
### 持仓快照
```python
positions = result.positions # pd.DataFrame
# 列:datetime, size, avg_price, market_value, unrealized_pnl
```
### 序列化输出
```python
# JSON 字符串
json_str = result.to_json()
# Python 字典(DataFrame 转为 records 列表)
data = result.to_dict()
# 打印概要到标准输出
result.summary()
```
### 配置快照
```python
config = result.config
# {"cash": 100000, "commission": 0.0003, "execution": "next_open",
# "position_mode": "full", "reject_policy": "reduce",
# "future_leak_warning": False}
```
---
## CLI 命令行
CLI 用法见 [cli-backtest.md](./cli-backtest.md)(回测/寻优/组合/run-all 命令与参数)。
## 进阶用法
### 预计算指标列
策略可使用 DataFrame 中预先计算的指标列。通过 `self.data.列名` 访问:
```python
from easy_tdx.backtest import BacktestEngine, Strategy
from easy_tdx.indicator import compute_indicators
class BollingerStrategy(Strategy):
def init(self):
# BOLL_UPPER 已在 DataFrame 中预计算
self.upper = self.data.BOLL_UPPER
self.lower = self.data.BOLL_LOWER
def next(self):
if self.data.close[0] < self.lower[0]:
self.buy(size=0) # 跌破下轨买入
elif self.data.close[0] > self.upper[0]:
self.sell(size=0) # 突破上轨卖出
# 预计算指标
df = compute_indicators(df, ["BOLL"])
engine = BacktestEngine(BollingerStrategy)
result = engine.run(df)
```
### 缠论结果注入
v1 提供手动注入接口,策略通过 `self.chanlun` 访问:
```python
from easy_tdx.backtest import BacktestEngine, Strategy
from easy_tdx.chanlun import ChanlunAnalyser
class ChanlunStrategy(Strategy):
def init(self):
pass
def next(self):
cl = self.chanlun
if cl is None:
return
# 使用缠论买卖点
# mmd_list = cl.get("mmd", [])
# ...
# 获取缠论结果
analyser = ChanlunAnalyser("SZ000001", "DAILY")
cl_result = analyser.process_klines(df)
# 注入引擎
engine = BacktestEngine(ChanlunStrategy)
result = engine.run(df, chanlun_result=cl_result.to_dict())
```
### 自定义策略文件
CLI 的 `--strategy-file` 加载 Python 文件,文件中必须包含一个 `Strategy` 子类:
```python
# my_strategy.py
from easy_tdx.backtest import Strategy, crossover
from easy_tdx import MyTT
class MyStrategy(Strategy):
"""双均线策略。"""
def init(self):
self.ma5 = self.I(MyTT.MA, self.data.close, 5)
self.ma20 = self.I(MyTT.MA, self.data.close, 20)
self.cross = crossover(self.ma5, self.ma20)
def next(self):
if self.cross[self._bar_index]:
self.buy(size=0)
elif self.position["size"] > 0:
self.sell(size=0)
```
使用:
```bash
easy-tdx backtest SZ 000001 --strategy-file my_strategy.py --table
```
---