数据存储与读取:别让数据拖了回测的后腿

做量化回测,最怕什么?

不是策略逻辑写错了,而是数据加载慢得像蜗牛。我早期做回测时,就吃过这个亏。策略跑得飞快,结果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?别闹了。那玩意儿读起来慢,存起来也占地方。我推荐两种:HDF5Parquet

特性 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是速度和压缩率的黄金平衡点。

性能优化:从“秒级”到“毫秒级”

数据读取的性能优化,说白了就三个方向:

  1. 减少IO量:只读需要的列和行
  2. 用对格式:Parquet > HDF5 > CSV
  3. 缓存热点数据:把常用数据留在内存里

我分享一个实际项目中的做法。当时要回测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天的数据,既保证了速度,又不会撑爆内存。记住:缓存不是越多越好,要按需加载。

知识体系总览

下面这张图,是我对本章知识结构的总结。你可以把它当作一个“地图”,随时回来对照。

数据存储与读取知识体系 内存模型 Pandas DataFrame 存储格式 HDF5 / Parquet 性能优化 缓存 / 列式读取 时间索引 类型优化 分组存储 压缩选项 列式读取 Snappy压缩 核心原则:减少IO → 用对格式 → 缓存热点

这张图把本章的核心内容串起来了。从内存里的DataFrame,到硬盘上的Parquet,再到读取时的缓存优化,每一步都有讲究。

记住:数据存储不是“存进去就行”,而是要为后续的回测效率服务。你想想看,如果每次加载数据都要等半分钟,那你还怎么快速迭代策略?

我的经验总结:

  • 小数据(<1GB):用CSV也行,但记得用dtype参数优化类型
  • 中数据(1-10GB):Parquet + snappy,读取速度提升10倍
  • 大数据(>10GB):分片存储 + 内存映射,配合缓存策略

好了,数据存储这块就聊到这儿。你回去可以试试,把你手头的CSV数据转成Parquet,感受一下什么叫“飞一般的感觉”。


专注资料整理