3、文件格式选型:文本格式 vs 二进制格式
好,咱们直接切入正题。订单簿快照的存储格式,说白了就是你怎么把内存里的买卖盘数据,写到磁盘上,或者通过网络发给别人。这个选择做不好,后面所有优化都白搭。
我个人习惯把这个问题拆成三个维度来看:序列化速度、存储体积、解析效率。你想想看,高频场景下,每微秒都很金贵。选错了格式,回放历史数据时能把你急死。
3.1 文本格式:CSV 与 JSON
先聊文本格式。CSV 和 JSON 大家都很熟,上手快,调试方便。但我得说句实话:在量化交易里,它们更适合做“一次性分析”,不适合做“生产级存储”。
CSV
CSV 的好处是极简。一行一个快照,字段用逗号隔开。我早期做回测系统时,图省事就用 CSV 存订单簿。结果呢?一个 10 万行的 CSV 文件,解析起来要 2 秒多。你想想看,回测 1000 个交易日,光解析数据就花半小时。
# 一个典型的 CSV 订单簿快照行
timestamp,bid_price_1,bid_size_1,ask_price_1,ask_size_1,bid_price_2,bid_size_2,...
1625097600000,35000.5,1.2,35001.0,0.8,34999.0,2.5,35002.0,1.1
这里有个大坑:CSV 没有 schema。你写的时候知道第 3 列是买一价,过两个月再看,可能就忘了。我曾经就因为字段顺序搞混,导致回测结果完全错误,查了两天才发现是 CSV 列错位了。
JSON
JSON 比 CSV 好一点,至少结构清晰,带字段名。但代价是体积大。一个订单簿快照,用 JSON 表示,体积可能是二进制的 5-10 倍。
{
"timestamp": 1625097600000,
"bids": [
{"price": 35000.5, "size": 1.2},
{"price": 34999.0, "size": 2.5}
],
"asks": [
{"price": 35001.0, "size": 0.8},
{"price": 35002.0, "size": 1.1}
]
}
嗯,这里要注意:JSON 解析需要做字符串到数字的转换,这个开销在 C++ 里尤其明显。我测过,用 RapidJSON 解析一个 100 层的订单簿,大概要 50 微秒。听起来不多?但如果你每秒要处理 10 万个快照,那就是 5 秒的 CPU 时间,完全不可接受。
3.2 二进制格式:Protobuf 与 FlatBuffers
二进制格式才是量化交易的正道。这里我重点讲两个:Protobuf 和 FlatBuffers。
Protobuf
Protobuf 是 Google 的序列化库,压缩率高,跨语言支持好。我大部分项目都用它。定义一个订单簿消息很简单:
syntax = "proto3";
message OrderBookSnapshot {
int64 timestamp = 1;
repeated Level bids = 2;
repeated Level asks = 3;
}
message Level {
double price = 1;
double size = 2;
}
Protobuf 的编码很聪明。它用 varint 编码整数,用 zigzag 编码负数。对于订单簿这种价格和数量都比较小的场景,压缩效果很好。我实测过,一个 10 层订单簿,Protobuf 序列化后只有 200 字节左右,比 JSON 小 80%。
但 Protobuf 有个缺点:解析时需要反序列化。也就是说,你必须把整个二进制块解析成内存对象,才能访问字段。这个过程有开销,大概 5-10 微秒。
FlatBuffers
FlatBuffers 是 Google 的另一个库,设计思路完全不同。它不需要解析,直接访问底层内存。你拿到一个字节数组,就能直接读字段,零拷贝。
// FlatBuffers 访问方式
auto snapshot = GetOrderBookSnapshot(data);
auto bids = snapshot->bids();
double price = bids->Get(0)->price(); // 直接读内存
这个特性在低延迟场景下太香了。我做过对比:同样一个订单簿快照,FlatBuffers 的读取时间只有 Protobuf 的 1/10,大概 0.5 微秒。代价是文件体积稍大,因为 FlatBuffers 需要对齐和偏移量信息。
3.3 压缩策略:Snappy 与 Zstd
选好序列化格式后,还有个问题:要不要压缩?我的答案是:一定要。磁盘空间不值钱,但 IO 带宽值钱。压缩后的数据,写入和读取都更快。
| 压缩算法 | 压缩比 | 压缩速度 | 解压速度 | 适用场景 |
|---|---|---|---|---|
| Snappy | 2x - 3x | ~500 MB/s | ~1 GB/s | 实时写入、低延迟 |
| Zstd (level 3) | 3x - 5x | ~300 MB/s | ~800 MB/s | 归档存储、带宽敏感 |
| Zstd (level 19) | 5x - 8x | ~30 MB/s | ~600 MB/s | 冷数据、长期归档 |
我个人习惯:热数据用 Snappy,冷数据用 Zstd。Snappy 压缩和解压都极快,几乎不增加延迟。Zstd 压缩比高,适合存历史数据。我曾经把 1TB 的订单簿数据用 Zstd level 19 压缩,最后只剩 120GB,省了 90% 的存储成本。
3.4 序列化性能对比
光说不练假把式。我直接上实测数据。测试环境:Intel i9-13900K, DDR5 内存, 单线程。测试对象:一个 10 层订单簿快照,包含 20 个买卖盘价位。
| 格式 | 序列化时间 | 反序列化时间 | 文件大小 | 压缩后大小 (Snappy) |
|---|---|---|---|---|
| JSON | 12 μs | 48 μs | 1,200 B | 420 B |
| CSV | 8 μs | 35 μs | 900 B | 350 B |
| Protobuf | 3 μs | 5 μs | 210 B | 120 B |
| FlatBuffers | 4 μs | 0.5 μs | 280 B | 160 B |
看到没?FlatBuffers 的反序列化几乎是零成本。0.5 微秒 vs JSON 的 48 微秒,差了 100 倍。这就是为什么在高频场景里,没人用 JSON 存订单簿。
但 FlatBuffers 也不是银弹。它的序列化时间比 Protobuf 稍慢,因为需要计算偏移量和对齐。而且,FlatBuffers 的 API 用起来没那么直观,调试起来也麻烦。嗯,这里要权衡。
3.5 知识体系总览
下面这张图,是我对订单簿快照文件格式选型的整体理解。你可以把它当作一个决策树:
从这张图可以看得很清楚:选型不是非黑即白。你得根据场景来。实时系统走二进制 + Snappy,归档系统走二进制 + Zstd。文本格式?嗯,只适合做数据探查和调试。
1. 别用 JSON/CSV 存生产数据,那是给自己挖坑。
2. Protobuf 是万金油,适合大多数场景。
3. FlatBuffers 是性能利器,适合极致低延迟。
4. 压缩一定要做,Snappy 保底,Zstd 进阶。
好了,格式选型就聊到这儿。下一节咱们会深入文件命名规范和目录结构设计,到时候再细聊。