4、高频数据特征与预处理:Tick数据与分笔数据、数据清洗、时间对齐与聚合

做高频交易的人都知道一句话:「垃圾进,垃圾出」。这句话在高频数据预处理阶段,简直是铁律。

我刚开始接触高频数据时,以为拿到交易所的原始Tick数据就能直接建模。结果呢?跑出来的策略回测曲线漂亮得像假的一样——后来才发现,是数据里混了「脏东西」。嗯,今天我们就来聊聊,怎么把这些「脏东西」洗干净。

4.1 Tick数据与分笔数据:到底有什么区别?

先搞清楚两个概念。很多新手容易混淆。

  • Tick数据:交易所每发生一笔交易,就生成一条记录。它是最细粒度的数据。包含:时间戳、成交价、成交量、买卖方向(有些市场有)。
  • 分笔数据:其实是Tick数据的「快照」。比如每500毫秒或每笔交易后,记录当前的最优买卖报价(Bid/Ask)和累计成交量。

说白了,Tick是「每一枪」的记录,分笔是「每隔几枪拍一张照片」。我个人习惯,做流动性建模时更偏爱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股,就生成一条数据。这样,每条数据都包含了「有意义的交易活动」。

具体做法:

  1. 设定一个目标成交量(比如1000股)。
  2. 从第一笔开始累加成交量,直到达到目标。
  3. 记录这一段的VWAP和总成交量。
  4. 重置计数器,继续下一段。
为什么成交量间隔更好?因为流动性建模的核心是「交易成本与成交量的关系」。等间隔聚合会丢失这个关系——比如在冷清时段,1秒内可能只有1笔交易,但这一笔交易的成本可能很高。成交量间隔能更准确地捕捉到这种非线性关系。

4.4 知识体系总览

下面这张图,是我自己整理的高频数据预处理流程。每次做新项目,我都会先对照这张图走一遍,确保没有遗漏。

高频数据预处理流程 原始Tick/分笔数据 数据清洗 异常值过滤 | 缺失值处理 | 时间戳校正 时间对齐与聚合 等间隔聚合 | 成交量间隔聚合 标准化高频数据 常见坑点 • 不要用全局均值判断异常 • 停牌期间不要插值 • 等间隔聚合会丢失流动性信息 • 成交量间隔需要动态调整 • 时间戳精度要统一 • 注意交易所的撮合规则 图:高频数据预处理流程与常见坑点

4.5 一些实战建议

最后,分享几个我在项目中积累的经验:

  • 先看数据,再写代码。拿到原始数据后,先画几张图:价格序列、成交量分布、时间间隔分布。你会发现很多问题。
  • 保留原始数据。我每次做预处理,都会保留一份原始数据的副本。因为清洗逻辑可能会变,有了原始数据,随时可以重来。
  • 注意时间戳的时区。不同交易所用的时区不同,有的用UTC,有的用本地时间。我曾经因为时区问题,把夜盘数据对齐到了白天,结果模型在夜盘时段完全失效。
  • 成交量间隔的阈值要动态调整。早盘和尾盘流动性好,可以设大一点的阈值;午盘流动性差,阈值要设小一点。我一般用过去5分钟的日均成交量来动态调整。
避坑指南:我曾经在做一个股指期货的流动性模型时,用了固定的成交量阈值(100手)。结果在开盘前5分钟,市场成交量暴增,100手阈值导致数据量爆炸,内存直接爆了。后来我改成动态阈值——用过去1分钟的平均成交量乘以一个系数。问题就解决了。

好了,高频数据预处理这部分就聊到这里。数据洗干净了,后面的建模才能站得住脚。下一节我们会讨论如何从这些干净数据中提取流动性特征——那才是真正有意思的部分。


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