第四章:数据预处理——从原始数据到可用特征

大家好,我是你们的量化实战讲师。今天咱们聊聊数据预处理。说实话,这章内容看起来没那么「性感」,但我要告诉你一个残酷的事实:我见过太多漂亮的策略模型,最后都死在了数据预处理上。嗯,这不是危言耸听。

高频数据有个特点——又脏又乱。你从交易所拿到的原始数据,说白了就是一堆带时间戳的数字。里面可能有跳空、有缺失、有异常报价。如果你直接拿这些数据去训练模型,结果会很惨。我刚开始做高频策略时,就吃过这个亏。

核心观点:数据预处理占整个量化项目工作量的60%-80%。模型再牛,也救不了垃圾数据。

高频数据预处理知识体系 原始高频数据 异常值检测与处理 3σ法则 · IQR · 截尾 缺失值填充方法 前向填充 · 插值 · 模型预测 时间对齐与重采样 Tick→1秒·1分·Tick级对齐 特征工程基础 价格特征·量特征·衍生特征 清洗 → 填充 → 对齐 → 特征化

4.1 异常值检测与处理

高频数据里的异常值,说白了就是那些「明显不对劲」的报价。比如某只股票突然出现一笔成交价是前一笔的10倍,或者卖一价突然变成0.01元。这些数据如果不处理,模型会学出一些奇怪的东西。

我常用的检测方法有三种:

  1. 3σ法则:假设数据服从正态分布,超出均值±3倍标准差的值视为异常。这个方法简单粗暴,但高频数据往往不服从正态分布,所以效果一般。
  2. IQR方法:用四分位距来检测。Q1-1.5*IQR 到 Q3+1.5*IQR 之外的值视为异常。这个方法对偏态分布更鲁棒。
  3. 百分比截尾:直接去掉上下0.5%或1%的极值。这个方法最暴力,但有时候最有效。

我的经验:对于高频Tick数据,我建议先用IQR方法做第一轮检测,再用业务规则做第二轮。比如价格不能为负、买卖价差不能超过某个阈值。我曾经遇到过一个案例,某只股票因为数据源问题,连续出现了几十笔价格为0的成交记录,IQR根本检测不出来,但业务规则一下就抓住了。

import pandas as pd
import numpy as np

def detect_outliers_iqr(df, column, multiplier=1.5):
    """使用IQR方法检测异常值"""
    Q1 = df[column].quantile(0.25)
    Q3 = df[column].quantile(0.75)
    IQR = Q3 - Q1
    
    lower_bound = Q1 - multiplier * IQR
    upper_bound = Q3 + multiplier * IQR
    
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

# 使用示例
outliers, lb, ub = detect_outliers_iqr(tick_data, 'price')
print(f"检测到 {len(outliers)} 个异常值")
print(f"正常范围: [{lb:.2f}, {ub:.2f}]")

处理异常值,我一般用两种方式:

  • 直接删除:如果异常值占比很小(比如<0.1%),直接删掉最省事。
  • 截尾处理:把异常值替换为边界值。比如把超过上界的值设为上界。这样做的好处是保留了样本数量。

注意:千万不要在回测时「偷看未来」!异常值检测应该基于滚动窗口,而不是全量数据。否则你的回测结果会过于乐观。

4.2 缺失值填充方法

高频数据缺失的原因很多:交易所数据中断、网络延迟、撮合引擎故障等等。你想想看,如果某只股票在10:00:00到10:00:01之间没有成交记录,那这一秒的数据就是缺失的。

我常用的填充方法:

方法 适用场景 优点 缺点
前向填充(ffill) 价格、成交量等 简单、符合市场逻辑 会引入滞后
后向填充(bfill) 少量缺失 简单 会引入未来信息
线性插值 连续缺失较少 平滑、合理 不适合跳跃行情
模型预测填充 缺失较多 精度高 计算量大

我个人习惯用前向填充。为什么?因为高频数据的缺失往往意味着「没有新的成交」,用上一笔的价格填充是最符合市场实际情况的。但要注意,如果连续缺失超过5笔,我会标记为「无效区间」,而不是强行填充。

