订单簿核心操作:添加、撤销、成交与盘口更新

大家好,我是你们的量化交易讲师。今天我们来聊聊订单簿最核心的四个操作——说白了,就是订单从生到死的完整生命周期。

我个人习惯把订单簿想象成一个「排队系统」。每个订单进来,要么排队等着,要么插队成交,要么中途离开。嗯,就这么简单。但简单背后,藏着不少坑。

1. 订单添加(Add)—— 让订单进入队列

订单添加,就是把一个新订单挂到订单簿上。听起来简单?其实要考虑两个关键问题:插到哪怎么插

我见过不少新手直接往列表尾部追加,结果价格优先原则全乱了。正确的做法是:

  • 价格优先:买单从高到低排,卖单从低到高排
  • 时间优先:同价格下,先来的先成交

举个例子,假设当前卖单队列是这样的:

价格数量时间戳
10.0210009:30:01
10.0120009:30:00
10.0015009:29:59

现在来了一个卖单:价格10.01,数量50。它应该插在哪?

答案是:排在10.01那个200手的后面。因为价格相同,时间晚的排在后面。

核心逻辑:添加订单时,先按价格找到对应的价格档位,再按时间戳插入到该档位的末尾。

2. 订单撤销(Cancel)—— 让订单离开队列

撤销操作,说白了就是「我不玩了,把我的单子撤回来」。但这里有个容易踩的坑——撤销的订单可能已经部分成交了

我曾经在实盘项目中遇到过这种情况:一个1000手的买单,已经成交了300手,用户点击撤销。这时候你只能撤销剩下的700手,已经成交的部分是撤不回来的。

注意:撤销操作必须检查订单的「已成交数量」。如果已成交数量等于原始数量,这个订单已经不存在了,直接返回错误。

撤销的流程其实很简单:

  1. 根据订单ID找到对应的订单
  2. 检查订单状态(是否已完全成交)
  3. 从队列中移除该订单
  4. 更新对应的盘口深度

3. 订单成交(Execute)—— 订单簿的核心引擎

成交操作,是订单簿最刺激的部分。一个新订单进来,可能一口气吃掉好几个对手单。

你想想看,一个买单进来,它会从最低的卖单开始「吃」,直到自己的数量被满足,或者卖单被吃光。

成交的逻辑是这样的:

def execute_order(new_order, order_book):
    # 新订单是买单,从卖单队列最低价开始匹配
    if new_order.side == 'buy':
        while new_order.remaining_qty > 0 and order_book.asks:
            best_ask = order_book.asks[0]  # 最低卖价
            if new_order.price < best_ask.price:
                break  # 价格不够,无法成交
            
            # 计算成交数量
            trade_qty = min(new_order.remaining_qty, best_ask.remaining_qty)
            
            # 更新双方剩余数量
            new_order.remaining_qty -= trade_qty
            best_ask.remaining_qty -= trade_qty
            
            # 如果卖单完全成交,移除它
            if best_ask.remaining_qty == 0:
                order_book.asks.pop(0)
    
    # 如果新订单还有剩余,挂到买单队列
    if new_order.remaining_qty > 0:
        order_book.add_order(new_order)

嗯,这里要注意一个细节:部分成交。一个订单可能只成交了一部分,剩下的部分要挂回订单簿。我刚开始做的时候,就忘了处理这个情况,结果订单凭空消失了,被测试同事骂了一顿。

小技巧:每次成交后,记得更新订单簿的「最新成交价」和「累计成交量」。这两个数据对策略回测很重要。

4. 更新买卖盘口(Order Book Update)—— 保持数据新鲜

盘口更新,说白了就是每次操作后,重新计算「最佳买价」和「最佳卖价」。这个操作看似简单,但频率极高——每次添加、撤销、成交后都要做。

我个人习惯把盘口更新做成一个独立函数,而不是散落在各个操作里。这样代码更干净,也方便加日志。

def update_bbo(order_book):
    # 最佳买价:买单队列的最高价
    if order_book.bids:
        order_book.best_bid = order_book.bids[0].price
        order_book.best_bid_qty = order_book.bids[0].total_qty
    else:
        order_book.best_bid = 0
        order_book.best_bid_qty = 0
    
    # 最佳卖价:卖单队列的最低价
    if order_book.asks:
        order_book.best_ask = order_book.asks[0].price
        order_book.best_ask_qty = order_book.asks[0].total_qty
    else:
        order_book.best_ask = float('inf')
        order_book.best_ask_qty = 0
    
    # 计算买卖价差
    order_book.spread = order_book.best_ask - order_book.best_bid

这里有个坑:当买单队列为空时,最佳买价应该是0还是None?我建议用0,因为后续计算价差时不会报错。卖单队列为空时,最佳卖价用无穷大。

知识体系总览

说了这么多,我们来画张图,把四个操作的关系理清楚:

订单簿核心操作流程图 订单簿 买卖盘口 + 订单队列 添加 (Add) 撤销 (Cancel) 成交 (Execute) 盘口更新 每次操作后,都需要触发盘口更新,保持数据一致性

从这张图你能看到,四个操作不是孤立的。添加和撤销会改变订单队列,成交会消耗订单,而盘口更新则是每次操作后的「收尾工作」。

总结一下

订单簿的四个核心操作,说白了就是:

  • 添加:按价格时间排队
  • 撤销:检查状态再移除
  • 成交:从最优价格开始吃单
  • 盘口更新:每次操作后刷新最佳价格

嗯,这些基础操作看起来简单,但它们是整个订单簿仿真系统的地基。地基不稳,上面的策略验证全是白搭。我建议你动手写一遍代码,把每个操作的边界条件都测一测——比如空队列时撤销、价格刚好相等时成交、大单吃掉多个档位等等。

下一章我们会聊订单簿的数据结构设计,到时候你会看到,不同的数据结构对性能影响有多大。


专注资料整理