3、订单簿构建:Level2数据解析、订单簿数据结构设计、增量更新与快照

做市商的核心工作,说白了就是跟订单簿打交道。你每天盯着的那个买卖盘口,背后就是订单簿在实时运转。我个人习惯把订单簿比作一个「战场地图」——你得知道敌人在哪、兵力多少、动向如何,才能排兵布阵。

这一章,我们就来拆解这个地图是怎么画出来的。

3.1 Level2数据解析:别被原始数据吓到

交易所给的Level2数据,其实是一堆结构化的二进制流或者JSON。我刚开始接触的时候,看着那密密麻麻的字段也头疼。但后来发现,核心就三样东西:价格、数量、方向

拿沪深交易所的Level2快照举例,它长这样:

{
  "timestamp": 1628000000000,
  "bids": [
    [10.01, 1000],
    [10.00, 2000],
    [9.99, 1500]
  ],
  "asks": [
    [10.02, 800],
    [10.03, 1200],
    [10.04, 900]
  ]
}

bids是买单,asks是卖单。每个数组里,第一个数是价格,第二个数是挂单量。嗯,这里要注意:价格精度和数量精度,不同交易所可能不一样。我在项目中遇到过,某次因为没处理好精度,导致订单簿价格错位,差点把策略搞崩了。

避坑指南: 我曾经因为忽略了交易所的「价格最小变动单位」,直接把浮点数当整数用,结果订单簿里出现了0.001这样的无效价格。后来我统一用整数(比如乘以10000)来存储价格,再也没出过问题。

3.2 订单簿数据结构设计:选对容器,事半功倍

数据结构选不好,后面增量更新就是灾难。你想想看,每秒几千笔订单进来,如果每次都要全量排序,性能肯定扛不住。

我个人推荐用红黑树或者跳表来实现订单簿。为什么?因为这两种结构天然支持有序插入和删除,时间复杂度都是O(log n)。

下面是我常用的一个简化版设计:

class OrderBook {
    constructor() {
        this.bids = new Map();  // 价格 -> 数量
        this.asks = new Map();  // 价格 -> 数量
        this.bestBid = null;    // 最优买价
        this.bestAsk = null;    // 最优卖价
    }

    // 更新某个价格档位的挂单量
    update(side, price, size) {
        const book = side === 'buy' ? this.bids : this.asks;
        if (size === 0) {
            book.delete(price);  // 撤单
        } else {
            book.set(price, size);  // 新增或修改
        }
        this.updateBestPrice(side);
    }
}

这里用Map来存储,其实是为了快速查找。但如果你需要频繁获取「最优五档」或者「前N档」,那还是得用有序结构。我建议用SortedMap或者自己维护一个双向链表。

小技巧: 如果你用Python,可以用sortedcontainers库里的SortedDict。C++的话,直接用std::map。Java有TreeMap。别自己造轮子,除非你想练手。

3.3 增量更新与快照:别让数据「打架」

增量更新,就是只告诉你「变了什么」,而不是「全部是什么」。比如交易所发来一条消息:

{
  "type": "update",
  "side": "buy",
  "price": 10.01,
  "size": 500
}

意思是:买一价10.01,挂单量变成500。注意,是覆盖,不是累加。我之前就犯过这个错,以为增量是「加500」,结果订单簿总量越算越大,跟实际盘口对不上。

快照呢?就是交易所每隔一段时间(比如500ms)给你一份完整的订单簿。增量更新和快照要配合使用:

  • 启动时:先拿快照,建立初始订单簿。
  • 运行中:用增量更新来维护。
  • 异常时:如果发现数据对不上,重新请求快照。

我画了一张流程图,帮你理清这个逻辑:

启动订单簿 请求初始快照 构建初始订单簿 循环:接收增量更新消息 序列号连续? 应用增量更新 重新请求快照

看到没?核心就是序列号校验。每条增量消息都带一个序列号,你本地维护一个lastSeq。如果新消息的seq != lastSeq + 1,说明中间丢了数据,这时候必须重新请求快照,否则订单簿就「脏」了。

重点: 增量更新和快照的配合,是订单簿稳定性的基石。我见过不少团队,为了省流量只做增量不做快照校验,结果数据跑偏了都不知道。做市商的钱,亏在这种地方太冤了。

3.4 实战中的几个坑

讲几个我踩过的坑,你遇到了可以少走弯路:

  • 时间戳对齐:交易所的时钟和你的本地时钟可能有偏差。我建议用交易所的时间戳,别用本地时间。
  • 重复消息:网络重传可能导致同一条增量消息收到两次。幂等处理很重要,我一般用seq去重。
  • 内存管理:订单簿如果长时间运行,Map里的key会越来越多。定期清理「僵尸档位」(比如挂单量为0且超过一定时间没变动的价格),能省不少内存。
场景 推荐做法 不推荐做法
价格存储 用整数(如乘以10000) 直接用浮点数
数据结构 红黑树 / 跳表 数组 + 每次排序
增量更新 覆盖模式(set) 累加模式(add)
异常恢复 重新请求快照 尝试本地修复

好了,订单簿这块的核心逻辑就这些。说白了,就是数据结构选对、增量更新做对、异常处理到位。你把这三点拿捏住,订单簿这块基本就稳了。

个人习惯: 我每次上线新策略前,都会用历史数据回放一遍订单簿构建逻辑,确保增量更新和快照能完美衔接。这一步虽然费时间,但能省掉后面无数个排查数据的夜晚。

专注资料整理