4、高频数据特征与预处理:Tick数据与分笔数据、数据清洗、时间对齐与聚合
做高频交易的人都知道一句话:「垃圾进,垃圾出」。这句话在高频数据预处理阶段,简直是铁律。
我刚开始接触高频数据时,以为拿到交易所的原始Tick数据就能直接建模。结果呢?跑出来的策略回测曲线漂亮得像假的一样——后来才发现,是数据里混了「脏东西」。嗯,今天我们就来聊聊,怎么把这些「脏东西」洗干净。
4.1 Tick数据与分笔数据:到底有什么区别?
先搞清楚两个概念。很多新手容易混淆。
- Tick数据:交易所每发生一笔交易,就生成一条记录。它是最细粒度的数据。包含:时间戳、成交价、成交量、买卖方向(有些市场有)。
- 分笔数据:其实是Tick数据的「快照」。比如每500毫秒或每笔交易后,记录当前的最优买卖报价(Bid/Ask)和累计成交量。
说白了,Tick是「每一枪」的记录,分笔是「每隔几枪拍一张照片」。我个人习惯,做流动性建模时更偏爱Tick数据,因为它保留了完整的交易节奏。但分笔数据也有优势——数据量小,处理快。
4.2 数据清洗:那些年我们踩过的坑
高频数据有多脏?我举个例子。有一次我处理某交易所的Tick数据,发现某条记录的时间戳竟然比前一条早了3个小时。你想想看,这要是直接拿去算收益率,模型不崩才怪。
4.2.1 异常值处理
高频数据里的异常值,常见的有这么几类:
- 价格异常:比如突然出现一个0.01元的成交价,明显是数据录入错误。
- 成交量异常:某笔成交量突然是前一笔的100倍,可能是大单拆分后的合并记录。
- 时间戳异常:时间倒流、时间跳跃、或者非交易时段的数据。
我的处理原则很简单:先过滤,再插值。对于明显错误的数据,直接删除;对于可疑数据,用前后窗口的中位数替换。
# 一个简单的异常值过滤逻辑(伪代码)
def filter_tick_anomalies(df, price_col='price', vol_col='volume'):
# 价格异常:超过前后5笔均值的3倍标准差
df['price_ma'] = df[price_col].rolling(5, center=True).mean()
df['price_std'] = df[price_col].rolling(5, center=True).std()
df['price_anomaly'] = (abs(df[price_col] - df['price_ma']) > 3 * df['price_std'])
# 成交量异常:单笔成交量超过日均成交量的1%
daily_avg_vol = df[vol_col].resample('1D').mean()
df['vol_anomaly'] = df[vol_col] > daily_avg_vol * 0.01
# 合并过滤
df_clean = df[~(df['price_anomaly'] | df['vol_anomaly'])]
return df_clean
4.2.2 缺失值处理
高频数据里的缺失值,通常不是「空值」,而是「该有数据的时候没有数据」。比如某只股票突然停牌了10分钟,这10分钟内的Tick数据就是缺失的。
我曾经犯过一个错误:用线性插值去填充停牌期间的缺失值。结果呢?模型以为价格在平稳变化,实际上市场已经发生了重大事件。后来我学乖了:停牌期间的缺失值,不要填充,直接标记为「无交易区间」。
对于正常的、短暂的缺失(比如几秒钟内没有成交),我建议用前向填充(Forward Fill)或者用Bid/Ask中价来替代。
4.3 时间对齐与聚合:把不规则数据变规则
Tick数据是不等间隔的。有的秒成交100笔,有的秒一笔都没有。但大多数模型要求输入数据是等间隔的。所以,我们需要做时间对齐和聚合。
4.3.1 等间隔聚合
最常用的方法:把时间轴切成固定长度的小格子(比如1秒、100毫秒),每个格子内取一笔代表性数据。
具体怎么取?我一般用这几种方式:
- 最后价格法:取格子内的最后一笔成交价。简单,但容易受尾端噪音影响。
- 成交量加权平均价(VWAP):用格子内的成交量加权。我个人更推荐这个,因为它反映了格子内的整体交易成本。
- 中位数法:取格子内所有成交价的中位数。抗噪能力强,但计算量大。
# 等间隔聚合示例:1秒间隔,VWAP
def aggregate_to_second(df, freq='1S'):
# 计算每笔的成交额
df['turnover'] = df['price'] * df['volume']
# 按秒聚合
agg_dict = {
'turnover': 'sum',
'volume': 'sum',
'price': lambda x: (x * df.loc[x.index, 'volume']).sum() / df.loc[x.index, 'volume'].sum()
}
df_agg = df.resample(freq, on='timestamp').agg(agg_dict)
df_agg['vwap'] = df_agg['turnover'] / df_agg['volume']
return df_agg
4.3.2 成交量间隔聚合
等间隔聚合有个问题:市场活跃时,一个格子内可能有几十笔交易;市场冷清时,一个格子内可能一笔都没有。这会导致数据稀疏。
所以,我更喜欢用成交量间隔。比如每成交1000股,就生成一条数据。这样,每条数据都包含了「有意义的交易活动」。
具体做法:
- 设定一个目标成交量(比如1000股)。
- 从第一笔开始累加成交量,直到达到目标。
- 记录这一段的VWAP和总成交量。
- 重置计数器,继续下一段。
4.4 知识体系总览
下面这张图,是我自己整理的高频数据预处理流程。每次做新项目,我都会先对照这张图走一遍,确保没有遗漏。
4.5 一些实战建议
最后,分享几个我在项目中积累的经验:
- 先看数据,再写代码。拿到原始数据后,先画几张图:价格序列、成交量分布、时间间隔分布。你会发现很多问题。
- 保留原始数据。我每次做预处理,都会保留一份原始数据的副本。因为清洗逻辑可能会变,有了原始数据,随时可以重来。
- 注意时间戳的时区。不同交易所用的时区不同,有的用UTC,有的用本地时间。我曾经因为时区问题,把夜盘数据对齐到了白天,结果模型在夜盘时段完全失效。
- 成交量间隔的阈值要动态调整。早盘和尾盘流动性好,可以设大一点的阈值;午盘流动性差,阈值要设小一点。我一般用过去5分钟的日均成交量来动态调整。
好了,高频数据预处理这部分就聊到这里。数据洗干净了,后面的建模才能站得住脚。下一节我们会讨论如何从这些干净数据中提取流动性特征——那才是真正有意思的部分。
公众号:蓝海资料掘金营,微信deep3321