本文面向有经验的工程师与架构师,旨在深入剖析现代金融交易系统中最核心的两种订单撮合模式:集合竞价(Call Auction)与连续竞价(Continuous Auction)。我们将从市场微观结构与算法原理出发,穿透到系统实现、性能权衡,最终探讨一个高性能撮合引擎的架构演进路径。这不仅仅是概念的辨析,更是对高频、低延迟、高并发系统设计哲学的一次深度审视,尤其适用于股票、期货、数字货币交易所等核心系统的设计者。
现象与问题背景
任何一个成熟的金融交易所,其交易日都不是铁板一块的“连续交易”。以典型的A股市场为例,一个交易日被精确地划分为几个阶段:
- 9:15 – 9:25 开盘集合竞价: 在正式开盘前,有一个10分钟的窗口期。投资者可以下单和撤单,但系统并不立即撮合。所有订单汇集在一起,旨在产生一个公平的“开盘价”。
- 9:30 – 11:30, 13:00 – 14:57 连续竞价: 这是我们最熟悉的盘中交易时段。订单遵循“价格优先、时间优先”的原则,一旦有匹配的对手方订单,立即成交。价格在毫秒间不断波动。
- 14:57 – 15:00 收盘集合竞价: 与开盘类似,为确定一个公允的“收盘价”,避免尾盘价格被少数大单操纵。
这种制度设计并非偶然,它直接回答了两个根本性问题:
- 如何处理信息真空期的海量订单? 经过一夜或一个周末的休市,大量影响价格的宏观新闻、公司公告、市场情绪得以积累。若在9:30开市瞬间直接采用连续竞价,第一笔成交价极可能因偶然的订单顺序而产生剧烈、无序的跳动,这不符合市场效率。集合竞价通过汇聚一段时间内的所有意图,寻找一个能最大化成交量的“共识价格”,有效地实现了价格发现(Price Discovery)功能,平滑了开盘波动。
- 如何在流动性充裕时提供即时性? 在盘中交易时段,市场信息流动快,参与者众多。此时,交易者最关心的是订单能否被立即执行。连续竞价模式提供了这种即时性(Immediacy),保证了市场的流动性。任何一个新订单的到来都可能改变最优报价(Best Bid/Offer)并立即触发成交。
因此,双向拍卖(集合竞价)与连续竞价并非互相取代,而是在不同市场阶段、解决不同核心矛盾的两种关键机制。理解其底层原理与实现差异,是构建任何高性能交易系统的基石。
关键原理拆解:从市场微观结构到算法
作为一名架构师,我们必须回归计算机科学的基础原理。撮合交易的本质,是一个基于特定规则的匹配算法。这两种模式的差异,根源在于算法的目标函数和执行时机不同。
(教授声音)
首先,我们必须定义交易的核心数据结构:限价订单簿(Limit Order Book, LOB)。LOB是一个动态的、按价格排序的买卖订单队列。所有未成交的限价单(Limit Order)都栖身于此。它的状态遵循一个黄金法则:价格优先、时间优先(Price-Time Priority)。
- 价格优先: 买单(Bid)按出价从高到低排序,出价最高的排在最前面;卖单(Ask)按出价从低到高排序,出价最低的排在最前面。
- 时间优先: 在同一价格水平上,先提交的订单排在前面。
基于这个数据结构,我们来剖析两种算法的数学本质。
集合竞价(Call Auction)算法
集合竞价的目标是在一个指定时间点,找出一个唯一的清算价格(Clearing Price),使得在该价格下,可成交的买单和卖单总量最大。这是一个典型的离散优化问题。
其算法步骤如下:
- 数据收集: 在指定时间窗口内(如9:15-9:25),收集所有买卖方向的限价订单,放入一个临时的订单池中,不对它们进行任何撮合。
- 生成供需曲线:
- 对所有买单,按价格从高到低排序。计算每个价格水平上的累计买方需求量。即,在价格P,愿意购买的总量是所有出价≥P的买单数量之和。
- 对所有卖单,按价格从低到高排序。计算每个价格水平上的累计卖方供给量。即,在价格P,愿意出售的总量是所有出价≤P的卖单数量之和。
- 寻找均衡点: 遍历所有出现过的价格点P,计算在每个P上的可匹配成交量,即
min(累计买方需求量@P, 累计卖方供给量@P)。 - 确定清算价格: 能够使可匹配成交量达到最大的那个价格P,就是清算价格。
这里存在一个工程上的关键细节:如果多个价格点都能产生相同的最大成交量怎么办?这时就需要引入 tie-breaking 规则。不同交易所的规则略有差异,但通常遵循以下层层递进的原则:
- 最小未成交量原则: 选择那个使得买卖双方未成交总量最小的价格。
- 参考价接近原则: 如果最小未成交量也相同,则选择最接近上一个交易日收盘价(或其他参考价)的价格。
- 交易所预设高低原则: 如果上述条件都相同,则按交易所规定,取较高的或较低的价格。
集合竞价的本质,是在一个静态的订单集合上,求解一个全局最优解。它牺牲了即时性,换取了价格的公允性和稳定性。
连续竞价(Continuous Auction)算法
连续竞价则完全不同。它的目标是即时性。算法是事件驱动的,每当一个新订单进入系统,就立即尝试与订单簿中已存在的对手方订单进行匹配。
其算法步骤如下(以一个新买单为例):
- 订单到达: 一个新的买单(比如,买入100股,限价$10.05)进入撮合引擎。
- 扫描对手方订单簿: 引擎立即查看卖方订单簿(Ask Book)的顶部,即价格最低的卖单。假设当前最优卖单是卖100股,价格$10.02。
- 价格匹配判断: 新买单的出价($10.05) ≥ 最优卖单的出价($10.02),满足成交条件。
- 执行撮合:
- 成交价格遵循订单簿价格优先原则,即以老订单(在簿内的订单)价格为准。所以成交价是$10.02。
- 成交数量是双方数量的最小值。假设买单和卖单都是100股,则成交100股。
- 双方订单的数量被更新。在这个例子中,两个订单都完全成交,从订单簿中移除。
- 持续撮合或入簿: 如果新买单的数量大于最优卖单(例如,新买单要买200股),则在完全吃掉第一个卖单后,继续与下一个最优卖单(比如价格是$10.03)进行比较和撮合,直到新买单被完全满足,或者其价格不再优于卖方报价。
- 入簿(Posting): 如果新买单在吃掉所有可成交的卖单后仍有剩余(或者一开始就无法与任何卖单匹配,比如出价太低),那么这个剩余的买单将被按照价格优先、时间优先的原则,插入到买方订单簿(Bid Book)的相应位置,成为新的流动性。
连续竞价是一个贪心算法,它在每个时间点都做出局部最优决策,即立即撮合所有可以撮合的订单。它保证了市场的即时响应和高流动性,但价格发现过程是碎片化的,容易受到大单冲击。
系统架构总览:撮合引擎的位置与交互
理论必须落地到架构。一个典型的交易系统,撮合引擎是其心跳中枢,但它并非孤立存在。其上下游通常包括:

