订单簿数据结构:买单队列与卖单队列的存储逻辑

做量化交易这些年,我见过不少新手一上来就盯着K线看,却忽略了订单簿这个真正的「战场」。说白了,订单簿就是交易所里所有挂单的实时快照。你看到的每一笔成交,背后都是订单簿里买单和卖单的激烈博弈。

今天我们就来拆解一下,订单簿最核心的数据结构——买单队列和卖单队列,到底是怎么存储的。

一、订单簿的基本构成

一个标准的订单簿,其实就两个队列:

  • 买单队列(Bid Queue):按价格从高到低排列。谁出价高,谁排前面。
  • 卖单队列(Ask Queue):按价格从低到高排列。谁卖得便宜,谁排前面。

为什么这么排?因为交易所要优先撮合「最高买入价」和「最低卖出价」。这是最朴素的供需逻辑。

核心概念:买单队列的最高价叫「买一」,卖单队列的最低价叫「卖一」。买一和卖一的价差,就是「点差」(Spread)。点差越小,市场流动性越好。

二、存储逻辑:价格优先 + 时间优先

嗯,这里要注意。订单簿的排序规则是「价格优先,时间优先」。什么意思?

  • 价格优先:买单价格高的排在前面,卖单价格低的排在前面。
  • 时间优先:相同价格下,先挂单的排在前面。

我刚开始做回测系统时,就踩过这个坑。当时我简单用了一个数组来存订单,每次插入新订单都遍历一遍找位置。结果回测跑起来慢得离谱。后来才意识到,订单簿的插入和删除操作极其频繁,必须用高效的数据结构。

三、数据结构选型:我为什么偏爱跳表

常见的实现方式有这么几种:

数据结构 插入复杂度 删除复杂度 查询复杂度 实际应用
有序数组 O(n) O(n) O(log n) 不推荐,太慢
红黑树 O(log n) O(log n) O(log n) C++常用,实现复杂
跳表(Skip List) O(log n) O(log n) O(log n) 我个人的首选
哈希表 + 双向链表 O(1) 平均 O(1) O(1) 内存占用大

我个人习惯用跳表。为什么?因为它实现简单,而且支持范围查询。你想想看,做量化交易时经常需要查「买一到买十」或者「卖一到卖十」,跳表天然支持这种操作。

我的经验:在低延迟场景下,跳表的性能非常稳定。我曾经把一个基于红黑树的订单簿改成跳表实现,延迟抖动降低了40%左右。当然,如果你用C++,也可以考虑用std::map(底层是红黑树),但Java和Go的话,跳表更友好。

四、代码示例:一个极简的订单簿

下面我用Python写一个简化版的订单簿,帮你理解核心逻辑。实际生产环境比这复杂得多,但原理是一样的。

import bisect
from collections import defaultdict

class OrderBook:
    def __init__(self):
        # 买单队列:价格从高到低
        self.bids = []  # 存储价格列表
        self.bid_orders = defaultdict(list)  # 价格 -> 订单列表
        
        # 卖单队列:价格从低到高
        self.asks = []
        self.ask_orders = defaultdict(list)
    
    def add_order(self, order):
        """添加订单"""
        if order.side == 'buy':
            # 买单:价格高的优先
            if order.price not in self.bid_orders:
                bisect.insort(self.bids, -order.price)  # 取负值实现降序
            self.bid_orders[order.price].append(order)
        else:
            # 卖单:价格低的优先
            if order.price not in self.ask_orders:
                bisect.insort(self.asks, order.price)
            self.ask_orders[order.price].append(order)
    
    def get_top_n(self, n=5):
        """获取前N档行情"""
        top_bids = []
        for neg_price in self.bids[:n]:
            price = -neg_price
            total_vol = sum(o.volume for o in self.bid_orders[price])
            top_bids.append((price, total_vol))
        
        top_asks = []
        for price in self.asks[:n]:
            total_vol = sum(o.volume for o in self.ask_orders[price])
            top_asks.append((price, total_vol))
        
        return top_bids, top_asks

这段代码虽然简单,但已经包含了订单簿的核心逻辑:

  • 用两个列表维护价格顺序
  • 用字典按价格分组存储订单
  • 插入时自动排序

避坑指南:我曾经在实盘环境中犯过一个低级错误——直接用浮点数作为价格key。结果因为浮点数精度问题,两个明明相同的价格被当成了不同的key,导致订单簿错乱。后来我统一改用整数(比如价格乘以10000)来存储,再也没出过问题。

五、订单簿的实时更新流程

订单簿不是静态的,它每时每刻都在变化。主要操作就三种:

  1. 新增订单:插入到对应队列的正确位置
  2. 撤销订单:从队列中删除指定订单
  3. 成交订单:从队列头部移除已成交的订单

你想想看,一个高频交易系统每秒可能要处理几万笔订单操作。如果数据结构选得不好,光排序就能把CPU吃满。

我记得有一次帮一家做市商优化系统,他们用的就是数组+遍历的方式。在行情剧烈波动时,订单簿更新延迟高达几十毫秒。换成跳表后,延迟降到了微秒级。这就是数据结构的威力。

六、知识体系结构图

下面我用一张SVG图来总结本章的核心逻辑:

订单簿数据结构核心逻辑 订单簿 (Order Book) 买单队列 (Bids) 卖单队列 (Asks) 价格从高到低 ↓ 价格从低到高 ↑ 数据结构:跳表 / 红黑树 数据结构:跳表 / 红黑树 核心操作 新增订单 撤销订单 成交撮合 价格优先 + 时间优先 | 买一卖一点差 = 市场流动性指标

七、总结

订单簿的数据结构,说白了就是两个有序队列的管理问题。选对数据结构,你的交易系统就成功了一半。

我个人建议:

  • 如果追求极致性能,用C++的红黑树
  • 如果追求开发效率,用跳表
  • 如果只是做研究,Python的有序字典也能凑合

但不管选什么,一定要理解背后的排序逻辑和操作复杂度。否则,等你的策略跑起来才发现性能瓶颈,那就晚了。