从集合竞价到连续竞价:深入剖析金融交易核心撮合模式

本文面向具备一定分布式系统和底层技术认知的中高级工程师,旨在深入剖析金融交易系统的心脏——撮合引擎的两种核心模式:集合竞价(双向拍卖)与连续竞价。我们将不仅停留在业务概念,而是下探到底层数据结构、算法复杂度、系统架构的权衡,以及在真实的高频、低延迟场景下,这两种模式如何与操作系统、网络协议栈进行深度博弈,最终勾勒出一条从简单到复杂的架构演进路线图。

现象与问题背景

在任何一个金融交易市场,无论是股票、期货还是数字货币,其核心职能都是“价格发现”(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):按价格从低到高排序。如果价格相同,则按订单提交时间从早到晚排序。

当一个新订单进入系统时,撮合逻辑如下:

  1. 新买单进入:系统会查看订单簿的卖单侧。如果新买单的价格高于或等于卖单侧的最低报价(Best Ask),则发生撮合。成交价格为卖单的报价。撮合会一直持续,直到新买单被完全成交,或者它的价格低于下一个卖单的报价。如果还有剩余数量,则该剩余买单被存入订单簿的买单侧。
  2. 新卖单进入:系统会查看订单簿的买单侧。如果新卖单的价格低于或等于买单侧的最高报价(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 则是实现快速撤单的关键。工程坑点:

  • 内存管理:在交易时段,频繁的 newdelete 会造成堆内存碎片化和性能抖动。必须使用内存池(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。

延伸阅读与相关资源

  • 想系统性规划股票、期货、外汇或数字币等多资产的交易系统建设,可以参考我们的
    交易系统整体解决方案
  • 如果你正在评估撮合引擎、风控系统、清结算、账户体系等模块的落地方式,可以浏览
    产品与服务
    中关于交易系统搭建与定制开发的介绍。
  • 需要针对现有架构做评估、重构或从零规划,可以通过
    联系我们
    和架构顾问沟通细节,获取定制化的技术方案建议。
滚动至顶部