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 的时间戳比下一笔还晚。你说这策略能信吗?

数据清洗,我总结了三个核心步骤:

  1. 时间戳对齐:不同交易所、不同品种的 Tick 时间戳格式可能不同。统一转为纳秒级时间戳,按时间排序。
  2. 异常值过滤:价格突变、成交量异常、买卖价差为负等。这些数据必须剔除或修正。
  3. 缺失值处理:某些时间段可能没有 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 知识体系总览

说了这么多,我画了一张图,帮你把这一章的核心逻辑串起来:

高频Tick数据存储与读取核心流程 原始Tick数据 数据清洗与对齐 高效存储 Parquet(分析用) HDF5(回测用) mmap 内存映射读取

这张图其实就讲了一件事:原始数据 -> 清洗 -> 存储 -> 读取。每一步都有坑,每一步都有技巧。你把这四个环节打通了,高频回测的数据基础就算打牢了。

最后说一句:数据存储和读取,看起来是「脏活累活」,但恰恰是高频交易系统的基石。我见过太多团队,策略模型再牛,数据层一塌糊涂,最终回测结果根本不敢信。所以,别嫌麻烦,把这一步做扎实了。


公众号:蓝海资料掘金营,微信deep3321