高频数据清洗:别让脏数据毁了你的因子
做量化研究这几年,我踩过最大的坑,不是策略失效,而是数据本身。
你想想看,辛辛苦苦写了个因子,回测曲线漂亮得不行,结果一上实盘就崩。为什么?大概率是数据清洗没做到位。高频数据尤其如此——逐笔成交、订单簿,这些东西天生就是脏的。
今天咱们就聊聊,怎么把这些原始数据收拾干净。
逐笔成交数据清洗:从源头把关
逐笔成交数据,说白了就是交易所发出来的每一笔交易记录。但这里有个问题——交易所也会出错。
我遇到过的情况:某只股票突然出现一笔成交价是前一笔的10倍。你以为是重大利好?不,就是数据错误。
- 价格合理性检查:成交价是否在当日涨跌停范围内?是否偏离前笔成交价超过阈值(比如5%)?
- 数量合理性检查:成交量是否超过该股票的可流通股数?单笔成交量是否异常大(比如超过总股本1%)?
- 时间顺序检查:时间戳是否严格递增?有没有出现时间倒流的情况?
- 买卖方向检查:成交记录中的买卖方向是否与订单簿匹配?
嗯,这里要注意一点:有些交易所的逐笔数据里,买卖方向是"B"和"S",但有些是"1"和"2"。别搞混了。
# 一个简单的价格异常检测示例
def check_price_anomaly(trade_df, threshold=0.05):
"""
检查逐笔成交价格是否异常
"""
# 计算价格变化率
trade_df['price_change'] = trade_df['price'].pct_change()
# 标记异常:变化率超过阈值
anomaly = trade_df['price_change'].abs() > threshold
# 我个人习惯,把异常数据单独存一份,而不是直接删除
# 万一后面需要复盘呢?
trade_df['is_anomaly'] = anomaly
return trade_df
订单簿数据对齐:让买卖双方对上话
订单簿数据,就是某一时刻所有未成交的买单和卖单。但这里有个麻烦事——不同交易所的订单簿快照时间不一样。
比如,上交所可能是每100毫秒拍一张快照,深交所可能是每50毫秒。你拿两个市场的订单簿直接对比?那肯定对不上。
我建议的做法是:
- 统一时间基准:把所有订单簿快照对齐到同一个时间网格上(比如每100毫秒一个点)
- 线性插值:对于缺失的时间点,用前后两个快照做线性插值
- 买卖盘匹配:确保同一时刻的买一价和卖一价是合理的(买一价 < 卖一价)
时间戳标准化与聚合:把时间变成你的朋友
不同数据源的时间戳格式五花八门。有的用Unix时间戳(秒级),有的用毫秒级,还有的用字符串格式"2024-01-15 09:30:00.123"。
我的经验是:统一转成毫秒级整数时间戳。为什么?因为计算快,而且不会有时区问题。
# 时间戳标准化示例
def standardize_timestamp(df, time_col='time'):
"""
将各种格式的时间戳统一为毫秒级整数
"""
# 如果是字符串格式
if df[time_col].dtype == 'object':
df[time_col] = pd.to_datetime(df[time_col])
# 转成毫秒级时间戳
df['timestamp_ms'] = df[time_col].astype('int64') // 10**6
return df
聚合这块,说白了就是把高频数据压成低频数据。比如把逐笔成交数据聚合成1分钟K线。
但这里有个细节:聚合时要不要考虑交易量加权? 我个人习惯,计算均价时用VWAP(成交量加权平均价),而不是简单平均。因为大单的影响更大,简单平均会失真。
异常值检测与处理:该删就删,该补就补
异常值检测,我把它分成两类:
| 异常类型 | 检测方法 | 处理方式 |
|---|---|---|
| 价格异常 | 3-sigma法则、IQR方法 | 删除或替换为前值 |
| 成交量异常 | 相对中位数偏差 | 标记为缺失,后续插值 |
| 买卖价差异常 | 超过历史99%分位数 | 用中位数替代 |
| 时间戳异常 | 间隔超过阈值 | 拆分或合并 |
你可能会问:为什么不用机器学习来检测异常?
嗯,说实话,我试过。但高频数据的异常模式太复杂,而且样本量太大,跑一次模型要半天。相比之下,基于规则的检测方法又快又稳。
知识体系总览
下面这张图,是我做高频数据清洗时脑子里始终绷着的一根弦:
这张图把整个清洗流程串起来了。你从原始数据出发,经过四个维度的处理,最终得到干净可用的数据。每一步都有对应的检测方法和处理手段。
说实话,数据清洗这件事,做起来枯燥,但回报巨大。我见过太多人花90%的时间写策略,只花10%的时间处理数据。结果呢?策略跑出来的全是噪音。
记住一句话:垃圾进,垃圾出。数据干净了,因子才有意义。
公众号:蓝海资料掘金营,微信deep3321