订单簿数据结构:价格-数量矩阵、Level 2/Level 3数据、订单簿快照与增量更新

做量化交易,尤其是高频策略,你绕不开一个东西——订单簿。

说白了,它就是交易所里所有挂单的实时汇总。谁在什么价格想买多少,谁想卖多少,全在这张表里。我刚开始接触这个领域时,以为订单簿就是个简单的“买一卖一”表格。后来才发现,这里面的门道深着呢。

价格-数量矩阵:订单簿的骨架

先讲最基础的概念。订单簿本质上是一个二维矩阵:价格数量

每一档价格,对应一个挂单总量。买盘(Bid)从高到低排列,卖盘(Ask)从低到高排列。中间那个价差,就是买卖双方的博弈地带。

核心要点:价格-数量矩阵是订单簿的静态快照。它告诉你“此时此刻,市场深度长什么样”。

举个例子,假设某股票当前订单簿长这样:

方向 价格 数量 累计
卖5 10.05 200 1500
卖4 10.04 300 1300
卖3 10.03 500 1000
卖2 10.02 300 500
卖1 10.01 200 200
—— —— —— ——
买1 9.99 150 150
买2 9.98 400 550
买3 9.97 250 800
买4 9.96 100 900
买5 9.95 300 1200

看到没?买一卖一之间差了2分钱。这2分钱就是价差(Spread)。

我个人习惯把价格-数量矩阵看作市场的“地形图”。价格是海拔,数量是土方量。你想想看,如果某个价格档位突然堆了巨量挂单,那就像一座大山横在那里。价格很难轻易翻过去。

Level 2 与 Level 3 数据:深度与细节

很多新手分不清Level 2和Level 3。我简单解释一下。

Level 2 数据,也叫市场深度数据。它展示的是每个价格档位的聚合总量。就像上面那张表,你看到卖1有200股,但你不知道这200股是1个人挂的,还是10个人挂的。

Level 3 数据,那就更细了。它展示的是每一笔挂单的明细。谁挂的、挂了多少、什么时候挂的、订单ID是什么……全都有。

实战经验:我在做市商系统时,必须用Level 3数据。因为你要精确管理自己的每一笔挂单,知道哪些单子被吃了,哪些还在排队。Level 2数据对于普通策略够用,但做市商不行。

为什么会有这种区别?

说白了,Level 2是“汇总视图”,Level 3是“明细视图”。交易所为了降低数据传输量,通常只给普通用户Level 2。Level 3需要特殊权限,而且数据量巨大——每秒可能产生几千条更新。

我曾经在一个项目中,需要同时处理5个交易所的Level 3数据。那感觉就像用消防水管喝水。数据量太大,网络带宽和CPU都扛不住。后来我们做了增量更新,才把问题解决。

订单簿快照与增量更新

这是订单簿数据处理的核心问题。

快照(Snapshot):某一时刻订单簿的完整状态。就像给市场拍了一张照片。

增量更新(Incremental Update):从上一个快照到现在,发生了哪些变化。比如“有人撤单了”、“有人新挂单了”、“有人成交了”。

为什么需要增量更新?

你想想看,如果每次都要传输整个订单簿,那数据量太大了。尤其在高频场景下,订单簿每秒变化几十次。每次都传全量数据,网络直接爆炸。

所以交易所的做法是:

  1. 先给你一个初始快照
  2. 然后持续推送增量更新
  3. 你本地维护一个订单簿副本,根据增量更新实时修改
  4. 每隔一段时间(比如1分钟),再发一次快照,用于校验和修复

注意:增量更新必须严格按顺序处理。如果漏了一条,或者顺序错了,你本地的订单簿就会和交易所不一致。我遇到过这种情况,策略根据错误的订单簿下单,结果全被拒了。那叫一个惨。

增量更新的消息格式通常长这样:

{
  "type": "update",
  "side": "bid",
  "price": 9.99,
  "size": 300,
  "action": "modify"  // 或 "add" 或 "delete"
}

你收到这条消息后,就在本地订单簿的买一档(9.99元)把数量改成300。就这么简单。

但实际处理时,有几个坑:

  • 重复消息:网络抖动可能导致你收到两次同样的更新。需要做去重。
  • 乱序消息:后发的消息可能先到。需要根据时间戳或序列号重新排序。
  • 快照与增量对齐:收到新快照后,要清空本地订单簿,用快照重建。然后才能继续处理后续增量。

我个人习惯在本地维护一个红黑树跳表来存储订单簿。因为订单簿需要按价格排序,而且频繁插入删除。用数组的话,性能扛不住。

数据结构的工程实现

讲点实际的。如果你要自己实现一个订单簿引擎,核心数据结构就两个:

  1. 价格档位映射表:价格 -> 该价格下的挂单列表
  2. 订单ID映射表:订单ID -> 该订单的详细信息

为什么需要两个表?

因为操作场景不同。按价格查询时,你需要知道某个价格档位有多少单子。按订单ID查询时,你需要知道某个具体订单的状态(比如撤单时要用)。

代码示例(伪代码):

class OrderBook {
    // 价格档位映射
    bids: TreeMap<Price, Level>  // 买盘,按价格降序
    asks: TreeMap<Price, Level>  // 卖盘,按价格升序
    
    // 订单ID映射
    orders: HashMap<OrderId, Order>
    
    // 处理增量更新
    void applyUpdate(Update update) {
        if (update.action == "add") {
            addOrder(update.order)
        } else if (update.action == "delete") {
            removeOrder(update.orderId)
        } else if (update.action == "modify") {
            modifyOrder(update.orderId, update.newSize)
        }
    }
}

嗯,这里要注意。处理修改操作时,不能直接改数量。因为修改可能涉及价格变动。如果价格变了,你要先删除旧订单,再添加新订单。否则价格档位就乱套了。

避坑指南:我曾经在修改操作上栽过跟头。当时图省事,直接改了订单的数量字段。结果订单的价格没变,但数量变成了负数。后来排查了半天,才发现是修改操作没处理好价格变动的情况。

数据可视化:订单簿的“心电图”

光看数字不够直观。我习惯把订单簿画成图。

下面这张SVG图,展示了一个典型的订单簿深度图。蓝色是买盘,红色是卖盘。横轴是价格,纵轴是累计数量。

订单簿深度图示例 累计数量 1200 900 600 300 0 9.95 9.97 9.99 10.01 10.03 价差 买盘 (Bid) 卖盘 (Ask)

这张图能让你一眼看出市场的支撑位和阻力位。买盘阶梯越陡,说明下方承接越强。卖盘阶梯越陡,说明上方抛压越重。

我在做市商策略时,每天开盘前都会看一眼这个图。如果某个价格档位突然出现巨量挂单,那大概率是某个机构在“画线”。这时候就要小心了,别傻乎乎地冲进去。

总结一下

订单簿数据结构,说白了就是三件事:

  • 价格-数量矩阵:静态快照,告诉你市场深度
  • Level 2 vs Level 3:聚合 vs 明细,看你需要多细的粒度
  • 快照与增量更新:如何高效维护本地订单簿副本

这些是订单簿微观结构的基础。后面讲做市商行为时,你会发现所有策略都建立在这些数据结构之上。数据没处理好,策略再牛也是白搭。

嗯,今天就先聊到这儿。记住,订单簿是市场的“心跳”。读懂它,你就能听懂市场在说什么。


专注资料整理