订单簿数据结构:Level2行情、快照与增量更新、内存存储模型
做量化交易的朋友都知道,订单簿是咱们吃饭的家伙。但说实话,很多人对订单簿的理解还停留在「买一卖一」的层面。这远远不够。
我个人习惯把订单簿比作一个「动态的战场」。每一笔挂单都是一个士兵,价格是他们的站位,数量是他们的火力。而Level2行情,就是让你看清这个战场全貌的望远镜。
Level2行情到底多了什么?
普通行情只给你看最优的5档或10档。Level2呢?它给你看全部的挂单明细。我记得刚接触Level2时,第一反应是「数据量真大」。没错,光一个合约的订单簿,每秒可能更新几百次。
具体来说,Level2行情包含这些核心字段:
| 字段 | 含义 | 我的备注 |
|---|---|---|
| price | 价格 | 浮点数,注意精度 |
| size | 挂单数量 | 可能是币数或张数 |
| side | 买卖方向 | bid/ask |
| seq | 序列号 | 用于增量更新 |
| timestamp | 时间戳 | 纳秒级最佳 |
你想想看,有了这些数据,你就能画出完整的订单簿曲线。斜率怎么算?流动性在哪?一目了然。
快照与增量更新:两种模式
这里有个关键问题:行情数据怎么传输?
两种方式:快照和增量。
快照就是给你当前订单簿的完整拷贝。简单粗暴,但数据量大。每次快照可能几十KB,高频传输带宽扛不住。
增量呢?只告诉你「变了什么」。比如「价格100.5的买单增加了200张」。数据量小,但需要你自己维护状态。
我在项目中遇到过一个问题:增量更新如果丢了一条,整个订单簿就全乱了。所以实际生产中,通常是「增量为主,快照为辅」。每隔一段时间发一次快照,用来校正本地状态。
内存存储模型:怎么存最快?
好了,数据拿到了,怎么存?
你可能会想:「用数据库呗」。不行。订单簿更新频率太高,磁盘IO根本扛不住。必须用内存。
我个人推荐两种结构:
- 红黑树(TreeMap):按价格排序,查找、插入、删除都是O(log n)。适合需要按价格范围查询的场景。
- 跳表(SkipList):类似红黑树,但实现更简单,并发性能更好。
举个例子,我用Java实现过一个简单的订单簿:
public class OrderBook {
// 买单:价格从高到低
private TreeMap<Double, Long> bids = new TreeMap<>(Comparator.reverseOrder());
// 卖单:价格从低到高
private TreeMap<Double, Long> asks = new TreeMap<>();
public void applySnapshot(List<Level2Entry> entries) {
bids.clear();
asks.clear();
for (Level2Entry e : entries) {
if (e.side == Side.BID) {
bids.put(e.price, e.size);
} else {
asks.put(e.price, e.size);
}
}
}
public void applyUpdate(Level2Update update) {
Map<Double, Long> target = update.side == Side.BID ? bids : asks;
if (update.size == 0) {
target.remove(update.price); // 撤单
} else {
target.put(update.price, update.size); // 新增或修改
}
}
}
嗯,这里要注意:价格精度是个坑。不同交易所的价格精度不一样,有的支持小数点后8位。用Double做key会有精度问题。我建议用Long,把价格乘以10的N次方转成整数。
增量更新的应用逻辑
增量更新的核心逻辑其实就三步:
- 收到增量消息,解析出价格、数量、方向
- 在内存订单簿中找到对应价格档位
- 数量为0则删除该档位,否则更新数量
说起来简单,但实际处理时有个细节:序列号校验。每条增量消息都有一个递增的序列号。如果发现序列号不连续,说明丢包了。这时候必须请求一次快照,重新构建订单簿。
我习惯的做法是:维护一个本地序列号,每次收到增量就+1。如果收到的序列号不等于本地序列号+1,立即触发快照请求。
内存布局优化
做高频交易的朋友对延迟特别敏感。内存布局也能优化?当然能。
我见过一个极致的方案:用两个大数组分别存储价格和数量,价格数组保持有序。这样CPU缓存命中率更高。但代价是插入和删除需要移动元素,适合低频更新的场景。
对于大多数场景,TreeMap或SkipList已经够用了。别过度优化。
下面这张图展示了订单簿数据在内存中的流转过程:
说白了,订单簿的数据结构设计,就是在「快」和「准」之间找平衡。快照保证你永远有正确的基准,增量保证你跟上市场的节奏。而内存模型的选择,决定了你能跑多快。
我个人建议:刚开始做的时候,先用TreeMap实现一个简单版本跑通流程。等遇到性能瓶颈了,再考虑优化。别一开始就想着搞什么花哨的数据结构,先把业务逻辑跑对再说。
嗯,关于订单簿的数据结构,今天就聊这么多。记住一句话:数据结构选对了,后面的事情就顺了。