基于Python存储L2逐笔数据的方案探讨
针对你处理L2 tick数据遇到的存储和后续关联分析问题,结合行业通用做法和你的技术栈(h5py/vaex),给你几个实用的解决方案:
1. 嵌套列存储(对齐KDB的思路,保留快照完整性)
这和你提到的KDB存储方式完全契合,利用HDF5支持嵌套/可变长度数组的特性,把每个订单簿快照作为一行,其中bid/ask的档位数据以数组形式存储在对应列中。
用h5py实现的话,可以定义复合dtype来描述快照结构:
import h5py import numpy as np # 定义包含嵌套数组的复合dtype book_dtype = np.dtype([ ("book_time", "datetime64[us]"), ("sym", "U8"), ("bid_times", "O"), # 存储可变长度的时间数组 ("bid_prices", "O"), ("bid_sizes", "O"), ("ask_times", "O"), ("ask_prices", "O"), ("ask_sizes", "O"), ]) # 构造单条快照数据 snapshot = np.array( ( np.datetime64("2017-11-06T14:57:08.532152"), "EURUSD", np.array(["20171106-14:57:08.528", "20171106-14:57:08.428"], dtype="<U21"), np.array([1.30699, 1.30698]), np.array([100000.0, 250000.0]), np.array(["20171106-14:57:08.528"], dtype="<U21"), np.array([1.30709]), np.array([100000.0]), ), dtype=book_dtype ) # 写入HDF5 with h5py.File("l2_book.h5", "a") as f: if "book_snapshots" not in f: f.create_dataset("book_snapshots", data=[snapshot], dtype=book_dtype, maxshape=(None,)) else: ds = f["book_snapshots"] ds.resize(ds.shape[0] + 1, axis=0) ds[-1] = snapshot
vaex可以直接读取这种嵌套结构,后续做asof join时,以book_time为关联键即可,完全满足你的需求。这种方式的优势是能完整保留订单簿快照的结构,适合做订单簿回放类分析。
2. 长表格式(行业主流,最适合关联分析)
这是量化领域处理L2数据最常用的方式:把每个档位拆成单独的一行,用book_time标识所属的快照,level标记档位序号,side区分买卖盘。数据结构如下:
| book_time | sym | side | level | quote_time | price | size |
|---|---|---|---|---|---|---|
| 2017-11-06 14:57:08.532152 | EURUSD | bid | 1 | 2017-11-06 14:57:08.528 | 1.30699 | 100000.0 |
| 2017-11-06 14:57:08.532152 | EURUSD | bid | 2 | 2017-11-06 14:57:08.428 | 1.30698 | 250000.0 |
| 2017-11-06 14:57:08.532152 | EURUSD | ask | 1 | 2017-11-06 14:57:08.528 | 1.30709 | 100000.0 |
用vaex实现追加写入非常简单:
import vaex # 构造单快照展开后的行数据 update_rows = [ { "book_time": np.datetime64("2017-11-06T14:57:08.532152"), "sym": "EURUSD", "side": "bid", "level": 1, "quote_time": np.datetime64("2017-11-06T14:57:08.528"), "price": 1.30699, "size": 100000.0 }, { "book_time": np.datetime64("2017-11-06T14:57:08.532152"), "sym": "EURUSD", "side": "bid", "level": 2, "quote_time": np.datetime64("2017-11-06T14:57:08.428"), "price": 1.30698, "size": 250000.0 }, { "book_time": np.datetime64("2017-11-06T14:57:08.532152"), "sym": "EURUSD", "side": "ask", "level": 1, "quote_time": np.datetime64("2017-11-06T14:57:08.528"), "price": 1.30709, "size": 100000.0 } ] # 追加写入HDF5 df = vaex.from_records(update_rows) df.export_hdf5("l2_long_format.h5", mode="a")
这种格式的最大优势是完美适配列存储和asof join:后续和交易数据关联时,直接用vaex.join.asof()按book_time和sym关联即可,逻辑清晰且性能优异。而且不需要处理不规则数组,数据结构简单易维护,是绝大多数量化系统的首选。
3. 固定长度数组+掩码(折中方案,兼顾性能和空间)
如果既想保留快照的行式结构,又不想用嵌套数组,可以用固定长度数组(比如20档)存储所有档位,再用掩码列标记有效档位:
import h5py import numpy as np # 定义带掩码的固定长度dtype fixed_book_dtype = np.dtype([ ("book_time", "datetime64[us]"), ("sym", "U8"), ("bid_times", "datetime64[us]", (20,)), ("bid_prices", "f8", (20,)), ("bid_sizes", "f8", (20,)), ("bid_valid", "?", (20,)), # 标记该档位是否有效 ("ask_times", "datetime64[us]", (20,)), ("ask_prices", "f8", (20,)), ("ask_sizes", "f8", (20,)), ("ask_valid", "?", (20,)), ]) # 构造快照数据,无效档位用NaN填充,掩码设为False snapshot = np.zeros(1, dtype=fixed_book_dtype)[0] snapshot["book_time"] = np.datetime64("2017-11-06T14:57:08.532152") snapshot["sym"] = "EURUSD" # 填充bid档位 snapshot["bid_times"][:2] = [np.datetime64("2017-11-06T14:57:08.528"), np.datetime64("2017-11-06T14:57:08.428")] snapshot["bid_prices"][:2] = [1.30699, 1.30698] snapshot["bid_sizes"][:2] = [100000.0, 250000.0] snapshot["bid_valid"][:2] = True # 填充ask档位 snapshot["ask_times"][0] = np.datetime64("2017-11-06T14:57:08.528") snapshot["ask_prices"][0] = 1.30709 snapshot["ask_sizes"][0] = 100000.0 snapshot["ask_valid"][0] = True # 写入HDF5 with h5py.File("l2_fixed_mask.h5", "a") as f: if "book_snapshots" not in f: f.create_dataset("book_snapshots", data=[snapshot], dtype=fixed_book_dtype, maxshape=(None,)) else: ds = f["book_snapshots"] ds.resize(ds.shape[0] + 1, axis=0) ds[-1] = snapshot
这种方案空间开销可控(20档的空值占用很小),数据结构规整,h5py/vaex处理性能优异,适合对速度要求较高的场景。
总结与选型建议
- 如果你的核心需求是和交易数据做asof join关联分析,优先选长表格式,这是行业通用的最优解,操作简单且性能拉满。
- 如果需要频繁还原完整订单簿快照(比如做订单簿回放、流动性分析),选嵌套列存储,和KDB的思路对齐,能完整保留快照结构。
- 如果追求极致性能且档位波动不大,选固定长度数组+掩码,兼顾空间和速度。
不需要拆成120列那么繁琐,以上三种方案都能完美解决你的问题。
内容的提问来源于stack exchange,提问作者mkst
相关产品推荐
相关产品推荐