def fill_missing_ticks(tick_df, max_gap=5):
    """填充缺失的Tick数据"""
    # 先重采样到固定频率
    tick_df = tick_df.set_index('timestamp')
    tick_df = tick_df.resample('1s').asfreq()
    
    # 前向填充,但限制最大连续缺失数
    tick_df['price'] = tick_df['price'].ffill(limit=max_gap)
    tick_df['volume'] = tick_df['volume'].fillna(0)
    
    # 标记无效区间
    tick_df['is_valid'] = ~tick_df['price'].isna()
    
    return tick_df.reset_index()

避坑指南:我曾经犯过一个错误——用后向填充来处理缺失值。结果在回测时模型表现很好,但实盘一跑就崩。后来才发现,后向填充引入了未来信息,相当于让模型「偷看」了未来的价格。所以,回测时只能用前向填充或插值,千万别用后向填充。

4.3 时间对齐与重采样

不同数据源的时间戳可能不一致。比如交易所A的Tick数据是精确到毫秒的,而交易所B的数据是精确到秒的。如果你想把它们放在一起分析,就必须做时间对齐。

时间对齐的两种思路:

  1. Tick级对齐:把所有数据对齐到同一个时间轴上。比如以交易所A的时间为基准,把交易所B的数据插值到A的时间点上。
  2. 重采样对齐:把高频数据重采样到更低频率。比如把Tick数据重采样到1秒、1分钟或5分钟K线。

我个人更推荐重采样对齐。为什么?因为Tick级对齐太容易出错了。你想想看,两个交易所的Tick时间戳可能差几毫秒,你强行对齐反而会引入噪声。

def resample_to_1min(tick_df):
    """将Tick数据重采样为1分钟K线"""
    tick_df = tick_df.set_index('timestamp')
    
    # 重采样到1分钟
    ohlc = tick_df['price'].resample('1min').ohlc()
    volume = tick_df['volume'].resample('1min').sum()
    
    # 合并
    minute_df = pd.concat([ohlc, volume], axis=1)
    minute_df.columns = ['open', 'high', 'low', 'close', 'volume']
    
    return minute_df.reset_index()

关键点:重采样时要注意「边界对齐」。比如从9:30:00到9:30:59的数据应该归到9:30:00这一根K线,还是9:31:00?不同的对齐方式会影响后续的特征计算。我建议统一使用「左闭右开」的区间,即[9:30:00, 9:31:00)的数据归到9:30:00这根K线。

4.4 特征工程基础

特征工程,说白了就是把原始数据变成模型能「理解」的东西。原始的价格和成交量数据太粗糙了,我们需要从中提取出更有预测能力的特征。

我常用的几类特征:

  • 价格衍生特征:收益率、对数收益率、价格动量、移动平均线等。
  • 量价关系特征:成交量加权平均价(VWAP)、资金流向、买卖压力等。
  • 波动率特征:真实波动幅度(ATR)、已实现波动率、价格极差等。
  • 微观结构特征:买卖价差、订单簿不平衡、交易强度等。
def create_features(tick_df):
    """构建高频特征"""
    df = tick_df.copy()
    
    # 收益率
    df['return'] = df['price'].pct_change()
    
    # 对数收益率
    df['log_return'] = np.log(df['price'] / df['price'].shift(1))
    
    # 价格动量(过去5笔)
    df['momentum_5'] = df['price'] / df['price'].shift(5) - 1
    
    # 波动率(过去10笔的收益率标准差)
    df['volatility_10'] = df['return'].rolling(10).std()
    
    # 成交量变化
    df['volume_change'] = df['volume'].pct_change()
    
    # 买卖压力(如果有买卖方向数据)
    if 'buy_volume' in df.columns and 'sell_volume' in df.columns:
        df['buy_pressure'] = df['buy_volume'] / (df['buy_volume'] + df['sell_volume'])
    
    return df.dropna()

我的经验:特征不是越多越好。我见过有人一口气造了200多个特征,结果模型过拟合得一塌糊涂。我建议从10-20个核心特征开始,逐步增加。另外,高频数据的特征要注意「时效性」——过去1秒的特征比过去1分钟的特征更有预测力。

嗯,到这里数据预处理的核心内容就讲完了。总结一下:先做异常值检测,把明显不对的数据处理掉;然后用前向填充处理缺失值;接着做时间对齐或重采样,让数据在统一的时间轴上;最后构建有预测力的特征。每一步都很关键,缺一不可。

记住一句话:好的数据预处理,是成功策略的一半。


专注资料整理