本文面向具备一定分布式系统和底层技术认知的中高级工程师,旨在深入剖析金融交易系统的心脏——撮合引擎的两种核心模式:集合竞价(双向拍卖)与连续竞价。我们将不仅停留在业务概念,而是下探到底层数据结构、算法复杂度、系统架构的权衡,以及在真实的高频、低延迟场景下,这两种模式如何与操作系统、网络协议栈进行深度博弈,最终勾勒出一条从简单到复杂的架构演进路线图。
现象与问题背景
在任何一个金融交易市场,无论是股票、期货还是数字货币,其核心职能都是“价格发现”(Price Discovery)与“交易执行”(Trade Execution)。这背后依赖一套精确、高效且公平的规则来匹配买卖双方的订单,这套规则的计算机实现就是撮合引擎。然而,我们观察到市场在不同时间段会采用截然不同的撮合方式。例如,A股市场在上午9:15到9:25采用集合竞价来决定开盘价,而在9:30之后则切换到连续竞价模式进行盘中交易。为何需要这两种模式?它们分别解决了什么核心问题?
这个现象背后隐藏着一个根本性的问题:市场状态是动态变化的。在开盘前,市场经历了休市期,大量的信息(如宏观政策、公司财报、外盘走势)被累积,导致买卖双方的意愿出现了巨大的不确定性。此时,市场的首要任务是在一个“信息混沌”的状态中,找出一个能最大程度满足各方交易意愿、最能代表此刻资产公允价值的“均衡价格”。这就是集合竞价的核心使命。
而一旦开盘,市场进入了连续交易时段,价格围绕开盘价波动,信息以流式的方式到达。此时,市场的核心矛盾从“寻找均衡价”转变为“高效处理连续不断的订单流,并提供即时流动性”。交易者追求的是速度和确定性,即“我的订单能否以当前最优价格立刻成交”。这便是连续竞egaio的需求场景。
因此,这两种模式并非简单的替代关系,而是在不同市场环境下,为达成不同目标(价格发现 vs. 交易效率)而设计的两种截然不同的机制。理解它们的底层原理与工程实现,是构建任何高性能交易系统的基石。
关键原理拆解
作为一名架构师,我们必须穿透业务术语,回归到计算机科学和微观经济学的基础原理。这两种撮合模式,本质上是两种不同的算法和数据结构的应用。
集合竞价:离散时间下的全局最优解
集合竞价,学术上更接近于“双向拍卖”(Double Auction)或“叫价市场”(Call Market)。其核心思想是在一个指定的时间窗口内(例如开盘前10分钟),只接收订单而不进行任何匹配。当时间窗口关闭时,系统会根据一个全局最优原则,计算出一个唯一的“清算价格”(Clearing Price),并用这个价格来撮合所有符合条件的订单。
这个“全局最优原则”通常是最大成交量原则。其算法步骤可以严谨地描述如下:
- 1. 数据收集与准备:在指定时间段内,收集所有买入(Bid)和卖出(Ask)的限价订单。将所有买单按价格从高到低排序,价格相同的按时间优先;将所有卖单按价格从低到高排序,价格相同的按时间优先。
- 2. 构建累计需求与供给曲线:遍历所有出现过的有效申报价位。对于每一个价位
P,我们可以计算出:- 累计买方需求量:所有报价
>= P的买单数量之和。 - 累计卖方供给量:所有报价
<= P的卖单数量之和。
- 累计买方需求量:所有报价
- 3. 寻找最优价格:遍历所有价位
P,计算在该价位下的潜在成交量,即min(累计买方需求量, 累计卖方供给量)。那个使得这个成交量最大的价位P*,就是最终的清算价格。 - 4. 处理平局(Tie-Breaking):如果存在多个价位都能实现最大成交量,则需要额外的规则来确定唯一价格。常见的规则有:
- 最小剩余失衡原则:选择那个使得(无法匹配的买单量 + 无法匹配的卖单量)最小的价格。
- 参考价优先原则:选择最接近上一交易日收盘价(或其他参考价)的价格。
- 5. 成交执行:所有买价高于
P*的买单和所有卖价低于P*的卖单,都以P*这个价格成交。报价等于P*的买单和卖单,则根据剩余可成交量,按照时间优先的原则依次成交。
从算法角度看,如果订单数量为 N,排序的时间复杂度为 O(N log N)。构建累计需求/供给曲线和寻找最优价格的过程,如果价位空间是离散且有限的,其复杂度与价位数量 M 和订单数 N 相关,通常可以优化到 O(N + M)。因此,集合竞价的计算开销主要在排序,它是一个批处理过程,延迟是其固有属性。
连续竞价:流式处理下的局部最优匹配
连续竞价,又称“持续市场”(Continuous Market),是我们在盘中交易时最熟悉的模式。它遵循价格优先、时间优先的原则,对每一笔新进入的订单进行即时处理。这背后依赖一个核心的数据结构——订单簿(Order Book)。
订单簿在逻辑上分为两个部分:买单侧(Bid side)和卖单侧(Ask side)。
- 买单侧 (Bids):按价格从高到低排序。如果价格相同,则按订单提交时间从早到晚排序。
- 卖单侧 (Asks):按价格从低到高排序。如果价格相同,则按订单提交时间从早到晚排序。
当一个新订单进入系统时,撮合逻辑如下:
- 新买单进入:系统会查看订单簿的卖单侧。如果新买单的价格高于或等于卖单侧的最低报价(Best Ask),则发生撮合。成交价格为卖单的报价。撮合会一直持续,直到新买单被完全成交,或者它的价格低于下一个卖单的报价。如果还有剩余数量,则该剩余买单被存入订单簿的买单侧。
- 新卖单进入:系统会查看订单簿的买单侧。如果新卖单的价格低于或等于买单侧的最高报价(Best Bid),则发生撮合。成交价格为买单的报价。撮合过程类似,直到卖单被完全成交或无法继续匹配。剩余数量则存入订单簿的卖单侧。
从数据结构与算法角度看,订单簿的实现至关重要。它需要支持高效的插入、删除和查找最优报价(min/max)的操作。一个简单的数组或链表无法胜任高频场景。实践中,通常采用:
- 平衡二叉搜索树(如红黑树):每个节点代表一个价格档位,节点内部用一个双向链表存储该价位的所有订单。这样,查找、插入、删除操作的平均时间复杂度都是 O(log P),其中 P 是价格档位的数量。
- 哈希表 + 双向链表:用哈希表实现对具体订单的 O(1) 访问(便于快速撤单),用双向链表维护订单在同一价位上的时间顺序。价格档位之间可以用另一层数据结构(如跳表或平衡树)来组织。
连续竞价是一个事件驱动的流式处理过程。其核心优势是低延迟,理论上只要有对手方订单存在,交易就可以瞬时完成。其时间复杂度为 O(log P) 或 O(log N),远低于集合竞价的批处理模式。
系统架构总览
一个工业级的撮合系统,绝不仅仅是上述算法的简单实现,它是一个复杂的分布式系统。我们可以将其核心链路抽象为以下几个关键组件,这些组件共同确保了订单处理的顺序性、一致性和高可用性。
逻辑架构图描述:
整个系统的数据流是单向的。用户请求通过 接入层网关(Gateway) 进入系统,网关负责协议转换(如从FIX协议转为内部二进制协议)、用户认证和初步风控。合法的请求被序列化后,发送到 定序器集群(Sequencer)。定序器是系统的咽喉,它为所有进入撮合引擎的指令(下单、撤单)赋予一个全局严格递增的序号,确保了全市场操作的“全序关系”。这个带有序号的指令流被持久化到 日志存储(Journal/Log),然后被分发给核心的 撮合引擎(Matching Engine)。撮合引擎是单线程或基于内存事务模型的,它按照序号消费指令流,在内存中维护订单簿并执行撮合算法。撮合产生的结果,如成交回报(Trade)、订单状态更新和行情快照(Market Data),会通过 行情发布网关(Market Data Publisher) 以低延迟的方式(如UDP组播)广播出去,同时也会被持久化。整个系统需要一个高可用的主备或多活架构来保证业务连续性。
核心模块设计与实现
我们现在切换到极客工程师的视角,深入到代码层面,看看这些核心模块的实现要点和坑点。
订单簿(Order Book)的实现
别用教科书里的简单数组。在高频场景下,订单簿是性能瓶颈的核心。一个务实且高性能的实现可能长这样:
// 这是一个极简化的C++伪代码示例,展示了核心数据结构
// Order对象,包含ID、价格、数量、时间等
struct Order { uint64_t id; int64_t price; uint64_t qty; ... };
// PriceLevel,存储一个价位上的所有订单
struct PriceLevel {
int64_t price;
uint64_t total_qty;
std::list<Order*> orders; // 用链表保证时间优先
};
class OrderBook {
private:
// 买单簿:价格从高到低
std::map<int64_t, PriceLevel, std::greater<int64_t>> bids_;
// 卖单簿:价格从低到高
std::map<int64_t, PriceLevel> asks_;
// 为了O(1)撤单,需要一个订单ID到订单指针的哈希表
std::unordered_map<uint64_t, Order*> order_map_;
public:
// ... 方法 ...
void AddOrder(Order* new_order);
void CancelOrder(uint64_t order_id);
};
这里的 std::map 在C++中通常是红黑树实现,提供了 O(log P) 的价格档位查找。std::list 保证了同一价位订单的 O(1) 插入和删除。unordered_map 则是实现快速撤单的关键。工程坑点:
- 内存管理:在交易时段,频繁的
new和delete会造成堆内存碎片化和性能抖动。必须使用内存池(Object Pool)来预分配和复用Order、PriceLevel等对象。 - 价格表示:绝对不要用浮点数(float/double)来表示价格和数量!精度问题会导致灾难。必须使用定点数,比如用
int64_t存储价格,实际价格是这个整数值除以一个固定的精度因子(如10000)。
连续竞价撮合逻辑
当一个买单进来时,核心撮合逻辑伪代码如下:
// 伪代码: 处理一个新的买单 (newBuyOrder)
func (book *OrderBook) ProcessLimitBuy(newBuyOrder *Order) {
trades := make([]Trade, 0)
// 遍历卖单簿,从价格最低的开始
for askLevelIter := book.Asks.MinPriceLevel(); askLevelIter != nil; askLevelIter = askLevelIter.Next() {
if newBuyOrder.Price < askLevelIter.Price {
// 买单价格低于最低卖价,无法撮合,直接入簿
break
}
// 价格交叉,可以撮合
for _, sellOrder := range askLevelIter.Orders {
tradeQty := min(newBuyOrder.RemainingQty, sellOrder.RemainingQty)
// 生成成交记录
trades = append(trades, CreateTrade(newBuyOrder.ID, sellOrder.ID, sellOrder.Price, tradeQty))
newBuyOrder.RemainingQty -= tradeQty
sellOrder.RemainingQty -= tradeQty
if sellOrder.RemainingQty == 0 {
// 对手单已完全成交,从订单簿中移除
book.remove(sellOrder)
}
if newBuyOrder.RemainingQty == 0 {
// 新订单已完全成交,撮合结束
goto end_matching
}
}
}
end_matching:
if newBuyOrder.RemainingQty > 0 {
// 新订单未完全成交,将其放入买单簿
book.add(newBuyOrder)
}
// 发布成交回报和行情更新
PublishTrades(trades)
PublishMarketDataUpdate(book)
}
工程坑点:
- 事务性:整个撮合过程必须是原子性的。要么全部成功,要么完全不发生。在单线程模型里这相对简单,但在分布式或多线程模型里,需要严格的锁机制或软件事务内存(STM)。
- 公平性:定序器保证了“先到先处理”的公平性。撮合引擎必须严格按照定序器给出的顺序执行,不能有任何跳跃或重排,否则就是严重的合规问题。
集合竞价算法实现
这个算法不像连续竞价那样是流式的,它是一个批处理任务。核心是构建累计量数组。
# 伪代码: 计算集合竞价清算价格
def calculate_clearing_price(bids, asks):
# bids 按价格降序, asks 按价格升序
bids.sort(key=lambda o: o.price, reverse=True)
asks.sort(key=lambda o: o.price)
# 获取所有可能的价格点
prices = sorted(list(set([o.price for o in bids] + [o.price for o in asks])))
max_volume = 0
clearing_price = 0
min_imbalance = float('inf')
for p in prices:
cumulative_bid_vol = sum(o.qty for o in bids if o.price >= p)
cumulative_ask_vol = sum(o.qty for o in asks if o.price <= p)
matchable_volume = min(cumulative_bid_vol, cumulative_ask_vol)
if matchable_volume > max_volume:
max_volume = matchable_volume
clearing_price = p
imbalance = abs(cumulative_bid_vol - cumulative_ask_vol)
min_imbalance = imbalance
elif matchable_volume == max_volume:
# 处理平局:选择失衡量更小的
imbalance = abs(cumulative_bid_vol - cumulative_ask_vol)
if imbalance < min_imbalance:
min_imbalance = imbalance
clearing_price = p
# 此处还可加入参考价优先等更复杂的规则
return clearing_price, max_volume
工程坑点:这个过程计算量较大,如果订单数和价位特别多,可能会成为开盘前的性能瓶颈。优化点在于如何高效地计算累计量,可以预先处理成离散的价位-数量数组,然后用前缀和等技巧来加速计算。
性能优化与高可用设计
交易系统对性能和可用性的要求是极致的,任何一点优化都可能带来巨大的业务优势。
延迟对抗:从毫秒到微秒的战争
- CPU亲和性(CPU Affinity):将撮合引擎的执行线程绑定到特定的CPU核心上。这能极大减少线程在不同核心间的迁移所带来的上下文切换开销,并最大化利用CPU的L1/L2缓存。当订单簿等核心数据结构能一直“热”在某个核的缓存里时,内存访问延迟会显著降低。
- 内核旁路(Kernel Bypass):传统的网络数据包从网卡到用户态应用程序,需要经历中断、内核协议栈处理、多次内存拷贝,延迟在几十微秒级别。使用DPDK或Solarflare OpenOnload这类技术,可以让应用程序直接从网卡DMA内存中读取数据,绕过内核,将网络延迟降至个位数微秒。
- 无锁数据结构:在多线程场景下(如行情发布和撮合核心解耦),使用锁会带来争用和延迟。可以采用无锁队列(Lock-Free Queue)等数据结构在线程间传递数据,避免锁的开销。
- 数据布局与缓存行对齐:精心设计数据结构在内存中的布局,让高频访问的数据位于同一个缓存行(Cache Line,通常是64字节),可以减少缓存伪共享(False Sharing)和缓存未命中(Cache Miss),这是硬件层面的极致优化。
高可用设计:系统永不眠
- 确定性与可重放日志:撮合引擎必须是确定性的,即给定相同的初始状态和相同的输入指令序列,必须产生完全相同的结果。这是实现高可用的基石。所有输入指令都被定序器序列化并写入不可变日志(如Kafka或自研的Raft-based Log)。
- 主备热备(Active-Passive):这是最经典的HA方案。一个主引擎处理实时流量,一个或多个备用引擎在另一台机器上,以只读模式消费同一份指令日志,在内存中构建一模一样的订单簿状态。主引擎通过心跳维持状态,一旦心跳超时,备用引擎可以被提升为主,因为它拥有几乎最新的状态,接管过程可以在毫秒级完成。
- 关键数据快照:虽然可以从头重放日志来恢复状态,但在系统重启或新节点加入时,这会非常耗时。因此,需要定期对内存中的订单簿状态做快照(Snapshot)并持久化。恢复时,先加载最新的快照,再从快照点开始重放后续的日志,大大缩短恢复时间(RTO)。
架构演进与落地路径
没有一个系统是一蹴而就的,一个健康的交易系统架构应该具备清晰的演进路径。
第一阶段:单体MVP(验证期)
对于初创项目或非核心业务,可以从一个简单的单体应用开始。一个进程内包含网络接入、撮合逻辑和数据持久化。撮合引擎采用连续竞价模式,使用前述的基于std::map的订单簿实现,持久化可以直接写入关系型数据库(如PostgreSQL)。这个阶段的目标是快速验证业务逻辑,性能和可用性不是首要矛盾。
第二阶段:服务化与解耦(成长期)
当流量增长,单体应用的瓶颈出现。需要将接入网关、撮合引擎、行情服务拆分为独立的服务。引入一个可靠的消息队列(如Kafka)作为定序器和系统总线,实现核心组件的解耦。撮合引擎实现主备热备HA架构。这个阶段的核心是提升系统的可扩展性和可用性,为更大规模的流量做准备。
第三阶段:极致性能优化(成熟期)
当业务进入高频或机构交易领域,延迟成为核心竞争力。此时需要进行深度优化:撮合引擎采用CPU绑核、内存池等技术;网关和行情系统采用内核旁路网络栈;通信协议从JSON/FIX转向更高效的二进制协议(如SBE或Protobuf)。整个核心链路的硬件部署在同一机架,甚至使用专用网络设备以降低网络跳数。
第四阶段:混合模式与功能完备(平台期)
系统已经非常稳定和高效,需要支持更复杂的交易产品和规则。此时,可以引入集合竞价模块,用于处理开盘、收盘或某些特定产品(如IPO)的定价。系统需要能够根据时间或产品状态,在集合竞价和连续竞价模式之间平滑切换。架构上,这可能意味着撮合引擎内部需要有状态机来管理当前的撮合模式,并能调度执行不同的撮合算法。
通过这样的分阶段演进,架构始终与业务的复杂度、流量规模和性能要求相匹配,避免了过度设计和过早优化,确保了技术投入的最高ROI。
延伸阅读与相关资源
-
想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
交易系统整体解决方案。 -
如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
产品与服务
中关于交易系统搭建与定制开发的介绍。 -
需要针对现有架构做评估、重构或从零规划,可以通过
联系我们
和架构顾问沟通细节,获取定制化的技术方案建议。