3. Tick数据清洗:缺失值处理、异常值检测与数据对齐
Tick数据是高频交易与微观结构分析的基石,但其原始质量往往受限于交易所传输延迟、撮合引擎异常、网络抖动及存储截断等因素。未经清洗的Tick数据会直接污染特征工程与标签校准结果,导致回测过拟合或实盘信号失真。本章围绕三个核心清洗任务展开:缺失值处理、异常值检测与数据对齐,并提供可落地的检测规则与处理流程。
核心原则:清洗策略必须在“保留有效信息”与“剔除噪声”之间取得平衡。对于Tick数据,宁可保留少量可疑记录并标记,也不应盲目删除导致微观结构信息丢失。
3.1 缺失值处理
Tick数据中的缺失值通常表现为:某时间戳下缺少价格、成交量或买卖盘口深度字段。常见成因包括交易所快照推送周期与逐笔成交流不同步、数据订阅中断、以及存储时字段截断。
3.1.1 缺失类型与检测规则
| 缺失类型 | 典型表现 | 检测方法 |
|---|---|---|
| 整行缺失 | 时间戳序列出现跳秒或跳毫秒 | 计算相邻时间戳差值,超过预期间隔(如500ms)则标记 |
| 字段缺失 | 价格或成交量为NaN/None | 逐字段检查空值,统计缺失率 |
| 盘口深度缺失 | 买卖一档价量为0或空 | 检查 bid/ask 价格与数量是否同时为0 |
3.1.2 处理策略
- 前向填充(Forward Fill):适用于价格、盘口深度等慢变字段。使用最近一个非缺失值填充,最大填充窗口建议不超过5个Tick。
- 线性插值(Linear Interpolation):适用于成交量、累计成交笔数等单调递增字段。在缺失区间两端已知值之间做时间加权插值。
- 删除策略:当连续缺失超过10个Tick,或缺失率超过该交易日总Tick数的1%时,建议删除该段数据并记录日志。
- 标记策略:对填充后的数据增加
is_filled标志列,供后续特征工程与标签校准阶段参考。
# Python示例:前向填充与插值
import pandas as pd
# 假设df包含 'timestamp', 'price', 'volume' 列
df['price'] = df['price'].fillna(method='ffill', limit=5)
df['volume'] = df['volume'].interpolate(method='linear', limit=10)
df['is_filled'] = df['price'].isna() | df['volume'].isna()
经验提示:对于盘口买卖价差(spread)字段,不建议直接填充。若买卖一档同时缺失,应将该Tick标记为“盘口不可用”,在后续特征计算中跳过。
3.2 异常值检测
Tick数据异常值主要分为三类:价格跳跃、零值异常、重复时间戳。每类异常对特征工程的影响不同,需采用差异化检测规则。
3.2.1 价格跳跃检测
价格跳跃指相邻Tick之间价格变动幅度远超正常市场波动范围。常见原因:大额订单冲击、数据源拼接错误、或撮合引擎异常。
- 绝对阈值法:设定最大价格变动绝对值(如股指期货Tick最大变动不超过10个最小变动价位)。
- 相对阈值法:计算相邻价格变动百分比,超过当日该品种平均波动率的5倍标准差则标记。
- Z-Score法:在滚动窗口(如50个Tick)内计算价格变动的Z-Score,|Z| > 4 视为跳跃。
# Python示例:滚动Z-Score跳跃检测
window = 50
df['price_diff'] = df['price'].diff()
df['rolling_mean'] = df['price_diff'].rolling(window).mean()
df['rolling_std'] = df['price_diff'].rolling(window).std()
df['z_score'] = (df['price_diff'] - df['rolling_mean']) / df['rolling_std']
df['is_jump'] = df['z_score'].abs() > 4
3.2.2 零值异常检测
零值在Tick数据中具有双重含义:合法零值(如成交量为0的行情快照)与非法零值(数据损坏或字段截断)。
| 字段 | 零值合法性判断 | 处理方式 |
|---|---|---|
| 价格 | 价格为零几乎总是异常(除某些品种开盘前状态) | 标记为缺失,执行前向填充 |
| 成交量 | 零成交量在快照数据中合法,在逐笔成交中异常 | 逐笔成交中零成交量直接删除该行 |
| 盘口数量 | 零数量表示该档位无挂单,合法 | 保留,但特征计算时需处理除零错误 |
3.2.3 重复时间戳检测
重复时间戳指两个或多个Tick记录具有完全相同的时间戳(毫秒或微秒级)。这通常由数据源去重失败或交易所同一毫秒内多笔成交但未附加序列号导致。
- 检测规则:按时间戳分组,统计每组记录数。记录数 > 1 即为重复。
- 处理策略:
- 若重复记录的价格与成交量完全相同,保留一条,删除其余。
- 若价格不同但时间戳相同,保留第一条,并将后续记录的时间戳微调+1微秒(需确保不与其他Tick冲突)。
- 若重复记录超过5条且价格差异超过阈值,整组标记为“异常簇”并删除。
# Python示例:重复时间戳处理
df_dup = df[df.duplicated(subset=['timestamp'], keep=False)]
# 按时间戳分组处理
for ts, group in df_dup.groupby('timestamp'):
if group['price'].nunique() == 1:
df.drop(group.index[1:], inplace=True)
else:
# 微调时间戳
for i, idx in enumerate(group.index[1:], start=1):
df.at[idx, 'timestamp'] += pd.Timedelta(microseconds=i)
注意:微调时间戳仅适用于回测与离线分析场景。实盘生产环境中,应要求数据源提供带序列号的唯一标识,避免依赖时间戳微调。
3.3 数据对齐
数据对齐解决的是多源Tick数据在时间轴上的同步问题。典型场景包括:逐笔成交与买卖盘口快照的对齐、同一品种不同交易所的Tick对齐、以及Tick数据与1秒/1分钟K线的对齐。
3.3.1 对齐方式对比
| 对齐方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 精确时间戳对齐 | 同一数据源内不同字段 | 无信息损失 | 要求时间戳精度一致 |
| 最近邻对齐 | 不同数据源(如交易所A与B) | 实现简单,容忍微小时间差 | 可能引入前瞻偏差 |
| 重采样对齐 | Tick转K线或固定频率数据 | 标准化输出,便于建模 | 丢失微观结构信息 |
3.3.2 对齐流程
- 统一时间基准:将所有数据源的时间戳转换为同一时区(通常为UTC)与同一精度(毫秒或微秒)。
- 排序与去重:按时间戳升序排列,执行3.2.3节的重复时间戳处理。
- 选择对齐主表:以数据量最大或时间最完整的数据集为主表,其他数据集作为从表。
- 执行对齐:对从表每条记录,在主表中查找时间戳最接近的记录(允许最大时间差,如1毫秒)。
- 后向检查:验证对齐后数据的时间顺序是否严格递增,防止因对齐导致的时间倒挂。
# Python示例:最近邻对齐(使用pandas.merge_asof)
# 主表:trade_df(逐笔成交),从表:quote_df(盘口快照)
aligned_df = pd.merge_asof(
trade_df.sort_values('timestamp'),
quote_df.sort_values('timestamp'),
on='timestamp',
direction='nearest',
tolerance=pd.Timedelta('1ms')
)
3.4 清洗质量评估指标
完成清洗后,建议使用以下指标量化清洗效果,并记录在数据清洗日志中:
| 指标 | 计算公式 | 目标值 |
|---|---|---|
| 缺失率 | 缺失Tick数 / 总Tick数 | < 0.1% |
| 跳跃率 | 标记为跳跃的Tick数 / 总Tick数 | < 0.05% |
| 重复率 | 重复时间戳组数 / 总时间戳数 | < 0.01% |
| 对齐误差 | 对齐后时间差超过1ms的记录占比 | < 0.5% |
最佳实践:将清洗规则与参数(如Z-Score阈值、填充窗口大小)保存为YAML配置文件,每次清洗时自动记录版本号与参数快照,确保清洗过程可复现、可审计。