Skip to main content
撮合是内核执行里的一个阶段,不是链去调用的服务。它按已提交的次序取出本区块的订单,逐笔与订单簿相碰,产出成交。它不移动任何人的余额——那是清算所的事,发生在撮合完成之后。 把这两件事分开,正是引擎可测试的原因。撮合回答的是什么与什么成交了;清算所回答的是这要花多少钱、现在谁欠谁。

订单簿

每个品种有自己的订单簿,在内存中由三个相互配合的结构承载:
Slab arena — 一块预先分配的订单槽位区
价位表有序映射,价格 → 价位盘口只需走到端点就能找到,不必扫描
订单索引订单 ID → 槽位撤单和改单是常数时间
订单
订单
订单
订单
订单
一条按到达次序串起订单的双向链表,因此同一价位内的优先级由位置决定,而不是算出来的。
槽位复用次序为什么是固定的被释放的槽位按固定次序复用,索引也用固定种子。这不是性能选择:两个验证者以不同次序复用槽位就会分叉。
各价位的队首
直接查找
订单存放在一个 slab arena 中——一块预先分配好的区域,分配与释放都是常数时间。因此订单簿操作在热路径上不做内存分配,被释放的槽位也按固定次序复用,而不是分配器碰巧放到哪里就是哪里。最后这一点不是性能选择:如果两个验证者以不同次序复用槽位,任何能观察到槽位布局的东西都会发散。 订单索引也守同一条规矩:用固定种子,不用随机种子。按进程随机播种的哈希表,是抵御碰撞攻击的标准做法;但放在共识执行路径上,它就是一次分叉。 价格全程都是整数——单位是 subtick,不是小数。它与你提交的数值如何对应,见精度。

优先级

优先级先看价格,再看它在该价位链表里的位置。这里的“时间”,指的是这笔订单在已提交区块序列中的规范位置,不是它到达某个节点的时刻。 这消除了区块内的延迟竞速。同一区块中的两笔订单有一个确定的先后,每个验证者算出的结果都一样,离某个节点再近也改变不了它。在区块之间,到达时间仍然重要——但竞争的单位是区块,不是微秒。 内核的阶段次序进一步强化了这一点:一个区块之内,撤单先于主动吃单执行;挂着的报价,只要它的撤单和对方订单落在同一区块,就不会被吃掉。

撮合一笔订单

新到订单
与订单簿交叉?
吃掉对手方最优价位
产出成交
结束
按订单有效期,把剩余部分挂单或拒绝GTC 挂单 · IOC 撤销剩余 · FOK 不能全部成交就一点也不执行 · 只挂单宁可被拒也不交叉
还有剩余 — 吃下一个价位
没有剩余
撮合器反复吃掉对手方的队首,每吃掉一个挂单方就产出一笔成交,直到新到订单用尽,或订单簿不再交叉。剩余部分怎么处理,由订单有效期决定:
  • GTC — 剩余部分挂到订单簿上。
  • IOC — 撤销剩余部分。
  • FOK — 如果订单无法全部成交,就一点也不执行。
  • ALO — 只挂单:这笔订单要是会吃掉流动性,就直接拒单,不让它穿过去。
成交在产生的同时就带上归属信息。每一笔成交都记着自己在本品种成交序列中的位置;组装输出时,这些按品种的位置归并成整个区块内的单一顺序。正因如此,日后才能把一个事件追回到引起它的那笔交易,以及区块中的那个确切位置。

自成交防护

新到的订单要是会和同一所有者挂着的流动性成交,这次撮合就被抑制,不予执行。由哪一方让路,可以配置: 这样撤掉的挂单会在撮合过程中收集起来,在同一区块内移除,订单簿上不会留着一笔已经被抑制的订单。 这项检查里的“所有者”,按订单簿跟踪的账户层级判定。交易侧的说明见自成交防护。

撮合不做什么

它不计算手续费、不实现盈亏、不调整仓位、不检查保证金。这些都发生在撮合之后的清算所里,由撮合产出的成交驱动。 它也不判断一笔订单是否被允许存在。保证金是否充足、挂单数量上限、只减仓约束,以及市价转限价,都在订单到达订单簿之前就已处理完毕。等撮合器看到一笔订单时,唯一的问题只剩它该放在订单簿的什么位置。

后续阅读

清算所

成交出现之后,余额和仓位会怎样。

订单类型

交易侧的视角:你可以提交什么,每一种如何表现。

订单簿

深度、价位,以及作为交易者如何读盘。

IntentionKernel

撮合在区块执行中处于什么位置。