3、核心数据结构:Order类设计、OrderBook类设计、价格队列与时间优先队列、Level2数据模型
好,咱们今天聊点实在的。
做量化交易,说白了就是跟订单簿打交道。你想想看,不管策略多花哨,最终都要落到「挂单、撤单、成交」这三件事上。我刚开始写交易系统那会儿,觉得数据结构嘛,随便搞搞就行。结果呢?回测跑得飞起,实盘一上就崩——订单簿更新慢了,撮合逻辑错了,资金算不对了……嗯,从那以后我再也不敢小看这几个类了。
3.1 Order类设计——交易系统的「身份证」
一个订单,在系统里到底长什么样?
我个人习惯,Order类必须包含三块信息:身份标识、业务属性、状态流转。缺一个,后面排查问题就得哭。
核心字段清单:
- order_id:全局唯一,我一般用「时间戳+自增序列」生成,避免重复
- symbol:合约代码,比如 "BTC-USDT"
- side:买卖方向,BUY 或 SELL
- type:订单类型,LIMIT / MARKET / STOP 等
- price:限价单的价格,市价单设为 0 或 None
- quantity:原始数量
- filled_quantity:已成交数量
- status:状态枚举,PENDING / PARTIAL_FILLED / FILLED / CANCELLED / REJECTED
- timestamp:订单创建时间,精确到微秒
这里有个坑,我踩过。很多人把 filled_quantity 和 quantity 混在一起算,结果撤单时剩余数量算错了。我的做法是:永远保留原始数量,成交数量单独累加。这样不管撤单、改单、部分成交,逻辑都清晰。
@dataclass
class Order:
order_id: str
symbol: str
side: str # 'BUY' or 'SELL'
type: str # 'LIMIT' or 'MARKET'
price: float
quantity: float
filled_quantity: float = 0.0
status: str = 'PENDING'
timestamp: int = 0
@property
def remaining(self) -> float:
return self.quantity - self.filled_quantity
def is_filled(self) -> bool:
return self.filled_quantity >= self.quantity
小技巧:用 @dataclass 省去一堆模板代码。但注意,filled_quantity 是可变字段,记得用 field(default=0.0) 初始化。
3.2 OrderBook类设计——撮合引擎的「心脏」
OrderBook 是什么?说白了就是买卖双方的「排队簿」。买方出价从高到低排,卖方出价从低到高排。中间那个价差,就是市场流动性。
我设计 OrderBook 时,核心就两个数据结构:买盘(bids) 和 卖盘(asks)。每个盘口都是一个价格队列的映射。
OrderBook 核心接口:
add_order(order):添加订单到对应价格队列cancel_order(order_id):根据订单ID撤销订单match_order(order):尝试撮合,返回成交记录列表get_top_of_book():获取最优买卖价get_depth(level):获取指定深度的盘口数据
我曾经犯过一个错误:用 list 来存价格队列,每次插入都 O(n) 复杂度。后来换成 sortedcontainers 库的 SortedDict,性能直接翻倍。你想想看,高频场景下每秒几千笔订单,O(n) 和 O(log n) 的差距就是生与死。
class OrderBook:
def __init__(self, symbol: str):
self.symbol = symbol
self.bids = SortedDict(lambda k: -k) # 买盘,价格降序
self.asks = SortedDict() # 卖盘,价格升序
self.order_map = {} # order_id -> (price, side)
def add_order(self, order: Order):
price = order.price
side = order.side
book = self.bids if side == 'BUY' else self.asks
if price not in book:
book[price] = []
book[price].append(order)
self.order_map[order.order_id] = (price, side)
注意:买盘用降序排列,因为买方出价越高越优先。卖盘用升序排列,因为卖方出价越低越优先。这个顺序搞反了,撮合逻辑全错。
3.3 价格队列与时间优先队列——谁先来谁先走
价格优先好理解:买一价高于买二价,买一先成交。但价格相同时呢?这就轮到时间优先了。
我习惯用 FIFO 队列 来实现时间优先。每个价格档位维护一个 collections.deque,新订单从右边入队,撮合时从左边出队。这样天然保证了「先到先得」。
from collections import deque
class PriceLevel:
def __init__(self, price: float):
self.price = price
self.orders = deque() # 时间优先队列
self.total_quantity = 0.0
def add(self, order: Order):
self.orders.append(order)
self.total_quantity += order.remaining
def remove(self, order_id: str) -> bool:
# 实际项目中用 order_map 快速定位,这里简化
for i, o in enumerate(self.orders):
if o.order_id == order_id:
self.total_quantity -= o.remaining
del self.orders[i]
return True
return False
def peek(self) -> Order:
return self.orders[0] if self.orders else None
这里有个细节:部分成交的订单不要移出队列。比如你挂了10个BTC,成交了3个,还剩7个。这7个还在队列里排队,等后续买单继续吃。我曾经在项目里把部分成交的订单直接删了,结果客户少成交了,差点被投诉。
避坑指南:部分成交后,记得更新 total_quantity,但不要动队列顺序。只有完全成交或撤单时,才从队列中移除。
3.4 Level2数据模型——盘口的「高清摄像头」
Level2 数据,也叫深度数据。它比普通盘口多了一个维度:每个价位的挂单量明细。普通行情只告诉你买一价和量,Level2 告诉你买一到买十甚至更深。
我设计的 Level2 模型,核心是一个 快照 + 增量 的结构。为什么?因为全量推送太费带宽了,每秒几十次全量,服务器扛不住。
Level2 数据模型:
| 字段 | 类型 | 说明 |
|---|---|---|
| symbol | str | 合约代码 |
| timestamp | int | 数据生成时间(微秒) |
| bids | list[tuple] | 买盘深度,[(price, quantity), ...] |
| asks | list[tuple] | 卖盘深度,[(price, quantity), ...] |
| type | str | 'snapshot' 或 'incremental' |
增量更新怎么搞?举个例子:交易所推送一条消息说「买一价 100.5 的量从 10 变成 8」。你收到后,找到买一队列,把总量减2就行。如果某个价格档位的量变成0,就把它从盘口移除。
@dataclass
class Level2Snapshot:
symbol: str
timestamp: int
bids: list # [(price, qty), ...]
asks: list # [(price, qty), ...]
@dataclass
class Level2Incremental:
symbol: str
timestamp: int
changes: list # [(side, price, qty), ...]
# side: 'BUY' or 'SELL'
# qty=0 表示删除该价位
我个人建议,维护一个本地的 Level2 缓存。收到快照时全量替换,收到增量时局部更新。这样既能保证数据实时性,又不会丢失精度。
注意:增量更新时,一定要校验时间戳。如果收到一个旧时间的增量,直接丢弃。否则盘口数据会乱掉。
3.5 知识体系总览
说了这么多,咱们用一张图把核心逻辑串起来。下面这张 SVG 图展示了从 Order 到 OrderBook 再到 Level2 的完整链路。
从这张图你能看到,Order 是最小的数据单元,OrderBook 把它们组织成价格队列和时间优先队列,最后通过 Level2 模型对外输出市场深度。每一层都有明确的职责,这也是事件驱动框架的精髓——各司其职,互不干扰。
好了,核心数据结构就聊到这儿。代码我都贴出来了,建议你照着敲一遍,跑几个测试用例。只有亲手踩过坑,才能真正理解这些设计背后的权衡。
公众号:蓝海资料掘金营,微信deep3321