技术博客

行情回放系统:复盘与回测的数据基础设施

第 76 / 141 篇 · 2025-03-04 · secfin · 行情系统回放回测

前四篇讲的都是"行情怎么实时走"。这一篇反着来:怎么把行情存下来、再原样放出来。回放系统的需求方很多——量化团队要拿真实行情回测策略,运维要复盘某次行情断线的全过程,测试要拿历史洪峰压新上线的接收机——需求不同,但底层是同一件事:录制保真,回放可控。

录什么:原始流,不是解析结果

第一个决策是录制点在协议栈的哪一层。答案是尽可能靠底层:直接录 UDP 报文(pcap 或接收机上应用层落盘),带内核或网卡时间戳。为什么不录解析后的盘口快照?因为解析是有损的——十档视野外的档位、已被合并的逐笔、乱序到达的原始顺序,解析完就丢了。原始流是无损的:任何时候都可以用最新版解析器重放一遍,得到和当年一致(或修正 bug 后更好)的结果。

录制格式要带两样东西:每个报文的到达时间戳和接收通道号。时间戳精度要到微秒级——回放按原始间隔重演时,毫秒误差在洪峰段会显著改变流量形状。通道号用于多频道并行流的交错记录,回放时按通道还原各自的序列号空间。

落盘成本要精打细算。全市场 Level-2 一天原始流几十 GB,直接存一年是 PB 级。常规手段:按证券切热冷(全量留近期三个月,远期只留主力证券或降采样),压缩用 zstd(二进制流熵低,5~10 倍压缩率很常见),逐笔数据里价格字段做 delta 编码(相邻成交价差值小,整型压缩收益巨大)。

三种回放语义

"回放"这个词掩盖了三种不同的需求,系统要分开提供:

按原速回放。运维复盘、故障重现用。报文按录制时间戳的相对间隔依次发出,流量形状和当天一致。看的是"当时到底发生了什么",速度不重要,保真才重要。

加速回放。回测和压测用,N 倍速:$T_{\text{out}} = T_{\text{recorded}} / N$。策略回测一天行情等 20 分钟跑完。工程要点是发送端不能被下游拖慢——加速回放时出口带宽是原始洪峰的 N 倍,网卡队列和下游接收机的承受能力都要校核,否则回放系统自己成了瓶颈,测的是回放机而不是被测系统。

定点注入。从某个时刻、某个证券、某条消息开始放,前后状态可控制。测试环境造数据用这个:构造"十档瞬间蒸发"这种极端场景,实时行情里等一个月未必等到一次,录制数据里随时可调。

时间戳的暗线

回放的可信度全压在时间戳上,这里有三个坑。

时钟源一致性。录制时的时钟(网卡硬件戳、内核软件戳、应用层 gettimeofday)精度等级不同,跨机器录制的流混用时钟源不统一,回放交错就错位。严肃的录制集群要 PTP 对时(《时钟同步与交易时间戳》讲的同一套设施),至少保证同机房微秒级一致。

回放时钟漂移。加速回放靠 sleep 控制节奏,但 sleep 的精度是毫秒级且不准(定时器合并、调度延迟)。N 倍速下系统误差也乘 N。低要求的场景容忍,压测场景不行——用忙等加单调时钟(CLOCK_MONOTONIC)自旋到目标时刻,CPU 换确定性。

相对时间与绝对时间。回放报文里的交易所时间戳是历史绝对时间,下游如果用"报文时间戳 vs 本机现在"算延迟会得出荒谬结论(回放昨天的行情,延迟显示 24 小时)。回放器要么改包重写时间戳(破坏校验和,需下游配合),要么下游明确知道"这是回放模式"切换参照系。工程上后者干净,但要求所有下游程序都实现了回放开关——这是回放系统推广时最烦的兼容性工作。

录制质量本身是数据

录制系统自己会丢包,而且它丢的包会静默污染所有下游分析——回测策略在一段有缺口的行情上跑出漂亮曲线,上线实盘就露馅。所以录制侧要做和接收侧同等级的质量监控:序列号空洞计数、落盘延迟分布、磁盘写满保护(写满宁可停录加告警,不能覆盖旧数据)。每份录制文件带质量元数据(空洞数、丢包率、时钟源),下游使用前先查元数据,缺口大的时段自动从分析集剔除。

回放系统的终极价值是把"行情"从不可复现的流变成可寻址的数据集:某年某月某日某频道,像取文件一样取出来。有了这个底座,压测、复盘、回测、演练才是工程而不是玄学。

录制-存储-回放:原始流全程无损,语义在出口处分叉接收机录制UDP 原始报文压缩存储zstd + delta 编码原速回放(复盘)N 倍速回放(压测/回测)定点注入(造数)每个报文带:到达时间戳(微秒)+ 频道号;跨机录制需 PTP 对时N 倍速 = 出口带宽放大 N 倍,先校核网卡与下游接收机回放文件带质量元数据:序列号空洞数、丢包率、时钟源,用前先查下游需感知回放模式,禁用 报文时间戳减本机时钟 的延迟算法