3、数据清洗实战:处理缺失Tick、异常价格过滤(Winsorize)、时间戳对齐与重采样

做高频交易的人都知道一句话:垃圾进,垃圾出。你模型再漂亮,策略再花哨,数据一塌糊涂,实盘就是送钱。

我刚开始做高频的时候,就吃过这个亏。当时回测曲线漂亮得不行,结果一上实盘,直接亏到怀疑人生。后来排查了三天,发现是数据里混了几个异常Tick,把整个统计量都带偏了。嗯,从那以后,我再也不敢轻视数据清洗这一步。

这一章,我们就来聊聊高频数据清洗的三个核心环节:缺失Tick处理异常价格过滤时间戳对齐与重采样。说白了,就是让你手里的原始数据变得干净、可用、靠谱。

3.1 缺失Tick处理:别让空值毁了你的策略

高频数据里,缺失Tick是家常便饭。交易所网络抖动、行情网关丢包、数据供应商抽风——原因太多了。你想想看,如果某只股票在10:00:00.000这个毫秒没有Tick,你的策略就少了一个决策点。

我个人习惯把缺失Tick分成两类:

  • 完全缺失:某个时间点没有任何数据
  • 部分缺失:有数据但某些字段为空(比如只有卖价没有买价)

处理方式也不一样。对于完全缺失,我一般用前向填充(Forward Fill)。为什么?因为高频数据里,价格变化是离散的,没有新Tick意味着价格没变。你想想看,如果上一笔成交价是10.01,下一笔是10.02,中间没有Tick,那中间时刻的价格就是10.01。

代码实现很简单:

import pandas as pd

# 假设df是原始Tick数据,index是时间戳
# 先重采样到1ms精度
df_resampled = df.resample('1ms').asfreq()
# 前向填充
df_filled = df_resampled.ffill()

但这里有个坑——如果连续缺失太多,前向填充就不靠谱了。比如某只股票停牌了10分钟,你用10分钟前的价格填充,那完全是误导。我一般设置一个阈值,比如最多填充100个Tick,超过就标记为缺失。

注意:前向填充只适用于价格类字段。对于成交量、成交笔数这类累积量,应该用0填充,而不是前向填充。否则你的成交量会虚高。

3.2 异常价格过滤:Winsorize的正确打开方式

高频数据里,异常价格太常见了。我记得有一次,某只股票的正常价格在50块左右,突然冒出一个500块的Tick。你猜怎么着?是交易所的测试数据没过滤干净。

处理异常价格,我首推Winsorize(缩尾处理)。它不是直接删除异常值,而是把极端值拉到某个分位数上。这样做的好处是:保留了数据量,又控制了极端值的影响。

具体做法:

  1. 计算价格序列的上下分位数(比如1%和99%)
  2. 把低于1%分位数的值拉到1%分位数
  3. 把高于99%分位数的值拉到99%分位数

代码示例:

from scipy.stats.mstats import winsorize
import numpy as np

# 假设prices是价格序列
# 上下各缩尾1%
prices_clean = winsorize(prices, limits=[0.01, 0.01])

# 如果你用的是pandas
def winsorize_series(s, limits=[0.01, 0.01]):
    lower = s.quantile(limits[0])
    upper = s.quantile(1 - limits[1])
    return s.clip(lower, upper)

但这里有个细节:分位数怎么选? 我个人习惯先看数据分布。如果是流动性好的股票,0.5%就够。如果是垃圾股,可能得放宽到2%。你可以在回测里跑一下不同参数,看看对策略的影响。

小技巧:不要只对价格做Winsorize。买卖价差、成交量、订单簿深度这些字段,同样需要处理。我曾经遇到过一个策略,因为某次大单的成交量异常,导致整个仓位管理模块崩了。

3.3 时间戳对齐:不同数据源的统一战场

做高频交易,你很少只用单一数据源。交易所的Level 2数据、第三方行情、自己抓的Tick——这些数据的时间戳格式可能完全不同。有的用纳秒,有的用毫秒,有的甚至用字符串。

