3、订单簿数据结构:Python 中如何用字典和列表模拟订单簿
做量化交易,订单簿就是你的战场地图。我刚开始接触这个领域时,第一反应是——这东西不就是个价格和数量的表格吗?后来在实盘对接中才发现,订单簿的更新速度、数据结构的选型,直接决定了你的策略能不能跑赢市场。
说白了,订单簿就是记录当前市场上所有买单和卖单的电子账本。买方挂单叫Bid,卖方挂单叫Ask。每一档价格上,都堆着一定数量的订单。我们做策略,就是盯着这个账本的变化来决策。
3.1 订单簿的核心要素
一个标准的订单簿,至少包含以下信息:
- 价格档位:每个价格点就是一个档位
- 挂单数量:该价格上所有订单的总量
- 订单方向:买还是卖
- 时间戳:数据生成的时间
嗯,这里要注意——不同交易所的订单簿深度不一样。有的只给前20档,有的给到100档。我个人习惯在本地维护一个完整的快照,然后增量更新。
3.2 用字典模拟订单簿
Python里最直观的方式就是用字典。键是价格,值是数量。为什么?因为价格是唯一的,而且查询速度极快。
# 一个简单的订单簿字典结构
order_book = {
'bids': { # 买单
100.50: 200, # 价格100.50,数量200
100.40: 150,
100.30: 300
},
'asks': { # 卖单
100.60: 180,
100.70: 250,
100.80: 100
}
}
# 获取最优买价(最高买价)
best_bid = max(order_book['bids'].keys())
print(f"最优买价: {best_bid}, 数量: {order_book['bids'][best_bid]}")
# 获取最优卖价(最低卖价)
best_ask = min(order_book['asks'].keys())
print(f"最优卖价: {best_ask}, 数量: {order_book['asks'][best_ask]}")
我在项目中遇到过一个问题:字典虽然快,但价格是浮点数。浮点数做键,精度问题会坑死人。比如100.50和100.5000001,在字典里是两个不同的键。所以,我建议把价格乘以10000转成整数来存。
3.3 用列表模拟订单簿
字典适合随机访问,但如果你需要按价格排序遍历,列表更合适。尤其是做深度分析时,列表的切片操作非常方便。
# 用列表存储订单簿,每个元素是 [价格, 数量]
bids = [
[100.50, 200],
[100.40, 150],
[100.30, 300]
]
asks = [
[100.60, 180],
[100.70, 250],
[100.80, 100]
]
# 计算买卖价差
spread = asks[0][0] - bids[0][0]
print(f"当前价差: {spread}")
# 计算前5档买单总量
total_bid_volume = sum([item[1] for item in bids[:5]])
print(f"前5档买单总量: {total_bid_volume}")
你想想看,列表的好处是天然有序。但坏处也很明显——插入和删除操作是O(n)的。如果订单簿每秒更新几百次,列表的性能就扛不住了。
3.4 字典+列表的组合方案
实战中,我更喜欢把两者结合起来。字典负责快速查找,列表负责有序遍历。这样既保证了速度,又保留了顺序。
class OrderBook:
def __init__(self):
self.bids_dict = {} # 价格 -> 数量
self.asks_dict = {} # 价格 -> 数量
self.bids_list = [] # 按价格降序排列
self.asks_list = [] # 按价格升序排列
def update(self, side, price, volume):
"""更新订单簿"""
if side == 'buy':
self.bids_dict[price] = volume
self.bids_list = sorted(self.bids_dict.items(), reverse=True)
else:
self.asks_dict[price] = volume
self.asks_list = sorted(self.asks_dict.items())
def get_best_bid(self):
"""获取最优买价"""
return self.bids_list[0] if self.bids_list else None
def get_best_ask(self):
"""获取最优卖价"""
return self.asks_list[0] if self.asks_list else None
def get_depth(self, levels=10):
"""获取指定深度的订单簿"""
return {
'bids': self.bids_list[:levels],
'asks': self.asks_list[:levels]
}
3.5 订单簿的增量更新
交易所推送的数据,通常是增量快照。比如「价格100.50的买单数量变为0」,意味着这个档位被吃掉了。我曾经踩过一个坑:直接覆盖字典,结果漏掉了某些档位的删除操作。
def apply_snapshot(order_book, snapshot):
"""
应用增量快照
snapshot格式: [(price, volume), ...]
volume为0表示删除该档位
"""
for price, volume in snapshot:
if volume == 0:
# 删除档位
order_book.bids_dict.pop(price, None)
order_book.asks_dict.pop(price, None)
else:
# 更新或新增档位
# 这里需要判断是买单还是卖单
# 实际项目中会从snapshot的side字段获取
pass
# 重新排序
order_book.bids_list = sorted(order_book.bids_dict.items(), reverse=True)
order_book.asks_list = sorted(order_book.asks_dict.items())
3.6 订单簿的可视化结构
为了让你更直观地理解订单簿,我画了一张图。这张图展示了买单和卖单如何堆叠,以及买卖价差的位置。
3.7 实战中的选择建议
说了这么多,到底该用哪种方案?我根据经验给你列个表:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 高频策略(毫秒级) | 纯字典 + 排序列表缓存 | 字典O(1)查询,列表缓存减少排序开销 |
| 中低频策略(秒级) | 列表 + 二分查找 | 数据量不大,列表操作简单直观 |
| 回测模拟 | 字典 + 列表组合 | 需要频繁插入删除,字典更灵活 |
| 数据存储 | Pandas DataFrame | 方便做统计分析,但实时性差 |
最后说一句——订单簿的数据结构没有银弹。你得根据策略的频率、数据量、交易所的API特性来选。但不管怎么选,字典做索引、列表做视图这个思路,基本能覆盖90%的场景。