成交只是开始
撮合循环跑完一圈,簿子变了,双方的钱货要划转——但先别急着划。交易所此刻还欠全世界一个交代:把"刚刚发生了什么"准确、有序、不丢不重地通知出去。这一章讲的就是成交之后的消息体系。别小看它——真实交易所的撮合内核可能只占代码量的两成,剩下八成都在伺候"让别人知道"。
第一张网:成交回报,只给你看的私信
你的订单进门之后,它的一生会产生一串消息,这叫成交回报(execution report)——交易所发给你的私信,告诉你"你的单现在什么状态"。一张订单的状态机长这样:
已接收 → 已确认(进了簿子)→(部分成交 → 部分成交 → …)→ 全部成交
↘ 已撤销 / 已拒绝 / 已过期
每个状态变化都是一条回报,带上订单号、本次成交量、本次成交价、剩余量。第五章那张1500股的市价单,你会收到三条"部分成交"回报加一条"全部成交"。
设计回报流时有一个铁律:客户端必须能从回报流重建出订单的完整状态,不需要额外去问。因为"问"是有竞态的——你查询的瞬间状态又变了。流是唯一的真相来源,查询只能做兜底。这个原则在第十一章的日志重放里会放大成整个系统的架构哲学。
第二张网:行情广播,给所有人看的公告
回报是私信,行情是公告。交易所把簿子的变化广播给所有订阅者,这就是你在行情软件里看到的东西。行情分几个档次,贵的不一样:
- L1(一档行情):只有买一卖一的价格和量,加最新成交价。免费行情软件给你看的就是这个。
- L2(多档行情):买卖各5档、10档的总量——第二章那张盘口快照。这是"按价位聚合"的视图,同一价位十个人的单合并成一个总数。
- L3(逐笔/全量行情):每一笔订单的增、删、改、成交都单独广播,不聚合。你能看到队列里每张单的生与死,能自己重建出完整的簿子和排队顺序。高频交易者花大价钱买的就是它。
注意一个因果方向,很多人会搞反:行情不是数据库查出来的,是撮合过程的副产品。撮合引擎每改一次簿子(新单入簿、成交、撤单),就顺带吐出一条"簿子增量"。L2行情的每一档总量,就是这些增量累加出来的。这直接决定了实现方式:不要定时截图订单簿去广播(那样要么丢变化要么发重复),要让簿子的每次修改天然产生一条增量消息。
序列号:让"丢没丢"变成可检测的
行情走网络广播,网络会丢包。怎么让接收方知道自己丢没丢?答案简单到优雅:给每条消息编一个严格递增的序列号(sequence number)。
你收到1001、1002、1004——立刻知道丢了1003,本地簿子不能信,必须修复。修复方式两种:重放(找交易所要1003这条,或从某个序号开始重发)和快照(重新拉一份当前完整簿子,再从快照对应的序列号继续接增量)。币所的WebSocket深度流就是这套机制:先拉快照,缓存增量,把序列号对上了再开始应用,对不上就重来。
你自己实现时有两个决策点:序列号是全所一个还是每个品种一个(后者扩展性好,主流做法);快照里必须带"这份快照对应到哪个序列号",否则增量接不回去。
大白话:行情流就像一本连载小说,每页有页码。页码断了,别猜,回头补那一页或者从最近的全家福重新接——绝不允许"大概齐"。金融数据的对,是"可证明的对"。
第三张网:对账,深夜里的慢功夫
白天的回报流和行情流再严密,交易所每天收盘后还要干一件笨事:对账——把每个账户"理论上应该有的钱和货"跟实际余额核一遍。理论上应该有 = 开盘余额 + 全天所有成交、费用、充提的累加。
对账的意义不是"平时会错",而是必须能证明没错。它是整个系统正确性的最终裁判:撮合引擎、清算、费用、任何一环出bug,对账都会不平。不平就是事故,停下来查。所以好的交易所系统,从第一天就把每笔成交设计成"可重算"的——这又为第十一章的事件溯源埋了线。
收拢一下
成交之后,交易所同时维持三张网:给当事人的回报流(私信、状态机)、给所有人的行情流(公告、L1/L2/L3、序列号可修复)、给自己的对账流(慢、全量、最终裁判)。三张网共用同一个数据源:撮合循环吐出来的事件。把这句话记住,下一章我们终于开始写代码——你会发现,数据结构的设计早已被前八章的规则悄悄决定了。