4. 数据存储与读取:高效存储Tick数据
做高频回测,第一关就是数据。
我见过太多人,策略写得天花乱坠,结果一跑回测,光读数据就花了半小时。你说这还怎么玩?
高频Tick数据,说白了就是「海量、高频、有噪声」。一整天下来,光是沪深300的Tick数据就能轻松上千万条。怎么存、怎么读,直接决定了你的回测效率。
4.1 存储格式选型:Parquet vs HDF5
我个人习惯,先看场景再选工具。高频Tick数据有两个核心诉求:压缩率高、读取快。
市面上常见的方案就两个:Parquet 和 HDF5。我当年在搭建第一版回测系统时,在这两个之间纠结了很久。后来都试了一遍,才摸清它们的脾气。
| 特性 | Parquet | HDF5 |
|---|---|---|
| 压缩率 | 极高(列式存储 + 压缩算法) | 中等(支持压缩,但不如Parquet) |
| 读取速度 | 快(按列读取,适合分析) | 极快(按行读取,适合顺序访问) |
| 生态兼容 | Pandas、Spark、Dask 原生支持 | Pandas 支持,但跨语言稍弱 |
| 适用场景 | 数据分析、批量回测 | 实时回测、高频读写 |
嗯,这里要注意:不要盲目选。如果你做的是日内高频回测,需要频繁读取某一天的完整数据,HDF5 更合适。如果你做的是多日、多品种的统计分析,Parquet 的列式存储优势就出来了。
我的建议:用 Parquet 做数据仓库,用 HDF5 做回测引擎的实时数据源。两者互补,不冲突。
4.2 内存映射文件(mmap)技术
说到读取速度,就不得不提 mmap。
你想想看,传统读文件是什么流程?打开文件 -> 拷贝到内存 -> 解析。每次都要经过内核态和用户态的切换,慢就慢在这里。
mmap 的思路很直接:把文件直接映射到进程的虚拟地址空间。说白了,就是让操作系统帮你管理数据加载,你直接像访问内存一样访问文件。
我在做高频回测时,最头疼的就是数据加载延迟。后来用了 mmap,读取 1GB 的 Tick 数据,从原来的 3 秒降到了 0.2 秒。效果立竿见影。
下面是一个简单的 mmap 使用示例:
import numpy as np
import mmap
# 假设我们已经将Tick数据保存为二进制文件
def load_tick_data_mmap(file_path):
with open(file_path, 'rb') as f:
# 映射整个文件
mmapped_file = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
# 将映射数据转换为numpy数组
data = np.frombuffer(mmapped_file, dtype=np.dtype([
('timestamp', 'i8'),
('price', 'f8'),
('volume', 'i4'),
('bid', 'f8'),
('ask', 'f8')
]))
return data
# 使用示例
tick_data = load_tick_data_mmap('tick_20240101.bin')
print(f"加载了 {len(tick_data)} 条Tick数据")
print(f"第一条数据: {tick_data[0]}")
小技巧:mmap 特别适合「只读不写」的场景。高频回测中,历史数据就是只读的,用 mmap 再合适不过。
4.3 数据对齐与清洗
数据存好了,读得快了,但还有一个大坑等着你——数据质量。
我曾经接手过一个项目,回测结果漂亮得不像话,年化收益 80%。结果一查,发现数据里有大量「未来数据」——某条 Tick 的时间戳比下一笔还晚。你说这策略能信吗?
数据清洗,我总结了三个核心步骤:
- 时间戳对齐:不同交易所、不同品种的 Tick 时间戳格式可能不同。统一转为纳秒级时间戳,按时间排序。
- 异常值过滤:价格突变、成交量异常、买卖价差为负等。这些数据必须剔除或修正。
- 缺失值处理:某些时间段可能没有 Tick 数据(比如午休、熔断)。需要根据策略需求决定是填充还是跳过。
下面是我常用的清洗函数:
import pandas as pd
import numpy as np
def clean_tick_data(df):
"""
清洗Tick数据
"""
# 1. 时间戳对齐:统一为纳秒
df['timestamp'] = pd.to_datetime(df['timestamp']).astype(np.int64)
# 2. 按时间排序
df = df.sort_values('timestamp').reset_index(drop=True)
# 3. 过滤异常价格(比如价格突变超过10%)
price_changes = df['price'].pct_change().abs()
df = df[price_changes < 0.1].copy()
# 4. 过滤买卖价差为负的情况
df = df[df['ask'] > df['bid']].copy()
# 5. 去除重复时间戳(保留最后一条)
df = df.drop_duplicates(subset='timestamp', keep='last')
return df
# 使用示例
raw_data = pd.read_parquet('tick_data.parquet')
cleaned_data = clean_tick_data(raw_data)
print(f"清洗前: {len(raw_data)} 条, 清洗后: {len(cleaned_data)} 条")
注意:数据清洗不是越狠越好。过度清洗会导致数据失真,回测结果偏离真实市场。我的原则是:宁可保留可疑数据,也不要随意删除。可以在回测时设置一个「数据质量标记」,让策略自己决定是否使用。
4.4 知识体系总览
说了这么多,我画了一张图,帮你把这一章的核心逻辑串起来:
这张图其实就讲了一件事:原始数据 -> 清洗 -> 存储 -> 读取。每一步都有坑,每一步都有技巧。你把这四个环节打通了,高频回测的数据基础就算打牢了。
最后说一句:数据存储和读取,看起来是「脏活累活」,但恰恰是高频交易系统的基石。我见过太多团队,策略模型再牛,数据层一塌糊涂,最终回测结果根本不敢信。所以,别嫌麻烦,把这一步做扎实了。
公众号:蓝海资料掘金营,微信deep3321