订单簿快照文件格式设计:数据结构基础

大家好,我是你们这趟课程的老朋友。今天咱们聊聊订单簿的数据结构基础。说实话,我刚开始做量化那会儿,觉得订单簿不就是个买盘卖盘列表嘛,有啥好研究的?直到有一次,我负责的某个策略在高频场景下频繁丢单,排查了三天才发现是数据结构设计不合理导致的。嗯,从那以后,我再也不敢小看这个基础模块了。

订单簿的核心组成:买盘与卖盘

订单簿,说白了就是一个实时更新的「价格-数量」映射表。它记录着市场上所有未成交的买卖意愿。

我个人习惯把订单簿想象成一个双层的书架:

  • 买盘(Bid):买家愿意出的最高价,从上往下价格递减。比如有人愿意100块买,有人愿意99.5块买。
  • 卖盘(Ask):卖家愿意接受的最低价,从上往下价格递增。比如有人愿意100.5块卖,有人愿意101块卖。

每个订单层级(Level)包含三个核心字段:

  • 价格(Price):这个不用多说,买卖双方博弈的锚点。
  • 数量(Volume/Size):在这个价格上,有多少手或多少币等着成交。
  • 订单层级序号(Level Index):从最优价开始,第1层、第2层……依次排列。

避坑指南:我曾经在对接某家交易所时,发现它的买盘价格是降序排列,而卖盘是升序排列。这本身没问题,但如果你写代码时假设两边都是降序,那取最优价时就会出错。所以,拿到数据后第一件事——确认排序方向。

Level 1 / Level 2 / Level 3 数据区别

这三个级别,说白了就是数据颗粒度的差异。你想想看,就像看一张照片:

  • Level 1:只看缩略图——最优买价、最优卖价、最新成交价。够用吗?做简单行情展示够了,做策略?差远了。
  • Level 2:看高清大图——能看到买盘前N层和卖盘前N层的完整价格和数量。大部分策略用这个级别就够了。
  • Level 3:看原始底片——能看到每一笔挂单的详细信息,包括订单ID、下单时间、隐藏数量等。这是做市商和极高频策略的专属领域。
级别 数据内容 典型用途 数据量级
Level 1 最优买卖价、最新价 行情展示、简单监控 极低(每秒几条)
Level 2 前N层买卖盘(通常5-20层) 趋势策略、套利策略 中等(每秒几百条)
Level 3 全部挂单明细(含订单ID) 做市、高频、订单流分析 极高(每秒数万条)

我的经验:如果你刚开始做回测,别一上来就追求Level 3数据。Level 2的数据量已经足够模拟大部分市场微观结构了。Level 3的数据清洗成本非常高,我曾经花了两周时间才把某家交易所的Level 3数据对齐到时间轴上。

时间戳与序列号设计

这是订单簿快照文件里最容易踩坑的地方。我见过太多人把时间戳和序列号混为一谈,结果回测时数据对不上。

时间戳(Timestamp):记录这个快照「什么时候」生成的。注意,这里有两个时间:

  • 交易所时间:服务器生成快照的时刻。这是最权威的时间源。
  • 本地接收时间:你的程序收到数据的时刻。这个时间受网络延迟影响,只能做参考。

序列号(Sequence Number):记录这个快照「是第几个」生成的。它解决了两个问题:

  • 网络传输中可能丢包或乱序,序列号能帮你检测到。
  • 同一毫秒内可能有多个快照,时间戳无法区分先后,序列号可以。

我曾经踩过的坑:有一次回测结果异常,发现是某交易所的序列号在每天凌晨会重置为0。我的代码没处理这个边界情况,导致回测时把凌晨的数据当成了重复数据过滤掉了。所以,设计文件格式时,一定要考虑序列号的溢出和重置策略。

下面是我个人比较推荐的一种快照文件结构设计:

// 订单簿快照结构体(伪代码)
struct OrderBookSnapshot {
    uint64  exchange_timestamp;   // 交易所时间戳(纳秒级)
    uint64  local_timestamp;      // 本地接收时间戳(纳秒级)
    uint64  sequence_number;      // 全局递增序列号
    uint32  symbol_id;            // 交易对ID
    uint8   level_count_bid;      // 买盘层级数量
    uint8   level_count_ask;      // 卖盘层级数量
    Level   bids[];               // 买盘层级数组
    Level   asks[];               // 卖盘层级数组
};

struct Level {
    int64   price;                // 价格(通常用整数表示,如 1.2345 -> 12345)
    uint64  volume;               // 数量
    uint32  order_count;          // 该价格上的挂单数量(可选字段)
};

你可能会问:为什么价格要用整数?因为浮点数在二进制里是不精确的,比较两个浮点数是否相等时容易出问题。用整数表示价格,比如把1.2345美元存成12345(精度4位小数),比较和计算都更可靠。

知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

订单簿快照数据结构知识体系 订单簿快照 核心组成 买盘 (Bid) 卖盘 (Ask) 订单层级 数据级别 Level 1 Level 2 Level 3 时间戳与序列号 交易所时间 本地时间 序列号 三者共同构成完整的订单簿快照 核心组成 → 定义数据内容 数据级别 → 定义数据粒度 时间戳与序列号 → 定义数据时序

这张图把本章的三个核心知识点串起来了。你想想看,一个完整的订单簿快照,其实就是「在什么时间(时间戳+序列号),以什么粒度(Level 1/2/3),记录了哪些买卖意愿(买盘/卖盘/层级)」。三者缺一不可。

一个小建议:设计文件格式时,尽量把时间戳和序列号放在文件头部的固定位置。这样即使文件内容损坏,你至少还能知道这个快照是什么时候的、是第几个。我在做数据恢复时,这个设计救过我一次。

好了,这一章的内容就到这里。数据结构是地基,地基稳了,后面的文件格式设计、压缩策略、回放机制才能站得住脚。下一章我们会深入具体的文件格式设计,包括二进制 vs 文本格式的选择、字段对齐策略等。


专注资料整理