2. 订单簿数据结构与存储:用Python字典与列表模拟订单簿
做市商系统里,订单簿就是我们的战场地图。你想想看,每秒几万笔订单砸过来,数据结构选不对,系统直接崩给你看。今天我就带你手撸一个订单簿的核心骨架。
2.1 为什么选字典+列表?
我个人习惯用Python字典来模拟订单簿。原因很简单:价格是唯一的键,每个价格档位上的订单量就是值。这玩意儿天然就是KV结构,字典的哈希查找O(1),比遍历列表快几个数量级。
我在项目中遇到过一个问题:用纯列表存订单簿,每次查询价格档位都要for循环。回测时数据量小还好,一上实盘,延迟直接飙到毫秒级。后来改成字典,延迟降到微秒级。嗯,这就是数据结构的威力。
核心思路: 买一价、卖一价作为字典的key,对应的订单数量/订单列表作为value。
2.2 Level 2数据的组织方式
Level 2数据,说白了就是能看到每个价格档位上有多少挂单。不像Level 1只给你一个最佳买卖价。做市商要是只看Level 1,那跟闭着眼睛开车差不多。
一个典型的Level 2订单簿长这样:
| 价格 | 买盘数量 | 卖盘数量 |
|---|---|---|
| 100.05 | 0 | 1,200 |
| 100.00 | 0 | 800 |
| 99.95 | 500 | 0 |
| 99.90 | 1,000 | 0 |
注意看,买盘从高到低排列,卖盘从低到高排列。中间那个gap就是买卖价差,做市商赚的就是这个差价。
2.3 用Python字典模拟订单簿
直接上代码。我习惯把买盘和卖盘分开存:
class OrderBook:
def __init__(self):
# 买盘:价格 -> 总数量
self.bids = {} # 价格降序排列
# 卖盘:价格 -> 总数量
self.asks = {} # 价格升序排列
def update_bid(self, price: float, quantity: float):
"""更新买盘某个价格档位的数量"""
if quantity == 0:
# 数量为0,删除这个档位
self.bids.pop(price, None)
else:
self.bids[price] = quantity
def update_ask(self, price: float, quantity: float):
"""更新卖盘某个价格档位的数量"""
if quantity == 0:
self.asks.pop(price, None)
else:
self.asks[price] = quantity
def get_best_bid(self):
"""获取最佳买价(最高买价)"""
if not self.bids:
return None
return max(self.bids.keys())
def get_best_ask(self):
"""获取最佳卖价(最低卖价)"""
if not self.asks:
return None
return min(self.asks.keys())
小技巧: 我建议用Decimal代替float存价格。float有精度问题,做市商算盈亏时差一个tick就是几万块。我曾经因为这个踩过坑,后来全改成Decimal了。
2.4 订单簿快照与增量更新
交易所给的数据有两种形式:快照和增量。快照就是某一时刻的完整订单簿,增量就是变化的部分。
快照(Snapshot): 全量数据,一般每几秒发一次。用来初始化或者做数据校验。
增量(Incremental): 只发变化的部分。比如「价格100.00的卖盘增加了200手」。这才是高频交易真正用的数据。
我处理增量的逻辑是这样的:
def apply_incremental_update(self, update_type: str,
side: str, price: float, quantity: float):
"""
应用增量更新
update_type: 'new' 新增, 'change' 修改, 'delete' 删除
side: 'bid' 买盘, 'ask' 卖盘
"""
book = self.bids if side == 'bid' else self.asks
if update_type == 'delete':
book.pop(price, None)
elif update_type == 'new' or update_type == 'change':
book[price] = quantity
else:
raise ValueError(f"未知的更新类型: {update_type}")
注意: 增量更新必须按顺序应用。如果顺序乱了,订单簿就废了。我见过有人多线程处理增量,结果价格档位全对不上,回测数据直接作废。
2.5 知识体系结构图
下面这张图帮你理清订单簿数据结构的核心逻辑:
2.6 实战中的避坑指南
讲几个我踩过的坑:
- 价格精度问题: 不同交易所的价格精度不一样。有的支持小数点后8位,有的只支持2位。我建议统一用整数存储(比如乘以10000),避免浮点误差。
- 订单簿深度: 不是所有价格档位都要存。我一般只保留买卖各50档,超过的直接丢弃。你想想看,第100档的价格基本不会成交,存了也是浪费内存。
- 并发更新: 增量更新和快照更新可能同时到达。我习惯用锁保护订单簿对象,或者用队列串行化处理。曾经有一次忘了加锁,订单簿里出现了负数数量,排查了一整天才找到原因。
我的建议: 刚开始做订单簿系统时,先用快照模式跑通流程。等逻辑稳定了,再上增量更新。一步到位容易翻车。
好了,订单簿的数据结构就聊到这儿。记住一句话:数据结构选对了,性能问题就解决了一半。字典+列表的组合,简单、高效、够用。别整那些花里胡哨的,做市商系统最怕的就是复杂。