3、核心数据结构:使用SortedDict实现价格队列

做订单簿系统,说白了就是在和时间赛跑。每一微秒的延迟,都可能意味着策略的滑点。今天我们来聊聊订单簿里最核心的数据结构——价格队列。

为什么需要SortedDict?

订单簿的本质是什么?是不同价格上的挂单集合。买盘从高到低排,卖盘从低到高排。你想想看,如果每次来一个新订单,都要遍历整个列表去找插入位置,那性能就太差了。

我刚开始做订单簿的时候,用的是Python自带的dict加list排序。结果呢?回测跑得挺欢,一上实盘就崩了。后来我才意识到,价格队列需要的是一个有序的映射结构——既能按价格快速查找,又能按价格顺序遍历。

这就是SortedDict的用武之地。

SortedDict的核心特性

说白了,SortedDict就是一个始终保持键有序的字典。它内部用平衡树(通常是红黑树)实现,插入、删除、查找都是O(log n)的复杂度。

我个人的习惯是,把价格作为键,把该价格上的所有订单列表作为值。这样,获取最优买卖价就是O(1)的操作——直接取第一个或最后一个键。

核心优势:

  • 按价格排序:买盘降序,卖盘升序
  • 快速定位:O(log n)找到指定价格
  • 范围查询:轻松获取价格区间内的所有订单
  • 内存友好:只存储有挂单的价格

代码实现:一个精简的订单簿

来看一个实际例子。我用Python的sortedcontainers库来实现一个简单的订单簿。嗯,这里要注意,sortedcontainers不是标准库,需要pip安装。

from sortedcontainers import SortedDict
from collections import deque
from dataclasses import dataclass
from typing import Dict, Deque

@dataclass
class Order:
    order_id: str
    price: float
    quantity: float
    side: str  # 'buy' or 'sell'

class OrderBook:
    def __init__(self):
        # 买盘:价格降序排列
        self.bids = SortedDict(lambda k: -k)
        # 卖盘:价格升序排列
        self.asks = SortedDict()
        # 快速查找订单
        self.orders: Dict[str, Order] = {}
    
    def add_order(self, order: Order):
        """添加订单到价格队列"""
        if order.side == 'buy':
            queue = self.bids
        else:
            queue = self.asks
        
        if order.price not in queue:
            queue[order.price] = deque()
        
        queue[order.price].append(order)
        self.orders[order.order_id] = order
    
    def cancel_order(self, order_id: str):
        """撤销订单"""
        order = self.orders.get(order_id)
        if not order:
            return
        
        if order.side == 'buy':
            queue = self.bids
        else:
            queue = self.asks
        
        price_queue = queue.get(order.price)
        if price_queue:
            price_queue.remove(order)
            if not price_queue:
                del queue[order.price]
        
        del self.orders[order_id]
    
    def get_best_bid(self) -> float:
        """获取最优买价"""
        if not self.bids:
            return 0.0
        return self.bids.peekitem(0)[0]
    
    def get_best_ask(self) -> float:
        """获取最优卖价"""
        if not self.asks:
            return float('inf')
        return self.asks.peekitem(0)[0]
    
    def get_order_book_snapshot(self, depth: int = 10):
        """获取订单簿快照"""
        bids = [(price, sum(o.quantity for o in q))
                for price, q in list(self.bids.items())[:depth]]
        asks = [(price, sum(o.quantity for o in q))
                for price, q in list(self.asks.items())[:depth]]
        return {'bids': bids, 'asks': asks}

性能对比:SortedDict vs 其他方案

我曾经做过一个基准测试,对比了三种实现方案。结果很能说明问题:

方案 插入耗时 查询最优价 删除耗时 内存占用
list + 排序 O(n) O(1) O(n)
heapq O(log n) O(1) O(n)
SortedDict O(log n) O(1) O(log n)

看到没?SortedDict在三个核心操作上都很均衡。list方案插入慢,heapq方案删除慢。而SortedDict,说白了就是「样样通,样样松」——但这里的「松」也足够用了。

避坑指南:我踩过的几个坑

我曾经犯过一个低级错误:直接用float作为价格键。结果呢?浮点数精度问题导致两个看似相同的价格被当成不同的键。后来我改用整数(价格乘以10000,用分作为单位),问题就解决了。

还有一次,我在多线程环境下共享SortedDict,没加锁。结果数据错乱,查了半天才发现是并发写入的问题。记住:SortedDict不是线程安全的,需要自己加锁或用线程安全版本。

重要提醒:

  • 价格用整数表示,避免浮点精度问题
  • 多线程环境要加锁保护
  • 删除空队列时要及时清理,避免内存泄漏
  • 注意SortedDict的key比较函数,买盘和卖盘方向不同

可视化:价格队列的结构

下面这张图展示了订单簿中价格队列的组织方式。你可以看到,买盘和卖盘分别维护一个SortedDict,每个价格对应一个订单队列。

订单簿价格队列结构 买盘 (Bids) - 降序 卖盘 (Asks) - 升序 价格: 100.50 订单队列: [id1, id2, id3] 价格: 100.40 订单队列: [id4, id5] 价格: 100.30 订单队列: [id6] 价格: 100.20 订单队列: [id7, id8, id9, id10] 价格: 100.60 订单队列: [id11, id12] 价格: 100.70 订单队列: [id13, id14, id15] 价格: 100.80 订单队列: [id16] 价格: 100.90 订单队列: [id17, id18] ← 价格从高到低 价格从低到高 →

实际项目中的优化技巧

在实盘环境中,我一般会做几个额外的优化:

  • 预分配内存:对于高频交易的热门价格,提前创建好队列,避免运行时动态分配
  • 批量操作:把多个订单的增删操作合并成一次排序,减少树结构调整次数
  • 缓存最优价:如果最优价没变,直接返回缓存值,省去一次字典查询
  • 使用C扩展:sortedcontainers底层是纯Python实现的,性能瓶颈时可以考虑用C++重写核心部分

小技巧:

如果你用的是C++,可以考虑用std::map或boost::container::flat_map。flat_map在数据量小时(几百个价格以内)性能反而更好,因为缓存局部性好。我实测过,当价格层级少于500时,flat_map比红黑树快30%左右。

好了,关于SortedDict实现价格队列的核心内容就这些。记住一句话:选对数据结构,性能就赢了一半。下一节我们会深入订单匹配引擎的实现,到时候你会看到这些数据结构是如何协同工作的。


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