第四章:订单簿实现
做市商的核心,说白了就是一本「账本」。
谁在什么价格想买多少,谁在什么价格想卖多少,全记在上面。这本账本,就是订单簿。我做了这么多年量化,见过太多人把精力花在策略上,结果订单簿一塌糊涂,撮合速度慢得像蜗牛。嗯,今天我们就来聊聊,怎么用 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 本章知识体系
下面这张图,是我对订单簿实现的核心总结。你可以把它当作一个「路线图」:
我的建议:如果你刚开始做订单簿,先用 Python 的 sortedcontainers 库快速验证逻辑。等跑通了,再自己实现跳表做性能优化。别一上来就造轮子,容易翻车。
好了,这一章的内容就到这里。订单簿是报价引擎的心脏,把它搞扎实了,后面的策略实现才能稳如老狗。
公众号:蓝海资料掘金营,微信deep3321