第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数据:精确到毫秒
  • 你的订单执行数据:精确到微秒
  • 第三方行情数据:可能只精确到秒

这些数据要放在一起分析,时间戳必须对齐到同一个基准上。

我常用的对齐策略有三种:

  1. 向下对齐:全部截断到毫秒级,简单粗暴,但会丢失精度
  2. 向上对齐:全部补齐到微秒级,需要插值
  3. 事件对齐:以某个关键事件(比如你的订单成交)为基准,找前后最近的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 数据清洗的完整流程

好了,咱们把上面这些串起来,形成一个完整的清洗流水线。我一般按这个顺序来:

  1. 加载原始数据,检查字段完整性
  2. 时间戳排序,确保时间递增
  3. 去除重复记录,保留第一条或最后一条
  4. 异常值过滤,价格、量、盘口分别处理
  5. 缺失值处理,前向填充或插值
  6. 时间戳对齐,统一到目标精度
  7. 输出清洗后的数据,保存为Parquet或HDF5格式

下面这张图展示了整个流程:

Tick数据清洗与预处理流程 原始Tick数据 排序 & 去重 异常值过滤 缺失值处理 时间戳对齐 输出清洗数据 异常数据日志 数据质量报告

16.5 实战中的避坑指南

最后,分享几个我在实战中踩过的坑:

  • 不要用全局阈值:不同股票、不同时段的价格波动差异很大。我建议按股票代码分组处理。
  • 注意集合竞价数据:9:15-9:25的集合竞价数据,价格和量都是虚拟的,不能直接用于冲击成本计算。
  • 保留原始数据:清洗后的数据要另存一份,千万别覆盖原始文件。你永远不知道什么时候需要回溯。
  • 记录清洗日志:删了多少条、改了多少条,都要记录下来。方便后续排查问题。

我的习惯:每次清洗完数据,我都会生成一份数据质量报告,包含异常值比例、缺失率、时间戳连续性等指标。这样心里有底,模型跑出来也敢信。

好了,数据清洗这块就聊到这儿。记住一句话:数据清洗花的时间,会在模型效果上十倍地还给你