数据结构设计:用 heapqdeque 搭建订单簿骨架

做量化交易,尤其是搞限价订单簿,最核心的问题就是:数据怎么存,才能又快又准?

我刚开始写订单簿的时候,第一反应是用列表硬存。结果一跑回测,速度慢得让人崩溃。后来才明白,订单簿本质上是个优先级队列——买单要按价格从高到低排,卖单要按价格从低到高排。这不就是堆(heap)的经典场景吗?

今天我们就来聊聊,怎么用 Python 的 heapqdeque,把订单簿的骨架搭起来。

为什么选 heapq 做价格队列?

先问个问题:订单簿里,买单和卖单各自要维护一个价格序列。买单要取最高价,卖单要取最低价。如果用列表,每次插入新订单都要排序,复杂度 O(n log n)。如果订单量一上来,比如每秒几千笔,系统直接卡死。

heapq 就不一样了。它底层是最小堆,插入和弹出都是 O(log n)。卖单天然适合最小堆,因为我们要取最低卖价。那买单呢?取最高买价,其实可以存负数,把最大堆问题转成最小堆。

核心思路:

  • 卖单队列:直接用最小堆,价格越低优先级越高
  • 买单队列:存价格负数,堆顶就是最高买价

我在项目中遇到过一个问题:同一个价格可能有多个订单。如果只存价格,没法区分先后。这时候就需要价格-时间双队列了。

deque 管理时间队列

价格相同的情况下,谁先来谁先成交。这是订单簿的基本规则。所以每个价格档位,都需要一个先进先出(FIFO)的队列。

Python 的 deque 就是干这个的。它从左边弹出,右边插入,时间复杂度 O(1)。比列表的 pop(0) 快得多——列表 pop(0) 是 O(n),因为要移动所有元素。

小技巧:我习惯用 dequepopleft()append() 来模拟订单的到达和撮合。这样代码读起来很直观。

完整的数据结构设计

好了,我们把两个东西拼起来。每个价格档位,其实就是一个 deque,里面存着该价格下的所有订单 ID。而所有价格档位,用 heapq 来维护。

看代码:

import heapq
from collections import deque

class OrderBook:
    def __init__(self):
        # 买单:存负数价格,堆顶是最高买价
        self.buy_heap = []      # [(-price, price)]
        self.buy_orders = {}    # {price: deque([order_id1, order_id2, ...])}
        
        # 卖单:存正数价格,堆顶是最低卖价
        self.sell_heap = []     # [(price, price)]
        self.sell_orders = {}   # {price: deque([order_id1, order_id2, ...])}
        
    def add_buy_order(self, price, order_id):
        # 如果这个价格还没出现过,就加入堆
        if price not in self.buy_orders:
            heapq.heappush(self.buy_heap, (-price, price))
            self.buy_orders[price] = deque()
        # 订单加入时间队列
        self.buy_orders[price].append(order_id)
        
    def add_sell_order(self, price, order_id):
        if price not in self.sell_orders:
            heapq.heappush(self.sell_heap, (price, price))
            self.sell_orders[price] = deque()
        self.sell_orders[price].append(order_id)
        
    def get_best_bid(self):
        # 获取最高买价
        if not self.buy_heap:
            return None
        neg_price, price = self.buy_heap[0]
        return price
        
    def get_best_ask(self):
        # 获取最低卖价
        if not self.sell_heap:
            return None
        price, _ = self.sell_heap[0]
        return price

这段代码看着简单,但已经能跑起来了。你想想看,一个订单进来,先判断价格档位是否存在,不存在就入堆,存在就直接追加到 deque 尾部。取最优价格时,直接看堆顶,O(1) 时间。

注意:当某个价格档位的所有订单都成交或撤销后,记得清理堆。否则堆顶会残留无效价格。我曾经因为这个 bug,回测数据全乱了,排查了一整天。

价格队列与时间队列的联动

光有数据结构还不够,关键是怎么联动。比如撮合时,我们要从买一和卖一开始匹配。

流程是这样的:

  1. 取卖一堆顶价格,再取买一堆顶价格
  2. 如果卖一价 ≤ 买一价,就成交
  3. 从对应价格的 deque 里弹出最早订单
  4. 如果 deque 空了,从堆里弹出该价格
  5. 重复直到无法成交

我建议把撮合逻辑单独封装成一个方法,这样测试和调试都方便。

def match_orders(self):
    while self.buy_heap and self.sell_heap:
        bid_price = self.get_best_bid()
        ask_price = self.get_best_ask()
        
        if bid_price is None or ask_price is None:
            break
            
        if bid_price < ask_price:
            break  # 无法成交
            
        # 取最早订单
        buy_order = self.buy_orders[bid_price].popleft()
        sell_order = self.sell_orders[ask_price].popleft()
        
        # 处理成交逻辑(省略具体实现)
        print(f"成交:买 {buy_order} 和 卖 {sell_order},价格 {ask_price}")
        
        # 清理空队列
        if not self.buy_orders[bid_price]:
            heapq.heappop(self.buy_heap)
            del self.buy_orders[bid_price]
            
        if not self.sell_orders[ask_price]:
            heapq.heappop(self.sell_heap)
            del self.sell_orders[ask_price]

性能对比:为什么选这两个结构?

我拿实际数据测过。假设有 10 万个订单,分布在 1000 个价格档位上。用列表 + 列表的方式,插入一个订单平均要 0.5 毫秒。用 heapq + deque,只要 0.02 毫秒。差了 25 倍。

你想想看,高频交易里,一毫秒可能就错过一笔交易。这个差距是致命的。

操作 列表 + 列表 heapq + deque
插入订单 O(n) 或 O(n log n) O(log n) + O(1)
取最优价格 O(n) O(1)
弹出最早订单 O(n) O(1)
清理空档位 O(n) O(log n)

避坑指南

我曾经犯过一个错:直接用 heapq 存订单对象,而不是存价格。结果堆比较的是对象的内存地址,价格顺序全乱了。记住,堆里只存价格元组,订单本身放在 deque 里。

还有一个坑:heapq 不是线程安全的。如果你的订单簿要支持多线程,记得加锁。我一般用 threading.Lock 包裹关键操作。

个人习惯:我会在 __init__ 里初始化一个锁,然后在 add_ordermatch_orders 方法里用 with self.lock 包裹。这样既安全又清晰。

知识体系图

下面这张图展示了本章的核心逻辑:价格队列和时间队列如何协同工作。

订单簿数据结构设计 买单队列 (Bids) 卖单队列 (Asks) heapq (价格堆) [(-101.5, 101.5), (-101.0, 101.0), ...] deque (时间队列) {101.5: deque([id1, id2]), ...} heapq (价格堆) [(101.0, 101.0), (101.5, 101.5), ...] deque (时间队列) {101.0: deque([id3, id4]), ...} 撮合引擎:比较堆顶价格,弹出deque最早订单 价格堆负责优先级排序,时间队列负责FIFO顺序

说白了,heapq 管的是「谁的价格好」,deque 管的是「谁来得早」。两者配合,订单簿的核心逻辑就稳了。

嗯,这一章的内容就到这里。数据结构搭好了,下一章我们就可以往里面填真正的订单数据了。


公众号:蓝海资料掘金营,微信deep3321