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_quantityquantity 混在一起算,结果撤单时剩余数量算错了。我的做法是:永远保留原始数量,成交数量单独累加。这样不管撤单、改单、部分成交,逻辑都清晰。

@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 类 - order_id, symbol - side, type, price - quantity, filled_qty - status, timestamp OrderBook 类 - bids: SortedDict(降序) - asks: SortedDict(升序) - order_map: dict + add_order() + cancel_order() + match_order() Level2 - snapshot - incremental - 深度缓存 价格队列 (PriceLevel) - price: float - orders: deque (FIFO) - total_quantity: float 时间优先队列 - 同价格下,先到先成交 - 部分成交不改变顺序 - 完全成交/撤单才移除 核心原则:价格优先 → 时间优先 → 增量更新 Order 是基础单元,OrderBook 是撮合引擎,Level2 是市场深度视图

从这张图你能看到,Order 是最小的数据单元,OrderBook 把它们组织成价格队列和时间优先队列,最后通过 Level2 模型对外输出市场深度。每一层都有明确的职责,这也是事件驱动框架的精髓——各司其职,互不干扰

好了,核心数据结构就聊到这儿。代码我都贴出来了,建议你照着敲一遍,跑几个测试用例。只有亲手踩过坑,才能真正理解这些设计背后的权衡。


公众号:蓝海资料掘金营,微信deep3321