Tick级数据与Order Book数据结构
做高频因子挖掘,第一步就是跟Tick数据和Order Book打交道。说实话,我刚入行那会儿,觉得这不就是一堆数字嘛,有什么好研究的?结果第一次实盘回测,策略在模拟环境跑得飞起,一上实盘就亏成狗。后来才发现,问题就出在数据理解上——你对Tick数据的字段含义理解不到位,Order Book的更新机制没吃透,策略再牛逼也是白搭。
今天我们就来聊聊这个基础但极其关键的话题。我会结合我自己的踩坑经历,把Tick数据的字段、Level2行情解析、Order Book的构建与更新,以及存储格式的选择,一次性讲清楚。
Tick数据的字段含义
Tick数据,说白了就是每一笔成交的“快照”。它记录的是市场在那一瞬间发生了什么。我个人习惯把Tick数据分成两类:成交Tick和报价Tick。两者虽然都叫Tick,但含义完全不同。
成交Tick的典型字段包括:
- 时间戳:精确到毫秒甚至微秒。注意,不同交易所的时间戳精度不一样,我遇到过因为时间戳精度不够导致回测结果偏差的情况。
- 价格:成交价格。这里有个坑——有些交易所的成交价格是带小数点的,有些是整数,解析时一定要确认好精度。
- 成交量:该笔成交的股数或合约数。
- 成交额:价格×成交量,有些交易所直接提供,有些需要自己算。
- 买卖方向:主动买还是主动卖。这个字段在Level2行情里通常叫“Side”或“Direction”。
- 成交编号:唯一标识一笔成交的ID,用于去重。
报价Tick则记录的是买卖盘口的快照:
- 买一价、买一量:最优买价及其挂单量。
- 卖一价、卖一量:最优卖价及其挂单量。
- 买二到买N、卖二到卖N:深度盘口数据,Level2行情通常提供10档或20档。
- 总买量、总卖量:所有买盘和卖盘的挂单总量。
重点提醒:成交Tick和报价Tick的时间戳往往不是同步的。成交Tick记录的是成交发生的时间,报价Tick记录的是盘口变化的时间。两者混用时,一定要对齐时间轴,否则你的因子计算会出大问题。
Level2行情数据解析
Level2行情,说白了就是比普通行情更“深”的数据。普通行情只给你最优买卖盘口,Level2给你10档甚至20档的深度,还有逐笔成交、逐笔委托等详细信息。
我记得第一次解析Level2数据时,被它的数据格式搞晕了。不同交易所的Level2数据格式差异很大,但核心结构其实差不多:
// 一个典型的Level2行情数据包结构
{
"timestamp": 1630000000000, // 毫秒级时间戳
"symbol": "000001.SZ", // 股票代码
"bid_prices": [10.01, 10.00, 9.99, ...], // 买盘价格,从高到低
"bid_volumes": [1000, 2000, 1500, ...], // 对应买盘量
"ask_prices": [10.02, 10.03, 10.04, ...], // 卖盘价格,从低到高
"ask_volumes": [800, 1200, 1800, ...], // 对应卖盘量
"trades": [ // 逐笔成交
{"price": 10.01, "volume": 500, "side": "buy", "time": 1630000000001},
...
]
}
解析Level2数据时,有几个关键点要注意:
- 数据对齐:盘口数据和成交数据的时间戳可能不同步,需要按时间戳排序后对齐。
- 增量更新:很多交易所的Level2数据是增量推送的,不是全量快照。你需要维护一个本地Order Book,然后根据增量数据更新它。
- 数据去重:同一笔成交可能被推送多次,需要根据成交编号去重。
我的经验:解析Level2数据时,建议先写一个数据校验模块。我曾经因为交易所改了数据格式没通知,导致解析出来的数据全是错的,回测结果完全不可用。加个校验模块,能帮你快速发现问题。
Order Book的构建与更新机制
Order Book,也就是订单簿,是高频因子挖掘的核心数据结构。它记录了当前市场上所有未成交的买卖订单。说白了,就是一张“挂单表”。
构建Order Book其实不复杂,但更新机制需要仔细设计。我常用的方法是:
- 初始化:用第一个全量快照数据初始化Order Book。
- 增量更新:后续每收到一条增量数据,就更新Order Book中对应的价格档位。
- 定时全量同步:每隔一段时间(比如1分钟),用全量快照重新同步一次,防止增量数据累积误差。
这里有个关键点:增量更新的顺序。如果增量数据到达的顺序乱了,Order Book就会出错。我遇到过因为网络延迟导致增量数据乱序,Order Book里的买卖盘口完全对不上。后来我加了一个序列号校验,确保增量数据按顺序处理。
// 一个简单的Order Book更新逻辑(伪代码)
class OrderBook:
def __init__(self, snapshot):
self.bids = snapshot['bids'] // 买盘,按价格降序
self.asks = snapshot['asks'] // 卖盘,按价格升序
self.seq_no = snapshot['seq_no']
def update(self, delta):
// 检查序列号,防止乱序
if delta['seq_no'] <= self.seq_no:
return // 忽略旧数据
self.seq_no = delta['seq_no']
// 更新买盘
for price, volume in delta['bid_changes']:
if volume == 0:
self.bids.pop(price, None) // 撤单
else:
self.bids[price] = volume // 新增或修改
// 更新卖盘
for price, volume in delta['ask_changes']:
if volume == 0:
self.asks.pop(price, None)
else:
self.asks[price] = volume
注意:Order Book的更新频率非常高,尤其是在开盘和收盘时段。如果处理速度跟不上,数据就会堆积。我建议用C++或Rust实现核心的Order Book逻辑,Python只做上层调用。或者用NumPy的向量化操作来加速。
数据存储格式选择:Parquet vs HDF5
Tick数据和Order Book数据量巨大,存储格式的选择直接影响后续的分析效率。我个人主要用两种格式:Parquet和HDF5。两者各有优劣,我分别说说。
Parquet:
- 优点:列式存储,压缩率高,读取速度快。特别适合按列分析(比如只读取价格列)。
- 缺点:写入速度相对较慢,不适合频繁追加数据。
- 适用场景:批量处理、数据分析、机器学习特征工程。
HDF5:
- 优点:写入速度快,支持分层数据结构,适合存储大量小数据集。
- 缺点:读取速度不如Parquet,压缩率一般。
- 适用场景:实时数据存储、需要频繁追加数据的场景。
| 特性 | Parquet | HDF5 |
|---|---|---|
| 存储方式 | 列式 | 行式/分层 |
| 压缩率 | 高(通常50%-80%) | 中(通常30%-50%) |
| 读取速度 | 快(按列读取) | 中(按行读取) |
| 写入速度 | 慢 | 快 |
| 适用场景 | 批量分析、特征工程 | 实时存储、频繁追加 |
我个人习惯是:原始Tick数据用HDF5存储,因为写入速度快,适合实时接收数据。等数据积累到一定量后,再转换成Parquet格式用于分析。这样既保证了实时性,又兼顾了分析效率。
避坑指南:我曾经把所有Tick数据都存成Parquet,结果每天写入数据要花好几个小时,根本来不及处理。后来改成先用HDF5缓存,再批量转Parquet,问题就解决了。你想想看,数据存储格式的选择,其实是在读写速度和存储效率之间做权衡。
知识体系结构图
下面这张图展示了本章的核心知识体系,从Tick数据到Order Book,再到存储格式,一目了然:
这张图把本章的核心内容串起来了。从Tick数据出发,到Level2行情解析,再到Order Book的构建与更新,最后落到数据存储格式的选择。每个环节都环环相扣,缺一不可。
好了,关于Tick级数据与Order Book数据结构,我就讲这么多。记住,数据是高频因子挖掘的基石,这块搞不定,后面的因子计算、回测、实盘都是空中楼阁。希望我的经验能帮你少走一些弯路。