第16章:数据清洗与预处理:Tick数据清洗、异常值处理、对齐时间戳
做量化交易的朋友都知道一句话:垃圾进,垃圾出。Tick数据尤其如此。
我刚开始做高频策略那会儿,拿到交易所的原始Tick数据,心想这玩意儿还能有问题?结果一跑回测,收益曲线跟心电图似的,忽上忽下。后来一查,原来是数据里混了几个异常值,把整个策略逻辑都带偏了。嗯,从那以后,我再也不敢轻视数据清洗这一步了。
今天咱们就聊聊Tick数据的清洗与预处理。说白了,就是三件事:去脏、纠偏、对齐。
16.1 Tick数据长什么样?
先看看原始Tick数据的典型结构。以国内股票市场为例,一条Tick记录通常包含:
| 字段 | 说明 | 示例 |
|---|---|---|
| 时间戳 | 精确到毫秒或微秒 | 2024-01-15 09:30:01.250 |
| 最新价 | 当前成交价格 | 15.68 |
| 成交量 | 当前笔成交量 | 200 |
| 累计成交量 | 当日累计 | 1256800 |
| 买卖盘口 | 五档或十档 | 买一价/量... |
看着挺规整对吧?但实际数据里,什么妖魔鬼怪都有。
16.2 异常值处理:那些不该出现的数据
我遇到过最离谱的一次,某只股票在收盘前最后一秒,最新价突然跳到了9999.99。你想想看,这明显是交易所的撮合系统抽风了。如果不处理,你的冲击成本模型会算出个天文数字。
常见的异常值类型有:
- 价格异常:超出合理范围(比如涨停价±10%之外)
- 量异常:单笔成交量超过当日总成交量的1%
- 时间异常:时间戳乱序或重复
- 盘口异常:买卖价差为负或为零
核心原则:宁可删除可疑数据,也不要让脏数据污染模型。
我个人习惯用3σ法则做第一道过滤。什么意思呢?就是计算价格序列的均值和标准差,把超出均值±3倍标准差的数据标记为异常。
import pandas as pd
import numpy as np
def filter_price_outliers(df, col='price', z_thresh=3):
"""
基于Z-score过滤价格异常值
"""
mean = df[col].mean()
std = df[col].std()
z_scores = np.abs((df[col] - mean) / std)
# 标记异常
df['is_outlier'] = z_scores > z_thresh
# 我习惯保留异常标记,而不是直接删除
# 这样后续可以分析异常原因
return df[~df['is_outlier']].copy()
小技巧:对于高频数据,我建议用滚动窗口计算均值和标准差,而不是全局的。因为价格波动在不同时段差异很大,开盘和收盘的波动率明显高于午盘。
16.3 时间戳对齐:让数据站在同一条起跑线上
做冲击成本建模,最头疼的就是时间戳对齐。为什么?因为不同数据源的时间精度不一样。
举个例子:
- 交易所的Tick数据:精确到毫秒
- 你的订单执行数据:精确到微秒
- 第三方行情数据:可能只精确到秒
这些数据要放在一起分析,时间戳必须对齐到同一个基准上。
我常用的对齐策略有三种:
- 向下对齐:全部截断到毫秒级,简单粗暴,但会丢失精度
- 向上对齐:全部补齐到微秒级,需要插值
- 事件对齐:以某个关键事件(比如你的订单成交)为基准,找前后最近的Tick
我个人更推荐第三种。为什么呢?因为冲击成本建模的核心是「你的订单对市场造成了多大影响」,所以应该以你的订单时间为中心,去匹配市场数据。
def align_to_events(tick_df, order_df, time_col='timestamp', window='1ms'):
"""
将Tick数据对齐到订单事件
"""
# 对每个订单,找到前后最近的Tick
aligned = []
for _, order in order_df.iterrows():
t = order[time_col]
# 找订单发生前最近的Tick
before = tick_df[tick_df[time_col] <= t].iloc[-1] if len(tick_df[tick_df[time_col] <= t]) > 0 else None
# 找订单发生后最近的Tick
after = tick_df[tick_df[time_col] >= t].iloc[0] if len(tick_df[tick_df[time_col] >= t]) > 0 else None
aligned.append({
'order_time': t,
'tick_before': before,
'tick_after': after
})
return pd.DataFrame(aligned)
注意:时间戳对齐时,一定要考虑时区和夏令时。我曾经因为没注意夏令时切换,导致回测数据差了整整一个小时,那叫一个惨。
16.4 数据清洗的完整流程
好了,咱们把上面这些串起来,形成一个完整的清洗流水线。我一般按这个顺序来:
- 加载原始数据,检查字段完整性
- 时间戳排序,确保时间递增
- 去除重复记录,保留第一条或最后一条
- 异常值过滤,价格、量、盘口分别处理
- 缺失值处理,前向填充或插值
- 时间戳对齐,统一到目标精度
- 输出清洗后的数据,保存为Parquet或HDF5格式
下面这张图展示了整个流程:
16.5 实战中的避坑指南
最后,分享几个我在实战中踩过的坑:
- 不要用全局阈值:不同股票、不同时段的价格波动差异很大。我建议按股票代码分组处理。
- 注意集合竞价数据:9:15-9:25的集合竞价数据,价格和量都是虚拟的,不能直接用于冲击成本计算。
- 保留原始数据:清洗后的数据要另存一份,千万别覆盖原始文件。你永远不知道什么时候需要回溯。
- 记录清洗日志:删了多少条、改了多少条,都要记录下来。方便后续排查问题。
我的习惯:每次清洗完数据,我都会生成一份数据质量报告,包含异常值比例、缺失率、时间戳连续性等指标。这样心里有底,模型跑出来也敢信。
好了,数据清洗这块就聊到这儿。记住一句话:数据清洗花的时间,会在模型效果上十倍地还给你。