10. 过程数据(proc.*)与动态回测(设计说明)
本章说明 qteasy 中过程数据(process data)的定义、访问方式,以及回测时静态分支与动态分支的选择与一致性约定,供实现与扩展时参考。使用层面的说明可在《策略如何声明与使用数据》与 API 文档中配合阅读。
10.1. 1. 背景与目标
1.1 过程数据的含义
部分策略需要依赖随回测或实盘执行路径而变化的数据,例如:
当前/历史持仓、可用现金;
历史成交数量、成交价格、交易成本;
由持仓与价格推导的市值、总资产等。
这类数据无法在回测开始前一次性预生成,只能由 Backtester(回测)或 Trader(实盘)在运行过程中维护,并在策略生成信号的每一步按“当前可见范围”提供给策略。我们将其统称为过程数据。
1.2 设计目标
统一入口:过程数据与静态历史数据一样,通过
Strategy.get_data()获取,降低学习成本。无前视:策略在生成第 k 步信号时,不能看到第 k 步的成交结果,只能使用已完成步的历史。
回测/实盘一致:同一套策略与
get_data('proc.xxx')调用方式,在回测与实盘中均可使用;当需要过程数据时走动态执行路径,否则可与原有静态路径在结果上保持一致。
10.2. 2. 过程数据的统一定义(proc.*)
2.1 命名与曝光方式
所有过程数据对策略以
proc.<field_name>的形式曝光,例如proc.own_cash、proc.trade_records。过程数据不需要在策略的
__init__中通过data_types声明,由 Backtester / Trader 在运行时注入,策略只需在realize()中按需调用get_data('proc.xxx', ...)。
2.2 已实现的内建字段
当前版本已实现并可在策略中使用的过程数据字段包括:
类别 |
字段名 |
含义 |
|---|---|---|
账户标量 |
|
当前步开始时账户总现金 |
|
当前步开始时可用于下单的现金 |
|
|
当前步开始时总资产市值(持仓估值 + 现金) |
|
持仓向量 |
|
当前步开始时各标的总持仓数量 |
|
当前步开始时各标的可卖出数量 |
|
|
当前步开始时各标的持仓市值(由内部 price 与持仓计算) |
|
成交结果 |
|
各步各标的的实际成交数量(正买负卖) |
|
各步各标的的交易成本 |
|
|
各步各标的的成交价格 |
上述字段在回测与实盘中的时间语义与可见性约束见第 4 节。
2.3 后续可扩展字段(可选)
设计上可继续扩展的字段包括:proc.realized_pnl、proc.unrealized_pnl、proc.last_trade_price、proc.last_trade_volume 等,具体以实现与文档为准。
10.3. 3. 访问接口:Strategy.get_data() 与 proc.*
3.1 静态数据(无 proc. 前缀)
单源:
self.get_data('close_E_d');多源:self.get_data('close_E_d', 'high_E_d')。静态数据不支持
lag/window参数;若传入则抛出英文ValueError。
3.2 过程数据(proc. 前缀)
调用示例:
self.get_data('proc.own_cash'):截至当前步的现金序列;self.get_data('proc.own_cash', lag=0):最近一步的现金;self.get_data('proc.own_cash', lag='1d'):按时间回溯 1 天对应的步;self.get_data('proc.own_cash', window='5d'):过去 5 天内的窗口切片。
约束:
一次调用仅允许一个
proc.*字段;多字段或与静态数据混合调用时抛出英文ValueError。lag与window不可同时指定;支持lag为整数(步)或字符串(如'1d'、'8h'),window为字符串(如'5d'、'8h')。
返回值:统一为
np.ndarray,形状与数据类型以 API 文档为准。
10.4. 4. 回测分支选择与过程数据协作
4.1 静态分支与动态分支
静态分支(
_backtest_static_operator):一次性对所有时间步调用run_strategies生成全部信号,再用 Numba 向量化函数backtest_batch_steps等完成回测。适用于不依赖过程数据的策略。动态分支(
_backtest_dynamic_operator):按时间步循环,每步生成信号 → 解析并模拟成交 → 更新持仓与现金,再进入下一步。过程数据由 Backtester 维护并在每步前注入 Operator,供策略通过get_data('proc.xxx')访问。
Backtester 在运行前通过 Operator.check_dynamic_data() 决定走哪条分支。
4.2 check_dynamic_data() 的判定逻辑(当前实现)
以下任一为真则返回 True,走动态分支:
op_type == ‘stepwise’:Operator 显式配置为逐步模式。
策略源码中使用 proc.*:通过
_strategies_use_proc_data()检查各策略realize()的源码是否包含'proc.'或"proc.",若包含则视为依赖过程数据。
因此,只要在 realize() 中调用了 get_data(‘proc.xxx’),无需任何声明即可自动走动态分支。过程数据仅通过 proc.* 访问,不再支持通过 DataType 声明(旧式 op_* 类型已移除)。
4.3 无前视保证
策略在第 k 步生成信号时:
账户/持仓类(如
own_cashes、own_amounts)最多可见到索引[0..k](即当前步开始时的状态);成交类(如
trade_records、trade_cost)最多可见到[0..k-1],不包含本步尚未发生的成交。
实现上由 Operator 的
_current_signal_index与 Strategy 的_get_process_data_single()按上述范围切片,Backtester 在每步生成信号前更新索引,保证无前视。
4.4 Backtester 与 Operator 的注入关系
Backtester(动态分支):在
_backtest_dynamic_operator入口处将own_cashes、available_cashes、own_amounts_array、available_amounts_array、trade_records_array、trade_cost_array、trade_price_array、trade_price_data等以_process_data_sources注入 Operator,并将_process_time_index设为与op_signal_index对齐的时间轴。Operator:在
run_strategy(step_index)中,在每次调用stg.generate()前根据group_timing_table与group_merge_type计算并更新_current_signal_index,供 Strategy 截取“当前可见”的过程数据。
4.5 实盘(Trader)中的过程数据
当 operator.check_dynamic_data() 为 True 时,Trader 在 _run_strategy() 中会:
将当前账户现金、持仓、可用数量及当前价格等组装为单步的
_process_data_sources与_process_time_index(实盘单次运行视为一步);策略在该步内调用
get_data('proc.own_cash')等即可获得当前账户/持仓视图;成交历史在该步内为空,与“尚未发生本步成交”的语义一致。
10.5. 5. 动态/静态路径一致性约定
当策略不使用过程数据时:应走静态分支;若因其他原因走动态分支,回测结果应与静态分支在数值上完全一致(相同配置与数据下)。
当策略使用过程数据时:必须走动态分支,否则
_process_data_sources未注入会触发 RuntimeError。测试约定:使用
StaticSignalStg(纯静态)与ProcAwareButStaticLogicStg(调用 proc 但不用于信号)在相同配置下回测,对own_cashes、own_amounts_array、trade_records_array等做数值一致性断言,见tests/test_process_data_api.py的 B 组。
10.6. 6. 测试与文档索引
专门测试:
tests/test_process_data_api.pyA 组:
check_dynamic_data()在纯静态 / 使用 proc.* 策略下的行为;B 组:静态策略与“调用 proc 但逻辑等价”策略的回测数组一致性;
C 组:
get_data对静态多源、lag/window 拒绝、proc 单字段与混合调用的报错行为;D 组:
proc.trade_records无前视验证;E 组:基于 process data 的真实动态策略路径正确性。
项目记忆:
.cursor/rules/process-data-and-dynamic-backtest.mdc(实现与约定摘要)。策略数据总览: 策略如何声明与使用数据;回测入口与模式:回测、实盘与优化。