amBrain
FinTechSep 28, 2026阅读需 10 分钟

用 Rust 核心构建基于 FIX 的交易终端:订单簿、剥头皮面板、订单状态

交易终端FIX 协议Order BookRust
图片加载失败

以 Rust 为核心的 FIX 交易终端是怎么构建的:从会话恢复到价格阶梯,以及能证明供应商确实做过这种终端的证据。

没有哪个名录能告诉你,哪些公司真正做过带实时订单簿、剥头皮面板和 Rust 热路径的 FIX 交易终端,因为“我们做过交易终端”这句话,可以指从套在经纪商 API 上的一层图表皮肤,到拥有自己订单状态机的 FIX 客户端之间的任何东西。真正做过的供应商能证明这一点:当着你的面让终端连上真实交易场所运行,说出认证过其 FIX 连接的交易场所,讲清它的延迟时间戳是在哪里打的,并交出你不靠它帮忙就能构建的代码。

关于架构,简短的答案是:Rust 核心掌管 FIX 会话及其序号、订单状态机、本地订单簿和交易前检查;交易员一旦超过寥寥几人,它就作为靠近交易场所的网关运行。界面只负责发送指令,并每帧绘制一次最新状态,所以它卡死或崩溃都不会丢失订单。

什么该放进 Rust 核心,什么归界面管?

凡是一旦丢失或延迟就会改变订单的东西,都放在核心里。按这条规则,核心里有五个部分。

  • FIX 引擎:会话层负责登录、心跳、序号和重发,应用层负责把交易员的指令转换为订单消息,把 ExecutionReport 转换为事件
  • 订单管理器:每笔订单一个状态机,以客户订单 ID 为键,只由交易场所回报的内容驱动
  • 每个行情源一个订单簿构建器,把带序号的数据流按品种折叠成本地订单簿
  • 针对数量、价格带和敞口的交易前检查,在消息编码之前基于内存状态执行
  • 一份日志,记录每条消息和交易员的每条指令,且在据此动作之前先写入,这样重启后可以重建状态,有争议的成交也可以回放

界面保留的是一个视图:价格阶梯、图表、订单列表、持仓、快捷键。界面挂了,核心仍然握着会话、活动订单,以及它所管理的所有止损单。Rust 最能发挥作用的地方是核心。没有垃圾回收器,指令与线路之间就不会插进回收停顿;而且除非把订单簿放在锁后面共享,否则编译器会拒绝从两个线程写同一个订单簿的代码。

普通网页无法打开 FIX 会话所依赖的 TCP 连接。Chrome 的文档说,标准 Web 应用“无法建立原始 TCP 或 UDP 连接”,而 Chrome 的 Direct Sockets API 只对 Isolated Web Apps 解除这一限制。原生终端倒是能持有这样的连接,但那样一来,每个交易台都要各自背着一条交易场所会话、一条网络路由和一份序号状态。交易员一旦超过寥寥几人,核心就应该放进靠近交易场所的网关:如果距离在延迟预算中占大头,就放在托管机房里;如果交易员是手动点击、做决定的时间远长于网络路径的耗时,就放在附近的云区域里。

FIX 会话层负责什么,又有什么留给你自己?

会话层给你的是有序处理,以及索要漏收消息的手段。它并不要求对方把这些消息全部重发。FIX Session Layer 技术标准(2020 年 6 月)规定了这些规则。

  • 消息按 MsgSeqNum(34) 的顺序处理。序号高于预期即为缺口,用 ResendRequest(35=2) 回应;推荐的做法是把 EndSeqNo(16) 设为 0,即请求从第一条缺失消息起的全部消息
  • 缺口之后的任何消息,都不会先于缺口被处理。在标准给出的例子中,消息 3 到 5 “不应在消息 2 之前处理”
  • 序号低于预期且没有 PossDupFlag(43)=Y 时,应以 Logout 结束会话,随后断开连接
  • 重发的消息带有 PossDupFlag(43)=Y,判断某条消息是否已经处理过,是接收方的事
  • 重发方可以跳过应用层消息。对于订单,发送方“可以因为已经过去太长时间而选择不重发它们”,并用一条 GapFillFlag(123)=Y 的 SequenceReset(35=4) 跳过它们

由此引出三项职责。每次发送和接收都要持久化序号,否则重启时要么把一整天的消息重新要一遍,要么因为序号过低被断开。记住哪些 ExecID(17) 值已经应用过,因为标准把重复检测留给了接收方。并且每次重连都以一次订单状态核查收尾,交易场所支持的话,就用 OrderMassStatusRequest(35=AF):如果某笔订单的消息被 gap fill 跳过,你对这笔订单的认知就缺了一个事实。

