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;            // 价差
};

这里有个细节要注意:价格和数量必须成对出现。光知道价格不知道量,你没法判断这个价位到底能吃掉多少单子。

小技巧: 我习惯把 spread 也缓存起来,而不是每次计算。因为价差在很多策略里会被频繁引用,实时计算反而浪费 CPU。

3.3 更新逻辑:什么时候需要重新计算?

Top of Book 不是每毫秒都去重新算一遍的。那样太蠢了。只有在特定事件发生时,才需要更新。我总结了一下,主要有三种情况:

  1. 新增订单:新订单的价格如果比当前最优价更优,就需要更新
  2. 撤销订单:如果撤销的订单正好是最优价上的单子,需要重新找最优
  3. 成交订单:成交导致最优价上的数量减少或归零,需要更新

举个例子,假设当前最优买价是 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 更新的完整路径:

Top of Book 增量更新流程图 订单事件到达 是否影响 最优价? 更新 Top of Book 行情推送 注:只有影响最优价的事件才需要更新,否则直接跳过

3.6 实战中的几个坑

嗯,这里我必须多说几句。Top of Book 看起来简单,但实际落地时坑不少。我把自己踩过的几个坑分享给你:

  • 精度问题:价格比较时一定要用整数或定点数,别用浮点数。我曾经因为浮点数精度问题,导致 100.50 和 100.5000001 被当成两个价格,盘口数据全乱了。
  • 并发安全:Top of Book 是全局共享数据,多线程更新时一定要加锁或用原子操作。我见过一个系统因为没加锁,导致最优买价比最优卖价还高,直接崩了。
  • 边界情况:订单簿为空时,最优买卖价应该返回什么?我习惯返回 0 或者一个特殊标记,而不是报错。
  • 快照与增量:系统重启后,需要从快照重建 Top of Book。这时候必须全量扫描一次,不能依赖增量日志。
我的个人习惯: 每次更新 Top of Book 后,我都会顺手打一条日志,记录更新前后的值。这样排查问题时,能快速定位是哪笔订单导致了盘口变化。

3.7 性能优化建议

最后聊点性能优化的东西。Top of Book 的更新频率极高,每笔订单都可能触发。所以代码要写得尽量轻量:

  1. 避免内存分配:更新函数里不要 new 对象,用栈变量或者预分配的内存池
  2. 减少分支预测失败:把最可能发生的分支放在前面,比如「新增订单」比「撤销订单」更常见
  3. 用整数代替浮点:价格乘以 10000 转成整数,比较和赋值都快很多
  4. 缓存友好:Top of Book 结构体尽量紧凑,最好能塞进一个缓存行(64 字节)

说白了,Top of Book 的更新就是一场和时间的赛跑。你每快 1 纳秒,你的策略就比别人多一分优势。


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