数据模块搭建:Tick级数据获取、清洗与对齐、存储方案

做高频回测,数据就是你的命根子。我见过太多人策略写得漂亮,最后死在数据上——不是缺了字段,就是时间戳对不上,回测结果跟实盘差了十万八千里。今天咱们就把数据模块彻底讲透。

一、Tick级数据获取:从哪里来,怎么拿

先说说数据源。我个人习惯用三类来源:

  • 交易所直连:延迟最低,但门槛高,适合机构
  • 数据服务商:如Wind、聚宽、RiceQuant,省心但贵
  • 开源数据:比如Binance的API,免费但需要自己清洗

拿Binance的Tick数据举个例子。你想想看,每秒可能产生上千条成交记录,怎么高效拉取?

import requests
import pandas as pd
from datetime import datetime

def fetch_binance_ticks(symbol, start_time, end_time):
    """
    获取Binance的Tick级成交数据
    """
    url = f"https://api.binance.com/api/v3/aggTrades"
    params = {
        'symbol': symbol,
        'startTime': int(start_time.timestamp() * 1000),
        'endTime': int(end_time.timestamp() * 1000),
        'limit': 1000  # 单次最多1000条
    }
    
    resp = requests.get(url, params=params)
    data = resp.json()
    
    # 转成DataFrame
    df = pd.DataFrame(data)
    df['timestamp'] = pd.to_datetime(df['T'], unit='ms')
    df = df.rename(columns={
        'a': 'agg_trade_id',
        'p': 'price',
        'q': 'quantity',
        'T': 'trade_time'
    })
    
    return df[['timestamp', 'price', 'quantity', 'agg_trade_id']]

这里有个坑:Binance的aggTrades接口一次最多返回1000条。如果你要拉一整天的数据,得做分页。我在项目中遇到过,直接循环请求会被限流,后来加了随机延时才搞定。

注意:高频数据获取一定要做断点续传。我曾经因为网络波动丢了2小时数据,回测结果完全失真。建议每次拉取后记录last_trade_id,下次从这个ID继续。

二、数据清洗:脏数据是回测的隐形杀手

拿到原始Tick数据后,你会发现什么妖魔鬼怪都有。我总结了几类常见问题:

问题类型 表现 处理方法
时间戳错乱 后一条的时间比前一条还早 按时间排序后,删除逆序记录
价格异常 价格突然变成0或负数 设定合理阈值,超出则剔除
重复数据 同一条成交记录出现多次 按trade_id去重
缺失字段 某些Tick缺少成交量 用前一条数据填充或直接丢弃

嗯,这里要重点说说时间戳对齐。不同交易所的时间戳精度不一样,有的到毫秒,有的到微秒。你如果直接拿来做回测,会发现买卖单对不上。

def clean_ticks(df):
    """
    清洗Tick数据:去重、排序、异常值处理
    """
    # 1. 去重
    df = df.drop_duplicates(subset=['agg_trade_id'])
    
    # 2. 排序
    df = df.sort_values('timestamp').reset_index(drop=True)
    
    # 3. 价格异常过滤
    median_price = df['price'].median()
    df = df[df['price'].between(median_price * 0.9, median_price * 1.1)]
    
    # 4. 时间戳对齐到毫秒
    df['timestamp_ms'] = df['timestamp'].dt.floor('ms')
    
    return df
我的经验:清洗时别太激进。有一次我把价格偏离中位数5%的数据全删了,结果发现那天正好有次闪崩,真实行情就是那样。建议先做可视化看看异常值的分布,再决定阈值。

三、数据对齐:让买卖双方对上话

高频回测里,最头疼的就是数据对齐。你想想看,买一价和卖一价的时间戳可能差了几微秒,但就是这几微秒,决定了你的订单能不能成交。

我常用的对齐策略有三种:

  • 精确对齐:按时间戳精确匹配,适合纳秒级数据
  • 插值对齐:用前后数据插值,适合毫秒级数据
  • 聚合对齐:按固定时间窗口(如1ms)聚合,适合大多数场景
def align_ticks(bid_df, ask_df, freq='1ms'):
    """
    将买卖Tick数据对齐到统一时间轴
    """
    # 设置时间索引
    bid_df = bid_df.set_index('timestamp_ms')
    ask_df = ask_df.set_index('timestamp_ms')
    
    # 重采样到固定频率
    bid_resampled = bid_df['price'].resample(freq).last().ffill()
    ask_resampled = ask_df['price'].resample(freq).last().ffill()
    
    # 合并
    aligned = pd.DataFrame({
        'bid_price': bid_resampled,
        'ask_price': ask_resampled
    })
    
    # 计算中间价
    aligned['mid_price'] = (aligned['bid_price'] + aligned['ask_price']) / 2
    
    return aligned.dropna()

说白了,对齐的本质就是「把不同频率的数据强行塞进同一个时间框架」。但要注意,ffill(向前填充)会引入未来信息,回测时容易过拟合。我建议用bfill(向后填充)或者只保留完全对齐的时间点。

四、数据存储:HDF5 vs Parquet

数据清洗对齐完了,得存起来。高频数据量巨大,一天可能几个GB,选错存储格式会让你欲哭无泪。

我对比过HDF5和Parquet,直接上结论:

特性 HDF5 Parquet
压缩率 中等(默认zlib) 高(snappy/zstd)
读取速度 快(支持列切片) 非常快(列式存储)
写入速度 慢(追加模式还行) 快(批量写入)
跨语言支持 Python为主 多语言(Spark、Java等)
文件大小 较大 小(通常小30-50%)

我个人习惯:日常研究用HDF5,因为Pandas直接支持,读写方便。生产环境用Parquet,压缩率高,而且后续如果要上Spark做大规模回测,直接就能用。

# HDF5存储示例
def save_to_hdf5(df, filepath, key='ticks'):
    """
    将Tick数据存储为HDF5格式
    """
    with pd.HDFStore(filepath, mode='a') as store:
        store.put(key, df, format='table', 
                  complib='zlib', complevel=5)
        store.get_storer(key).attrs.description = 'Tick级成交数据'

# Parquet存储示例
def save_to_parquet(df, filepath, partition_cols=None):
    """
    将Tick数据存储为Parquet格式
    """
    df.to_parquet(filepath, 
                  engine='pyarrow',
                  compression='snappy',
                  partition_cols=partition_cols)
核心建议:别把所有数据塞进一个文件。按日期分区存储,比如 ticks/2024/01/01.parquet。这样查询某一天的数据时,只需要加载一个文件,速度能快几十倍。

五、完整的数据流水线

最后,我把整个数据模块串起来,画了一张流程图。你看一眼就明白了:

数据获取 交易所/API 数据清洗 去重/排序/过滤 数据对齐 时间轴统一 存储格式选择 HDF5 存储 Parquet 存储 研究用 生产用

这条流水线跑下来,你的数据模块基本就稳了。记住:数据质量决定了回测的天花板。策略再牛,数据是脏的,结果就是垃圾进垃圾出。

避坑指南:我曾经为了省存储空间,把Tick数据压缩成1秒K线。结果回测时发现,策略在实盘里根本捕捉不到那些微秒级的价差机会。所以,做高频就别想着省空间,该存Tick就存Tick。

数据模块搭建完了,接下来就是回测引擎的核心逻辑了。不过那是另一章的内容,咱们先把数据这关过了再说。

专注资料整理