8、基差回归模型的Python实现:statsmodels库的使用、OLS函数详解、模型拟合与预测
好,咱们进入实战环节。
前面聊了那么多理论,什么均值回归、协整关系、套利区间……说白了,最终都得落到代码上。你想想看,光靠眼睛盯盘,你能盯几个品种?我反正不行。所以,用Python把基差回归模型跑起来,才是真正的生产力。
8.1 为什么选statsmodels?
做回归分析,Python里有两个主流库:scikit-learn 和 statsmodels。我个人习惯用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 完整流程总结
来,我把整个流程串起来,画个图你感受一下:
这个流程看着简单,但每一步都有坑。比如数据准备阶段,价格序列的对齐问题——不同品种的交易时间不一样,夜盘也不一样。我建议用收盘价,或者用同一时间戳的切片数据。
再比如残差分析,光看标准化残差还不够。我一般会再跑一个ADF检验,确认残差是平稳的。如果残差不平稳,说明两个品种之间没有长期均衡关系,这个回归模型就不能用。
核心要点:
- statsmodels的OLS是基差回归的首选工具
- 残差是交易信号的来源,不是回归系数本身
- 滚动回归比固定窗口更适应市场变化
- 永远要检查残差的平稳性
好了,代码部分就到这里。你回去把这段代码跑一遍,看看螺纹钢和热卷的基差回归效果怎么样。有问题随时问我。