TL;DR
- 工业级回测的本质是一个确定性的离散事件模拟(DES):所有逻辑以事件时间推进,靠最小堆事件队列按因果顺序驱动,才能从根上避免“未来函数 / look-ahead bias”。
- 回测里要跑的不是“简化模型”,而是生产撮合的状态机副本:同一输入事件序列,必须得到同一订单簿状态与成交输出(可复现、可对账)。
- 两大地雷:行情乱序/重复/丢包(必须用交易所序列号清洗重建确定性事件流)以及同时间戳事件的次级排序(否则两次回测可能跑出不同结果)。
- 性能上别幻想“多线程事件循环”:确定性通常要求核心循环单线程;吞吐靠数据列式存储 + 异步预取 + 缓冲、以及对订单簿/内存布局的极致优化;真正的并行来自任务级并行(多参数、多策略网格)。
1. 为什么很多回测会“看起来赚钱、实盘送钱”
- 未来数据污染:T 时刻策略不小心看到了 T+1 的信息(时间戳/切片处理失误),收益曲线像开挂,实盘直接破防。
- 低保真模拟:只用分钟/日线做回测,忽略盘口深度与成交路径;一旦策略依赖微观结构(盘口失衡、撮合细节),结果几乎没参考价值。
- 非确定性:相同策略+相同数据两次回测结果不同(竞态、随机性、外部依赖);对严肃研究来说这是“不可接受”。
- 性能拖垮迭代:逐笔/L2 数据动辄几十 GB/天,回测一年跑几天,策略迭代速度会慢到没有竞争力。
2. 关键原理:把回测当成“确定性 DES 系统”来设计
核心心智模型
市场状态只在“事件”发生时改变:新单、撤单、成交、行情更新、定时任务……
回测时钟不连续流动,而是直接跳到下一个事件时间点。
这要求你用一个中心化的事件优先级队列(最小堆)驱动整个系统。
- 事件时间 vs 处理时间:所有判断与状态变更只能基于事件时间;处理时间只用于性能指标。混用两者是逻辑 Bug 的温床。
- 状态机复制:撮合引擎本身就是状态机(核心状态:订单簿)。回测的“仿真撮合”必须是生产逻辑的高保真副本,才能做一致性对账与复现。
- 并发的边界:为了确定性,事件处理通常要定义清晰的顺序;同时间戳事件必须有 tie-break(例如:行情先于策略订单),否则会出现“时间旅行”或结果漂移。
3. 事件队列怎么落地:最小堆 + 明确的 tie-break
事件循环的核心很朴素:不断从堆顶取出最早的事件 → 推进模拟时钟 → 分发处理 → 产生新事件再入堆。
真正的工程难点不在“写出循环”,而在确保任何一次运行都严格相同:
- 时间戳精度(高频常用纳秒级);
- 同时间戳事件的稳定排序规则(行情/回报/策略请求的优先级);
- 循环内部严禁慢操作(磁盘 I/O、网络请求),否则你不是在回测,是在“慢动作演出”。
4. 最容易踩的坑:行情数据清洗与乱序处理
原始交易所 Feed 天生就“脏”:UDP/多路分发会带来乱序、重复、丢包。
直接拿来回放,等于在随机噪声上做科学实验。
工程建议
- 用交易所序列号重建确定性事件流:排序、去重、补洞/标洞;按(交易日, 品种)分区处理,处理跨文件边界与序列号重置。
- 当乱序窗口过大时,缓存可能撑爆内存:需要可控的溢写策略(buffer spill)。
- 产出数据最好是严格按时间戳+序列号排序的列式格式(如 Parquet),让回测侧顺序扫描、最大化吞吐。
5. 性能与保真度:别在错误的地方“省略现实”
- 订单簿数据结构决定 CPU:常见组合是“价格档位用平衡树/跳表 + 订单ID用哈希索引”,以便 O(1) 查单、O(logN) 找最优价位。
- I/O往往是第一瓶颈:列式存储 + 只读所需列 + Snappy/ZSTD + 异步预取/缓冲,避免事件循环被卡住。
- 市场冲击要内生模拟:大市价单会吃掉多档深度并改变后续成交价;不模拟冲击,很多策略回测会严重乐观。
- 延迟模型要可配置:订单事件的时间戳应为 T + latency(固定值或分布采样),否则回测无法反映网络抖动与处理排队带来的影响。
6. 适用场景:什么时候值得上“工业级回测”
- 策略依赖盘口/逐笔微观结构(做市、订单簿信号、抢占队列等)。
- 你需要对撮合/风控/网关做一致性验证,或者要让回测与实盘共享撮合逻辑。
- 回测迭代速度已经成为瓶颈(回一次要几小时/几天)。