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数据中普遍存在重复记录、异常价格、时间戳错乱、缺失字段等问题。若不经清洗直接用于特征计算,会引入严重的偏差。标准清洗流程包含以下四个步骤:
- 时间戳规范化:统一为毫秒级Unix时间戳或标准UTC时间,处理交易所非交易时段(如9:25-9:30集合竞价)的异常时间戳。
- 价格合理性校验:剔除价格为负、超过当日涨跌停板、或与上一笔Tick价格偏离超过阈值(如期货可设1%)的记录。
- 成交量与金额校验:剔除成交量为0或负值、成交金额与价格×数量不匹配的记录。
- 精确去重:基于
股票代码 + 时间戳 + 价格 + 成交量四元组进行去重,保留首次出现的记录。
常见陷阱: 部分数据源在行情中断后恢复时,会重发之前已推送的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)} 行')
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分钟。这一差距在特征工程高频迭代场景下至关重要。