4、缺失值处理:空价格/空量的识别、前向填充、插值策略、删除阈值设定
做高频交易的人都知道,订单簿数据里最让人头疼的,就是缺失值。
我刚开始接触这个领域时,天真地以为交易所给的数据都是完美的。结果第一次回测,策略跑出来的收益曲线漂亮得不像话——后来才发现,是因为某天的价格数据空了,程序自动用0填充,算出来的收益率直接爆炸。嗯,从那以后,我对缺失值就格外敏感。
说白了,订单簿的缺失值主要分两类:空价格和空量。价格空了,你的策略可能以0元成交;量空了,你的订单可能永远排不上队。这两种情况,处理方式完全不同。
4.1 空价格/空量的识别
先说说怎么识别。我个人习惯用pandas的isnull()方法,但要注意,订单簿数据里有些“空”不是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
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
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 知识体系总览
下面这张图,是我自己总结的缺失值处理流程。你可以把它当成一个检查清单,每次处理数据时对照着来。
这张图的核心逻辑很简单:先识别,再判断,最后处理。低缺失率用填充,高缺失率直接删。别想着用一种方法解决所有问题——我在项目中试过,结果往往是两头不讨好。
公众号:蓝海数据掘金营,微信deep3321