多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

ARIMA+SVM混合模型:股票价格预测的残差建模与Python实战

ARIMA+SVM混合模型:股票价格预测的残差建模与Python实战 简介这份资源面向具备一定MATLAB基础、希望入门时间序列与机器学习组合建模的金融数据分析学习者核心是用支持向量机改进ARIMA股票价格预测。包内共3个文件以2个m脚本和1个xlsx数据表为主压缩包约13KB脚本承担ARIMA建模与SVM回归预测流程数据表提供上证指数历史收盘价作为训练与验证样本。项目先由ARIMA对开盘价生成基础预测再以SVM结合核函数对残差或预测值做非线性修正并借助R方、均方差和相对误差等指标对比改进前后的精度。已有1039人学习下载读者可据此掌握参数自动定阶、SVM训练调优与预测评估的完整实现思路并替换为其他股票或更长时间范围的数据开展研究是学习时间序列分析与机器学习算法结合的实践案例。1. 为什么单靠 ARIMA 做股票价格预测总差一口气用 ARIMA 做股票价格预测的人大多经历过同一个场景模型在训练集上拟合得漂漂亮亮残差看着也像白噪声可一到实盘或者样本外测试预测曲线就变成一条几乎水平的直线涨跌全抓不住。这不是代码写错了而是 ARIMA 作为线性模型天生只能捕捉序列里的线性自相关结构对股票价格里那些非线性、由情绪和突发事件驱动的成分无能为力。于是就有了「ARIMASVM」这条路线先用 ARIMA 把序列里的线性部分榨干再把 ARIMA 没解释掉的残差交给支持向量机SVM去学非线性规律最后把两段预测相加。这个思路在股票价格预测里被反复验证过逻辑清晰、复现成本低特别适合有一定 Python 基础、想从单模型跨到组合模型的从业者。这篇笔记就按「先立住原理、再动手复现、最后讲坑」的顺序把 ARIMASVM 这套组合拳拆开讲清楚让你看完能自己跑通一条完整链路。2. ARIMA 与 SVM 的分工残差里到底藏着什么2.1 线性部分交给 ARIMA非线性部分交给 SVMARIMAp,d,q的本质是把非平稳序列差分 d 次变成平稳序列然后用 p 阶自回归和 q 阶移动平均去拟合。它的假设是当前值可以由过去若干期的值和过去若干期的预测误差线性组合出来。股票价格里确实存在这种线性惯性比如短期动量、均值回复ARIMA 能吃掉这一块。但股票价格同时受政策、财报、市场情绪影响这些因素和价格之间不是线性关系。SVM 做回归SVR时通过核函数把输入映射到高维空间在高维空间里找一条容忍误差的回归超平面天然适合拟合这种非线性映射。把两者串起来等于让 ARIMA 负责「能算出来的规律」让 SVM 负责「算不出来但能学出来的规律」。常见做法是构造混合模型y_t L_t N_t其中 L_t 是 ARIMA 拟合的线性部分N_t 是残差里的非线性部分。先对原序列建 ARIMA拿到残差序列 e_t再用 SVM 对 e_t 建模输入用残差的滞后项。预测时把 ARIMA 的预测值和 SVM 对残差的预测值相加就是最终结果。2.2 为什么不是「ARIMA 预测完直接上 SVM」而是「残差建模」很多人第一反应是既然 SVM 这么强为什么不直接拿原始价格序列喂给 SVM原因有两个。第一原始价格序列非平稳SVM 对非平稳输入的泛化能力差容易过拟合训练段的趋势。第二直接上 SVM 会丢掉 ARIMA 已经捕捉到的线性结构等于让 SVM 从零学起浪费了 ARIMA 的先验。残差建模的好处是ARIMA 已经把线性成分拿走残差理论上接近白噪声如果残差里还有结构那一定是非线性的正好是 SVM 的用武之地。这样两个模型各司其职不会互相抢活。我一般会先画残差的 ACF/PACF 图确认线性成分是否被抽干净再看残差和自身滞后项的散点图如果能看到明显的非线性弯曲就说明 SVM 有东西可学。2.3 数据准备与平稳性检验的最小步骤动手前先把数据理顺。股票价格预测常用的输入是日线收盘价取一段足够长的历史我一般用 3 到 5 年先做平稳性检验再决定差分阶数 d。import pandas as pd import numpy as np from statsmodels.tsa.stattools import adfuller # 读取收盘价假设 csv 有两列date, close df pd.read_csv(stock.csv, parse_dates[date], index_coldate) series df[close].astype(float) # ADF 检验p 值大于 0.05 说明非平稳需要差分 def adf_report(s, name): result adfuller(s.dropna(), autolagAIC) print(f{name} ADF统计量{result[0]:.4f}, p值{result[1]:.4f}) adf_report(series, 原始序列) adf_report(series.diff().dropna(), 一阶差分) adf_report(series.diff().diff().dropna(), 二阶差分)这段代码做三件事读数据、对原始序列和一阶二阶差分分别做 ADF 检验、打印统计量和 p 值。参数上autolagAIC让 statsmodels 自动选滞后阶数避免手动试。判断标准是 p 值小于 0.05 认为平稳。绝大多数股票价格一阶差分后就平稳也就是 d1如果一阶差分还不平稳再考虑 d2但 d 越大信息损失越多一般不超过 2。提示ADF 检验对趋势敏感如果序列有明显趋势先做对数变换再差分能缓解异方差。3. 用 Python 把 ARIMASVM 混合模型跑通3.1 定阶用 AIC 网格搜索确定 ARIMA 的 p 和 qd 定下来之后p 和 q 用网格搜索配合 AIC 准则选。AIC 越小模型在拟合优度和复杂度之间平衡得越好。import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings(ignore) d 1 best_aic np.inf best_order None # p、q 各试 0 到 5够覆盖大多数日线场景 for p in range(6): for q in range(6): try: model ARIMA(series, order(p, d, q)) fitted model.fit() if fitted.aic best_aic: best_aic fitted.aic best_order (p, d, q) except Exception: continue print(最优阶数:, best_order, AIC:, round(best_aic, 2))逻辑说明双重循环遍历 p 和 q 的候选组合每组都建一次 ARIMA 并拟合记录 AIC 最小的那组。order(p, d, q)里 d 固定为前面检验得到的值。参数说明p 是自回归项数q 是移动平均项数范围 0 到 5 是经验值日线数据很少超过 5如果数据是分钟级高频可以放宽到 8 到 10但计算量会明显上升。try/except是为了跳过不收敛的组合实际跑的时候会看到部分高阶组合报错属正常现象。3.2 提取残差并用滞后项构造 SVM 的训练集ARIMA 拟合完残差就是 SVM 的标签输入用残差的滞后项。这里有个关键点SVM 的输入维度不能太高否则维度灾难会让效果变差我一般用 3 到 5 个滞后。# 用最优阶数重新拟合拿到残差 final_arima ARIMA(series, orderbest_order).fit() resid final_arima.resid # 用前 lag 个残差预测当前残差 lag 4 X, y [], [] resid_values resid.values for i in range(lag, len(resid_values)): X.append(resid_values[i-lag:i]) y.append(resid_values[i]) X, y np.array(X), np.array(y) # 按时间顺序切分不能打乱 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:]逻辑说明循环从第 lag 个点开始每次取前 lag 个残差作为特征当前残差作为标签构造监督学习数据集。参数说明lag4是滞后阶数可以试 3、4、5 对比验证集效果split0.8表示前 80% 做训练、后 20% 做测试时间序列必须按顺序切绝不能随机打乱否则会引入未来信息这是血泪经验。切分后 X_train 的每一行是一个 lag 维向量y_train 是对应的残差值。3.3 训练 SVR 并做特征标准化SVM 对特征尺度敏感残差的量纲虽然和原序列一致但不同滞后项之间方差可能不同标准化是必须的。核函数先用 RBF它在非线性回归里最稳。from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, mean_absolute_error scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) svr SVR(kernelrbf, C100, gammascale, epsilon0.01) svr.fit(X_train_scaled, y_train) resid_pred svr.predict(X_test_scaled) print(残差预测 MSE:, round(mean_squared_error(y_test, resid_pred), 4))逻辑说明先用训练集拟合 scaler再分别变换训练集和测试集注意测试集只能用训练集的均值和方差变换不能重新 fit否则数据泄露。SVR 用 RBF 核C 控制惩罚力度gamma 控制核宽度epsilon 是不敏感损失带宽。参数说明C100 是偏大的值适合残差波动较大的场景gammascale 让 sklearn 按特征数自动算比手动设更省事epsilon0.01 表示预测误差在 0.01 以内不计损失这个值要根据残差量级调残差大就调大。跑完看残差预测的 MSE如果比直接预测残差均值即全零还差说明 SVM 没学到东西要回头检查 lag 和标准化。3.4 把两段预测相加混合模型的最终输出ARIMA 负责预测线性部分SVM 负责预测残差两者相加才是最终价格预测。注意 ARIMA 的预测要对应到测试集的时间段。# ARIMA 对测试段的预测 arima_pred final_arima.predict(startlen(series)-len(y_test), endlen(series)-1) # 混合预测 ARIMA 线性预测 SVM 残差预测 hybrid_pred arima_pred.values resid_pred # 对比纯 ARIMA 和混合模型 actual series.values[-len(y_test):] print(纯 ARIMA MAE:, round(mean_absolute_error(actual, arima_pred), 4)) print(混合模型 MAE:, round(mean_absolute_error(actual, hybrid_pred), 4))逻辑说明final_arima.predict按索引区间输出测试段的线性预测resid_pred是 SVM 对同一段残差的预测两者逐点相加得到混合预测。参数说明start和end用序列长度减去测试集长度来定位保证时间对齐如果索引是日期也可以直接传日期字符串。最后用 MAE 对比纯 ARIMA 和混合模型MAE 越小越好。我实测下来混合模型在波动大的测试段通常能把 MAE 压下去 10% 到 25%但具体幅度取决于残差里非线性成分的多少。4. 参数调优与效果验证别让模型停在「能跑」4.1 SVR 三个核心参数的调法SVR 的效果几乎全压在 C、gamma、epsilon 三个参数上瞎设等于白跑。我一般用网格搜索配时间序列交叉验证而不是普通 K 折因为普通 K 折会打乱时间顺序。from sklearn.model_selection import TimeSeriesSplit, GridSearchCV param_grid { C: [1, 10, 100, 1000], gamma: [scale, 0.01, 0.1, 1], epsilon: [0.001, 0.01, 0.1] } tscv TimeSeriesSplit(n_splits5) grid GridSearchCV(SVR(kernelrbf), param_grid, cvtscv, scoringneg_mean_absolute_error, n_jobs-1) grid.fit(X_train_scaled, y_train) print(最优参数:, grid.best_params_)逻辑说明TimeSeriesSplit按时间顺序切分每次用前面的数据训练、后面的数据验证避免未来信息泄露。scoringneg_mean_absolute_error是因为 sklearn 的评分函数要求越大越好所以用负 MAE。参数说明C 的候选从 1 到 1000覆盖欠拟合到过拟合gamma 的 scale 是自动值另外给三个手动值对比epsilon 给三个量级。n_jobs-1用满 CPU 核。跑完拿到最优参数后用全部训练集重新拟合一次再预测测试集。4.2 用方向准确率验证别只看 MAE股票价格预测里MAE 小不代表能赚钱因为涨跌方向错了误差再小也没用。我习惯再加一个方向准确率指标预测涨跌方向和实际一致的比例。def direction_accuracy(actual, pred): actual_diff np.diff(actual) pred_diff np.diff(pred) correct np.sum(np.sign(actual_diff) np.sign(pred_diff)) return correct / len(actual_diff) print(纯 ARIMA 方向准确率:, round(direction_accuracy(actual, arima_pred), 4)) print(混合模型方向准确率:, round(direction_accuracy(actual, hybrid_pred), 4))逻辑说明对实际值和预测值分别做一阶差分得到涨跌方向用np.sign转成 1/-1/0统计符号一致的比例。参数说明这个指标没有可调参数但要注意差分后长度少 1。方向准确率能到 55% 以上就算有价值50% 等于瞎猜。如果混合模型 MAE 降了但方向准确率没升说明 SVM 只是把幅度拟合得更准没抓住方向这时候要回头检查残差里是否真有方向性信息。4.3 滚动预测与多步预测的差别上面做的是一步预测实盘更关心多步。多步预测有两种做法直接法和滚动法。直接法是训练一个模型直接输出未来 h 步滚动法是一步一步预测把预测值填回输入再预测下一步。股票价格预测里滚动法更常用但误差会累积。# 滚动多步预测示意预测未来 5 步 horizon 5 history list(series.values) preds [] for _ in range(horizon): arima_model ARIMA(history, orderbest_order).fit() next_linear arima_model.forecast(1)[0] # 残差部分用最近 lag 个残差喂给 SVR recent_resid arima_model.resid.values[-lag:] next_resid svr.predict(scaler.transform([recent_resid]))[0] next_val next_linear next_resid preds.append(next_val) history.append(next_val)逻辑说明每预测一步就把预测值追加进历史序列重新拟合 ARIMA 再预测下一步残差部分用最近 lag 个残差喂给已训练好的 SVR。参数说明horizon5是预测步数步数越多误差累积越明显一般不超过 10每步都重新拟合 ARIMA 计算量大实际可以用固定参数的 ARIMA 只更新数据速度更快。滚动预测的 MAE 会随步数增加而上升这是正常现象重点看上升速度是否可接受。5. 避坑与排查ARIMASVM 最容易翻车的五个地方5.1 残差没做平稳性检验就直接喂 SVM现象SVM 在训练集上 MSE 很低测试集上 MSE 爆炸预测残差曲线剧烈震荡。原因ARIMA 残差如果还有单位根说明线性成分没抽干净SVM 在拟合一个非平稳序列泛化必然差。解决对残差再做一次 ADF 检验p 值大于 0.05 就说明 ARIMA 阶数不够回去加大 p 或 q或者提高差分阶数。5.2 标准化时对测试集重新 fit现象离线评估指标很好一上实盘就崩。原因测试集用fit_transform而不是transform把测试集的均值和方差泄露进了模型评估结果虚高。解决scaler 只在训练集上 fit测试集和实盘数据一律用transform。这个坑我踩过不止一次后来养成习惯只要看到fit_transform出现在测试集上就立刻改。5.3 用随机切分代替时间顺序切分现象交叉验证分数高得离谱实际预测完全不能用。原因随机切分让模型在训练时看到了未来的数据等于开卷考试。解决所有切分一律用TimeSeriesSplit或手动按时间切训练集永远在测试集之前。这条是时间序列建模的铁律没有例外。5.4 SVR 的 epsilon 设得和残差量级不匹配现象SVM 预测出来的残差几乎全是常数没有波动。原因epsilon 设得太大比如残差量级是 0.1 却设了 epsilon1模型觉得所有误差都在容忍范围内干脆不学。解决先看残差的标准差epsilon 设成标准差的 1% 到 10% 之间再用网格搜索微调。5.5 把混合模型当成万能药忽略交易成本现象方向准确率 56%回测看着能赚实盘一算手续费和滑点就亏。原因股票价格预测的精度提升往往只有几个百分点高频交易下交易成本会吃掉全部利润。解决回测时把手续费和滑点算进去降低交易频率或者只在高置信度信号出现时才交易。模型只是工具能不能赚钱还得看策略和执行。6. 把混合模型用对一个提升方向准确率的实用技巧跑通基础版之后最值得花时间的地方是提升方向准确率而不是继续压 MAE。我自己的做法是给 SVM 的输入加特征而不是只用残差滞后项。具体来说把 ARIMA 残差和几个能反映市场状态的技术指标拼在一起喂给 SVR让 SVM 在学残差非线性规律的同时也能感知当前市场处于什么状态。常用的补充特征有三类一是波动率类比如残差的滚动标准差反映当前不确定性高低二是动量类比如价格的 5 日、10 日收益率反映短期方向三是成交量类比如成交量的滚动均值比反映资金参与度。这些特征不需要多三到五个就够多了反而稀释残差本身的信息。# 在残差滞后项基础上拼接额外特征 def build_features(resid_values, close_values, lag4, window5): X, y [], [] for i in range(max(lag, window), len(resid_values)): resid_lag resid_values[i-lag:i] # 残差滚动标准差反映波动状态 vol np.std(resid_values[i-window:i]) # 价格 5 日收益率反映动量 ret (close_values[i] - close_values[i-window]) / close_values[i-window] # 成交量特征此处省略按同样方式拼接 feat np.concatenate([resid_lag, [vol, ret]]) X.append(feat) y.append(resid_values[i]) return np.array(X), np.array(y)逻辑说明循环里除了取 lag 个残差滞后项还额外算了残差滚动标准差和价格收益率拼成一个更长的特征向量。参数说明window5是滚动窗口对应一周交易日收益率用收盘价算如果数据里有成交量列可以再加一个成交量比值特征。加完特征后标准化和 SVR 训练流程不变但要注意特征维度变高后gamma 的 scale 会自动调整最好重新跑一次网格搜索。我实测下来加特征后方向准确率通常能再提 2 到 4 个百分点代价是模型复杂度上升、调参时间变长。值不值得做取决于你的策略对方向准确率的敏感度。如果只是做趋势跟踪2 个百分点可能就决定盈亏如果是低频配置MAE 的改善更实在。最后说个习惯我每次跑完混合模型都会把纯 ARIMA、纯 SVM、混合模型三者的预测曲线画在同一张图上肉眼过一遍。指标是冷的曲线是热的很多时候看图能发现指标看不出的问题比如某段区间混合模型反而更差那就要单独查那段区间的数据有没有异常。模型调优没有终点但每次把翻车点记下来下次就能少走一段弯路。希望帮到你。本文还有配套的精品资源点击获取
返回列表