账簿不是靠删除来修正的
一句话总结
已经发出去的成交,不是说它错了就把它擦掉,而是追加一条撤回的记录来修正。虽然要多占位置,但几个月之后无论谁来问,都能读出“这个数字为什么会变成这样”。
为什么需要它
成交并不是终点。如果价格输错了,或者对手方没有确认,这笔成交就会被撤销,如果只是值有一点错误,就会被更正。问题出在后面。成交账本已经流向了很多地方。持仓报告发出去了,风险限额的计算跑过了,盈亏也已经出来了。在这种状态下,如果悄悄改写账本里的一行,昨天发出去的报告与今天看到的账本就会对不上,而对不上的原因,在账本的任何地方都找不到。
所以金融账簿是往后写的。撤销,是保留原记录,再追加一行符号相反的冲销记录;更正,是先冲销原记录,再以修正后的值重新写一行。这是会计中沿用已久的方式,FIX 协议 的成交报告里,也专门有用来通知撤销和更正的位置。字段的含义可以在 Fiximate 中确认。撤销必须在几分钟内申请之类的具体流程,各个市场都不同,所以本实验把作为资料提供的规则文件当作本实验的假设。
工作原理
首先确认通知指向什么。撤销和更正通知中带有原成交的标识符,如果这个标识符在账本里没有,就什么也不能做。其中最危险的,是“找一笔相似的成交来应用”的实现。如果把不相干的成交撤回了,一笔错误就变成了两笔。
接下来是两次收到同一条通知的情况。重发是正常的事。如果不记住通知标识符,收到什么就冲销什么,那么已经被撤销过一次的成交,会被冲销两次,持仓就会翻到反方向。同样的输入无论放进去多少次,结果都必须相同,这就是幂等。
第三种是对已经被撤销的成交再次到来的通知。无论是再次撤销,还是对已撤销的成交作价格更正,都不能接受。为此,处理程序必须记住“到目前为止撤销了什么”。
更正比撤销更费事。可能只有价格变了,也可能只有数量变了,所以通知里没有的值,必须原样使用原来的值,并且随着变化的值,成交金额、平均单价和持仓都要一起变动。金额用 decimal 来处理,并预先确定舍入方式。如果用浮点数,一次更正就会让最后一位发生晃动,看起来像是没有修改的地方出了偏差。
最后是迟到的撤销。在持仓报告发出之后到达的通知,只修改账簿是不够的。已经发出的报告变成了错误的,所以要成为重新报告的对象。不过,也不是所有迟到的通知都会使持仓变动。只改价格的更正不触碰数量,所以持仓不变,只有金额不同。如果不把这两种情况分开,重新报告的清单就会无谓地变长,而变长的清单,没有人会去看。
在现场相遇的样子
有一次,批处理把夜间到达的撤销通知读了两遍,持仓整天都以相反的符号显示。原因只有一个,就是没有记录通知标识符。更糟的是,那个批处理还在覆盖原记录,所以为了查明什么东西在什么时候消失了,花了两天时间。
另一件事是持仓对账。重新计算的持仓与已经发出的报告,在各个标的上略有不同,差别大部分可以用迟到的撤销来解释,只有一个标的解释不了。解释不了的那一个标的,才是真正的缺陷。如果把差别笼统地记成“对账不一致”,那一笔就会被埋没。
第三种经常遇到的,是无法追溯的账簿。即使冲销和重新记账都做对了,如果没有记下这一行来自哪条通知,几周之后就没有人能解释。那时出现的问题总是一样的——这个标的的数量为什么会变成这样。如果在账簿的每一行上,都附上生成这一行的通知的标识符和原因,这个问题一次查询就能解决。附原因时,有一点要注意。重发的副本上也带有原因,但账簿上必须留下的,是实际被应用的通知的原因。从通知数据流中取最后一条的实现,会在这里悄悄地出错。
而且,所有这些处理都不应依赖顺序。通知可能不按原来的顺序到来,批处理也可能运行两次。如果把到目前为止应用了什么作为状态持有,并据此判定,那么同样的输入无论按什么顺序放进去多少次,都会得到同样的账簿。这种性质不应靠嘴来主张,而要通过把同样的输入放进去两次、再比较结果文件的方式来测试。没有这个测试,“幂等”这句话就只是一种希望。
下一项实验要做什么
做出成交账本、撤销与更正通知数据流,以及已经发出的持仓报告。把通知与原成交连起来,区分出要应用的和要丢弃的,然后保留原记录不动,追加冲销记录,做出新的账簿。反映更正,重新得出成交金额和平均单价,重新计算持仓,并与已有的报告对照。用文件确认,把同一个通知数据流放进去两次,账簿是否相同,并把报告时间之后到达的通知收集起来,生成重新报告对象清单和更正报告。