4. 数据清洗:处理缺失值、异常值与复权数据
数据清洗这步,说实话,是跨期套利系统里最磨人、也最容易翻车的地方。我见过太多策略,逻辑写得天花乱坠,结果一跑回测,亏得底裤都不剩——查到最后,往往是数据没洗干净。
你想想看,期货数据里什么妖魔鬼怪都有:节假日跳空、涨跌停板、换月价差、还有交易所偶尔抽风发出来的错误数据。如果不把这些处理干净,你的套利信号全是假的。
核心原则:宁可少做一笔交易,也不要做错一笔交易。数据清洗的底线是——不要让脏数据进入策略逻辑。
4.1 缺失值处理:别让空值毁了你的价差序列
期货数据缺失的原因很多。比如某个合约当天没有成交,或者交易所数据源断了。我个人习惯,先分清楚缺失的类型:
- 完全随机缺失:比如网络抖动丢了一笔tick,这种直接删掉或前向填充就行
- 非随机缺失:比如涨跌停板导致没有成交,这种要特别小心
我在项目中遇到过最坑的一次:某个品种连续三天涨停,近月合约完全没成交,远月合约还在正常交易。价差瞬间从50点拉到200点。如果直接用前向填充,策略会以为价差扩大了,开仓进去——结果第二天涨停打开,直接爆亏。
所以我的处理原则是:
- 日内缺失(几分钟内):用前向填充,或者线性插值
- 日间缺失(超过1天):标记为无效数据,不参与策略计算
- 涨跌停导致的缺失:单独标记,价差计算时跳过这些时间点
# 我个人常用的缺失值处理函数
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 复权数据:还原真实的价差走势
复权这步,很多人会忽略。但说实话,不复权的数据做跨期套利,结果基本是错的。
为什么?因为期货合约有到期日,需要换月。换月的时候,新旧合约之间天然存在价差。如果不做复权处理,你的价差序列里会多出一堆「假跳变」。
我记得刚开始做套利的时候,看到价差图上一堆尖刺,还以为发现了什么大机会。后来才发现,那些尖刺全是换月造成的。
复权的方法主要有两种:
- 前复权:调整历史数据,让价格序列连续。适合做技术分析。
- 后复权:调整当前数据,保持历史数据不变。适合做回测。
对于跨期套利,我建议用「价差复权法」。具体做法是:
- 找到换月的时间点
- 计算换月前后的价差跳变值
- 将这个跳变值从后续的价差中扣除
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 数据清洗的完整流程
嗯,到这里,我们把缺失值、异常值、复权数据都讲了一遍。最后总结一下我实际项目中用的清洗流程:
- 原始数据入库:从交易所或数据商拿到原始数据,先存一份备份
- 缺失值处理:按4.1的方法,填充或标记缺失值
- 异常值检测:用价差统计法,标记异常点
- 人工复核:把标记出来的异常点过一遍,确认是数据错误还是市场行为
- 换月复权:找到换月点,计算跳变值,调整价差序列
- 最终验证:检查清洗后的数据,确保价差序列平稳、无跳变
这个流程看起来简单,但每一步都可能踩坑。我建议你刚开始做的时候,每一步都加个日志输出,把处理前后的数据对比打印出来。这样万一出问题,也能快速定位。
最后说一句:数据清洗没有银弹。不同的品种、不同的时间周期、不同的策略逻辑,对数据的要求都不一样。我的经验是——先跑通,再优化。别想着一步到位,先把流程跑起来,后面慢慢迭代。