2. Tick数据获取与存储:常见数据源、存储格式与清洗去重

高频Tick数据的获取与存储是整个特征工程链条的起点。数据源的质量、存储格式的读写效率以及清洗去重的严谨程度,直接决定了后续特征计算的准确性与实时性。本章将系统梳理国内主流Tick数据源、三种核心存储格式的优劣对比,以及一套标准化的数据清洗与去重流程。

2.1 常见Tick数据源对比

国内量化生态中,Wind、聚宽、Tushare、米筐是获取Tick数据的四大主流渠道。它们在数据覆盖度、更新频率、获取方式与成本上各有侧重,下表从关键维度进行横向对比:

数据源 覆盖市场 Tick字段完整度 更新延迟 获取方式 成本
Wind 沪深A股、期货、期权 高(含买卖十档、逐笔成交) T+1(历史) / 实时(API) WindPy / C++ API 高(机构年费)
聚宽 沪深A股、期货 中高(五档行情、逐笔委托) 准实时(2-3秒) Python SDK / Web API 中(按量或会员)
Tushare 沪深A股、期货、指数 中(五档行情,无逐笔) T+1(历史) HTTP RESTful API 低(积分制,部分免费)
米筐 沪深A股、期货、期权 高(含逐笔成交、Level-2) 准实时(1-2秒) Python SDK / CSV导出 中高(按数据量计费)
选型建议: 机构级实盘策略优先选择Wind或米筐的Level-2数据;个人研究或回测场景,聚宽与Tushare的Tick数据已足够覆盖大部分特征工程需求。注意Tushare的Tick数据通常为T+1快照,不适合实时特征计算。

2.2 Tick数据存储格式:Parquet、HDF5与CSV

高频Tick数据具有海量、高维度、时间序列密集的特点。以沪深A股为例,单只股票日均产生约4000-5000条Tick记录,全市场5000只股票一天的数据量可达数千万行。选择正确的存储格式,能显著提升读写速度与磁盘利用率。

三种主流格式的核心差异如下:

特性 Parquet HDF5 CSV
存储类型 列式二进制 分层式二进制 行式文本
压缩率 极高(Snappy/Zstd) 高(Gzip/LZF) 低(文本冗余大)
读取速度(全列) 极快(列裁剪) 快(支持分块) 慢(全量解析)
读取速度(子集) 极快(谓词下推) 中(需指定路径) 慢(需全量读入)
元数据支持 内嵌Schema 组/数据集层级 无
跨语言兼容 优秀(Spark/Pandas/R) 良好(Python/Matlab) 通用(任何工具)
适用场景 大规模历史回测、分布式计算 单机大文件、科学计算 小样本调试、数据交换
工程实践结论: 生产环境推荐使用 Parquet 作为Tick数据的主存储格式。其列式存储特性在读取特定字段(如仅取最新价与成交量)时,可跳过无关列,IO效率比CSV提升10-50倍。HDF5适合需要频繁随机访问特定时间窗口的场景,但需注意其单文件锁机制在并发写入时存在瓶颈。

以下是一个使用Pandas将Tick数据写入Parquet的典型代码片段:

import pandas as pd

# 假设 tick_df 为包含 tick 数据的 DataFrame
# 字段:['ts_code', 'trade_time', 'price', 'volume', 'bid1', 'ask1', ...]

tick_df = pd.read_csv('raw_tick_20250115.csv')

# 按股票代码分区存储,便于后续按标的读取
for code, group in tick_df.groupby('ts_code'):
    # 按日期分区,避免单文件过大
    date_str = group['trade_time'].iloc[0][:10]
    file_path = f'tick_data/{code}/{date_str}.parquet'
    
    # 使用 snappy 压缩,平衡速度与压缩比
    group.to_parquet(
        file_path,
        engine='pyarrow',
        compression='snappy',
        index=False
    )
    print(f'Saved: {file_path} | Rows: {len(group)}')

2.3 数据清洗与去重

原始Tick数据中普遍存在重复记录、异常价格、时间戳错乱、缺失字段等问题。若不经清洗直接用于特征计算,会引入严重的偏差。标准清洗流程包含以下四个步骤:

  1. 时间戳规范化:统一为毫秒级Unix时间戳或标准UTC时间,处理交易所非交易时段(如9:25-9:30集合竞价)的异常时间戳。
  2. 价格合理性校验:剔除价格为负、超过当日涨跌停板、或与上一笔Tick价格偏离超过阈值(如期货可设1%)的记录。
  3. 成交量与金额校验:剔除成交量为0或负值、成交金额与价格×数量不匹配的记录。
  4. 精确去重:基于股票代码 + 时间戳 + 价格 + 成交量四元组进行去重,保留首次出现的记录。
常见陷阱: 部分数据源在行情中断后恢复时,会重发之前已推送的Tick快照,导致大量重复。此时仅按时间戳去重可能遗漏,必须结合价格与成交量联合判断。另外,期货夜盘数据的时间戳需注意日期跨天问题,建议统一转换为带日期的完整时间戳。

以下是一个完整的去重与清洗函数示例:

def clean_tick_data(df: pd.DataFrame) -> pd.DataFrame:
    """
    对Tick DataFrame执行标准清洗与去重。
    要求输入包含列:ts_code, trade_time, price, volume, amount
    """
    # 1. 删除完全重复的行
    df = df.drop_duplicates()
    
    # 2. 时间戳排序并重置索引
    df = df.sort_values(['ts_code', 'trade_time']).reset_index(drop=True)
    
    # 3. 价格合理性:剔除价格 <= 0 或超过涨跌停范围(此处以±10%为例)
    df = df[(df['price'] > 0) & (df['price'].notna())]
    
    # 4. 成交量校验
    df = df[(df['volume'] > 0) & (df['volume'].notna())]
    
    # 5. 基于关键字段精确去重
    df = df.drop_duplicates(subset=['ts_code', 'trade_time', 'price', 'volume'])
    
    # 6. 重置索引
    df = df.reset_index(drop=True)
    
    return df

# 使用示例
raw_df = pd.read_parquet('tick_data/000001.SZ/20250115.parquet')
cleaned_df = clean_tick_data(raw_df)
print(f'清洗前: {len(raw_df)} 行, 清洗后: {len(cleaned_df)} 行')
Tick数据获取、存储与清洗全流程 Wind 聚宽 Tushare 米筐 原始Tick数据(CSV / API响应) 数据清洗与去重 时间戳规范化 → 价格校验 → 成交量校验 → 精确去重 Parquet(推荐) HDF5(备选) CSV(调试用) 清洗后的Tick数据按股票代码+日期分区存储,便于后续特征工程并行读取

2.4 存储架构设计要点

在实际工程中,Tick数据的存储架构需要兼顾写入吞吐、查询效率与数据治理。以下是几条经过验证的设计原则:

  • 分区策略:按 股票代码 / 交易日期 两级分区,避免单文件过大。对于全市场回测,可进一步按行业或市值分组。
  • 列裁剪:Parquet格式下,特征工程阶段只读取需要的列(如price、volume、bid1、ask1),跳过元数据列,减少IO压力。
  • 增量更新:每日收盘后增量写入当日Tick数据,使用fastparquet或pyarrow的append模式,避免全量重写。
  • 数据校验:写入前对每个分区执行行数校验与时间戳连续性检查,确保数据完整性。
性能参考: 在单台NVMe SSD服务器上,使用Parquet格式读取全市场5000只股票一日Tick数据(约2亿行)的price与volume两列,耗时约3-5秒;同等条件下CSV格式需要2-3分钟。这一差距在特征工程高频迭代场景下至关重要。