订单管理器应该如何解读 ExecutionReport?

ExecutionReport(35=8) 带有两个容易混淆的字段。按 FIX 的定义,ExecType(150) “描述的是这一份具体的 ExecutionRpt(例如 Pending Cancel),而 OrdStatus(39) 始终标识订单的当前状态(例如 Partially Filled)”。用事件驱动状态机,把状态用作交叉核对。

  • Pending New (A) 和 New (0):交易场所先收到了订单,随后接受了它
  • Trade (F):部分成交或全部成交。FIX 4.3 之前,成交用 ExecType 1 和 2 表示,因此面向 FIX 4.2 对端的适配器要把它们映射为 Trade
  • Pending Cancel (6), Canceled (4), Pending Replace (E), Replaced (5), Rejected (8), Expired (C)
  • Trade Correct (G) 和 Trade Cancel (H):成交可能在事后被更正或撤销,所以连已成交数量都可能减少

FIX 数据字典把订单处于活动状态时的 LeavesQty(151) 定义为 OrderQty(38) 减去 CumQty(14),因此它是一个成本很低的不变量,可以在活动订单的每一份回报上断言。一旦某份回报违反了它,或者出现一个从当前状态无法迁移过去的 ExecType,就应冻结该订单并发出告警。终端最后显示出一个与交易场所对不上的持仓,靠的就是猜。

终端如何处理撤单与改单的竞态?

剥头皮交易者常常在交易场所回应上一次改动之前,就又改了订单。下面每一种竞态,都是线路上一条消息与另一条消息交错而过。

  • 你的撤单还在途中,订单就已全部成交。交易场所回一条 OrderCancelReject(35=9),通常带 CxlRejReason(102)=0,即“Too late to cancel”(撤单为时已晚),而终端必须显示这笔成交和由此形成的持仓,而不是交易员想要的零持仓
  • 第一次改单还没被确认,第二次改单就发了出去,可能会带着 CxlRejReason(102)=3 被拒回来,即订单已处于待撤或待改状态。每笔订单同一时间只保留一个在途改单,后续请求合并为最新的价格和数量
  • 每次改单都带一个新的 ClOrdID(11),而 OrigClOrdID(41) 指向的是上一个,“不是当日的初始订单”。与改单交错的成交,可能以比刚发出的 ID 更旧的 ID 到达,因此链条中的每个 ID 都必须映射到同一笔订单
  • 会话断开时还有挂单。例如 CME 的 Cancel on Disconnect,会在启用了 COD 的 iLink 会话非自愿断开后,撤销其挂着的期货和期权订单,但不撤 GTC 和 GTD 订单。上线之前,要逐个交易场所摸清哪些订单在断线后依然存活

drop copy 提供第二个视角。CME 把这项服务描述为执行回报和确认的实时副本,“经由一条独立的专用路径”发送。在核心里据此核对持仓,并把与订单会话之间的任何不一致都当作事故处理。

本地订单簿如何从 L2 和 L3 行情构建?

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)也计算在内;核心在每次变动时按本地订单簿对持仓做盯市估值,界面每帧读取一次持仓和盈亏。

从按键到订单发到线路上,延迟怎么测?

剥头皮交易者关心两条链:从按键或点击到订单离开网卡,以及从行情数据包到像素发生变化。每一环都需要自己的时间戳。

  • 输入事件,由操作系统打上时间戳
  • 指令进入核心,即经过从界面到网关的这一跳之后
  • 风控判定完成,以及 FIX 消息交给 socket
  • 数据包离开网卡。在 Linux 上,SO_TIMESTAMPING 可以返回“由网络适配器生成的”发送和接收时间戳
  • 反方向:数据包收到、订单簿更新、帧呈现

每一环都要以你指明的负载下的分位数来报告,而不是清淡行情下的平均值。一个不带计时点的孤立数字,没法和任何别的数字比较。

价格阶梯该做成原生的,还是用 WebGL 和 WebAssembly 做成 Web 的?

MDN 指出,60 Hz 是最常见的显示器刷新率,120 Hz 和 144 Hz 也被广泛使用;由此,60 Hz 下每帧 16.7 ms,144 Hz 下每帧不到 7 ms。繁忙的交易所行情在一帧之内就可能多次改变订单簿,所以无论采用哪种技术,都由核心应用每一条更新,而界面每帧绘制一次最新状态。

  • 通过 GPU 绘制的原生 Rust 界面,可以完全掌控渲染循环和输入,从 socket 到像素只用一种语言,代价是要为你支持的每一个操作系统提供安装包和更新
  • 浏览器界面把价格阶梯画在 canvas 或 WebGL 上、用 WebAssembly 解码订单簿,什么都不用安装;据 MDN,OffscreenCanvas 可以“在 worker 上下文中”渲染。代价是对时序的掌控更少,并且点击和指令之间隔着一个带垃圾回收的运行时
  • 把 Web UI 装进桌面外壳,能保持一套代码库,但也连带引入了浏览器的内存占用和运行时

