8、基差回归模型的Python实现:statsmodels库的使用、OLS函数详解、模型拟合与预测

好,咱们进入实战环节。

前面聊了那么多理论,什么均值回归、协整关系、套利区间……说白了,最终都得落到代码上。你想想看,光靠眼睛盯盘,你能盯几个品种?我反正不行。所以,用Python把基差回归模型跑起来,才是真正的生产力。

8.1 为什么选statsmodels?

做回归分析,Python里有两个主流库:scikit-learnstatsmodels。我个人习惯用statsmodels做基差回归。为什么?

  • 统计推断更完整:statsmodels直接给出p值、置信区间、R方、F检验等。这些在基差交易里非常重要——你得知道这个回归关系到底靠不靠谱。
  • 自带常数项处理:OLS默认帮你加截距,省事。
  • 残差分析方便:基差回归的核心是看残差是否平稳,statsmodels的残差可以直接拿来跑ADF检验。

我在项目中遇到过用sklearn做回归,结果发现p值还得自己算,太折腾了。从那以后,基差相关的回归我全用statsmodels。

8.2 OLS函数详解

OLS,全称Ordinary Least Squares,普通最小二乘法。说白了,就是找一条线,让所有点到这条线的垂直距离的平方和最小。

在statsmodels里,调用方式很简单:

import statsmodels.api as sm
import pandas as pd
import numpy as np

# 假设我们有两个品种:螺纹钢和热卷
# 基差 = 螺纹钢价格 - 热卷价格
# 我们想用热卷价格来预测螺纹钢价格

# 构造数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=500, freq='D')
hot_coil = np.cumsum(np.random.randn(500) * 10 + 0.1) + 3500  # 热卷价格
rebar = hot_coil * 1.05 + np.random.randn(500) * 20 + 50      # 螺纹钢价格

df = pd.DataFrame({
    'rebar': rebar,
    'hot_coil': hot_coil
}, index=dates)

# 添加常数项
X = sm.add_constant(df['hot_coil'])
y = df['rebar']

# 拟合模型
model = sm.OLS(y, X)
results = model.fit()

print(results.summary())

这里有几个关键点:

  • sm.add_constant():给自变量加一列1,代表截距项。如果不加,模型会强制过原点。在基差交易里,截距通常是有经济含义的——它代表两个品种之间的长期均衡价差。
  • sm.OLS(y, X):第一个参数是因变量,第二个是自变量。顺序别搞反了,我刚开始学的时候就犯过这个错。
  • results.summary():输出完整的回归报告。你会看到R方、系数、p值、Durbin-Watson统计量等等。

重要输出解读:

  • coef(系数):比如热卷的系数是1.05,意味着热卷涨1块钱,螺纹钢平均涨1.05块。这就是你的对冲比率。
  • P>|t|(p值):小于0.05说明系数显著。如果p值很大,说明这个回归关系可能不成立。
  • R-squared(R方):越接近1越好。但别迷信,金融数据里R方0.8以上就算不错了。

8.3 模型拟合与预测

模型拟合完了,接下来干嘛?当然是用来预测基差的回归水平。

# 获取拟合值(即模型预测的螺纹钢价格)
df['predicted_rebar'] = results.fittedvalues

# 计算残差(基差的回归成分)
df['residual'] = results.resid

# 预测未来一期
# 假设新来了一个热卷价格数据
new_hot_coil = np.array([1, 3850])  # 注意:第一列是常数项1
predicted_rebar_new = results.predict(new_hot_coil)
print(f"预测螺纹钢价格: {predicted_rebar_new[0]:.2f}")

# 计算当前基差与回归基差的偏离
current_basis = 3950 - 3850  # 假设当前螺纹钢3950,热卷3850
regression_basis = predicted_rebar_new[0] - 3850
deviation = current_basis - regression_basis
print(f"当前基差偏离: {deviation:.2f}")

