数据存储与读取:别让数据拖了回测的后腿
做量化回测,最怕什么?
不是策略逻辑写错了,而是数据加载慢得像蜗牛。我早期做回测时,就吃过这个亏。策略跑得飞快,结果90%的时间都花在读取CSV上。你说气不气人?
今天我们就聊聊,怎么把数据存好、读快。说白了,就是让数据不再成为你的瓶颈。
Pandas DataFrame:内存里的“工作台”
我们做回测,数据在内存里长什么样?
几乎清一色是Pandas的DataFrame。它就像一个电子表格,行是时间,列是价格、成交量这些字段。
核心要点:DataFrame是回测系统的“内存数据模型”。所有计算、分析、策略逻辑,都基于它展开。
我个人习惯,在读取原始数据后,第一时间做三件事:
- 设置时间索引:把datetime列设为index,方便时间切片
- 排序:按时间升序排列,避免后续计算出错
- 类型优化:把float64转成float32,能省一半内存
import pandas as pd
# 读取原始数据
df = pd.read_csv('btc_usdt_1m.csv')
# 我的标准预处理流程
df['timestamp'] = pd.to_datetime(df['timestamp'])
df.set_index('timestamp', inplace=True)
df.sort_index(inplace=True)
# 类型优化——这个技巧我用了好几年
for col in ['open', 'high', 'low', 'close', 'volume']:
df[col] = df[col].astype('float32')
print(df.info()) # 看看内存占用,你会惊喜的
小技巧:如果你只需要收盘价和成交量,就别把整个DataFrame都加载进来。用usecols参数只读需要的列,能大幅减少内存。
HDF5 vs Parquet:两种主流存储格式
数据在内存里是DataFrame,那存到硬盘上用什么格式?
CSV?别闹了。那玩意儿读起来慢,存起来也占地方。我推荐两种:HDF5和Parquet。
| 特性 | HDF5 | Parquet |
|---|---|---|
| 读取速度 | 快(列式读取) | 极快(列式存储) |
| 压缩率 | 中等 | 高(通常比CSV小80%) |
| 跨语言支持 | Python为主 | 多语言(Spark、R、Julia) |
| 元数据管理 | 支持分组、属性 | 内嵌Schema |
| 适用场景 | 单机、小团队 | 大数据、分布式 |
我个人更偏爱Parquet。为什么?
因为它读取时只加载你需要的列。你想想看,回测时我们经常只需要“close”这一列,Parquet能直接跳过其他列,速度自然快。
实战:用Parquet存储和读取
嗯,这里要注意一个细节。Parquet写入时,默认会按列压缩。但如果你数据量不大,压缩反而会拖慢速度。
# 写入Parquet
df.to_parquet('btc_usdt_1m.parquet', compression='snappy')
# 读取——只读需要的列
df_close = pd.read_parquet(
'btc_usdt_1m.parquet',
columns=['close'] # 只加载这一列,速度飞起
)
# 读取全部列
df_full = pd.read_parquet('btc_usdt_1m.parquet')
我曾经踩过的坑:有一次我存了100GB的tick数据,用默认的gzip压缩。结果写入花了3小时,读取也慢得要命。后来换成snappy压缩,写入时间降到20分钟,读取快了10倍。记住:回测场景下,snappy是速度和压缩率的黄金平衡点。
性能优化:从“秒级”到“毫秒级”
数据读取的性能优化,说白了就三个方向:
- 减少IO量:只读需要的列和行
- 用对格式:Parquet > HDF5 > CSV
- 缓存热点数据:把常用数据留在内存里
我分享一个实际项目中的做法。当时要回测5年的分钟线数据,大概2亿行。如果用CSV,每次启动回测要等40秒。换成Parquet后,降到3秒。
import os
import pandas as pd
class DataManager:
"""我的数据管理器——专门处理存储和读取"""
def __init__(self, data_dir='./data'):
self.data_dir = data_dir
self._cache = {} # 内存缓存
def load(self, symbol, columns=None):
"""加载数据,带缓存"""
cache_key = f"{symbol}_{columns}"
# 命中缓存就直接返回
if cache_key in self._cache:
return self._cache[cache_key]
file_path = os.path.join(self.data_dir, f"{symbol}.parquet")
# 只读需要的列
df = pd.read_parquet(file_path, columns=columns)
# 缓存起来,下次直接读内存
self._cache[cache_key] = df
return df
def clear_cache(self):
"""清理缓存——回测结束后调用"""
self._cache.clear()
避坑指南:我曾经把整个数据集都缓存起来,结果内存爆了。后来改成只缓存最近7天的数据,既保证了速度,又不会撑爆内存。记住:缓存不是越多越好,要按需加载。
知识体系总览
下面这张图,是我对本章知识结构的总结。你可以把它当作一个“地图”,随时回来对照。
这张图把本章的核心内容串起来了。从内存里的DataFrame,到硬盘上的Parquet,再到读取时的缓存优化,每一步都有讲究。
记住:数据存储不是“存进去就行”,而是要为后续的回测效率服务。你想想看,如果每次加载数据都要等半分钟,那你还怎么快速迭代策略?
我的经验总结:
- 小数据(<1GB):用CSV也行,但记得用
dtype参数优化类型 - 中数据(1-10GB):Parquet + snappy,读取速度提升10倍
- 大数据(>10GB):分片存储 + 内存映射,配合缓存策略
好了,数据存储这块就聊到这儿。你回去可以试试,把你手头的CSV数据转成Parquet,感受一下什么叫“飞一般的感觉”。