订单簿数据结构:价格-数量矩阵、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分钟),再发一次快照,用于校验和修复
注意:增量更新必须严格按顺序处理。如果漏了一条,或者顺序错了,你本地的订单簿就会和交易所不一致。我遇到过这种情况,策略根据错误的订单簿下单,结果全被拒了。那叫一个惨。
增量更新的消息格式通常长这样:
{
"type": "update",
"side": "bid",
"price": 9.99,
"size": 300,
"action": "modify" // 或 "add" 或 "delete"
}
你收到这条消息后,就在本地订单簿的买一档(9.99元)把数量改成300。就这么简单。
但实际处理时,有几个坑:
- 重复消息:网络抖动可能导致你收到两次同样的更新。需要做去重。
- 乱序消息:后发的消息可能先到。需要根据时间戳或序列号重新排序。
- 快照与增量对齐:收到新快照后,要清空本地订单簿,用快照重建。然后才能继续处理后续增量。
我个人习惯在本地维护一个红黑树或跳表来存储订单簿。因为订单簿需要按价格排序,而且频繁插入删除。用数组的话,性能扛不住。
数据结构的工程实现
讲点实际的。如果你要自己实现一个订单簿引擎,核心数据结构就两个:
- 价格档位映射表:价格 -> 该价格下的挂单列表
- 订单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图,展示了一个典型的订单簿深度图。蓝色是买盘,红色是卖盘。横轴是价格,纵轴是累计数量。
这张图能让你一眼看出市场的支撑位和阻力位。买盘阶梯越陡,说明下方承接越强。卖盘阶梯越陡,说明上方抛压越重。
我在做市商策略时,每天开盘前都会看一眼这个图。如果某个价格档位突然出现巨量挂单,那大概率是某个机构在“画线”。这时候就要小心了,别傻乎乎地冲进去。
总结一下
订单簿数据结构,说白了就是三件事:
- 价格-数量矩阵:静态快照,告诉你市场深度
- Level 2 vs Level 3:聚合 vs 明细,看你需要多细的粒度
- 快照与增量更新:如何高效维护本地订单簿副本
这些是订单簿微观结构的基础。后面讲做市商行为时,你会发现所有策略都建立在这些数据结构之上。数据没处理好,策略再牛也是白搭。
嗯,今天就先聊到这儿。记住,订单簿是市场的“心跳”。读懂它,你就能听懂市场在说什么。