1. 课程导入与场景锚定

在量化交易、交易所运维和券商自营的技术团队里,逐笔回放(tick-by-tick replay)是一项高频使用的基础能力。它把历史行情按原始逐笔成交、逐笔委托、快照等消息的时间顺序重新“播放”一遍,让策略、撮合、风控、监控等系统在接近真实的时间序列上重新运行。与直接读取历史K线或日频数据不同,逐笔回放保留了每一笔委托的到达顺序、价格档位变化和成交明细,因此能复现出盘中那些稍纵即逝的细节。

但逐笔数据量极大。以沪深A股为例,一只活跃股票一天的逐笔委托与逐笔成交合计可达数十万条,全市场一天的消息量在亿级;期货主力合约在夜盘与日盘叠加时,单合约单日十万级消息也很常见。如果按真实时间 1:1 回放,复盘一个交易日的异常往往要花掉一整个交易日。于是“倍速控制”和“断点跳转”成为回放系统里两个最直接、也最影响效率的功能:前者决定你多快看完一段行情,后者决定你能否直接落到关心的那一刻。

三类典型场景,三种不同的诉求

交易复盘关注的是“当时为什么做出这个决策”。研究员通常需要从开盘集合竞价看到收盘,重点观察某几笔大单出现前后的盘口变化。此时倍速控制的价值在于:平淡时段用 8 倍甚至 16 倍快速掠过,临近关键时点切回 1 倍逐笔细看。断点跳转则用于直接定位到某笔成交的时间戳,例如“10:23:41.500 那笔 5000 手买单前后 30 秒”。

策略验证关注的是“策略在完整历史序列上的表现是否可复现”。回测引擎本身是事件驱动的,但人工抽检某段行情时,需要把回放速度调到接近实盘节奏,观察信号触发、下单、成交回报的时序是否合理。这里倍速不能随意拉高,因为部分策略逻辑依赖消息间隔;断点跳转则用于反复回到同一段行情做参数对比。

异常排查关注的是“某个错误发生在哪一毫秒、由哪条消息触发”。例如撮合引擎在 14:57 集合竞价阶段出现委托价格越界,工程师需要精确跳到该时间点,以 0.5 倍甚至逐条单步的方式查看消息流。此时断点跳转的精度(能否按时间戳、按消息序号、按业务主键定位)比倍速更重要。

逐笔回放:场景诉求 → 核心能力 → 时间轴操作 交易复盘 看大单前后盘口变化 平淡段快进、关键段细看 诉求:效率优先 策略验证 信号→下单→回报时序 同段行情反复对比参数 诉求:可复现 异常排查 定位到毫秒级错误消息 单步、逐条查看消息流 诉求:精度优先 倍速控制 0.5× / 1× / 2× / 4× / 8× / 16× 按消息时间戳缩放播放节奏 影响:吞吐、时序保真度 断点跳转 按时间戳 / 消息序号 / 主键 定位后从该点继续回放 影响:定位精度、状态重建 回放时间轴(示例) 09:30 11:00 14:57 8× 快进 断点:10:23:41.500 大单 1× 细看 0.5× 单步排查 倍速决定“多快看完” 断点决定“从哪开始看” 两者组合 = 可控的回放节奏

倍速控制:不只是“快”,而是节奏可控

倍速控制的本质,是把消息时间戳之间的真实间隔按比例压缩或拉伸。设相邻两条消息的真实时间差为 Δt,倍速为 s,则回放时实际等待时间为 Δt / s。s=8 时,原本 1 秒的间隔变成 125 毫秒;s=0.5 时则拉长到 2 秒。看似简单,但工程上有三个必须处理的问题。

第一是时间基准。逐笔消息的时间戳可能来自交易所撮合时钟,也可能来自本地接收时钟,两者存在毫秒级偏差。回放系统应以消息自带的时间戳为准,而不是本地接收时间,否则倍速放大后偏差会被同步放大。第二是高倍速下的消息堆积。s=16 时,若某秒内有 2000 条消息,回放线程每秒要处理 32000 条,消费端(策略、撮合模拟器)可能来不及处理。常见做法是设置“最大消息速率”上限,超过时自动降速或丢弃非关键消息并记录告警。第三是倍速切换的平滑性。从 8× 切到 1× 时,若直接改参数,正在等待的那条消息可能被延迟很久;正确做法是在下一条消息到达时生效,并重置等待计时器。

一个可参考的实现片段如下(示例参数,非实测):

// 回放调度核心(伪代码,示例)
double speed = 8.0;              // 当前倍速
long lastTs = -1;                // 上一条消息时间戳(微秒)
long maxMsgPerSec = 50000;       // 消费端吞吐上限

void onMessage(TickMsg m) {
    if (lastTs >= 0) {
        long gapUs = m.ts - lastTs;          // 真实间隔
        long waitUs = (long)(gapUs / speed); // 按倍速缩放
        if (waitUs > 0) sleepMicros(waitUs);
    }
    lastTs = m.ts;
    dispatch(m);                  // 交给策略/撮合/监控
}

注意 speed 的修改应放在消息处理之外,由控制线程写入一个 volatile 变量,调度线程在每条消息前读取,这样切换倍速不会打断当前等待。

断点跳转:定位精度与状态重建

断点跳转要回答两个问题:跳到哪和跳过去之后系统状态对不对。定位方式通常有三种:按时间戳(如 10:23:41.500)、按全局消息序号(第 1,234,567 条)、按业务主键(某笔委托号或成交编号)。时间戳定位最直观,但同一毫秒内可能有多条消息,需要约定“跳到该时间戳的第一条”还是“最后一条”;消息序号定位最精确,适合排查;业务主键定位最贴近业务语义,但需要建立索引。

更关键的是状态重建。逐笔回放不是无状态地读文件——策略持仓、订单簿、风控额度都依赖此前所有消息的累积。如果直接从第 100 万条开始播,订单簿是空的,策略会做出错误判断。因此断点跳转通常有两种模式:快速重建(从当日开盘或最近快照开始,以最高倍速静默重放到断点,不触发业务回调,只更新内部状态)和快照加载(若系统定期保存订单簿与持仓快照,直接加载断点前最近的快照,再补放少量消息)。前者实现简单但耗时,后者快但依赖快照机制的完整性。

实践建议:把“跳转”设计成“重建 + 续播”两阶段。重建阶段关闭策略信号输出与日志,只更新状态;续播阶段恢复正常回调。这样既保证状态正确,又不会在重建过程中产生大量误触发记录。

本课程的任务地图

围绕这两个能力,本课程将沿着一条从“看懂”到“做对”的路径展开。先建立逐笔消息模型与时间轴的基本认知,理解委托、成交、快照三类消息的字段与顺序关系;再分别深入倍速控制的调度算法、速率上限、切换语义,以及断点跳转的定位索引、状态重建与快照配合;随后进入工程实现,包括回放引擎的线程模型、背压处理、与策略/撮合/监控系统的对接方式;最后通过交易复盘、策略验证、异常排查三个综合案例,把倍速与跳转组合成可复用的操作套路,并给出常见故障(时间戳乱序、跳转后状态漂移、高倍速丢消息)的排查思路。