TT Lab
开始
学习 学习路径 课程

资本市场与清结算

快照与增量接错,盘口就会悄悄出错

在 TT Lab 中继续学习

一句话总结

行情由一张快照,以及随后接连而来的增量更新组成。快照所标记的序号与增量的序号必须有重叠地衔接,只要序号断过一次,就不要再继续叠加增量,而要等待新的快照。漏掉这条规则的订单簿不会停止,也不会变空。它只是带着一点错误继续运转。

为什么需要它

如果每次都发送整个订单簿,带宽就承受不了。所以所有行情推送都采用同一种形态。先把当前状态拍成一张发过去(快照),此后只一个一个地发送变化的价位(增量)。接收方先用快照搭起订单簿,再依次叠加增量。

问题出在接缝处。快照拍下来需要时间,而在这期间增量仍在不断发出。所以快照里会同时带上“到这里为止都已经反映了”的序号。接收方必须把序号早于快照的增量丢掉,从那之后开始叠加。如果无视这个值,把收到的增量全部叠加,已经反映过的变更就会被再应用一次;反过来,如果把快照到达之前的增量全部丢掉,这期间的变更就会消失。FIX 系列规范在处理恢复时点时,总是一并携带“最后处理的序号”,原因就在这里(FIX Trading Community 标准文档,字段含义可以在 FIXimate 中查到)。

工作原理

增量的动作只有三种。新增、变更、删除。不过,大部分推送并不发送动作的名称。数量为 0 即删除,非 0 的值,如果该价格在订单簿中已有,就是变更,没有就是新增。也就是说,动作不是由消息决定的,而是由接收方手里所持有的状态决定的。所以,即使收到同一个增量文件,起始状态不同,结果也会不同。把数量 0 当作值存起来的实现,会留下数量为 0 的价位,而这个价位一旦占据了最优报价的位置,画面上最优报价处就会显示“0 股等待”。

订单簿有必须遵守的不变式。最优买价必须低于最优卖价。如果两者相等或者颠倒,就是交叉,交叉的订单簿现实中不存在。这种状态出现在画面上,就说明我的重构是错的。而且每个价位的数量必须是正数,排序必须始终保持。

在衡量不变式的计算里还有一个陷阱。为了看价差是不是一个最小变动价位(tick),把两个价格相减的那一刻,用二进制浮点数是得不出答案的。55.15 减去 55.10 会得到 0.04999999999999716,它不等于 0.05。价格是十进制小数,所以要用十进制来处理(decimal 模块文档)。这个失误不会抛出异常,所以会以“一个 tick 的价差一次都没有出现过”这样悄无声息的 0 表现出来。

最后是重新同步。序号断了,意味着有我没收到的变更,而那个变更是什么,没有任何办法知道。这时如果继续叠加增量,订单簿就会在断开的地方分叉,永远回不来。正确的处理是停止应用增量,等待下一张快照。所以推送会定期重新发送快照。

在现场相遇的样子

某个团队收到了“最优买价显示得比卖价高”的反馈。刷新画面之后就正常了,所以被当作画面问题受理。实际上,几分钟前出现过一个很短的缺口,当时本应消失的买入价位原样留了下来。刷新修好的不是画面,而是订单簿。在这几分钟里发出去的订单,是看着并不存在的报价来定价的。

另一件经常见到的是迟到。序号是过去的,却后到的行,如果按到达顺序叠加,已经被覆盖的值就会退回旧值。这同样不会出错就过去了。唯一的防御,是按序号顺序而不是到达顺序来叠加,并丢弃已经过去的序号。

第三种是略有不同的事故。它发生在重构出来的订单簿不止存在于一处的时候。如果画面和策略引擎各自叠加增量,两个订单簿即使接收同样的输入,也会互相不同。因为只有一方发现了缺口并重新同步,另一方却继续叠加。在这种地方,“哪一边是对的”,人用眼睛是分不清的。所以重构要在一处完成,再把结果分发出去,或者至少要让两个订单簿一并携带自己反映到了哪个序号。调查时最先要问的,也是这个——这个画面看到了第几号序号。

修改接收方时,还有一件必须一并确定的事。那就是重新同步期间,用什么来显示订单簿。如果让旧订单簿原样留着,人会以为那就是现在的行情,如果把它清空,下单画面又会像停住了一样。无论哪种选择,“处于这种状态”这个事实都必须在画面和日志中显露出来。如果把这个选择搁置,在把缺口检测修好之后,反而会增加“画面总是空的”这样的反馈。

下一项实验要做什么

做出四个标的在同一个通道上流动的合成推送,用一张快照搭起订单簿,再叠加 1,300 多行增量。加入丢弃序号重叠区间的规则,找出订单簿出现交叉的那一刻的序号,并数一数,在断开的地方继续叠加所得的结果,与从新快照重新搭起的结果,在多少个价位上出现分歧。最后,用加入了两条规则的实现运行整个数据流,确认它与基准快照没有一个价位的出入。