量化回测海量tick数据的读取瓶颈,主要不在磁盘容量,而在随机小文件读写和解析开销;优化核心是存储格式对齐、按需裁剪、并行预读。
tick数据读取慢,先分清卡在哪一层
很多人第一反应是换更快的硬盘,但换了NVMe之后,加载速度并没有本质提升,原因很简单:tick数据读取慢,多数时候不是顺序带宽不够,而是磁盘的IOPS被琐碎的小请求耗尽了,tick数据的特点是单条记录很小,一天全市场可能有上千万条,但文件又经常被切得七零八落,读一次历史回测,实际上在反复地“打开文件读取几个字节关闭文件”。
- 磁盘IO层:大量随机小文件访问,寻道和轮询占掉大半时间。
- 解析层:从CSV或文本里逐字段转换,字符串处理的CPU开销远高于数据本身的大小。
- 内存拷贝层:读入之后还要经过pandas或自研中间层,多次复制让延迟进一步放大。
行业共识认为,回测的读路径设计应尽量贴近策略的访问模式,如果策略按日期回测,数据却按股票代码分文件,那么每天都要跨几千个文件取数,再快的硬盘也会被拖垮,先确定访问模式再谈优化,比直接研究底层读取函数更重要。
一个典型的“读取慢”场景:按日切片
比如你写的是日内波动率策略:每天开盘前需要读取前一日的全部tick,计算累积成交量分布,这时候,如果存储布局是“每个股票一个目录,里面按小时切文件”,那么加载一天数据就需要打开几十个目录、几百个文件,反之,如果按日期分目录,每天目录下是当日的紧凑二进制文件,顺序读一遍就能拿到全部数据,这个调整操作简单,但回测提速却相当明显,用profile工具一看,绝大多数时间都耗在os.open和read调用上,而不是策略逻辑本身。
tick数据存储格式对比:Parquet不是万能药
很多量化团队从CSV迁到Parquet,以为问题就解决了,但实际跑下来,部分场景反而变慢了,这里先看一份常用格式的对比。

| 格式 | 读取性能 | 适用场景 | 主要坑点 |
|---|---|---|---|
| CSV/文本 | 差 | 小样本调试、数据交换 | 解析开销大,文件膨胀 |
| HDF5 | 中等 | 科学计算、中等规模 | 并发写锁冲突,索引不灵活 |
| Parquet | 较好 | 列裁剪、批量分析 | tick逐行访问时反序列化开销高 |
| 自定义二进制 | 最好 | 高频回测、生产环境 | 需要维护读写兼容 |
Parquet的强项是“读出一列中满足条件的所有行”,但tick回测往往需要“某个时间段内所有字段的序列”,这恰好不是Parquet最舒服的场景,如果只需要price和volume两列,Parquet的列裁剪优势又能发挥出来,选择格式前先统计策略的字段使用率:字段使用率高,就用行式紧凑二进制;字段使用率低,才考虑列式。
为什么列式存储有时反而帮倒忙
业内专家指出,tick数据的读取瓶颈往往在IOPS而不在吞吐量,列式存储能把吞吐量压得很低,但一旦需要按时间窗口重建行数据,就要做额外的行组拼接,反而增加了CPU占用,另一个坑是Parquet的元数据和页压缩,在扫描小时间窗口时,解压和过滤开销甚至超过原始数据读取,不要只盯着格式名字,要结合“按时间取整行”和“按字段取整列”两种访问习惯来选。
Python读取tick数据性能优化:从文件组织到并行预读
Python处理tick数据尤其容易慢,因为GIL限制了多线程读取的并发,而pandas的向量化操作在大量小请求下又发挥不出来,优化路径可以分三步走。
第一步,按日期分片,而不是按股票分片
把tick数据按交易日组织成/data/2024/20240102.tic,每个文件里只放当天的记录,按时间戳排序,这一步之后,回测单日数据只需要打开一个文件,顺序扫描即可,如果还需要按股票筛选,可以在文件内部用二分查找定位,或者维护一个小索引,这个文件组织方式能解决80%的随机读问题。

第二步,用numpy结构化数组加内存映射
具体做法是定义dtype为np.dtype([('time','i8'),('price','f8'),('volume','u4')]),然后用np.memmap直接映射文件,访问arr[i]时,操作系统负责从磁盘按页加载,这比pd.read_csv快得多,而且内存占用可控,需要注意的是,memmap文件应预先排序,否则随机访问性能会很差。
第三步,并行预读与进程间传递
用concurrent.futures.ProcessPoolExecutor把多个交易日的数据块分发给独立进程,每个进程用memmap读取自己负责的部分,然后以numpy数组形式返回,关键点是控制进程数,一般设为物理核心数或减一,避免上下文切换开销,操作路径上的小技巧:
- 使用
pyarrow.dataset配合filters参数,在读取阶段就过滤日期和符号。 - 进程间传递大数组时,用
numpy.ndarray.tobytes(),不要用pickle。 - 如果内存充足,可以将常用日期的数据缓存到内存字典中,二次回测直接命中。
控制进程数和内存上限
不是进程数越多越好,一个常见做法是用os.cpu_count()的一半作为进程数,同时给每个进程限制内存上限,防止并行加载把整台机器拖垮。
本地tick数据回测加速:流式读取更适合长周期回测
如果你的回测要跑三年全市场tick数据,一次全量加载到内存基本不现实,此时推荐流式读取:把时间轴切成片段,每段比如1分钟或5秒,策略按片段消费,这样内存占用恒定,回测过程也能及时看到进度,收敛到具体实现:
- 按时间段生成文件索引,例如
2024-01-02_09-30-00_09-35-00.tic。 - 用生成器逐段yield数据,策略解析完立即释放。
- 对需要反复使用的片段,用LRU缓存保留热点数据。
对比一下两种策略:

| 模式 | 内存占用 | 适用场景 | 实现复杂度 |
|---|---|---|---|
| 全量加载 | 高 | 单日或小样本 | 低 |
| 流式读取 | 低 | 数月以上长回测 | 中 |
| 混合模式 | 中 | 按需裁剪 | 较高 |
在混合模式下,可以只把当日热点数据驻留内存,历史数据走流式,这样既保证了盘中策略的响应速度,又不会让内存随回测周期线性膨胀,很多本地回测框架也支持“冷热分层”:把近期数据放SSD,远期数据放机械盘,通过流式读取把访问频率低的冷数据按需拉入内存。
量化回测海量tick数据的读取,本质上不是“把数据读完”,而是“按需把数据递到策略面前”,先把存储格式和文件组织对齐到访问模式,再叠加并行预读,通常就能把回测提速一个量级。
量化回测tick数据读取慢的常见问题
问:tick数据读取慢,是不是因为硬盘不够好?
答:高端NVMe SSD能降低延迟,但如果文件组织散乱,比如一个交易日被切出上百个小文件,顺序读也会退化成随机读,多数情况下,调整文件布局比换硬盘效果更明显。
问:Python读tick数据用pandas还是numpy更高效?
答:pandas的read_csv方便但慢,适用于原型验证,对海量tick数据,更推荐numpy结构化数组加memmap,或者用PyArrow的dataset接口做列裁剪,后者还能直接过滤时间范围,减少数据进入Python之前的大小。
问:本地量化回测加速时,直接把所有tick数据放进内存行不行?
答:如果单日数据量在几十GB,内存可能装得下,但会拖慢策略调试和重启速度,更合理的做法是先按需载入当天或当小时的数据块,再在内存里建立一个缓存池,这样反复回测可以跳过磁盘读取,这个结论在量化社区的公开技术分享中已被反复验证。