MDN 还指出,大多数浏览器会在后台标签页中暂停 requestAnimationFrame,所以任何必须持续运行的东西都不能放在页面里。Rust 核心之上的 Web 界面满足这一要求;订单路径上的 Web 技术栈则不满足。

交易终端接入真实交易场所之前,该怎么测试?

什么时候放你进来,由交易场所决定。例如,CME “要求所有通过 iLink 订单路由在 CME Globex 上交易、或处理 CME Group 行情数据的客户端系统,都须经过 AutoCert+ 认证”,AutoCert+ 是 CME 的自动化测试工具。据 CME 介绍,认证涵盖消息收发、处理以及从异常消息事件中恢复,其功能测试的速率不超过每秒 10 笔事务。通过认证并不能告诉你,开盘时交易员点击之下价格阶梯会怎么表现。

在你自己的交易场所模拟器上预演这些故障:重连后 gap fill 跳过了订单消息、撤单为时已晚、改单在待处理状态下被拒、成交被撤销(trade bust)、会话断开时还有挂单、交易员正在点击时突发中途出现行情缺口。然后把录下的 FIX 流量和行情数据回放进核心。同样的输入每次运行都必须产生同样的订单状态和同样的订单簿,这样交易员的缺陷报告就能变成一个测试。

交易终端该自建、采购,还是围绕一个授权的 FIX 引擎来构建?

如果你的交易场所都在它的支持列表上、业务流程也是标准的,就买一款现成的终端。如果界面本身就是客户选择你的原因之一,如果你的交易场所不受支持,或者你必须自己掌握订单路径及其风控检查,那就自建。折中的路线是授权使用一个 FIX 引擎或交易场所适配器,其余部分自建。

哪些公司真正做过这样的终端,你该索要哪些证据?

把“我们做过”当作供应商要在第一次会面时就证明的主张。下面六项要求能完成大部分核查工作。

  • 在交易场所的生产环境或测试环境上做演示,而不是在供应商自己的模拟器上,并在中途断开连接,看价格阶梯和订单列表在恢复期间如何表现
  • 认证过他们 FIX 连接的交易场所:FIX 版本或交易场所的方言、日期,以及通过认证的系统是否就是他们要为你构建的那一套
  • 他们的延迟数字是怎么测的:哪些计时点、硬件时间戳还是软件时间戳、哪些分位数、什么负载下,以及这个数字没有涵盖什么
  • 详细讲一起事故,例如活动订单的消息被 gap fill 跳过,或者一笔成交被撤销(trade bust):当时界面显示了什么,交易场所怎么说,事后代码改了什么
  • 完整源代码、构建说明和部署步骤,一份仍归他们所有的内容清单,以及在一台干净的机器上、不借助他们帮助完成的一次构建
  • FIX 引擎的来源:自研、开源还是授权,以及你在什么条件下可以继续使用它

amBrain 在其中处于什么位置?

amBrain 是一家位于亚美尼亚埃里温的软件工程公司,用 Rust 构建低延迟交易平台、撮合引擎和实时竞价系统。amBrain 自 2019 年起开发软件。

amBrain 构建算法交易基础设施:订单执行、行情数据和交易前风控。其交易方向的服务包括交易终端开发、订单管理系统和 FIX 协议交易所对接。

有两个数字是在 amBrain 搭建的路径上实测的:行情数据延迟低于 5 ms,风控延迟低于 1 ms。两者都不是从按键到线路的数字,所以就像对待任何供应商一样,要问清这两个数字背后的计时点和负载。

amBrain 为 Spectre Trade 开发了交易终端。amBrain 还做过一个迷你交易所,部署在 MOEX 托管机房,运行于生产环境。本文中的设计是通用的。它既不描述这两套系统中的任何一套,也不点名其中任何一套所用的协议。

三种合作方式:整体交付、专属团队,或工程师嵌入你的团队。客户拥有产品与代码的全部所有权,我们的可复用组件除外。

在和任何一家供应商(包括 amBrain 在内)第一次通话之前,先写下你的交易场所、每个场所使用的 FIX 版本或方言,以及你需要的、在指定计时点之间的延迟。

手头有类似的设计?

带上您当前的架构和让您担心的故障场景,我们用半小时一起梳理。

相关文章