3. Level 1 数据:最优买卖价(Top of Book)的计算与更新
好,咱们接着聊。前面我们把订单簿的骨架搭起来了,也搞清楚了限价单和市价单是怎么塞进去的。今天要聊的这个东西,是所有量化交易系统最基础、也最核心的一块——Top of Book,也就是最优买卖价。
说白了,Top of Book 就是当前市场上,买方愿意出的最高价,和卖方愿意卖的最低价。我习惯叫它「盘口第一档」。你打开任何一个交易软件,看到的那一买一卖,就是它。
- 最优买价(Best Bid):买方队列中价格最高的那个
- 最优卖价(Best Ask):卖方队列中价格最低的那个
- 买卖价差(Spread):Ask - Bid,衡量市场流动性的关键指标
3.1 为什么 Top of Book 这么重要?
你想想看,一个高频交易系统每秒要处理几万笔订单。如果每笔订单都要遍历整个订单簿去算最优价,那性能肯定扛不住。所以,我们得把 Top of Book 单独拎出来,实时维护。
我在项目中遇到过这么个事:有一次系统压测,订单量突然暴增,结果盘口数据更新慢了 2 毫秒。就这 2 毫秒,导致几个套利策略全部报错。嗯,从那以后,我对 Top of Book 的更新逻辑就格外小心。
Top of Book 的用途非常直接:
- 行情展示:给用户看当前最优报价
- 策略决策:判断是否应该立即成交
- 市场深度评估:价差大小直接反映市场流动性
- 价格发现:最优买卖价是市场价格的核心参考
3.2 数据结构设计
我个人习惯用两个变量来维护 Top of Book:
// 最优买卖价结构体
struct TopOfBook {
double best_bid_price; // 最优买价
uint64_t best_bid_size; // 最优买量
double best_ask_price; // 最优卖价
uint64_t best_ask_size; // 最优卖量
double spread; // 价差
};
这里有个细节要注意:价格和数量必须成对出现。光知道价格不知道量,你没法判断这个价位到底能吃掉多少单子。
3.3 更新逻辑:什么时候需要重新计算?
Top of Book 不是每毫秒都去重新算一遍的。那样太蠢了。只有在特定事件发生时,才需要更新。我总结了一下,主要有三种情况:
- 新增订单:新订单的价格如果比当前最优价更优,就需要更新
- 撤销订单:如果撤销的订单正好是最优价上的单子,需要重新找最优
- 成交订单:成交导致最优价上的数量减少或归零,需要更新
举个例子,假设当前最优买价是 100.50,有 1000 股。这时候来了一个买单,价格 100.60,数量 500 股。那最优买价就要更新成 100.60,数量 500。
反过来,如果撤销了一个 100.50 上的 200 股单子,那最优买价不变,但数量要减到 800。如果 100.50 上的单子全部撤光了,那就得去买方队列里找下一个最高的价格。
3.4 核心算法:增量更新 vs 全量重建
这里有两种思路,我分别说一下:
| 方式 | 原理 | 适用场景 | 性能 |
|---|---|---|---|
| 增量更新 | 只针对变化的价格档位做局部调整 | 高频交易、实时行情 | 极快,O(1) 或 O(log n) |
| 全量重建 | 每次更新都重新扫描整个订单簿 | 低频率、数据校验、系统启动 | 较慢,O(n) |
在实际生产环境中,99% 的情况都用增量更新。全量重建一般只在系统初始化或者数据一致性校验时用。
增量更新的伪代码大概长这样:
void updateTopOfBook(OrderEvent event) {
switch(event.type) {
case NEW_ORDER:
// 买单:如果价格高于当前最优买价,更新
if (event.side == BUY && event.price > top.best_bid_price) {
top.best_bid_price = event.price;
top.best_bid_size = event.size;
}
// 卖单:如果价格低于当前最优卖价,更新
if (event.side == SELL && event.price < top.best_ask_price) {
top.best_ask_price = event.price;
top.best_ask_size = event.size;
}
break;
case CANCEL_ORDER:
// 如果撤销的正好是最优价上的单子
if (event.price == top.best_bid_price && event.side == BUY) {
top.best_bid_size -= event.size;
if (top.best_bid_size <= 0) {
// 需要重新从买方队列找下一个最优价
top.best_bid_price = findNextBestBid();
top.best_bid_size = getSizeAtPrice(top.best_bid_price);
}
}
// 卖单同理
break;
case TRADE:
// 成交逻辑和撤销类似,只是数量减少
// 注意:成交可能同时影响买卖双方
break;
}
// 更新价差
top.spread = top.best_ask_price - top.best_bid_price;
}
3.5 可视化:Top of Book 的更新流程
为了让你更直观地理解,我画了一张流程图。这张图展示了从订单事件发生到 Top of Book 更新的完整路径:
3.6 实战中的几个坑
嗯,这里我必须多说几句。Top of Book 看起来简单,但实际落地时坑不少。我把自己踩过的几个坑分享给你:
- 精度问题:价格比较时一定要用整数或定点数,别用浮点数。我曾经因为浮点数精度问题,导致 100.50 和 100.5000001 被当成两个价格,盘口数据全乱了。
- 并发安全:Top of Book 是全局共享数据,多线程更新时一定要加锁或用原子操作。我见过一个系统因为没加锁,导致最优买价比最优卖价还高,直接崩了。
- 边界情况:订单簿为空时,最优买卖价应该返回什么?我习惯返回 0 或者一个特殊标记,而不是报错。
- 快照与增量:系统重启后,需要从快照重建 Top of Book。这时候必须全量扫描一次,不能依赖增量日志。
3.7 性能优化建议
最后聊点性能优化的东西。Top of Book 的更新频率极高,每笔订单都可能触发。所以代码要写得尽量轻量:
- 避免内存分配:更新函数里不要 new 对象,用栈变量或者预分配的内存池
- 减少分支预测失败:把最可能发生的分支放在前面,比如「新增订单」比「撤销订单」更常见
- 用整数代替浮点:价格乘以 10000 转成整数,比较和赋值都快很多
- 缓存友好:Top of Book 结构体尽量紧凑,最好能塞进一个缓存行(64 字节)
说白了,Top of Book 的更新就是一场和时间的赛跑。你每快 1 纳秒,你的策略就比别人多一分优势。
公众号:蓝海资料掘金营,微信deep3321