4. 数据清洗:处理缺失值、异常值与复权数据

数据清洗这步,说实话,是跨期套利系统里最磨人、也最容易翻车的地方。我见过太多策略,逻辑写得天花乱坠,结果一跑回测,亏得底裤都不剩——查到最后,往往是数据没洗干净。

你想想看,期货数据里什么妖魔鬼怪都有:节假日跳空、涨跌停板、换月价差、还有交易所偶尔抽风发出来的错误数据。如果不把这些处理干净,你的套利信号全是假的。

核心原则:宁可少做一笔交易,也不要做错一笔交易。数据清洗的底线是——不要让脏数据进入策略逻辑。

4.1 缺失值处理:别让空值毁了你的价差序列

期货数据缺失的原因很多。比如某个合约当天没有成交,或者交易所数据源断了。我个人习惯,先分清楚缺失的类型:

  • 完全随机缺失:比如网络抖动丢了一笔tick,这种直接删掉或前向填充就行
  • 非随机缺失:比如涨跌停板导致没有成交,这种要特别小心

我在项目中遇到过最坑的一次:某个品种连续三天涨停,近月合约完全没成交,远月合约还在正常交易。价差瞬间从50点拉到200点。如果直接用前向填充,策略会以为价差扩大了,开仓进去——结果第二天涨停打开,直接爆亏。

所以我的处理原则是:

  1. 日内缺失(几分钟内):用前向填充,或者线性插值
  2. 日间缺失(超过1天):标记为无效数据,不参与策略计算
  3. 涨跌停导致的缺失:单独标记,价差计算时跳过这些时间点
# 我个人常用的缺失值处理函数
def handle_missing(df, method='ffill', max_gap=5):
    """
    df: DataFrame,包含价格列
    method: 'ffill' 前向填充, 'linear' 线性插值, 'drop' 直接删除
    max_gap: 最大连续缺失数,超过则标记为NaN
    """
    if method == 'ffill':
        df = df.fillna(method='ffill', limit=max_gap)
    elif method == 'linear':
        df = df.interpolate(method='linear', limit=max_gap)
    elif method == 'drop':
        df = df.dropna()
    
    # 标记连续缺失过多的行
    df['missing_flag'] = df.isnull().any(axis=1)
    return df

小技巧:对于跨期套利,近月和远月合约的缺失要分开处理。如果近月缺失但远月正常,价差计算要特别谨慎。我一般会要求两个合约的数据必须同时存在,才计算价差。

4.2 异常值检测:把那些离谱的数据揪出来

异常值这东西,说白了就是数据里的「噪音」。期货市场里常见的异常值包括:

  • 价格瞬间跳变(比如从3000跳到30000,明显是输入错误)
  • 价差突然扩大或缩小(超过历史统计范围)
  • 成交量异常(比如平时几千手,突然变成0手)

我曾经因为没处理好异常值,回测结果漂亮得不行——年化收益50%,夏普比率3.0。结果一上实盘,亏得亲妈都不认识。后来查原因,发现回测数据里有个异常价差点,策略在那个点「捡了个大便宜」,但实盘根本不可能成交。

常用的异常值检测方法:

方法 适用场景 优缺点
标准差法(3σ) 价格序列服从正态分布 简单,但对极端值敏感
四分位距法(IQR) 非正态分布,有偏态 稳健,不受极端值影响
移动窗口法 价格有趋势和季节性 能适应市场变化,但参数难调
价差统计法 跨期套利专用 直接针对价差,效果最好

我个人最推荐的是「价差统计法」。为什么呢?因为跨期套利的核心就是价差。你直接看价差序列,哪些点明显偏离历史范围,一眼就能看出来。

def detect_outliers_by_spread(spread_series, window=20, n_std=3):
    """
    基于价差的异常值检测
    spread_series: 价差序列
    window: 滚动窗口大小
    n_std: 标准差倍数
    """
    rolling_mean = spread_series.rolling(window=window).mean()
    rolling_std = spread_series.rolling(window=window).std()
    
    upper_bound = rolling_mean + n_std * rolling_std
    lower_bound = rolling_mean - n_std * rolling_std
    
    outliers = (spread_series > upper_bound) | (spread_series < lower_bound)
    return outliers

注意:异常值不一定要直接删除。有时候异常值本身就是交易信号——比如价差突然拉大,可能是套利机会。我的做法是:先标记异常值,然后人工复核。如果是数据错误就删除,如果是市场行为就保留。

4.3 复权数据:还原真实的价差走势

复权这步,很多人会忽略。但说实话,不复权的数据做跨期套利,结果基本是错的。

为什么?因为期货合约有到期日,需要换月。换月的时候,新旧合约之间天然存在价差。如果不做复权处理,你的价差序列里会多出一堆「假跳变」。

我记得刚开始做套利的时候,看到价差图上一堆尖刺,还以为发现了什么大机会。后来才发现,那些尖刺全是换月造成的。

复权的方法主要有两种:

  • 前复权:调整历史数据,让价格序列连续。适合做技术分析。
  • 后复权:调整当前数据,保持历史数据不变。适合做回测。

对于跨期套利,我建议用「价差复权法」。具体做法是:

  1. 找到换月的时间点
  2. 计算换月前后的价差跳变值
  3. 将这个跳变值从后续的价差中扣除
def adjust_spread_for_roll(spread_series, roll_dates, roll_gaps):
    """
    对价差序列进行换月复权
    spread_series: 原始价差序列
    roll_dates: 换月日期列表
    roll_gaps: 每个换月点的价差跳变值
    """
    adjusted = spread_series.copy()
    cumulative_gap = 0
    
    for date, gap in zip(roll_dates, roll_gaps):
        cumulative_gap += gap
        adjusted.loc[date:] = adjusted.loc[date:] - cumulative_gap
    
    return adjusted

避坑指南:我曾经在复权时犯过一个低级错误——把换月跳变值算反了。结果价差序列完全反着走,策略信号全部反向。后来我加了个验证步骤:复权后的价差序列,均值应该接近0,标准差应该稳定。如果发现均值明显偏离0,那肯定是复权方向搞反了。

4.4 数据清洗的完整流程

嗯,到这里,我们把缺失值、异常值、复权数据都讲了一遍。最后总结一下我实际项目中用的清洗流程:

  1. 原始数据入库:从交易所或数据商拿到原始数据,先存一份备份
  2. 缺失值处理:按4.1的方法,填充或标记缺失值
  3. 异常值检测:用价差统计法,标记异常点
  4. 人工复核:把标记出来的异常点过一遍,确认是数据错误还是市场行为
  5. 换月复权:找到换月点,计算跳变值,调整价差序列
  6. 最终验证:检查清洗后的数据,确保价差序列平稳、无跳变

这个流程看起来简单,但每一步都可能踩坑。我建议你刚开始做的时候,每一步都加个日志输出,把处理前后的数据对比打印出来。这样万一出问题,也能快速定位。

最后说一句:数据清洗没有银弹。不同的品种、不同的时间周期、不同的策略逻辑,对数据的要求都不一样。我的经验是——先跑通,再优化。别想着一步到位,先把流程跑起来,后面慢慢迭代。

数据清洗完整流程 原始数据入库 缺失值处理 异常值检测 人工复核 换月复权 最终验证 备份原始数据 填充或标记 价差统计法 确认数据性质 消除换月跳变 检查平稳性 异常值过多时 返回重新清洗

专注资料整理