3. 回测引擎与盘口数据对接
盘口交易策略的回测,成败往往不取决于策略逻辑本身,而取决于回测引擎能否"忠实还原"实盘那一刻的盘口环境。实盘中,策略看到的是逐笔委托、逐笔成交、盘口快照按时间戳连续推送的数据流;如果回测引擎只是把历史数据当成一张静态表格逐行读取,那么回测结果与实盘表现之间就会出现系统性偏差。本节围绕三个关键环节展开:盘口数据的加载与规范化、多源数据的时间对齐、以及事件驱动机制如何把数据"喂"给策略。
一、盘口数据的加载与规范化
盘口数据通常来自两类文件:一类是交易所或行情商提供的逐笔数据(tick-by-tick),包含逐笔委托、逐笔成交;另一类是定时切片生成的盘口快照(snapshot),例如每500毫秒或每3秒记录一次买卖十档价格与数量。回测引擎的第一步不是"读文件",而是把不同来源、不同格式的数据统一成内部事件模型。
以常见的快照数据为例,原始字段往往包含时间戳、最新价、成交量、买一至买十价量、卖一至卖十价量。加载时需要完成几件事:校验时间戳单调递增、剔除重复记录、把价格与数量转换为定点数或整型以避免浮点误差、把"累计成交量"转换为"区间增量成交量"。这一步看似琐碎,却直接决定后续事件是否可信。例如某条快照的累计成交量小于上一条,说明数据源发生了重置或乱序,引擎应标记异常而不是静默接受。
# 快照加载与规范化的核心逻辑(示例)
def load_snapshot(path):
rows = read_csv(path)
events = []
prev_cum_vol = 0
prev_ts = -1
for r in rows:
ts = parse_ts(r["timestamp"])
if ts <= prev_ts: # 时间戳非递增,跳过或告警
continue
cum_vol = int(r["cum_volume"])
delta_vol = cum_vol - prev_cum_vol
if delta_vol < 0: # 数据重置,标记异常
delta_vol = 0
events.append({
"ts": ts,
"bids": [(int(r[f"bid{i}_price"]), int(r[f"bid{i}_vol"])) for i in range(1, 11)],
"asks": [(int(r[f"ask{i}_price"]), int(r[f"ask{i}_vol"])) for i in range(1, 11)],
"delta_vol": delta_vol
})
prev_cum_vol, prev_ts = cum_vol, ts
return events
规范化后的每条记录,本质上是一个"盘口状态事件"。引擎内部应使用统一的事件结构,让快照、逐笔委托、逐笔成交都能映射到同一套字段上,这样策略代码在回测与实盘之间才能复用。
二、时间对齐:让不同频率的数据落在同一根时间轴上
实盘中,逐笔成交、逐笔委托、盘口快照是三条独立的数据流,各自带着自己的时间戳。回测时如果简单地"先读完A文件再读B文件",就会破坏它们之间的因果顺序。正确做法是建立一个统一的时间轴,把所有事件按时间戳归并排序,形成一条有序事件队列。
时间对齐要处理三个典型问题。第一是时钟源不一致:交易所时间、行情商时间、本地接收时间可能相差几十毫秒,需要明确以哪个时钟为准,通常以交易所撮合时间为准。第二是粒度不一致:逐笔数据是毫秒级,快照是秒级,策略若依赖快照,就必须在快照到达时才更新盘口视图,而不能用逐笔数据"提前"构造出未来才出现的盘口。第三是同一时间戳的排序:当成交与快照时间戳相同时,应约定"先成交、后快照"或反之,并在整个回测中保持一致,否则同一份数据两次回测可能得到不同结果。
三、事件驱动机制:把数据流变成策略可消费的信号
事件驱动是回测引擎与实盘环境保持一致的桥梁。引擎不应让策略主动"拉取"下一行数据,而应由引擎在事件到达时"推送"给策略。典型的事件循环如下:从有序队列取出最早的事件,更新引擎内部的盘口状态(订单簿),然后调用策略的对应回调,例如 on_snapshot、on_trade、on_order,策略在回调中做出决策并提交委托,委托再进入撮合模块。
这里有一个容易被忽视的细节:策略提交的委托,其成交判断必须使用"当前时刻之后"的盘口数据,而不能使用当前快照本身。否则就会出现"用未来信息成交"的乐观偏差。工程上通常把委托放入待撮合队列,在下一个事件到达时才尝试撮合,这样回测的成交假设才与实盘"报单后等待对手方"的时序一致。
四、把"一致"落到可验证的检查上
完成上述三步后,还需要一组可执行的校验来证明"回测输入与实盘盘口环境一致"。实践中常用以下检查:其一,时间戳单调性检查,遍历事件队列确认无回退;其二,盘口重建抽查,随机选取若干快照时刻,用引擎内部订单簿重建的买卖十档与原始快照逐档比对,价格与数量应完全一致;其三,未来函数扫描,在策略回调中注入断言,禁止访问时间戳大于当前事件的数据;其四,可复现性检查,同一份数据、同一参数连续回测两次,成交明细与最终盈亏应逐笔相同。
下面给出一份可直接用于对接验收的检查表,字段按盘口回测的实际需要设计:
| 检查项 | 检查方法 | 通过标准 |
|---|---|---|
| 时间戳单调性 | 遍历归并后事件队列 | 严格单调不减,无重复时间戳冲突 |
| 盘口重建一致性 | 随机抽取快照时刻比对十档价量 | 逐档价格、数量完全一致 |
| 同刻事件排序 | 构造成交与快照同时间戳用例 | 排序规则固定,两次回测结果相同 |
| 未来函数 | 回调内断言数据时间戳 ≤ 当前事件 | 无越界访问告警 |
| 成交时序 | 检查委托是否仅由后续事件撮合 | 无"当前快照自成交"记录 |
| 可复现性 | 同数据同参数回测两次 | 成交明细与盈亏逐笔一致 |
需要强调的是,上述检查中的具体数值(如快照频率、十档深度、时间戳精度)应依据实际数据源确定,示例中的500毫秒、十档等仅为说明性假设。真正决定回测可信度的,是加载阶段的数据清洗是否严格、对齐阶段的时钟与排序约定是否统一、事件循环中是否存在任何形式的未来信息泄漏。把这三件事做扎实,回测引擎输出的成交与盈亏才具备与实盘对照讨论的价值,后续的盘口策略评估与报告生成也才有可靠的数据基础。