高频数据清洗与预处理:Tick数据去重、异常值处理、时间戳对齐、交易量聚合
做高频交易的人都知道一句话:垃圾进,垃圾出。这话听着糙,但理不糙。我刚开始接触Tick数据时,以为直接从交易所拿到的数据就是干净的,结果回测时发现策略收益高得离谱——后来一查,原来是数据里混了重复的成交记录,把交易量算了两遍。
嗯,从那以后,我再也不敢跳过数据清洗这一步了。今天咱们就来聊聊,高频数据清洗到底该怎么做。
为什么Tick数据这么脏?
说白了,Tick数据就是交易所每笔成交的原始记录。它不像分钟K线那样已经被人处理过,而是带着各种“毛刺”的原始信号。你想想看,交易所的撮合引擎每秒要处理几万笔订单,网络延迟、系统缓存、数据重传……任何一个环节出点小问题,你的数据就会出问题。
我个人习惯把Tick数据的“脏”归纳为四类:
- 重复数据:同一笔成交被记录了两次
- 异常值:价格或交易量明显偏离正常范围
- 时间戳错位:成交时间与系统时间不一致
- 数据稀疏:某些时间段没有成交记录
这四类问题,每一个都能让你的策略分析结果跑偏。咱们一个一个来解决。
第一步:Tick数据去重
重复数据是最常见的问题。我在项目中遇到过,某次从数据商拿到的BTC永续合约数据,居然有3%的重复记录。这些重复数据会让你的交易量统计翻倍,流动性分析完全失真。
去重的核心逻辑其实很简单:找到能唯一标识一笔成交的字段组合。通常我们用“交易对+时间戳+价格+交易量”这四个字段来判断。
去重策略:
- 精确去重:四个字段完全一致,直接删除重复行
- 模糊去重:时间戳相差1毫秒以内,价格和交易量相同,视为重复
- 顺序去重:保留第一次出现的记录,删除后续重复
来看一段Python代码,这是我常用的去重方法:
import pandas as pd
def dedup_tick_data(df):
"""
Tick数据去重函数
df: DataFrame,包含列 ['symbol', 'timestamp', 'price', 'volume']
"""
# 先按时间排序
df = df.sort_values('timestamp')
# 精确去重
df_clean = df.drop_duplicates(
subset=['symbol', 'timestamp', 'price', 'volume'],
keep='first'
)
# 模糊去重:时间戳相差1ms内的重复
df_clean['time_group'] = (df_clean['timestamp'] * 1000).astype(int) // 2
df_clean = df_clean.drop_duplicates(
subset=['symbol', 'time_group', 'price', 'volume'],
keep='first'
)
return df_clean.drop('time_group', axis=1)
小技巧:去重前一定要先排序。我曾经因为没排序,结果把正常数据当重复删了,回测时发现策略信号全乱了。
第二步:异常值处理
异常值分两种:价格异常和交易量异常。价格异常可能是交易所的“胖手指”错误,交易量异常可能是数据采集时的截断错误。
我常用的方法是“三西格玛法则”加上“百分位截断”。说白了,就是先算均值和标准差,把超出3个标准差的数据标记为异常。但高频数据有个特点——价格波动本身就很大,直接用标准差可能会误杀正常数据。
所以我的做法是:
- 先计算滚动窗口(比如100笔)的均值和标准差
- 标记超出均值±5倍标准差的数据为异常
- 用前一笔正常数据填充异常值
def remove_outliers(df, window=100, z_thresh=5):
"""
异常值处理
"""
df['price_ma'] = df['price'].rolling(window).mean()
df['price_std'] = df['price'].rolling(window).std()
# 标记异常
df['is_outlier'] = (
(df['price'] - df['price_ma']).abs() > z_thresh * df['price_std']
)
# 用前一个正常值填充
df.loc[df['is_outlier'], 'price'] = None
df['price'].fillna(method='ffill', inplace=True)
return df
注意:千万不要直接删除异常值!高频数据的时间序列是连续的,删除会导致时间戳不连续,后续的对齐和聚合都会出问题。用前值填充是最稳妥的做法。
第三步:时间戳对齐
时间戳对齐是个大坑。不同交易所的时间精度不一样,有的精确到毫秒,有的精确到微秒,还有的干脆只到秒。你想想看,如果两个交易所的数据时间戳精度不同,你怎么做套利分析?
我的做法是:统一到毫秒级,然后做最近邻对齐。
具体来说:
- 把所有时间戳统一为毫秒级整数
- 设定一个对齐窗口(比如5毫秒)
- 在窗口内找到最接近的时间戳进行对齐
def align_timestamps(df, base_timestamps, window_ms=5):
"""
时间戳对齐
df: 待对齐的数据
base_timestamps: 基准时间戳序列
window_ms: 对齐窗口(毫秒)
"""
aligned = []
for ts in base_timestamps:
# 找到窗口内的数据
mask = (df['timestamp'] >= ts - window_ms) & \
(df['timestamp'] <= ts + window_ms)
window_data = df[mask]
if len(window_data) > 0:
# 取最接近的一条
idx = (window_data['timestamp'] - ts).abs().idxmin()
aligned.append(window_data.loc[idx])
return pd.DataFrame(aligned)
避坑指南:我曾经在回测时发现策略收益异常稳定,后来发现是对齐窗口设得太大了(50毫秒),导致把不同时间点的数据强行对齐了。窗口大小要根据交易对的流动性来定,流动性越好的品种,窗口可以越小。
第四步:交易量聚合
交易量聚合,说白了就是把高频的逐笔成交数据,聚合成固定时间间隔的“快照”。为什么要做聚合?因为逐笔数据太密了,直接分析会带来巨大的计算量,而且很多策略并不需要那么高的精度。
我常用的聚合方式有两种:
| 聚合方式 | 说明 | 适用场景 |
|---|---|---|
| 时间聚合 | 固定时间间隔(如1秒、100毫秒) | 流动性好的品种 |
| 交易量聚合 | 固定交易量(如每100BTC聚合一次) | 流动性差的品种 |
| 事件聚合 | 按价格变动事件聚合 | 波动率分析 |
def aggregate_volume(df, interval='1s'):
"""
交易量聚合
"""
# 设置时间索引
df = df.set_index('timestamp')
# 按时间间隔聚合
agg_df = df.resample(interval).agg({
'price': 'ohlc', # 开盘、最高、最低、收盘
'volume': 'sum', # 交易量求和
'side': lambda x: x.mode()[0] if len(x) > 0 else None # 买卖方向
})
return agg_df
知识体系总览
说了这么多,咱们来画个图,把整个清洗流程串起来。这张图我画了很多次,每次带新人时都会拿出来讲一遍:
写在最后
数据清洗这件事,看着简单,做起来全是坑。我见过太多人把80%的时间花在策略开发上,结果数据没洗干净,回测结果全是假的。说白了,数据清洗不是“要不要做”的问题,而是“怎么做才到位”的问题。
我个人习惯把清洗流程做成自动化管道,每次拿到新数据就跑一遍。虽然前期搭建管道花点时间,但后面省下的时间绝对值得。你想想看,如果每次都要手动检查数据,那还怎么做高频交易?
好了,这一章的内容就到这里。记住一句话:数据干净了,策略才有意义。