第四章:订单簿实现

做市商的核心,说白了就是一本「账本」。

谁在什么价格想买多少,谁在什么价格想卖多少,全记在上面。这本账本,就是订单簿。我做了这么多年量化,见过太多人把精力花在策略上,结果订单簿一塌糊涂,撮合速度慢得像蜗牛。嗯,今天我们就来聊聊,怎么用 Python 把订单簿写得又快又稳。

4.1 订单簿的基本结构

订单簿其实不复杂。它主要维护两个队列:

  • 买单队列(Bid):按价格从高到低排序
  • 卖单队列(Ask):按价格从低到高排序

每个价格点上,可能挂着多个订单。比如有人挂 100.5 买 100 手,另一个人也挂 100.5 买 200 手,那这个价格点的总量就是 300 手。

我个人习惯用 OrderBook 类来封装这一切。先看一个最基础的版本:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 总量
        self.asks = {}  # 价格 -> 总量
        self.best_bid = None
        self.best_ask = None

    def add_order(self, side, price, quantity):
        if side == 'buy':
            self.bids[price] = self.bids.get(price, 0) + quantity
            if self.best_bid is None or price > self.best_bid:
                self.best_bid = price
        else:
            self.asks[price] = self.asks.get(price, 0) + quantity
            if self.best_ask is None or price < self.best_ask:
                self.best_ask = price

这个版本能跑,但性能堪忧。为什么?因为每次查询最优价格,都得遍历整个字典。你想想看,如果每秒有几千笔订单进来,这遍历一次就够呛。

4.2 红黑树与跳表:谁更适合做订单簿?

说到高效查询,大家第一反应就是红黑树。C++ 里的 std::map 就是红黑树实现的,插入、删除、查找都是 O(log n)。

但 Python 呢?Python 标准库里没有红黑树。你当然可以自己写一个,但我劝你别这么做——调试红黑树的旋转操作,我曾经花了一整个周末,最后发现是颜色标记错了。

那怎么办?用 跳表(Skip List)

跳表是另一种有序数据结构,实现起来比红黑树简单得多。它的核心思想是:给链表加几层「快速通道」。底层是完整的有序链表,上层是稀疏的索引。查找时,从顶层开始,快速跳过中间节点。

关键对比:

特性红黑树跳表
实现复杂度高(旋转、颜色调整)低(随机层数)
平均查询O(log n)O(log n)
最坏查询O(log n)O(n)(概率极低)
范围查询需要中序遍历天然支持
Python 实现不推荐手写推荐

我个人更倾向跳表。原因很简单:订单簿经常需要做范围查询,比如「查一下 100 到 101 之间有多少卖单」。跳表沿着底层链表走一遍就行,红黑树还得做中序遍历,麻烦。

下面是一个简化的跳表节点定义:

import random

class SkipNode:
    def __init__(self, price, quantity, level):
        self.price = price
        self.quantity = quantity
        self.forward = [None] * (level + 1)

class SkipList:
    def __init__(self, max_level=16):
        self.max_level = max_level
        self.head = SkipNode(-float('inf'), 0, max_level)
        self.level = 0

    def random_level(self):
        lvl = 0
        while random.random() < 0.5 and lvl < self.max_level:
            lvl += 1
        return lvl

嗯,这里要注意:跳表的随机层数决定了它的性能。层数太高浪费内存,层数太低退化成链表。我一般设最大层数为 16,对于大多数订单簿场景已经足够。

4.3 高效价格查询:从 O(n) 到 O(log n)

有了跳表,查询最优价格就变成了 O(log n)。但光有数据结构还不够,我们还得考虑 如何快速找到最优买卖价

我的做法是:维护两个指针,一个指向最低卖价,一个指向最高买价。每次插入或删除时,检查是否需要更新这两个指针。

class OrderBook:
    def __init__(self):
        self.bids = SkipList()  # 买盘,降序
        self.asks = SkipList()  # 卖盘,升序
        self.best_bid = None
        self.best_ask = None

    def update_best_bid(self):
        # 跳表头节点的下一个就是最高买价
        node = self.bids.head.forward[0]
        self.best_bid = node.price if node else None

    def update_best_ask(self):
        node = self.asks.head.forward[0]
        self.best_ask = node.price if node else None

这里有个坑:价格相同但订单不同的情况。比如两个订单都是 100.5 买,但一个是 10 手,一个是 20 手。我建议在节点里维护一个订单列表,而不是简单累加总量。这样方便撤单时精确扣减。

避坑指南:我曾经在实盘中发现,如果只累加总量,撤单时很难判断该减多少。后来改成每个价格点维护一个订单 ID 到数量的映射,才彻底解决。

4.4 内存优化技巧

订单簿吃内存,这是老生常谈的问题。尤其是高频场景,每秒可能产生几万个订单对象。Python 的对象开销很大,一个简单的 Order 对象可能占用上百字节。

怎么优化?我分享几个实战经验:

  • __slots__ 减少对象开销:Python 每个对象默认有一个 __dict__,占内存。加上 __slots__ 可以省掉它。
  • 用数组代替字典:如果价格精度固定(比如小数点后两位),可以用数组索引代替字典。价格 100.50 对应索引 10050,直接 O(1) 访问。
  • 复用对象:订单撤单后,对象不要直接丢弃,放回对象池。我习惯用 collections.deque 维护一个空闲队列。
class Order:
    __slots__ = ('order_id', 'price', 'quantity', 'side')
    def __init__(self, order_id, price, quantity, side):
        self.order_id = order_id
        self.price = price
        self.quantity = quantity
        self.side = side

# 对象池
order_pool = deque()
def get_order(order_id, price, quantity, side):
    if order_pool:
        order = order_pool.pop()
        order.order_id = order_id
        order.price = price
        order.quantity = quantity
        order.side = side
        return order
    return Order(order_id, price, quantity, side)

你看,用了 __slots__ 之后,每个 Order 对象从大约 56 字节降到了 32 字节。别小看这 24 字节,如果同时有 10 万个订单,那就是 2.4MB 的差距。

4.5 本章知识体系

下面这张图,是我对订单簿实现的核心总结。你可以把它当作一个「路线图」:

订单簿实现核心知识体系 订单簿 OrderBook 数据结构选择 高效价格查询 内存优化技巧 红黑树 (C++ map) 跳表 (Python推荐) 有序字典 (bisect) 最优买卖价指针 O(log n) 查询 范围查询支持 __slots__ 减少开销 数组代替字典 对象池复用 核心目标:低延迟 + 低内存 Python 实现,兼顾开发效率与运行性能

我的建议:如果你刚开始做订单簿,先用 Python 的 sortedcontainers 库快速验证逻辑。等跑通了,再自己实现跳表做性能优化。别一上来就造轮子,容易翻车。

好了,这一章的内容就到这里。订单簿是报价引擎的心脏,把它搞扎实了,后面的策略实现才能稳如老狗。


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