4、缺失值处理:空价格/空量的识别、前向填充、插值策略、删除阈值设定

做高频交易的人都知道,订单簿数据里最让人头疼的,就是缺失值。

我刚开始接触这个领域时,天真地以为交易所给的数据都是完美的。结果第一次回测,策略跑出来的收益曲线漂亮得不像话——后来才发现,是因为某天的价格数据空了,程序自动用0填充,算出来的收益率直接爆炸。嗯,从那以后,我对缺失值就格外敏感。

说白了,订单簿的缺失值主要分两类:空价格空量。价格空了,你的策略可能以0元成交;量空了,你的订单可能永远排不上队。这两种情况,处理方式完全不同。

4.1 空价格/空量的识别

先说说怎么识别。我个人习惯用pandasisnull()方法,但要注意,订单簿数据里有些“空”不是NaN,而是0或者-1。比如某些交易所,卖一价如果为空,会用0表示。你想想看,价格是0,这合理吗?

import pandas as pd
import numpy as np

# 假设df是订单簿DataFrame,包含bid_price_1, bid_vol_1, ask_price_1, ask_vol_1等列
def identify_missing(df):
    # 标准缺失值
    nan_mask = df[['bid_price_1', 'ask_price_1', 'bid_vol_1', 'ask_vol_1']].isnull()
    
    # 非标准缺失值:价格为0或负数
    price_cols = ['bid_price_1', 'ask_price_1']
    zero_price_mask = df[price_cols] <= 0
    
    # 量为0或负数
    vol_cols = ['bid_vol_1', 'ask_vol_1']
    zero_vol_mask = df[vol_cols] <= 0
    
    return nan_mask, zero_price_mask, zero_vol_mask
⚠️ 注意:我曾经遇到过一个坑——某交易所的卖一价在收盘后会用-1填充。如果不处理,你的插值策略会把-1当成有效价格,结果可想而知。

4.2 前向填充

前向填充,说白了就是用上一个有效值填充当前缺失值。这在高频交易里很常用,因为订单簿的变化是连续的,短时间内价格不会突变。

我建议用ffill()方法,但要注意填充方向。比如,你处理的是时间序列,应该按时间顺序前向填充;但如果你在处理多档位数据,可能需要按档位顺序填充。

# 前向填充:用上一个有效值填充
def forward_fill(df, max_fill_limit=5):
    """
    max_fill_limit: 最多连续填充多少个缺失值
    我个人习惯限制在5个以内,超过就放弃
    """
    df_filled = df.copy()
    price_cols = ['bid_price_1', 'ask_price_1']
    vol_cols = ['bid_vol_1', 'ask_vol_1']
    
    # 先处理价格
    df_filled[price_cols] = df_filled[price_cols].ffill(limit=max_fill_limit)
    # 再处理量
    df_filled[vol_cols] = df_filled[vol_cols].ffill(limit=max_fill_limit)
    
    return df_filled
💡 小技巧:前向填充后,记得检查一下填充后的数据是否合理。比如,价格突然跳变超过10%,那可能是填充错了,需要回退。

4.3 插值策略

前向填充虽然简单,但遇到连续缺失时就不太灵了。这时候需要插值。

常用的插值方法有:

  • 线性插值:假设价格在缺失区间内线性变化
  • 时间加权插值:根据时间间隔加权,适合不均匀采样的数据
  • 样条插值:更平滑,但计算量大,高频交易里用得少

我个人比较推荐时间加权插值。为什么?因为订单簿的价格变化不是均匀的——开盘和收盘时变化快,中间相对平稳。线性插值会忽略这个特点。

from scipy import interpolate

def time_weighted_interpolation(df, time_col='timestamp'):
    """
    时间加权插值
    我在项目中遇到过,用线性插值回测效果很好,实盘却亏钱
    后来发现是插值方式不对,改成时间加权就好了
    """
    df_interp = df.copy()
    
    # 只对缺失值进行插值
    for col in ['bid_price_1', 'ask_price_1', 'bid_vol_1', 'ask_vol_1']:
        mask = df_interp[col].isnull()
        if mask.sum() == 0:
            continue
        
        # 获取有效数据的时间戳和值
        valid_idx = ~mask
        valid_time = df_interp.loc[valid_idx, time_col].values
        valid_val = df_interp.loc[valid_idx, col].values
        
        # 对缺失位置进行插值
        missing_time = df_interp.loc[mask, time_col].values
        f = interpolate.interp1d(valid_time, valid_val, kind='linear', 
                                 fill_value='extrapolate')
        df_interp.loc[mask, col] = f(missing_time)
    
    return df_interp

核心要点:插值不是万能的。如果连续缺失超过10个tick,我建议直接放弃这段数据,而不是强行插值。你想想看,10个tick在毫秒级的高频交易里,市场可能已经发生了翻天覆地的变化。

4.4 删除阈值设定

有些数据,与其强行填充,不如直接删除。但删除多少合适?这需要设定阈值。

我一般用两个维度来衡量:

维度 阈值建议 说明
单行缺失率 > 30% 一行数据中超过30%的字段缺失,直接删
连续缺失长度 > 10个tick 连续10个tick以上缺失,整段删除
整体缺失率 > 5% 整个数据集缺失超过5%,需要重新考虑数据源
def delete_by_threshold(df, row_threshold=0.3, seq_threshold=10):
    """
    删除阈值设定
    row_threshold: 单行缺失率阈值
    seq_threshold: 连续缺失长度阈值
    """
    # 计算每行缺失率
    missing_rate = df.isnull().mean(axis=1)
    
    # 删除缺失率过高的行
    df_clean = df[missing_rate <= row_threshold].copy()
    
    # 检查连续缺失
    # 我曾经遇到过一个极端情况:某只股票停牌了半小时
    # 连续缺失了3000多个tick,如果不删除,整个回测都是错的
    for col in ['bid_price_1', 'ask_price_1']:
        # 标记连续缺失
        is_missing = df_clean[col].isnull().astype(int)
        seq_len = is_missing.groupby((is_missing != is_missing.shift()).cumsum()).cumsum()
        
        # 删除连续缺失超过阈值的行
        df_clean = df_clean[seq_len < seq_threshold]
    
    return df_clean
⚠️ 注意:删除数据前,一定要备份原始数据。我见过有人删完数据才发现,原来那些“缺失”其实是交易所的合法状态(比如某些档位确实没有挂单)。删了,就再也找不回来了。

4.5 知识体系总览

下面这张图,是我自己总结的缺失值处理流程。你可以把它当成一个检查清单,每次处理数据时对照着来。

缺失值处理流程 原始订单簿数据 步骤1:识别空价格/空量 缺失率? 前向填充 / 插值 (连续缺失≤10个tick) 删除整段数据 (连续缺失>10个tick) 清洗后的订单簿数据 阈值可调:单行缺失率>30% 或 连续缺失>10个tick 时删除

这张图的核心逻辑很简单:先识别,再判断,最后处理。低缺失率用填充,高缺失率直接删。别想着用一种方法解决所有问题——我在项目中试过,结果往往是两头不讨好。

💡 最后说一句:缺失值处理没有银弹。你需要在速度和精度之间做权衡。高频交易里,速度优先,所以我个人更倾向于前向填充+删除的组合。但如果做的是统计套利,插值可能更合适。具体问题具体分析,这才是工程师该有的态度。

公众号:蓝海数据掘金营,微信deep3321