嗯,这里要注意:results.predict() 需要传入包含常数项的自变量矩阵。如果你忘了加常数项,预测结果会完全跑偏。我曾经因为这个原因,在回测里连续亏了三天才发现问题。

8.4 残差分析与交易信号生成

基差回归模型的精髓,不在回归本身,而在残差。残差代表的是「实际基差」与「理论基差」的偏离。当这个偏离足够大时,就是交易机会。

# 标准化残差
df['residual_z'] = (df['residual'] - df['residual'].mean()) / df['residual'].std()

# 生成交易信号
df['signal'] = 0
df.loc[df['residual_z'] > 2, 'signal'] = -1   # 基差过大,做空基差
df.loc[df['residual_z'] < -2, 'signal'] = 1   # 基差过小,做多基差

# 看看信号分布
print(df['signal'].value_counts())

我的经验:

阈值设2倍标准差是个不错的起点,但别死板。我一般会先用1.5倍标准差做初步筛选,然后结合品种的波动率动态调整。比如螺纹钢和热卷,波动率低的时候,1.5倍就够用了;但如果是铁矿石和焦炭,波动大,我可能会用到2.5倍甚至3倍。

8.5 滚动回归:让模型与时俱进

市场在变,基差关系也在变。用固定窗口的回归,时间长了会失效。我建议用滚动回归。

# 滚动回归:每次用最近60个交易日的数据
window = 60
df['rolling_beta'] = np.nan
df['rolling_alpha'] = np.nan

for i in range(window, len(df)):
    X_window = sm.add_constant(df['hot_coil'].iloc[i-window:i])
    y_window = df['rebar'].iloc[i-window:i]
    
    model_window = sm.OLS(y_window, X_window)
    results_window = model_window.fit()
    
    df.loc[df.index[i], 'rolling_beta'] = results_window.params['hot_coil']
    df.loc[df.index[i], 'rolling_alpha'] = results_window.params['const']

# 看看滚动系数变化
print(df[['rolling_alpha', 'rolling_beta']].tail())

注意:

滚动窗口大小怎么选?没有标准答案。我个人习惯:

  • 日频数据:60-120个交易日(约3-6个月)
  • 周频数据:52周(1年)
  • 月频数据:24个月(2年)

窗口太小,模型太敏感,噪声多;窗口太大,模型反应迟钝,跟不上市场变化。你可以多试几个窗口,看看哪个在历史回测里表现最好。

8.6 完整流程总结

来,我把整个流程串起来,画个图你感受一下:

基差回归模型完整流程 1. 数据准备 价格序列、基差计算 2. OLS回归 添加常数项、拟合模型 3. 残差分析 标准化、平稳性检验 4. 信号 生成交易 关键检查点: ① 回归系数是否显著?(p值 < 0.05) ② 残差是否平稳?(ADF检验 p值 < 0.05) ③ 滚动回归的系数是否稳定?(避免结构突变) ④ 基差偏离是否超过阈值?(通常1.5-3倍标准差) ⑤ 交易成本是否覆盖?(别忘了滑点和手续费) —— 以上五点,少一个都不行。我吃过亏,你记住。

这个流程看着简单,但每一步都有坑。比如数据准备阶段,价格序列的对齐问题——不同品种的交易时间不一样,夜盘也不一样。我建议用收盘价,或者用同一时间戳的切片数据。

再比如残差分析,光看标准化残差还不够。我一般会再跑一个ADF检验,确认残差是平稳的。如果残差不平稳,说明两个品种之间没有长期均衡关系,这个回归模型就不能用。

核心要点:

  • statsmodels的OLS是基差回归的首选工具
  • 残差是交易信号的来源,不是回归系数本身
  • 滚动回归比固定窗口更适应市场变化
  • 永远要检查残差的平稳性

好了,代码部分就到这里。你回去把这段代码跑一遍,看看螺纹钢和热卷的基差回归效果怎么样。有问题随时问我。

公众号:蓝海资料掘金营,微信deep3321