4. 数据清洗:处理缺失值、异常值、合约换月价差跳空
数据清洗这步,说实话,是整套策略里最枯燥但最要命的环节。我见过太多人花大把时间搞模型,结果数据一塌糊涂,回测曲线漂亮得像假的一样——嗯,其实就是假的。今天咱们把三个硬骨头啃掉:缺失值、异常值、还有那个让人头疼的换月跳空。
4.1 缺失值处理:别让空值毁了你的价差
做跨品种套利,数据源往往是多个合约拼接的。你想想看,一个合约到期了,下一个合约还没开始活跃,中间那段就是空白的。我个人习惯,先搞清楚缺失的原因,再决定怎么补。
核心原则: 价差序列的缺失值不能简单用前值填充,因为价差本身是差值,填充不当会引入虚假信号。
常见的缺失值处理方式有三种:
- 删除法: 缺失比例小于5%时,直接删掉。简单粗暴,但有效。
- 插值法: 线性插值适合短时间缺失,比如几分钟的数据。我一般用
pandas.interpolate()。 - 前向填充: 只适合非活跃合约的收盘价,价差序列慎用。
import pandas as pd
import numpy as np
# 假设我们有两个合约的价格序列
# 模拟缺失值
dates = pd.date_range('2024-01-01', periods=100, freq='D')
price_a = np.random.randn(100).cumsum() + 100
price_b = np.random.randn(100).cumsum() + 98
# 人为制造缺失
price_a[20:25] = np.nan
price_b[50:53] = np.nan
# 构建价差
spread = price_a - price_b
# 我的处理方式:先检查缺失比例
missing_ratio = spread.isna().sum() / len(spread)
print(f"缺失比例: {missing_ratio:.2%}")
# 缺失少,直接线性插值
spread_clean = spread.interpolate(method='linear')
# 缺失多?我会检查是不是合约换月导致的
# 如果是,用下面讲的rollover adjustment
我的小技巧: 处理缺失前,先画个图看看缺失位置。如果缺失集中在某个时间段,大概率是数据源问题,不是随机缺失。
4.2 异常值检测:那些离谱的价差点
异常值在价差序列里特别常见。为什么?因为两个合约的交易时间、流动性都不一样。比如螺纹钢和热卷,一个在白天活跃,一个在夜盘活跃,价差突然跳一下很正常。但有些跳是数据错误,必须处理。
我常用的方法有三种:
- 标准差法: 超过3倍标准差的,标记为异常。简单,但价差不一定是正态分布。
- 四分位距法(IQR): 更稳健,不受极端值影响。我个人更推荐这个。
- 滚动窗口法: 用过去N天的均值和标准差,动态判断。适合有趋势的价差。
def detect_outliers_iqr(series, multiplier=1.5):
"""
用IQR检测异常值
我在项目中习惯用1.5倍,但价差序列可以放宽到2倍
"""
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - multiplier * IQR
upper_bound = Q3 + multiplier * IQR
outliers = (series < lower_bound) | (series > upper_bound)
return outliers
# 应用
outliers = detect_outliers_iqr(spread_clean, multiplier=2.0)
print(f"检测到 {outliers.sum()} 个异常值")
# 处理方式:我一般用中位数替换,而不是删除
# 因为删除会破坏时间序列的连续性
spread_final = spread_clean.copy()
spread_final[outliers] = spread_clean.median()
避坑指南: 我曾经犯过一个错误——把价差的异常值直接删掉,结果回测时发现策略在异常点附近频繁开仓。后来才明白,那些异常点往往是市场情绪极端的时刻,恰恰是套利机会出现的时候。所以,先理解异常的原因,再决定是否处理。
4.3 合约换月价差跳空:最容易被忽视的坑
做跨品种套利,最头疼的就是换月。你想想看,主力合约每个月换一次,每次换月价差都会跳一下。如果不处理,你的策略会以为市场出现了大机会,实际上只是合约切换的假象。
换月跳空的原因很简单:新旧合约的价格本身就不连续。比如螺纹钢主力从2405切换到2410,两个合约的绝对价格差可能就有几十点。价差序列里就会多出一个台阶。
核心思路: 把换月当成分红处理。就像股票除权除息一样,我们把价差序列做后向调整,让历史数据与当前合约保持一致。
具体做法分三步:
- 第一步: 确定换月日期。我一般用持仓量和成交量来判断,当新合约的持仓量超过旧合约时,就是换月点。
- 第二步: 计算跳空幅度。在换月当天,新旧合约的价差差值就是跳空量。
- 第三步: 调整历史数据。把换月之前的所有价差数据,都加上这个跳空量。
def rollover_adjustment(spread_series, roll_dates, roll_gaps):
"""
价差序列的换月调整
spread_series: 原始价差序列
roll_dates: 换月日期列表
roll_gaps: 每个换月点的跳空幅度列表
"""
adjusted = spread_series.copy()
cumulative_gap = 0
for i, date in enumerate(roll_dates):
# 找到换月日期在序列中的位置
idx = adjusted.index.get_loc(date)
# 累计跳空:每次换月都要累加之前的调整量
cumulative_gap += roll_gaps[i]
# 调整换月之前的所有数据
adjusted.iloc[:idx] += cumulative_gap
return adjusted
# 示例:假设2024-03-15是换月日,跳空幅度为15点
roll_dates = ['2024-03-15']
roll_gaps = [15.0]
spread_adjusted = rollover_adjustment(spread_final, roll_dates, roll_gaps)
我的经验: 换月调整后,一定要检查调整后的序列是否平滑。如果调整后还有明显的台阶,说明跳空幅度算错了。我一般会在调整前后画个对比图,一目了然。
4.4 知识体系总览
下面这张图,把数据清洗的整个流程串起来了。你可以把它当成一个检查清单,每次做数据清洗时对照着来。
4.5 实战中的避坑指南
最后,分享几个我踩过的坑:
- 别一股脑全处理: 我曾经把整个价差序列的异常值全删了,结果发现策略在震荡市表现很好,一到趋势行情就崩。后来才明白,那些"异常值"其实是趋势启动的信号。
- 换月调整要双向: 如果你做的是多品种套利,两个合约可能在不同时间换月。这时候要分别调整,不能只调一个。
- 保留原始数据: 我习惯在清洗前备份一份原始数据。万一调整错了,还能回滚。这个习惯救过我很多次。
重要提醒: 数据清洗没有标准答案。同样的数据,不同策略的处理方式可能完全不同。关键是理解你的策略逻辑,让清洗方式服务于策略,而不是反过来。
好了,数据清洗这部分就讲到这里。记住一句话:垃圾进,垃圾出。数据清洗花的时间,会在后续建模和回测中十倍百倍地回报你。