4. 价差计算与特征工程:从原始数据到有效信号

好,我们进入第四章。这一章很关键,说白了就是解决一个问题:怎么把两个品种的价格数据,变成能喂给机器学习模型的特征

我个人习惯把这一步叫做“信号提纯”。原始价格数据里噪声太多,直接扔给模型,效果往往很差。我见过不少新手,上来就把两个价格序列拼在一起当特征,结果模型学到的全是市场噪音,回测漂亮实盘就崩。

4.1 价差计算:三种主流方式

价差是套利策略的灵魂。但怎么算价差,其实有讲究。我常用的有三种方式,每种适用场景不同。

4.1.1 简单价差

最直观的方式:价差 = 品种A价格 - 品种B价格

举个例子,螺纹钢和热卷,两者价格水平相近,用简单价差就挺好。

# 简单价差计算
import pandas as pd

def simple_spread(price_a, price_b):
    return price_a - price_b

# 示例
spread = simple_spread(rb_close, hc_close)

⚠️ 注意:当两个品种价格量级差异很大时(比如铜和铝),简单价差会失效。价格高的品种会主导价差走势。

4.1.2 对数价差

这是我个人最常用的方式。公式是:价差 = ln(品种A价格) - ln(品种B价格)

为什么用对数?两个原因:

  • 去量纲:把绝对价格差异变成相对比例关系
  • 稳定方差:对数变换能让价格序列的波动更平稳

我在做股指期货跨期套利时,就吃过简单价差的亏。当时IF和IC价差波动剧烈,用简单价差根本找不到规律。换成对数价差后,信号清晰多了。

# 对数价差计算
import numpy as np

def log_spread(price_a, price_b):
    return np.log(price_a) - np.log(price_b)

# 示例
log_sp = log_spread(if_close, ic_close)

4.1.3 标准化价差

也叫Z-score价差。公式:标准化价差 = (当前价差 - 均值) / 标准差

这个方式的好处是:把价差映射到标准正态分布上。你一眼就能看出当前价差偏离均值几个标准差,方便设置开仓阈值。

# 标准化价差计算
def zscore_spread(spread, window=20):
    mean = spread.rolling(window).mean()
    std = spread.rolling(window).std()
    return (spread - mean) / std

# 示例
z_spread = zscore_spread(log_sp, window=30)

💡 我的经验:先用对数价差做基础计算,再用滚动窗口做标准化。这样既解决了量纲问题,又得到了统计意义上的偏离度。

4.2 滚动统计特征:捕捉价差的动态变化

价差本身只是基础。真正有价值的是价差的统计特征。我习惯用滚动窗口来计算这些特征。

4.2.1 滚动均值与标准差

这两个是最基础的。滚动均值反映价差的“中枢”,滚动标准差反映价差的“波动幅度”。

# 滚动统计特征
def rolling_stats(spread, window=20):
    df = pd.DataFrame()
    df['spread'] = spread
    df['rolling_mean'] = spread.rolling(window).mean()
    df['rolling_std'] = spread.rolling(window).std()
    df['zscore'] = (spread - df['rolling_mean']) / df['rolling_std']
    return df

4.2.2 滚动偏度与峰度

这两个特征很多人会忽略,但我觉得它们很有用。

  • 偏度:衡量价差分布是否对称。正偏度意味着价差更容易出现正向极端值
  • 峰度:衡量价差分布的“厚尾”程度。高峰度意味着极端价差出现概率更高

我曾经在原油跨期套利中,发现价差峰度突然飙升,随后就出现了极端行情。这个特征帮我提前预警了好几次。

# 滚动偏度与峰度
def rolling_skew_kurt(spread, window=60):
    df = pd.DataFrame()
    df['skew'] = spread.rolling(window).skew()
    df['kurt'] = spread.rolling(window).kurt()
    return df

4.2.3 滚动分位数

分位数比均值更稳健。我常用的是25%、50%、75%分位数。

# 滚动分位数
def rolling_quantiles(spread, window=100):
    df = pd.DataFrame()
    df['q25'] = spread.rolling(window).quantile(0.25)
    df['q50'] = spread.rolling(window).quantile(0.50)
    df['q75'] = spread.rolling(window).quantile(0.75)
    df['iqr'] = df['q75'] - df['q25']  # 四分位距
    return df

📊 核心观点:滚动窗口大小的选择很关键。窗口太小,特征噪声大;窗口太大,特征反应迟钝。我一般先用20、60、120三个窗口做对比测试。

4.3 波动率特征:衡量价差的“脾气”

波动率特征,说白了就是衡量价差爱不爱动。价差波动率低的时候,套利机会少;波动率高的时候,机会多但风险也大。

4.3.1 历史波动率

最基础的方式:用滚动窗口计算价差收益率的标准差。

# 历史波动率
def historical_volatility(spread, window=20):
    log_ret = np.log(spread / spread.shift(1))
    hv = log_ret.rolling(window).std() * np.sqrt(252)  # 年化
    return hv

4.3.2 波动率锥

这个工具我特别喜欢。它把不同时间周期的波动率放在一起比较,能看出当前波动率处于什么水平。

# 波动率锥示例
def volatility_cone(spread, windows=[5, 10, 20, 60, 120]):
    log_ret = np.log(spread / spread.shift(1))
    cone = {}
    for w in windows:
        vol = log_ret.rolling(w).std() * np.sqrt(252)
        cone[f'vol_{w}d'] = vol
    return pd.DataFrame(cone)

4.3.3 波动率均值回归特征

价差波动率本身也有均值回归特性。我常用的是:当前波动率与历史均值的偏离度

# 波动率偏离度
def vol_deviation(spread, vol_window=20, mean_window=60):
    hv = historical_volatility(spread, vol_window)
    vol_mean = hv.rolling(mean_window).mean()
    vol_std = hv.rolling(mean_window).std()
    vol_z = (hv - vol_mean) / vol_std
    return vol_z

⚠️ 避坑指南:我曾经在波动率特征上犯过一个错误——直接用价格波动率代替价差波动率。结果模型学到的全是市场整体波动,跟套利信号关系不大。记住,一定要用价差本身的波动率

4.4 特征工程框架图

下面这张图,是我做特征工程时的核心框架。你可以把它当作一个检查清单。

价差计算与特征工程框架 原始价格数据 价差计算 简单价差 | 对数价差 | 标准化价差 (Z-score) 滚动统计特征 • 滚动均值/标准差 • 偏度/峰度 • 分位数/IQR 波动率特征 • 历史波动率 • 波动率锥 • 波动率偏离度 特征矩阵 → 机器学习模型

4.5 实战中的特征选择建议

特征不是越多越好。我总结了几条经验:

  1. 先做减法:从对数价差和滚动Z-score开始,这两个特征已经能解决80%的问题
  2. 逐步添加:如果模型表现不够好,再考虑加入偏度、峰度、波动率特征
  3. 注意多重共线性:滚动均值和滚动中位数高度相关,不要同时使用
  4. 窗口大小要合理:我一般用20、60、120三个窗口,分别对应短期、中期、长期

💡 一个小技巧:把不同窗口的特征都算出来,然后用随机森林的特征重要性排序,只保留排名前10的特征。这样既减少了计算量,又避免了过拟合。

好了,这一章的内容就到这里。价差计算和特征工程是套利策略的基石,花时间把这一步做好,后面的模型训练会事半功倍。


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