(此处应有一幅架构图,文字描述如下)
上图描绘了一个简化的交易系统架构。订单流转路径如下:
- 接入网关(Gateway): 系统的门户。负责处理客户端连接(通常使用标准化的FIX协议或自定义的TCP/UDP协议)、会话管理、认证鉴权和初步的格式校验。网关是用户态进程,通常可以水平扩展以应对大量连接。
- 订单管理系统(OMS): 订单的生命周期管理者。它负责对进入的订单进行风控检查(如保证金、仓位限制、黑名单等)、分配订单ID、并将合法订单送往撮合引擎。成交回报也经由OMS处理后,更新用户资产并返回给客户端。OMS通常需要与数据库交互,保证订单状态的持久化。
- 撮合引擎(Matching Engine): 系统的核心。它在内存中维护着所有交易对的订单簿。上述的集合竞价与连续竞价算法就在此运行。为了追求极致性能,撮合引擎通常是单线程或基于CPU核心绑定的多线程模型,且几乎不进行任何磁盘I/O操作。所有状态变更(新订单、取消、成交)会以事件的形式输出。
- 持久化与复制: 撮合引擎产生的事件流(如Aeron、Kafka或自定义日志)被下游系统消费。一个关键消费者是持久化服务,它将这些事件写入数据库或分布式账本,确保系统崩溃后可以从日志中恢复订单簿状态。这也是实现高可用的基础(主备引擎通过复制日志来同步状态)。
- 行情发布器(Market Data Publisher): 另一个关键消费者。它订阅撮合引擎的事件流,生成深度行情(L1/L2/L3 Market Data)和逐笔成交(Trade Ticker),并通过UDP多播或WebSocket等低延迟方式广播给所有市场参与者。
从用户下单到收到成交回报的核心路径延迟(Critical Path Latency),主要消耗在:网络传输 -> 网关 -> OMS风控 -> 撮合引擎处理 -> OMS更新 -> 网关 -> 网络传输。其中,撮合引擎的处理延迟是衡量系统性能的最关键指标,通常要求在微秒(μs)级别。
核心模块设计与实现:Order Book 与撮合逻辑
(极客工程师声音)
Talk is cheap. Show me the code. 理论再漂亮,最终也要落实到高效、无锁、对CPU Cache友好的代码上。撮合引擎的核心是订单簿的实现。
订单簿的数据结构
用什么数据结构来表示订单簿?一个naive的实现可能是用`std::map
在一线系统中,更常见的选择是结合多种数据结构,或者使用更底层的定制化结构。一个经典的组合是:
- 用一个平衡二叉搜索树(如红黑树)或跳表来索引价格水平(Price Level)。这保证了能以`O(log P)`的复杂度快速定位、插入、删除一个价格档位。
- 在每个价格水平节点上,挂载一个双向链表来存储该价格的所有订单。这保证了新订单追加到队尾是`O(1)`,订单成交或取消从链表中移除也是`O(1)`(需要一个哈希表或数组通过OrderID快速定位订单节点)。
– 卖单簿使用一个以价格为key的最小堆(Min-Heap)结构的树,买单簿则用最大堆(Max-Heap)结构的树。
下面是一个简化的Go语言数据结构示意:
// Order 代表一个具体的订单
type Order struct {
ID uint64
Side Side // BUY or SELL
Price int64 // 使用int64避免浮点数精度问题,例如 10.01元 存为 100100
Quantity uint64
Timestamp int64 // 用于实现时间优先
// 以下用于在双向链表中快速操作
level *PriceLevel // 指向所属的价格档位
Prev *Order
Next *Order
}
// PriceLevel 代表一个价格档位上的所有订单
type PriceLevel struct {
Price int64
TotalVolume uint64 // 该价格档位的总订单量
OrderCount int
Head *Order // 指向订单链表的头
Tail *Order // 指向订单链表的尾
}
// OrderBook 包含买卖双方的订单簿
// 内部的bids和asks通常用红黑树或类似的高效结构实现
type OrderBook struct {
bids *RedBlackTree // Key: Price, Value: *PriceLevel
asks *RedBlackTree // Key: Price, Value: *PriceLevel
orders map[uint64]*Order // 通过OrderID快速查找订单 O(1)
}
连续撮合实现
连续撮合的逻辑直截了当,就是一个循环。关键在于高效地遍历对手方订单簿并更新状态。
// processLimitOrder 是连续撮合的核心逻辑
func (ob *OrderBook) processLimitOrder(newOrder *Order) []Trade {
var trades []Trade
if newOrder.Side == BUY {
// 遍历卖方订单簿,从价格最低的开始
iterator := ob.asks.Iterator() // 假设红黑树提供了迭代器
for iterator.Next() {
level := iterator.Value().(*PriceLevel)
if newOrder.Price < level.Price {
// 新买单价格低于最低卖价,无法成交,直接跳出
break
}
// 遍历该价格档位上的订单(FIFO)
for bookOrder := level.Head; bookOrder != nil; bookOrder = bookOrder.Next {
if newOrder.Quantity == 0 { break }
tradeQuantity := min(newOrder.Quantity, bookOrder.Quantity)
trades = append(trades, createTrade(newOrder, bookOrder, level.Price, tradeQuantity))
newOrder.Quantity -= tradeQuantity
bookOrder.Quantity -= tradeQuantity
level.TotalVolume -= tradeQuantity
if bookOrder.Quantity == 0 {
// 对手方订单完全成交,从链表中移除
ob.removeOrder(bookOrder)
}
}
if level.OrderCount == 0 {
// 该价格档位已空,从红黑树中移除
ob.asks.Remove(level.Price)
}
}
} else { // SELL side logic is symmetric
// ...
}
if newOrder.Quantity > 0 {
// 新订单未完全成交,将其加入订单簿
ob.addOrder(newOrder)
}
return trades
}
这段代码的性能瓶颈在于循环。每一次循环都涉及内存访问,如果数据结构设计不当,CPU Cache的换出换入会是巨大的性能杀手。这就是为什么在极致性能场景,有人会用数组+指针代替标准库的树和链表,实现所谓的“Mechanical Sympathy”。
集合竞价实现
集合竞价的实现分为两步:价格确定和成交执行。代码更偏向于数据分析而非实时事务处理。
// determineCallAuctionPrice 决定集合竞价的清算价格
func (engine *MatchingEngine) determineCallAuctionPrice() (clearingPrice int64, matchVolume uint64) {
// 1. 获取所有订单簿中出现过的价格,并去重、排序
prices := engine.getUniquePrices()
sort.Slice(prices, func(i, j int) bool { return prices[i] < prices[j] })
var bestPrice int64 = -1
var maxVolume uint64 = 0
var minImbalance uint64 = math.MaxUint64
// 2. 遍历每个可能的价格,计算成交量和未匹配量
for _, p := range prices {
// getCumulativeVolumeAt(price) 是预先计算好的累计供需量
buyVolume := engine.getCumulativeBuyVolumeAt(p)
sellVolume := engine.getCumulativeSellVolumeAt(p)
currentVolume := min(buyVolume, sellVolume)
if currentVolume > maxVolume {
maxVolume = currentVolume
bestPrice = p
minImbalance = abs(buyVolume - sellVolume)
} else if currentVolume == maxVolume {
// 成交量相同,应用tie-breaking规则
imbalance := abs(buyVolume - sellVolume)
if imbalance < minImbalance {
minImbalance = imbalance
bestPrice = p
} else if imbalance == minImbalance {
// 更多规则,如比较与参考价的差距等...
}
}
}
return bestPrice, maxVolume
}
// 确定价格后,再遍历一次订单簿,执行所有符合条件的成交
func (engine *MatchingEngine) executeCallAuctionTrades(clearingPrice int64) {
// ...
}
这里的坑在于 `getUniquePrices` 和 `getCumulative...Volume` 的效率。在订单量巨大时,全量计算非常耗时。实践中,这些累计值可以在订单入池时增量维护,以空间换时间,避免在竞价时刻进行大规模计算。
性能、公平性与风险对抗
选择哪种模式,或如何组合它们,是一系列深刻的Trade-off。
- 延迟 vs 吞吐量:
- 连续竞价: 对单个订单的延迟极低。如果能立即成交,响应时间在微秒级。但其吞吐量受限于单线程处理能力,因为对订单簿的修改必须严格序列化。一个繁忙的交易对可能每秒处理几万到几十万笔订单,再高就会成为瓶颈。
- 集合竞价: 单个订单的延迟很高(必须等待窗口关闭)。但其吞吐量是脉冲式的,在清算瞬间可以处理掉百万甚至千万级别的订单。计算过程可以利用多核并行(例如,并行计算不同价格档位的累计量)。
- 价格发现 vs 波动性:
- 连续竞价: 价格发现是连续、渐进的。但也因此,容易受到“胖手指”错误或恶意大单的冲击,引发闪崩(Flash Crash)。价格轨迹更“毛糙”。
- 集合竞价: 价格发现是集中的、全局的。它能过滤掉瞬时的噪声,形成一个市场“共识价”,天然地抑制了开盘/收盘的剧烈波动。
- 公平性 vs 高频交易(HFT):
- 连续竞价: 是高频交易(HFT)的主战场。胜负手在于谁的速度更快——更近的服务器托管(Co-location)、更快的网络(微波/激光)、更优化的代码(Kernel Bypass、FPGA)。这导致了军备竞赛,对普通投资者可能不公平。
- 集合竞价: 在很大程度上抹平了速度优势。只要你的订单在窗口期内提交,无论是第一秒还是最后一秒,在价格计算上都是平等的(时间优先仅在清算价格上生效)。这使得市场更为“民主”。一些交易所甚至引入随机结束时间,以防止HFT在最后一毫秒“狙击”竞价。
架构演进与落地路径
一个交易系统并非一蹴而就,它会随着业务规模和性能要求的提升而演进。
- 阶段一:单体MVP(Minimum Viable Product)
适用于初创项目或内部系统。一台物理机,所有模块(网关、OMS、撮合引擎)都在一个进程内。撮合引擎只实现连续竞价模式。状态通过定时快照+操作日志的方式持久化到PostgreSQL或MySQL。简单、易于开发,但存在单点故障,性能和扩展性有限。
- 阶段二:服务化与高可用
业务量增长,必须拆分服务。网关、OMS、撮合引擎、行情服务各自成为独立进程。撮合引擎采用主备(Active-Passive)模式。主引擎处理所有订单,并将操作日志(比如使用Kafka或自研的低延迟消息队列)实时复制给备引擎。备引擎在内存中同步回放日志,保持与主引擎状态一致(Hot Standby)。当主引擎宕机,可通过Zookeeper等协调服务实现秒级切换。
- 阶段三:多交易对扩展与混合模式实现
系统需要支持成百上千个交易对。单个撮合引擎实例成为瓶颈。此时需要对撮合能力进行水平扩展(Sharding)。可以基于交易对名称(如`hash(symbol) % N`)将不同的交易对分配到不同的撮合引擎实例上。每个实例依然是主备模式。在此阶段,撮合引擎内部的状态机需要升级,实现对集合竞价和连续竞价模式的定时切换,从而支持完整的开盘、盘中、收盘流程。
- 阶段四:追求极致性能与全球化部署
面向全球顶级交易所的性能要求。延迟从微秒级压向纳秒级。软件层面,使用DPDK/Solarflare等技术绕过内核网络协议栈,直接在用户空间收发网络包。撮合逻辑用C++或Rust重写,并进行极致的内存布局优化和CPU Cache优化。硬件层面,部分最核心的逻辑(如订单簿操作)可能会用FPGA(现场可编程门阵列)实现,达到硬件级的处理速度。在全球部署多个接入点(PoP),通过专线网络连接到核心撮合中心,服务全球客户。
从简单的连续撮合算法,到支持混合模式、高可用、可扩展的全球化交易平台,这条演进之路不仅是技术复杂度的提升,更是对市场规则、系统可靠性和性能极限的不断探索与妥协。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。