以 Rust 为核心的 FIX 交易终端是怎么构建的:从会话恢复到价格阶梯,以及能证明供应商确实做过这种终端的证据。
没有哪个名录能告诉你,哪些公司真正做过带实时订单簿、剥头皮面板和 Rust 热路径的 FIX 交易终端,因为“我们做过交易终端”这句话,可以指从套在经纪商 API 上的一层图表皮肤,到拥有自己订单状态机的 FIX 客户端之间的任何东西。真正做过的供应商能证明这一点:当着你的面让终端连上真实交易场所运行,说出认证过其 FIX 连接的交易场所,讲清它的延迟时间戳是在哪里打的,并交出你不靠它帮忙就能构建的代码。
关于架构,简短的答案是:Rust 核心掌管 FIX 会话及其序号、订单状态机、本地订单簿和交易前检查;交易员一旦超过寥寥几人,它就作为靠近交易场所的网关运行。界面只负责发送指令,并每帧绘制一次最新状态,所以它卡死或崩溃都不会丢失订单。
凡是一旦丢失或延迟就会改变订单的东西,都放在核心里。按这条规则,核心里有五个部分。
界面保留的是一个视图:价格阶梯、图表、订单列表、持仓、快捷键。界面挂了,核心仍然握着会话、活动订单,以及它所管理的所有止损单。Rust 最能发挥作用的地方是核心。没有垃圾回收器,指令与线路之间就不会插进回收停顿;而且除非把订单簿放在锁后面共享,否则编译器会拒绝从两个线程写同一个订单簿的代码。
普通网页无法打开 FIX 会话所依赖的 TCP 连接。Chrome 的文档说,标准 Web 应用“无法建立原始 TCP 或 UDP 连接”,而 Chrome 的 Direct Sockets API 只对 Isolated Web Apps 解除这一限制。原生终端倒是能持有这样的连接,但那样一来,每个交易台都要各自背着一条交易场所会话、一条网络路由和一份序号状态。交易员一旦超过寥寥几人,核心就应该放进靠近交易场所的网关:如果距离在延迟预算中占大头,就放在托管机房里;如果交易员是手动点击、做决定的时间远长于网络路径的耗时,就放在附近的云区域里。
会话层给你的是有序处理,以及索要漏收消息的手段。它并不要求对方把这些消息全部重发。FIX Session Layer 技术标准(2020 年 6 月)规定了这些规则。
由此引出三项职责。每次发送和接收都要持久化序号,否则重启时要么把一整天的消息重新要一遍,要么因为序号过低被断开。记住哪些 ExecID(17) 值已经应用过,因为标准把重复检测留给了接收方。并且每次重连都以一次订单状态核查收尾,交易场所支持的话,就用 OrderMassStatusRequest(35=AF):如果某笔订单的消息被 gap fill 跳过,你对这笔订单的认知就缺了一个事实。
ExecutionReport(35=8) 带有两个容易混淆的字段。按 FIX 的定义,ExecType(150) “描述的是这一份具体的 ExecutionRpt(例如 Pending Cancel),而 OrdStatus(39) 始终标识订单的当前状态(例如 Partially Filled)”。用事件驱动状态机,把状态用作交叉核对。
FIX 数据字典把订单处于活动状态时的 LeavesQty(151) 定义为 OrderQty(38) 减去 CumQty(14),因此它是一个成本很低的不变量,可以在活动订单的每一份回报上断言。一旦某份回报违反了它,或者出现一个从当前状态无法迁移过去的 ExecType,就应冻结该订单并发出告警。终端最后显示出一个与交易场所对不上的持仓,靠的就是猜。
剥头皮交易者常常在交易场所回应上一次改动之前,就又改了订单。下面每一种竞态,都是线路上一条消息与另一条消息交错而过。
drop copy 提供第二个视角。CME 把这项服务描述为执行回报和确认的实时副本,“经由一条独立的专用路径”发送。在核心里据此核对持仓,并把与订单会话之间的任何不一致都当作事故处理。
L2 行情发布的是价位。Binance 的文档给出了一套快照加增量更新的流程:缓冲数据流,拉取快照,丢弃快照中已经包含的缓冲事件,其余按顺序应用;一旦某个更新 ID 被跳过,就从头再来。它的快照每侧最多 5000 档,所以更深的价位在发生变化之前都是未知的。
L3 行情发布的是逐笔订单。在 Nasdaq TotalView-ITCH 5.0 中,Add Order 消息带有一个订单参考号,之后的修改消息都回指这个号,而当显示股数降为零时,“该订单已失效,应从订单簿中移除”。构建器维护一张从参考号到订单的映射,并据此聚合出各个价位:代价是更多内存、每条消息一次查找,换来的是每个价位的订单笔数,以及对你自己的订单在队列中位置的估计。
对价格阶梯来说,围绕最优买卖价(touch)、以价格为索引的数组通常胜过树结构,因为价格是在一段连续区间内按 tick 变动的。每个品种由一个写入者持有订单簿,并记录它所反映的序号;缺口恢复和 fan-out 在关于突发下行情数据的那篇文章里讲过。终端只多加一条规则:处于恢复中的订单簿要画成恢复中,绝不能画成实时状态。
这个面板就是一个可以直接在上面交易的深度价格阶梯(DOM)。点击某个价格即下一笔限价单,拖动即移动它,按快捷键则发出预设数量的订单或把仓位平掉。由于没有确认对话框,安全网就放在核心里:核心对每一笔一键下单都按账户检查最大数量、价格带和敞口。关于订单路径中交易前风控的那篇文章专门讲了这些检查。
对于括号订单(bracket)和 OCO 订单,先决定它们放在哪里。FIX 在 NewOrderList(35=E) 上定义了 ContingencyType(1385),其中包括 One Cancels the Other 和 One Triggers the Other。交易场所有自己的版本就用它的;没有的话,就由核心通过监视成交、发出另一条腿来模拟。绝不要在界面进程里模拟:一台带着未受保护的仓位进入睡眠的笔记本电脑,正是括号订单要应对的情形。
持仓由成交得出,成交更正和成交撤销(bust)也计算在内;核心在每次变动时按本地订单簿对持仓做盯市估值,界面每帧读取一次持仓和盈亏。
剥头皮交易者关心两条链:从按键或点击到订单离开网卡,以及从行情数据包到像素发生变化。每一环都需要自己的时间戳。
每一环都要以你指明的负载下的分位数来报告,而不是清淡行情下的平均值。一个不带计时点的孤立数字,没法和任何别的数字比较。
MDN 指出,60 Hz 是最常见的显示器刷新率,120 Hz 和 144 Hz 也被广泛使用;由此,60 Hz 下每帧 16.7 ms,144 Hz 下每帧不到 7 ms。繁忙的交易所行情在一帧之内就可能多次改变订单簿,所以无论采用哪种技术,都由核心应用每一条更新,而界面每帧绘制一次最新状态。
MDN 还指出,大多数浏览器会在后台标签页中暂停 requestAnimationFrame,所以任何必须持续运行的东西都不能放在页面里。Rust 核心之上的 Web 界面满足这一要求;订单路径上的 Web 技术栈则不满足。
什么时候放你进来,由交易场所决定。例如,CME “要求所有通过 iLink 订单路由在 CME Globex 上交易、或处理 CME Group 行情数据的客户端系统,都须经过 AutoCert+ 认证”,AutoCert+ 是 CME 的自动化测试工具。据 CME 介绍,认证涵盖消息收发、处理以及从异常消息事件中恢复,其功能测试的速率不超过每秒 10 笔事务。通过认证并不能告诉你,开盘时交易员点击之下价格阶梯会怎么表现。
在你自己的交易场所模拟器上预演这些故障:重连后 gap fill 跳过了订单消息、撤单为时已晚、改单在待处理状态下被拒、成交被撤销(trade bust)、会话断开时还有挂单、交易员正在点击时突发中途出现行情缺口。然后把录下的 FIX 流量和行情数据回放进核心。同样的输入每次运行都必须产生同样的订单状态和同样的订单簿,这样交易员的缺陷报告就能变成一个测试。
如果你的交易场所都在它的支持列表上、业务流程也是标准的,就买一款现成的终端。如果界面本身就是客户选择你的原因之一,如果你的交易场所不受支持,或者你必须自己掌握订单路径及其风控检查,那就自建。折中的路线是授权使用一个 FIX 引擎或交易场所适配器,其余部分自建。
把“我们做过”当作供应商要在第一次会面时就证明的主张。下面六项要求能完成大部分核查工作。
amBrain 是一家位于亚美尼亚埃里温的软件工程公司,用 Rust 构建低延迟交易平台、撮合引擎和实时竞价系统。amBrain 自 2019 年起开发软件。
amBrain 构建算法交易基础设施:订单执行、行情数据和交易前风控。其交易方向的服务包括交易终端开发、订单管理系统和 FIX 协议交易所对接。
有两个数字是在 amBrain 搭建的路径上实测的:行情数据延迟低于 5 ms,风控延迟低于 1 ms。两者都不是从按键到线路的数字,所以就像对待任何供应商一样,要问清这两个数字背后的计时点和负载。
amBrain 为 Spectre Trade 开发了交易终端。amBrain 还做过一个迷你交易所,部署在 MOEX 托管机房,运行于生产环境。本文中的设计是通用的。它既不描述这两套系统中的任何一套,也不点名其中任何一套所用的协议。
三种合作方式:整体交付、专属团队,或工程师嵌入你的团队。客户拥有产品与代码的全部所有权,我们的可复用组件除外。
在和任何一家供应商(包括 amBrain 在内)第一次通话之前,先写下你的交易场所、每个场所使用的 FIX 版本或方言,以及你需要的、在指定计时点之间的延迟。
带上您当前的架构和让您担心的故障场景,我们用半小时一起梳理。