3. 委托队列数据结构设计

做量化交易的朋友都知道,委托队列是市场微观结构的核心数据。说白了,它就是交易所撮合引擎的「快照」——当前所有挂着的买单和卖单,按价格排好队,等着成交。

我个人习惯用 Python 的 Pandas DataFrame 来存储这些数据。为什么?因为 DataFrame 的向量化操作快,而且天然支持时间戳索引,做回测时特别顺手。今天我就带你手撸一个 OrderBook 类,把委托队列的数据结构讲透。

3.1 委托队列的核心字段

一个标准的委托队列,至少需要记录这些信息:

字段名 类型 说明
price float64 委托价格
volume int64 委托数量(股/手)
side str 'buy' 或 'sell'
order_id int64 委托单编号(可选)
timestamp datetime64 委托时间

嗯,这里要注意:价格精度是个坑。A股最小变动单位是0.01元,但期货可能是0.2、0.5。我建议统一用 float64,计算时再根据品种做精度控制。

3.2 用 DataFrame 组织数据

我一般这样初始化 OrderBook:

import pandas as pd
import numpy as np
from datetime import datetime

class OrderBook:
    def __init__(self, symbol: str):
        self.symbol = symbol
        self.bids = pd.DataFrame(columns=['price', 'volume', 'order_id', 'timestamp'])
        self.asks = pd.DataFrame(columns=['price', 'volume', 'order_id', 'timestamp'])
        self.bids = self.bids.astype({'price': 'float64', 'volume': 'int64', 'order_id': 'int64'})
        self.asks = self.asks.astype({'price': 'float64', 'volume': 'int64', 'order_id': 'int64'})

为什么把买单和卖单分开?因为它们的排序逻辑不同。买单按价格从高到低排,卖单从低到高排。混在一起反而麻烦。

核心原则:DataFrame 的 index 用 order_id,不要用 price。因为价格会重复,而 order_id 唯一。查询时用 price 做条件过滤,效率更高。

3.3 核心操作:增删改查

一个实用的 OrderBook 类,至少需要这四个方法。我在项目中踩过不少坑,下面直接给你看最终版本:

def add_order(self, price: float, volume: int, side: str, order_id: int, timestamp: datetime):
    """新增委托单"""
    new_order = pd.DataFrame([[price, volume, order_id, timestamp]], 
                             columns=['price', 'volume', 'order_id', 'timestamp'])
    if side == 'buy':
        self.bids = pd.concat([self.bids, new_order], ignore_index=True)
        self.bids.sort_values('price', ascending=False, inplace=True)
    else:
        self.asks = pd.concat([self.asks, new_order], ignore_index=True)
        self.asks.sort_values('price', ascending=True, inplace=True)

def cancel_order(self, order_id: int, side: str):
    """撤销委托单"""
    if side == 'buy':
        self.bids = self.bids[self.bids['order_id'] != order_id]
    else:
        self.asks = self.asks[self.asks['order_id'] != order_id]

def get_top_n(self, n: int = 5, side: str = 'buy'):
    """获取前N档行情"""
    if side == 'buy':
        return self.bids.head(n)
    else:
        return self.asks.head(n)

def get_spread(self):
    """获取买卖价差"""
    if self.bids.empty or self.asks.empty:
        return None
    best_bid = self.bids.iloc[0]['price']
    best_ask = self.asks.iloc[0]['price']
    return best_ask - best_bid

小技巧:每次 add_order 后重新排序,看起来有点笨。但实际测试下来,对于几千笔委托的规模,这个操作耗时不到1毫秒。你想想看,为了代码可读性,这点性能损失完全可以接受。

3.4 进阶:快照与增量更新

真实场景中,交易所会推送两种数据:

  • 全量快照:每隔几秒推送一次完整的委托队列
  • 增量更新:每笔成交或撤单时推送变化

我建议这样设计:

def apply_snapshot(self, snapshot_df: pd.DataFrame):
    """应用全量快照"""
    self.bids = snapshot_df[snapshot_df['side'] == 'buy'].copy()
    self.asks = snapshot_df[snapshot_df['side'] == 'sell'].copy()
    # 重新排序
    self.bids.sort_values('price', ascending=False, inplace=True)
    self.asks.sort_values('price', ascending=True, inplace=True)

def apply_increment(self, increment_df: pd.DataFrame):
    """应用增量更新"""
    for _, row in increment_df.iterrows():
        if row['type'] == 'new':
            self.add_order(row['price'], row['volume'], row['side'], 
                          row['order_id'], row['timestamp'])
        elif row['type'] == 'cancel':
            self.cancel_order(row['order_id'], row['side'])
        elif row['type'] == 'trade':
            # 成交:减少对应委托的数量
            self._reduce_volume(row['order_id'], row['side'], row['volume'])

我曾经踩过的坑:增量更新时,如果先收到撤单再收到新单,但 order_id 相同,会导致数据错乱。后来我加了一个版本号字段,每次更新时检查版本号是否递增,才彻底解决这个问题。

3.5 性能优化:别让 DataFrame 拖后腿

当委托数量超过10万笔时,Pandas 的 concat 操作会变慢。我试过几种方案:

  1. 预分配内存:初始化时指定一个较大的 size,用 loc 赋值而不是 concat
  2. 改用 dict of arrays:底层用 Python 字典存 numpy 数组,上层封装成 DataFrame
  3. 使用 polars:如果数据量真的很大,可以考虑 polars 替代 pandas

我个人推荐方案1,代码改动最小:

def __init__(self, symbol: str, max_orders: int = 100000):
    self.symbol = symbol
    self.max_orders = max_orders
    self.bids = pd.DataFrame({
        'price': np.zeros(max_orders, dtype=np.float64),
        'volume': np.zeros(max_orders, dtype=np.int64),
        'order_id': np.zeros(max_orders, dtype=np.int64),
        'timestamp': np.empty(max_orders, dtype='datetime64[ns]')
    })
    self.bids_count = 0  # 实际使用的行数

3.6 知识体系总览

下面这张图,帮你把本章的核心逻辑串起来:

委托队列数据结构设计 OrderBook 类 bids DataFrame asks DataFrame add_order cancel_order get_top_n get_spread apply_snapshot apply_increment 核心:DataFrame 存储 + 排序 + 快照/增量双模式

说白了,OrderBook 的设计就三件事:存什么、怎么存、怎么更新。存什么我们已经定了字段,怎么存用 DataFrame,怎么更新就是快照和增量两种模式。你想想看,是不是这个理?

最后说一句:别把 OrderBook 设计得太复杂。我见过有人给每个委托单加了十几个字段,结果回测时内存爆了。记住,够用就好。等你的策略跑通了,再慢慢加功能也不迟。


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