3. 订单簿数据结构:使用Python字典与列表实现Level 2订单簿,买卖盘口的维护
做量化交易的朋友都知道,订单簿是市场的核心。说白了,它就是交易所里所有未成交订单的实时快照。Level 2数据,也就是我们常说的深度行情,能看到买卖各档位的挂单量和价格。今天我就带你手撸一个精简版的Level 2订单簿。
3.1 订单簿的核心结构
我个人习惯用Python字典来组织订单簿。为什么?因为字典的键值对结构天然适合存储价格-数量映射。买盘(Bids)和卖盘(Asks)各维护一个字典,键是价格,量是数量。
举个例子,一个简单的订单簿长这样:
# 买盘:价格从高到低排序
bids = {
100.50: 200, # 有人愿意以100.50买200股
100.30: 150,
100.10: 300
}
# 卖盘:价格从低到高排序
asks = {
100.60: 180, # 有人愿意以100.60卖180股
100.80: 250,
101.00: 100
}
你想想看,这个结构是不是很直观?价格作为键,一眼就能找到对应的挂单量。
3.2 为什么需要列表辅助?
字典虽然查找快,但它不保证顺序。而订单簿的核心需求之一就是按价格排序——买盘从高到低,卖盘从低到高。这时候就需要列表来帮忙了。
我在项目中遇到过一个问题:直接用字典存储,每次取最优价都要遍历所有键,效率太低了。后来我加了一个列表来维护价格顺序,每次更新时同步排序,查询就变成了O(1)操作。
核心思路:字典负责快速查找和更新,列表负责维护价格顺序。两者配合,既快又稳。
3.3 买卖盘口的维护操作
订单簿的维护说白了就三个动作:新增、修改、删除。我们来看看具体怎么做。
3.3.1 新增订单
当新订单进来时,先判断价格是否已存在。如果存在,累加数量;如果不存在,插入新键值对,并更新价格列表。
def add_order(book, price, quantity, is_bid=True):
"""
新增订单到订单簿
book: 订单簿字典
price: 价格
quantity: 数量
is_bid: True表示买盘,False表示卖盘
"""
if price in book:
book[price] += quantity
else:
book[price] = quantity
# 更新价格列表(这里假设有全局的价格列表)
if is_bid:
bid_prices.append(price)
bid_prices.sort(reverse=True) # 买盘降序
else:
ask_prices.append(price)
ask_prices.sort() # 卖盘升序
3.3.2 修改订单
修改其实就是先删后加。不过要注意,修改可能改变价格,这时候要处理两个价格键。
def modify_order(book, old_price, new_price, new_quantity, is_bid=True):
"""
修改订单:先删除旧价格,再添加新价格
"""
# 删除旧订单
if old_price in book:
del book[old_price]
# 从价格列表中移除
if is_bid:
bid_prices.remove(old_price)
else:
ask_prices.remove(old_price)
# 添加新订单
add_order(book, new_price, new_quantity, is_bid)
注意:我曾经踩过一个坑——修改订单时忘了处理数量为0的情况。如果新数量为0,应该直接删除该价格档位,而不是保留一个空档。否则后续计算深度时会出问题。
3.3.3 删除订单
删除相对简单,但要注意完全成交的情况。当某个价格档位的数量被吃光时,要彻底清除。
def remove_order(book, price, quantity, is_bid=True):
"""
从订单簿中移除指定数量的订单
返回剩余数量(如果完全移除则返回0)
"""
if price not in book:
return 0
current_qty = book[price]
if quantity >= current_qty:
# 完全移除该档位
del book[price]
if is_bid:
bid_prices.remove(price)
else:
ask_prices.remove(price)
return 0
else:
# 部分移除
book[price] -= quantity
return book[price]
3.4 完整的订单簿类
把上面的逻辑封装成一个类,用起来就顺手多了。我一般这样设计:
class OrderBook:
def __init__(self):
self.bids = {} # 买盘字典
self.asks = {} # 卖盘字典
self.bid_prices = [] # 买盘价格列表(降序)
self.ask_prices = [] # 卖盘价格列表(升序)
def update(self, side, price, quantity):
"""
更新订单簿
side: 'bid' 或 'ask'
price: 价格
quantity: 数量(0表示删除该档位)
"""
book = self.bids if side == 'bid' else self.asks
prices = self.bid_prices if side == 'bid' else self.ask_prices
if quantity == 0:
# 删除档位
if price in book:
del book[price]
prices.remove(price)
else:
# 新增或更新
is_new = price not in book
book[price] = quantity
if is_new:
prices.append(price)
if side == 'bid':
prices.sort(reverse=True)
else:
prices.sort()
def get_top(self, n=5):
"""
获取前n档买卖盘口
"""
top_bids = [(p, self.bids[p]) for p in self.bid_prices[:n]]
top_asks = [(p, self.asks[p]) for p in self.ask_prices[:n]]
return top_bids, top_asks
def get_spread(self):
"""
获取买卖价差
"""
if not self.bid_prices or not self.ask_prices:
return None
best_bid = self.bid_prices[0]
best_ask = self.ask_prices[0]
return best_ask - best_bid
3.5 知识体系结构图
下面这张图帮你理清订单簿数据结构的核心逻辑:
3.6 实际应用中的注意事项
嗯,这里要注意几个实战中容易翻车的地方:
| 问题 | 表现 | 解决方案 |
|---|---|---|
| 浮点数精度 | 价格比较时出现意外 | 使用Decimal或乘以10000转整数 |
| 并发更新 | 数据不一致 | 加锁或使用原子操作 |
| 内存泄漏 | 废弃价格未清理 | 定期清理数量为0的档位 |
| 排序效率 | 频繁排序拖慢性能 | 使用bisect.insort插入排序 |
我的经验:刚开始做订单簿时,我直接用sorted()每次全量排序,结果回测速度慢得离谱。后来改用bisect模块的insort方法,只在插入时做一次二分查找,性能提升了10倍以上。代码就一行:
import bisect
bisect.insort(bid_prices, new_price) # 自动插入到正确位置
说白了,订单簿的数据结构并不复杂,难的是在毫秒级行情下保持数据的一致性和高性能。字典加列表的组合,是我用过的最平衡的方案——既保留了字典的O(1)查找,又通过列表保证了顺序。
你想想看,如果让你自己设计一个订单簿,你会怎么选?我个人觉得,先把基础结构搭好,后续优化才有方向。毕竟,跑得稳比跑得快更重要。
公众号:蓝海资料掘金营,微信deep3321