时间戳对齐,说白了就是把所有数据的时间基准统一。我建议统一用纳秒级Unix时间戳。为什么?因为精度够高,而且方便做数学运算。

常见的坑:

  • 时区问题:国内交易所用北京时间,国外用UTC。别搞混了。
  • 时钟漂移:不同服务器的系统时间可能有偏差。我见过偏差超过100毫秒的,这在高频里是灾难。
  • 精度丢失:Python的float存纳秒时间戳会丢精度。用int64。

代码示例:

import pandas as pd

# 假设原始时间戳是字符串格式 '2024-01-15 09:30:00.123456'
df['timestamp'] = pd.to_datetime(df['timestamp'])
# 转为纳秒级Unix时间戳
df['timestamp_ns'] = df['timestamp'].astype('int64')

# 对齐到最近的1ms
df['timestamp_aligned'] = (df['timestamp_ns'] // 1_000_000) * 1_000_000
核心原则:时间戳对齐不是简单的四舍五入。高频交易里,1毫秒的偏差可能意味着完全不同的市场状态。我建议用「向下取整」而不是「四舍五入」,因为向下取整更符合「事件发生时间」的逻辑。

3.4 重采样:从Tick到K线的艺术

原始Tick数据太密了,直接建模会爆炸。所以我们需要重采样——把不规则的时间序列变成等间隔的。

常见的重采样方式:

采样频率 适用场景 注意事项
1ms 高频策略、订单簿分析 数据量仍然很大,注意内存
100ms 中频策略、统计套利 能保留大部分微观结构
1s 日内策略、波动率建模 会丢失部分微观信息
1min 低频策略、组合管理 基本看不到微观结构了

重采样时,不同字段的处理方式不同:

  • 价格:用最后一条Tick的价格(Last Price)
  • 成交量:用区间内成交量之和
  • 买卖价差:用区间内的平均值或中位数
  • 最高/最低价:用区间内的极值

代码示例:

# 假设df是已经对齐好时间戳的Tick数据
# 重采样到100ms
df_100ms = df.resample('100ms').agg({
    'price': 'last',
    'volume': 'sum',
    'bid': 'last',
    'ask': 'last',
    'spread': 'mean'
})

这里有个我踩过的坑:重采样后的数据量。如果你从1ms重采样到100ms,数据量减少到1/100。但如果你从Tick级别(可能几微秒一条)重采样到1ms,数据量反而会增加。因为Tick是不规则的,有些毫秒可能没有数据。

避坑指南:我曾经在重采样后直接跑策略,结果发现回测收益翻了一倍。后来一查,是因为重采样时把「最后一条价格」当成了「收盘价」,但实际交易中,你不可能在毫秒级别精确成交在那个价格上。所以,重采样后的价格要加上滑点模型,才更接近真实情况。

3.5 本章知识体系

下面这张图,把数据清洗的整个流程串起来了。你可以把它当作一个检查清单,每次处理数据时对照着来。

高频数据清洗流程 原始Tick数据 缺失Tick处理(前向填充) 异常价格过滤(Winsorize) 时间戳对齐(纳秒级) 重采样(等间隔K线) 关键参数 • 前向填充阈值:100 Tick • Winsorize分位:1% • 时间戳精度:纳秒 • 重采样频率:100ms 常见陷阱 ✗ 成交量用前向填充 ✗ 时间戳四舍五入 ✗ 忽略时区差异 ✗ 重采样后不加滑点 输出格式 • 等间隔时间序列 • 无缺失值 • 异常值已控制

数据清洗这件事,说起来简单,做起来全是细节。我见过太多人把时间花在模型调参上,结果数据一塌糊涂。记住:干净的数据,比复杂的模型更重要

下一章,我们会聊聊高频数据的统计特征——那些你必须要知道的分布规律。嗯,到时候见。


公众号:蓝海资料掘金营,微信deep3321