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

资本市场与清结算

多打了一位数的订单会在哪里被拦下

在 TT Lab 中继续学习

目标

在订单数据流上,把单笔订单限额、价格区间、限制标的、重复、累计敞口和熔断开关,一层一层叠上去,并用数字确认更新累计的时机和检查的顺序,会怎样改变结果。

为什么重要

成交之后再发现,与发出之前就拦住,是两回事。已经发出的订单就在市场上,撤回的代价就是实打实的损失。所以检查被放在订单路径上。可是检查不是只要开着就行。如果把被拒绝的订单也计入累计限额,实际敞口只有限额一半的人就会被拦住;如果让没有参考价的标的直接通过,检查存在的理由就消失了。如果把暂挂算作拒绝,熔断开关就会在莫名其妙的地方打开;如果把阻断状态只放在内存里,重启一次就解除了。本实验要把这些陷阱一个一个亲手做出来,并用数字确认。

步骤

  1. 用 python3 在 /root/risk/data 中生成 orders.jsonl、limits.json、limits_tight.json、refprice.csv、restricted.txt 和 accounts.csv。
  2. 只设置单笔订单限额(数量、金额),生成 /root/risk/single.csv。
  3. 加入价格区间检查,生成 /root/risk/band.csv。没有参考价的标的是暂挂。
  4. 加入限制标的与重复订单检查,生成 /root/risk/screen.csv。
  5. 加入账户、交易台的累计敞口限额,生成 /root/risk/exposure.csv,并把把被拒绝的订单也计入累计的错误实现会拦住的订单,写入 /root/risk/naive_blocked.csv。
  6. 加入熔断开关,生成 /root/risk/final.csv,并把阻断状态留在 /root/risk/killswitch.json 中。
  7. 用把价格区间检查提前的顺序重新运行,把差别写入 /root/risk/order_compare.csv 和 /root/risk/order_compare.json。
  8. 把按原因统计的结果写入 /root/risk/stats.json,把与收紧了限额的配置的比较写入 /root/risk/tuning.json。

参考

生成订单数据流和限额定义

用 python3 在 /root/risk/data 中生成 orders.jsonl、limits.json、limits_tight.json、refprice.csv、restricted.txt 和 accounts.csv。请原样使用不使用随机数的生成脚本。

不能直接拿来客户的订单日志,所以要做出形态相同的合成数据。不使用随机数,是为了无论谁运行多少次都得到同样的数据,才能互相对照判定。时间也要钉在数据里——如果用今天的日期来做,明天再运行时判定就会不同。评分器会把数据转换成标准形式来核对指纹,所以如果手工修改,后面的步骤就会全部被堵住。

设置单笔订单限额

在 /root/risk/single.csv 中,首行写 order_id,decision,reason,notional,只设置数量上限和金额上限,把 47 笔订单全部各写一行。

金额是数量乘以限价。如果只设数量上限,在价格高的标的上就会漏过去,所以两个都要看。标准顺序中数量在金额之前,所以两者都超过的订单,原因是前一个。通过的订单的原因是 OK,金额写到小数点后第二位。

价格区间,与没有参考价的标的

在 /root/risk/band.csv 中,首行写 order_id,decision,reason,在上一步的两项检查之上加入价格区间检查,把 47 笔订单全部写出。没有参考价的标的是 HOLD 和 NO_REF_PRICE。

拦住偏离参考价超过允许百分比的限价。比较时如果用除法,就会掺进舍入,所以把差额乘以 100,再与允许百分比乘以参考价作比较。如果让没有参考价的标的直接通过,检查存在的理由就消失了。要为既不是通过也不是拒绝的情形设一个位置。

过滤限制标的与重复订单

在 /root/risk/screen.csv 中,首行写 order_id,decision,reason,在上一步之上加入限制标的检查和重复订单检查,把 47 笔订单全部写出。

限制标的是名单比对,成本最低,所以在标准顺序中排在最前面。重复是指账户、标的、方向、数量和限价都相同、并且落在窗口内的订单,而比较的对象只有之前通过的订单。如果把被拒绝的订单作为基准,出过一次错的人,连第二笔正常订单也会被拦住。

累计敞口限额,并与错误实现作比较

在 /root/risk/exposure.csv 中,以 order_id,decision,reason 写下加到账户、交易台累计限额为止的判定,并在 /root/risk/naive_blocked.csv 中,首行写 order_id,account,desk,reason_naive,写出把被拒绝的订单也计入累计的实现会拦住的订单。

累计要按账户、按交易台分别统计。只累加通过的订单的金额,拒绝和暂挂不消耗累计。然后请再故意做一个错误的实现——把订单日志直接扫一遍,累加所有订单的金额。在两个结果中,正确的一边是通过、错误的一边不是通过的订单,就是被冤枉拦住的订单。

打开熔断开关,并把状态留在文件里

在 /root/risk/final.csv 中,以 order_id,decision,reason 写下加到熔断开关为止的判定,并在 /root/risk/killswitch.json 中写入 engaged、engaged_at、trigger_order_id、rejects_in_window、threshold、window_sec 和 blocked_orders。

单个拒绝与整体阻断是不同的动作。如果窗口内拒绝累积到标准的笔数,从那时起就把这条路径整个关闭。暂挂和阻断不算作拒绝。让它打开的那笔订单自己保持原来的判定,从它之后的订单起全部阻断。而且,阻断状态务必留在文件里——如果只放在内存里,重启一次就解除了。

把检查顺序调换一位再运行

用把价格区间检查提前到数量上限之前的顺序重新运行,在 /root/risk/order_compare.csv 中,以 order_id,decision_canonical,reason_canonical,decision_band_first,reason_band_first 只写出变得不同的订单,并在 /root/risk/order_compare.json 中写入两种顺序的摘要。

看起来只有原因会变,其实不是这样。没有参考价的标的会变成暂挂,而暂挂不算作拒绝,所以一旦区间检查提前,就会有一笔拒绝变成暂挂,熔断开关打开的位置也会后移。在这期间进来的订单,不会被拦住,直接发了出去。摘要里请放入 accept_canonical、accept_band_first、reject_canonical、reject_band_first、hold_canonical、hold_band_first、blocked_canonical、blocked_band_first、kill_trigger_canonical、kill_trigger_band_first 和 diff_orders。

按原因统计,以及与收紧了限额的配置的比较

在 /root/risk/stats.json 中写入 orders、accept、reject、hold、blocked 和 by_reason,并在 /root/risk/tuning.json 中写入 base_accept、tight_accept、newly_rejected_count、newly_rejected 和 tight_by_reason。newly_rejected 是现在的配置下是通过、而收紧的配置下不是通过的订单的列表。

统计要从第 6 步的判定,也就是打开了熔断开关的结果中得出。然后把同一天用 limits_tight.json 再运行一遍。两套配置的差别,不只体现在按原因统计的数量上,也体现在通过笔数本身上。newly_rejected 按订单编号升序写出,并请通过输出确认,为什么收紧限额连熔断开关也会受到影响。