订单必须在发出之前拦下
一句话总结
人多敲一位数,是一定会发生的事。防住它的不是人的注意力,而是嵌在订单路径上的检查,而且检查不是只要开着就行——什么计入累计、按什么顺序检查,会改变结果。
为什么需要它
成交之后再发现,与发出之前就拦住,完全是两回事。已经发出的订单就在市场上了,要想撤回,要么做反向交易,要么求交易所取消。在这期间的价格变动,就是实打实的损失。所以风险检查被放在订单路径上,也就是订单发往市场之前的区段。
这在有些国家不是惯例,而是规定。美国证券交易委员会的规则 15c3-5,要求提供市场准入的券商,在订单发出之前实施财务和监管方面的风险控制。规定正文可以在 eCFR 的 240.15c3-5 中读到,为什么需要这样的规则,则留在 联邦公报上的采纳公告 里。规定要求的是“设置检查”,而不是“限额定为多少”。数字由各自来定。本文和实验中出现的限额数值,全部是合成值。
工作原理
检查通常分为六路。
单笔订单限额。为一笔订单的数量和金额设上限。多敲了一位数的订单,大部分会在这里被拦下。金额是数量乘以价格,所以如果只设数量上限,在价格高的标的上就会漏过去。
价格区间。拦住偏离参考价一定百分比以上的限价。这里必然会碰到一个问题——没有参考价的标的怎么办。可能是新上市,可能是行情推送断了,也可能是标的代码错了。这时如果按“没有可比较的东西,所以通过”处理,检查存在的理由就消失了。把它移到既不是通过也不是拒绝的暂挂,让人来看,才是正统做法。暂挂和拒绝要分开统计。后面这个差别会拉得很大。
限制标的名单与重复订单。前者是单纯的名单比对,后者则是抓住同样条件的订单在短时间内重复出现。以为画面卡住了,把按钮点了两次,或者重试逻辑没收到响应又发了一次,都会在这里被抓住。
累计限额。以账户、交易员、交易台为单位,对未成交敞口之和设上限。这里有本文最重要的规则。被拒绝的订单不消耗累计额度。这看似理所当然,却很容易搞错——如果直接把订单日志扫一遍求和,就连被拒绝的订单也会算进去,这样限额比实际更快被占满,完好的订单会接连被拦住。被拦住的人不知道原因。画面上只显示“超出限额”,而他的实际敞口只有限额的一半。
熔断开关。单个拒绝与整体阻断是不同的动作。如果短时间内拒绝集中出现,那很可能不是某一笔订单出了错,而是发送的一方出了故障。这时要把这条路径整个关闭。并且这个状态必须留在文件或存储里。如果只放在内存里,进程一重启,阻断就解除了,而出故障的一方仍然是坏的。
检查的顺序。顺序一变,同一笔订单所附的原因就会不同。如果只是原因不同,那是报告的问题,但如果区分暂挂与拒绝的检查前后移动了位置,情况就不同了。暂挂不算作拒绝,所以仅仅把价格区间检查提前,就会让熔断开关打开的时间点后移。在这期间进来的订单,不会被拦住,直接发了出去。
金额比较使用 decimal。如果用浮点数比较限额,恰好等于限额的金额,有的日子会通过,有的日子会被拦住。订单消息的字段名称和含义,遵循 FIX 标准 的规定。
在现场相遇的样子
有一次,收到反馈说某个交易台的订单每天下午都被拦住。数了一下敞口,只有限额的一半。原因是累计是从订单日志里数出来的。上午被拒绝的几笔大订单占用了限额,而拒绝在画面的任何地方都不会显示为敞口。
还有一次,熔断开关打开了,10 分钟后却自己解除了。部署运行时进程重启了,阻断状态只存在于内存里。同样的故障当天又发生了两次。
下一项实验要做什么
做出 47 笔订单和限额定义,从单笔订单限额开始,把价格区间、限制标的、重复、累计敞口和熔断开关一层一层叠上去。与把被拒绝的订单也算进累计的错误实现作比较,列出哪些订单被冤枉地拦住了,再把检查顺序调换一位,确认打开熔断开关的订单会变得不同。最后,用收紧了限额的第二套配置,把同一天再运行一遍,找出现在的配置所漏掉的订单。