数据结构设计:OrderBook类、价格队列与订单映射

好,咱们今天来聊聊订单簿的核心数据结构。说实话,这部分是整套系统的地基。地基没打好,后面盖多少层楼都得塌。我在做高频交易系统那会儿,就吃过数据结构的亏——当时图省事用了Python原生的dict来维护价格层级,结果回测时好好的,一上实盘就崩了。

嗯,咱们先理清一个概念:订单簿本质上是个什么东西?说白了,它就是个价格-订单的映射关系。买盘和卖盘各自维护一个价格序列,每个价格下面挂着一堆订单。就这么简单,但实现起来门道不少。

OrderBook类的整体设计

我个人习惯把OrderBook设计成一个容器类,它不关心订单是怎么来的,只负责维护当前的市场状态。来看下核心结构:

class OrderBook:
    def __init__(self):
        # 买盘:价格降序排列(最高价优先)
        self.bids = SortedDict(lambda x: -x)
        # 卖盘:价格升序排列(最低价优先)
        self.asks = SortedDict(lambda x: x)
        # 订单ID到订单对象的映射
        self.orders = {}
        # 价格到订单队列的映射
        self.price_queues = {}
        
    def add_order(self, order):
        """添加新订单"""
        pass
        
    def cancel_order(self, order_id):
        """撤销订单"""
        pass
        
    def execute_order(self, order_id, quantity):
        """成交订单"""
        pass
        
    def get_top_of_book(self):
        """获取最优买卖价"""
        pass

你可能会问:为什么买盘和卖盘要用不同的排序方向?其实很简单——交易员看盘时,买盘最上面永远是最高的那个价格,卖盘最上面永远是最低的价格。这样设计,取最优报价就是O(1)的操作。

价格队列的设计思路

价格队列,我习惯叫它PriceLevel。每个价格层级维护一个FIFO队列。为什么用FIFO?因为交易所的价格优先-时间优先规则决定了,同一价格下先到的订单先成交。

class PriceLevel:
    def __init__(self, price):
        self.price = price
        self.orders = deque()  # 订单队列
        self.total_quantity = 0  # 该价格总挂单量
        
    def add(self, order):
        self.orders.append(order)
        self.total_quantity += order.quantity
        
    def remove(self, order_id):
        # 注意:这里需要O(n)查找,实际项目中会用双向链表优化
        for order in self.orders:
            if order.order_id == order_id:
                self.orders.remove(order)
                self.total_quantity -= order.quantity
                return
                
    def get_quantity(self):
        return self.total_quantity

避坑指南:我曾经在PriceLevel里直接用list来存订单,结果撤单时remove操作是O(n)的。在每秒几万笔订单的场景下,这个O(n)直接让CPU飙到100%。后来改成了双向链表+哈希表的组合,撤单变成了O(1)。

订单映射:为什么需要它?

订单映射说白了就是个订单ID → 订单对象的字典。你可能觉得这玩意儿多余——订单不都在价格队列里吗?

嗯,这里有个实际场景:交易所发来一条撤单指令,只给了你一个订单ID。如果没有订单映射,你得遍历所有价格层级去找这个订单。想想看,如果盘口有100个价格层级,每个层级100个订单,那就是10000次遍历。有了订单映射,一次dict查找就搞定了。

class Order:
    def __init__(self, order_id, price, quantity, side, timestamp):
        self.order_id = order_id
        self.price = price
        self.quantity = quantity
        self.side = side  # 'buy' or 'sell'
        self.timestamp = timestamp
        self.status = 'active'  # active, cancelled, filled
        
# 订单映射的使用
def cancel_order(self, order_id):
    order = self.orders.get(order_id)
    if not order:
        return False  # 订单不存在
    
    # 从价格队列中移除
    price_level = self.price_queues[order.price]
    price_level.remove(order_id)
    
    # 如果价格队列空了,清理掉
    if price_level.total_quantity == 0:
        del self.price_queues[order.price]
        if order.side == 'buy':
            del self.bids[order.price]
        else:
            del self.asks[order.price]
    
    order.status = 'cancelled'
    return True

SortedDict:价格层级维护的秘密武器

为什么用SortedDict而不是普通的dict?我给你算笔账:

操作 普通dict SortedDict
插入价格 O(1) O(log n)
删除价格 O(1) O(log n)
获取最优价格 O(n)(需遍历) O(1)(直接取首尾)
获取价格范围 O(n)(需排序) O(k)(直接切片)

看到没?SortedDict用O(log n)的插入删除代价,换来了O(1)的最优价格获取。在订单簿场景下,这个交换非常划算——因为查询最优价格的频率远高于插入删除

核心要点:SortedDict底层是红黑树或跳表实现的。Python的sortedcontainers库用的是平衡树,插入删除都是O(log n)。我个人推荐用这个库,它比bisect+list的方案稳定得多。

完整的数据流图

下面这张图展示了订单从进入系统到被维护的全流程:

订单簿数据流与核心结构 订单进入 订单映射 (orders dict) order_id → Order对象 买卖判断 买盘 SortedDict (降序) price → PriceLevel 卖盘 SortedDict (升序) price → PriceLevel PriceLevel: deque[Order, Order, ...] PriceLevel: deque[Order, Order, ...]

个人经验:我在做回测系统时,发现SortedDict的key必须是可哈希的。价格用float会有精度问题,我建议把所有价格乘以10000转成整数来存。比如12.34存成123400,这样既避免了浮点误差,又保持了排序的正确性。

实际项目中的避坑指南

最后分享几个我踩过的坑:

  • 内存泄漏:订单撤销后,记得从orders dict和price_queues里都删掉。我曾经漏删了orders dict里的记录,跑了一天内存涨了2个G。
  • 并发问题:如果订单簿被多个线程访问,SortedDict不是线程安全的。我一般用读写锁来保护,读操作不锁,写操作加锁。
  • 价格精度:前面说了,用整数存价格。别问我为什么知道——有一次因为浮点精度问题,两个明明相等的价格在SortedDict里被当成了不同的key。
  • 空价格层级:当某个价格的所有订单都成交或撤销后,记得把该价格从SortedDict里删掉。不然get_top_of_book会返回一个空的价格层级。

嗯,数据结构这块就聊到这儿。记住一句话:数据结构选对了,性能问题就解决了一半。SortedDict + 订单映射 + 价格队列,这三板斧够你应付绝大多数订单簿场景了。