简介:这份资源面向具备一定MATLAB基础、希望入门时间序列与机器学习组合建模的金融数据分析学习者,核心是用支持向量机改进ARIMA股票价格预测。包内共3个文件,以2个m脚本和1个xlsx数据表为主,压缩包约13KB,脚本承担ARIMA建模与SVM回归预测流程,数据表提供上证指数历史收盘价作为训练与验证样本。项目先由ARIMA对开盘价生成基础预测,再以SVM结合核函数对残差或预测值做非线性修正,并借助R方、均方差和相对误差等指标对比改进前后的精度。已有1039人学习下载,读者可据此掌握参数自动定阶、SVM训练调优与预测评估的完整实现思路,并替换为其他股票或更长时间范围的数据开展研究,是学习时间序列分析与机器学习算法结合的实践案例。
1. 为什么单靠 ARIMA 做股票价格预测总差一口气
用 ARIMA 做股票价格预测的人,大多经历过同一个场景:模型在训练集上拟合得漂漂亮亮,残差看着也像白噪声,可一到实盘或者样本外测试,预测曲线就变成一条几乎水平的直线,涨跌全抓不住。这不是代码写错了,而是 ARIMA 作为线性模型,天生只能捕捉序列里的线性自相关结构,对股票价格里那些非线性、由情绪和突发事件驱动的成分无能为力。
于是就有了「ARIMA+SVM」这条路线:先用 ARIMA 把序列里的线性部分榨干,再把 ARIMA 没解释掉的残差交给支持向量机(SVM)去学非线性规律,最后把两段预测相加。这个思路在股票价格预测里被反复验证过,逻辑清晰、复现成本低,特别适合有一定 Python 基础、想从单模型跨到组合模型的从业者。这篇笔记就按「先立住原理、再动手复现、最后讲坑」的顺序,把 ARIMA+SVM 这套组合拳拆开讲清楚,让你看完能自己跑通一条完整链路。
2. ARIMA 与 SVM 的分工:残差里到底藏着什么
2.1 线性部分交给 ARIMA,非线性部分交给 SVM
ARIMA(p,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_col="date") series = df["close"].astype(float) # ADF 检验:p 值大于 0.05 说明非平稳,需要差分 def adf_report(s, name): result = adfuller(s.dropna(), autolag="AIC") 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 值。参数上autolag="AIC"让 statsmodels 自动选滞后阶数,避免手动试。判断标准是 p 值小于 0.05 认为平稳。绝大多数股票价格一阶差分后就平稳,也就是 d=1;如果一阶差分还不平稳,再考虑 d=2,但 d 越大信息损失越多,一般不超过 2。
提示:ADF 检验对趋势敏感,如果序列有明显趋势,先做对数变换再差分,能缓解异方差。
3. 用 Python 把 ARIMA+SVM 混合模型跑通
3.1 定阶:用 AIC 网格搜索确定 ARIMA 的 p 和 q
d 定下来之后,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, order=best_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 个残差作为特征,当前残差作为标签,构造监督学习数据集。参数说明:lag=4是滞后阶数,可以试 3、4、5 对比验证集效果;split=0.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(kernel="rbf", C=100, gamma="scale", epsilon=0.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 是不敏感损失带宽。参数说明:C=100 是偏大的值,适合残差波动较大的场景;gamma="scale" 让 sklearn 按特征数自动算,比手动设更省事;epsilon=0.01 表示预测误差在 0.01 以内不计损失,这个值要根据残差量级调,残差大就调大。跑完看残差预测的 MSE,如果比直接预测残差均值(即全零)还差,说明 SVM 没学到东西,要回头检查 lag 和标准化。
3.4 把两段预测相加:混合模型的最终输出
ARIMA 负责预测线性部分,SVM 负责预测残差,两者相加才是最终价格预测。注意 ARIMA 的预测要对应到测试集的时间段。
# ARIMA 对测试段的预测 arima_pred = final_arima.predict(start=len(series)-len(y_test), end=len(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_splits=5) grid = GridSearchCV(SVR(kernel="rbf"), param_grid, cv=tscv, scoring="neg_mean_absolute_error", n_jobs=-1) grid.fit(X_train_scaled, y_train) print("最优参数:", grid.best_params_)逻辑说明:TimeSeriesSplit按时间顺序切分,每次用前面的数据训练、后面的数据验证,避免未来信息泄露。scoring="neg_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, order=best_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。参数说明:horizon=5是预测步数,步数越多误差累积越明显,一般不超过 10;每步都重新拟合 ARIMA 计算量大,实际可以用固定参数的 ARIMA 只更新数据,速度更快。滚动预测的 MAE 会随步数增加而上升,这是正常现象,重点看上升速度是否可接受。
5. 避坑与排查:ARIMA+SVM 最容易翻车的五个地方
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 却设了 epsilon=1,模型觉得所有误差都在容忍范围内,干脆不学。解决:先看残差的标准差,epsilon 设成标准差的 1% 到 10% 之间,再用网格搜索微调。
5.5 把混合模型当成万能药,忽略交易成本
现象:方向准确率 56%,回测看着能赚,实盘一算手续费和滑点就亏。原因:股票价格预测的精度提升往往只有几个百分点,高频交易下交易成本会吃掉全部利润。解决:回测时把手续费和滑点算进去,降低交易频率,或者只在高置信度信号出现时才交易。模型只是工具,能不能赚钱还得看策略和执行。
6. 把混合模型用对:一个提升方向准确率的实用技巧
跑通基础版之后,最值得花时间的地方是提升方向准确率,而不是继续压 MAE。我自己的做法是给 SVM 的输入加特征,而不是只用残差滞后项。具体来说,把 ARIMA 残差和几个能反映市场状态的技术指标拼在一起喂给 SVR,让 SVM 在学残差非线性规律的同时,也能感知当前市场处于什么状态。
常用的补充特征有三类:一是波动率类,比如残差的滚动标准差,反映当前不确定性高低;二是动量类,比如价格的 5 日、10 日收益率,反映短期方向;三是成交量类,比如成交量的滚动均值比,反映资金参与度。这些特征不需要多,三到五个就够,多了反而稀释残差本身的信息。
# 在残差滞后项基础上拼接额外特征 def build_features(resid_values, close_values, lag=4, window=5): 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 个残差滞后项,还额外算了残差滚动标准差和价格收益率,拼成一个更长的特征向量。参数说明:window=5是滚动窗口,对应一周交易日;收益率用收盘价算,如果数据里有成交量列,可以再加一个成交量比值特征。加完特征后,标准化和 SVR 训练流程不变,但要注意特征维度变高后,gamma 的 "scale" 会自动调整,最好重新跑一次网格搜索。
我实测下来,加特征后方向准确率通常能再提 2 到 4 个百分点,代价是模型复杂度上升、调参时间变长。值不值得做,取决于你的策略对方向准确率的敏感度。如果只是做趋势跟踪,2 个百分点可能就决定盈亏;如果是低频配置,MAE 的改善更实在。
最后说个习惯:我每次跑完混合模型,都会把纯 ARIMA、纯 SVM、混合模型三者的预测曲线画在同一张图上,肉眼过一遍。指标是冷的,曲线是热的,很多时候看图能发现指标看不出的问题,比如某段区间混合模型反而更差,那就要单独查那段区间的数据有没有异常。模型调优没有终点,但每次把翻车点记下来,下次就能少走一段弯路。希望帮到你。
本文还有配套的精品资源,点击获取