DWT-ARIMA-GS-XGBoost股票混合预测与消融实践
简介围绕XGBoost混合模型开展股票预测研究的工程硕士学位论文面向金融数据分析、机器学习建模及量化预测方向的学习者与研究人员可用于理解集成学习在非线性金融时序中的应用思路。压缩包内为1个PDF文件大小3.48MB完整收录论文正文、摘要、目录及实验分析章节便于按绪论、理论基础、模型构建与结果对比等模块查阅。已有661人学习说明其在股票预测与XGBoost实践主题下具有一定参考价值。读者可从中获取特征工程、参数调整、XGBoost与随机森林/支持向量机等模型融合、MSE/MAE/R²评价指标设计及实验对比分析的完整研究框架也能了解过拟合讨论与后续改进方向适合作为课程论文、毕业设计或相关课题的技术参考也为量化策略验证与模型选择提供可复用的研究路径。1. 从“单模型调不动了”说起这套混合预测方案解决什么问题做股票预测的人大多经历过同一个阶段XGBoost 把参数调到自我感觉良好换一段行情就崩ARIMA 拟合出来的曲线平滑得像画上去的一遇到跳空就彻底跑偏。这套基于 XGBoost 的混合预测方案要解决的正是“单模型覆盖不了全部规律”这个老问题。它把股价序列拆成线性成分与非线性成分两条线先用离散小波变换做降噪与多尺度分解再由 ARIMA 拟合线性主趋势、由网格搜索优化的 XGBoost 拟合非线性残差最后融合输出预测值。整套东西适合正在做股票预测方向毕设或课设的人也适合手上有日频行情数据、需要拿出一份带消融对比实验的完整方案的人。读完之后你应该能自己搭出 GS-XGBoost、DWT-GS-XGBoost、DWT-ARIMA-GSXGB 这条逐级加模块的模型链并且知道每一级提升到底来自哪里。2. 总体架构设计DWT、ARIMA 与 GS-XGBoost 的三层拼装逻辑混合模型最容易写崩的地方不是算法本身而是结构没想清楚就动手写代码结果三个模块互相污染输入输出最后连“到底哪个模块带来了提升”都说不明白。所以在动手之前先把整体结构固定下来后面所有实验都是在这个骨架上换零件。2.1 三层结构降噪层、线性层与非线性层的职责划分我把整套方案拆成三层每层只干一件事层与层之间用明确的数据契约连接。第一层是降噪与分解层由离散小波变换DWT承担。原始收盘价序列里混着大量高频噪声直接喂给任何模型都会让模型去拟合噪声。DWT 的作用是把序列分解成近似分量低频主趋势和细节分量高频波动然后对细节分量做阈值处理再重构得到一条“去噪后的价格序列”。这一层的输出不是特征而是一条更干净的时序曲线。第二层是线性建模层由 ARIMA 承担。股价序列经过一阶差分后近似平稳ARIMA 擅长捕捉这种自相关结构也就是“昨天涨了今天大概率还涨一点”这类线性惯性。它的输出是预测值与残差序列两部分残差里剩下的就是线性模型解释不了的东西。第三层是非线性建模层由网格搜索优化的 XGBoostGS-XGBoost承担。它不直接预测价格而是拿 ARIMA 的残差作为学习目标同时把历史滞后项、滚动统计量、技术指标作为特征输入去拟合残差中的非线性结构。三层的关系是串行的DWT 输出干净序列 → ARIMA 输出预测值与残差 → GS-XGBoost 输出残差预测 → 最终预测值 ARIMA 预测值 残差预测值。这个结构的好处是每一层都能单独关掉做消融实验哪一层贡献了多少一目了然。提示不要把 DWT 放在 ARIMA 之后也不要把 XGBoost 直接拿去预测原始价格。前者会让 ARIMA 的差分阶数失去意义后者会让 XGBoost 退化成一个大号的自回归器树模型在纯外推任务上天然吃亏。2.2 数据流与模块边界从原始行情到预测输出的完整链路把三层落到代码目录上我一般会切成五个模块每个模块一个文件接口只暴露函数签名中间数据全部落盘成中间文件。这么做看起来笨但调试的时候可以把任意一个中间文件拿出来单独检查比在一条长脚本里打断点靠谱得多。模块职责输入输出data_loader读取行情、清洗缺失与停牌原始 CSV规整的 OHLCV 表dwt_filter小波分解、阈值去噪、重构收盘价序列去噪序列 各层分量arima_model定阶、拟合、预测去噪序列预测值 残差序列xgb_tuner网格搜索、训练、预测残差 特征表残差预测值evaluator指标计算、对比、绘图各模型预测结果指标表 对比图数据契约需要写死几个约定所有序列统一按交易日升序排列并以日期为索引所有特征只允许使用 t 时刻及之前的信息滚动窗口的shift(1)必须显式写出来训练集、验证集、测试集按时间切分绝不允许随机打乱。这三条约定后面会反复提到因为绝大多数“预测效果好得离谱”的翻车现场都是从这里开始的。特征表我一般固定成这么几类价格滞后项t-1 到 t-10 的收盘价、涨跌幅、滚动统计量5/10/20 日移动平均、滚动标准差、技术指标RSI、MACD 柱、布林带位置、成交量衍生量量比、换手率变化。特征数量控制在 30 到 50 之间太多了 XGBoost 的网格搜索会直接把你电脑跑成暖手宝。2.3 技术选型的取舍为什么是 XGBoost 而不是 LSTM这个问题答辩的时候几乎必被问。我的回答通常是三条样本量、可解释性、调参成本。日频行情数据一只股票十年的数据也就两千多个交易日扣掉滚动窗口的预热期真正能用来训练的样本不到两千条。LSTM 这类深度模型在这个样本量级上非常容易过拟合而且训练一次要几分钟做网格搜索基本不现实。XGBoost 在千级样本上表现稳定训练一次几秒钟几百组参数的网格搜索半小时内能跑完这对毕设阶段的迭代效率是决定性的。可解释性也是实打实的优势。XGBoost 能输出特征重要性答辩时可以直接告诉评委“模型主要看的是 5 日均线和 20 日波动率”而 LSTM 的门控权重很难讲成人话。至于 ARIMA 为什么还留着——因为它是这套方案里唯一能明确解释“线性惯性”的部分把它的残差交给 XGBoost等于让两个模型各干自己擅长的事这也是混合模型相对单一模型的核心论点。3. 关键模块实现网格搜索调参与小波分解的工程细节结构定完接下来是三个真正需要写代码的地方XGBoost 的网格搜索、DWT 的分解重构、ARIMA 与残差的融合。这三块每一块都有几个参数会直接决定结果好坏下面逐个拆。3.1 GS-XGBoost 的参数空间与网格搜索实现网格搜索的本质是暴力枚举所以关键不在算法而在搜索空间的设定。参数区间设宽了组合数爆炸设窄了容易错过最优点。我的经验是先粗后细两轮第一轮用大跨度粗搜定位量级第二轮在最优点附近用小步长细搜。import numpy as np import xgboost as xgb from sklearn.model_selection import TimeSeriesSplit # 第一轮粗搜跨度大、步长粗目的是定位参数量级 coarse_grid { max_depth: [3, 5, 7], # 树深日频数据一般不超过 7 learning_rate: [0.01, 0.05, 0.1], # 学习率小学习率配多轮迭代 subsample: [0.7, 0.9], # 行采样抑制过拟合 colsample_bytree: [0.7, 0.9], # 列采样特征多时很有用 min_child_weight: [1, 3, 5], # 叶节点最小样本权重越大越保守 } def grid_search_xgb(X, y, grid, n_splits5): 按时间序列切分做交叉验证禁止随机打乱 tscv TimeSeriesSplit(n_splitsn_splits) best_score, best_params np.inf, None for max_depth in grid[max_depth]: for lr in grid[learning_rate]: for sub in grid[subsample]: for col in grid[colsample_bytree]: for mcw in grid[min_child_weight]: scores [] for tr_idx, va_idx in tscv.split(X): model xgb.XGBRegressor( n_estimators800, # 配合早停实际不会跑满 max_depthmax_depth, learning_ratelr, subsamplesub, colsample_bytreecol, min_child_weightmcw, objectivereg:squarederror, early_stopping_rounds50, random_state42, ) model.fit( X.iloc[tr_idx], y.iloc[tr_idx], eval_set[(X.iloc[va_idx], y.iloc[va_idx])], verboseFalse, ) pred model.predict(X.iloc[va_idx]) scores.append(np.sqrt(np.mean((pred - y.iloc[va_idx]) ** 2))) mean_score np.mean(scores) if mean_score best_score: best_score, best_params mean_score, { max_depth: max_depth, learning_rate: lr, subsample: sub, colsample_bytree: col, min_child_weight: mcw, } return best_params, best_score这段代码有几个点值得单独说明。TimeSeriesSplit是必须的用KFold随机切分会让模型在训练时“看到未来”交叉验证分数会漂亮得不像话但上线预测立刻原形毕露。early_stopping_rounds50配合验证集能让n_estimators不必手工指定实际训练的树数量由验证集损失决定。min_child_weight这个参数容易被忽略它控制叶节点的最小样本权重对抑制过拟合的作用比max_depth更柔和日频小样本场景下建议从 3 开始试。第二轮细搜就在best_params附近取更密的点比如max_depth在最优值上下各取一个整数learning_rate在最优值附近取[0.03, 0.05, 0.08]。两轮下来通常能找到一组比较稳的参数别指望找到全局最优能在验证集上稳定就行。3.2 离散小波变换的分解层数与重构策略DWT 这块有两个参数必须定小波基和分解层数。小波基我一般用db4它在金融时序上的平滑性和局部性比较平衡分解层数按样本长度取日频数据取 4 到 6 层比较常见层数太多会让低频近似分量过于平滑丢掉趋势拐点。import pywt import numpy as np def dwt_denoise(series, waveletdb4, level4, threshold_modesoft): 对价格序列做小波分解对细节系数软阈值去噪后重构 coeffs pywt.wavedec(series, wavelet, levellevel) # 用第一层细节系数的中位数估计噪声标准差MAD 估计抗异常值 sigma np.median(np.abs(coeffs[-1])) / 0.6745 thr sigma * np.sqrt(2 * np.log(len(series))) # 通用阈值 new_coeffs [coeffs[0]] # 近似分量原样保留不参与阈值处理 for detail in coeffs[1:]: new_coeffs.append(pywt.threshold(detail, thr, modethreshold_mode)) return pywt.waverec(new_coeffs, wavelet)[:len(series)]关键在最后一行那个切片。waverec重构出来的长度可能比原序列多一两个点直接拿去和原序列做差会报长度不匹配这个坑我踩过不止一次。阈值这里用的是通用阈值sigma * sqrt(2*ln(N))sigma用第一层细节系数的 MAD 估计而不是标准差因为标准差本身会被极端值带偏而股价序列里最不缺的就是极端值。去噪之后务必把原始序列和去噪序列画在一张图上对比看。如果去噪后的曲线把明显的趋势拐点也抹平了说明阈值太大或者层数太高需要往下调。这一步不能省纯看指标数字是看不出曲线形态问题的。3.3 ARIMA 残差建模与混合模型的加权融合ARIMA 的定阶我用pmdarima的auto_arima先跑一遍拿到一个(p,d,q)的初始值再在附近手工微调。d通常取 1因为价格序列一阶差分后基本平稳p和q一般不超过 5再高就有过拟合嫌疑。import pmdarima as pm from statsmodels.tsa.arima.model import ARIMA def fit_arima(series, seasonalFalse): 先用 auto_arima 定阶再用 statsmodels 拟合以拿到残差 auto pm.auto_arima( series, seasonalseasonal, start_p0, max_p5, start_q0, max_q5, d1, # 一阶差分价格序列的常规操作 information_criterionaic, stepwiseTrue, suppress_warningsTrue, ) order auto.order model ARIMA(series, orderorder).fit() return model.predict(start0, endlen(series) - 1), model.resid, order融合方式有两个选择直接相加或者按验证集误差学习一个权重。直接相加final arima_pred xgb_resid_pred是最朴素也最稳的做法前提是残差预测的均值接近零、没有系统性偏移。如果发现最终预测整体偏高或偏低说明残差模型学到了偏差这时候才考虑加权# 在验证集上学习融合权重 w避免在测试集上偷看 w np.argmin([ np.sqrt(np.mean((arima_val w_ * xgb_val - y_val) ** 2)) for w_ in np.linspace(0, 1, 21) ]) / 20 final_pred arima_pred w * xgb_resid_pred权重一定要在验证集上确定然后固定下来用到测试集千万不要在测试集上调权重那是标准的自欺欺人。4. 实验设计与评价指标让对比结果站得住脚模型跑通只是第一步能不能把“混合模型更好”这个结论讲扎实取决于实验设计。这一章讲三件事数据怎么切、指标怎么算、对比怎么做。4.1 数据划分与滑窗构造数据划分按时间顺序切常见的比例是 6:2:2 或 7:1.5:1.5。我倾向于用 6:2:2因为验证集要同时承担调参和融合权重学习两个任务留多一点更稳。滑窗构造要注意两点。第一窗口长度要覆盖特征里最长的滚动统计量比如用了 20 日均线窗口至少 30 天起步否则前期样本的特征全是 NaN。第二预测步长要明确是预测 t1 的收盘价还是预测 t5 的收益率这两个任务的难度和评价口径完全不同写论文时必须说清楚。我一般做 t1 的收盘价回归因为它和 ARIMA 的原生输出口径一致融合起来最自然。样本量这边给个参考单只股票十年日频数据约 2400 条扣掉预热期和测试集训练样本大约 1400 条验证集约 480 条测试集约 480 条。如果要做多只股票的横向对比建议先在一只股票上把流程跑通再扩展到 3 到 5 只别一上来就上几十只调参时间会失控。4.2 RMSE / MAE / MAPE / R² 的口径统一四个指标各有侧重论文里最好四个都给但要在正文里说明各自说明什么问题。指标公式要点说明什么注意事项RMSE均方误差开根号对大误差敏感惩罚离群预测量纲与价格一致便于直观理解MAE绝对误差平均平均偏差水平抗离群与 RMSE 差距大说明存在极端误差MAPE绝对百分比误差平均相对误差跨股票可比价格接近 0 时会爆炸低价股慎用R²1 - SS_res/SS_tot拟合优度解释方差比例负值说明不如直接取均值有个细节必须统一所有指标都在还原到原始价格量纲之后再计算。如果模型是在差分序列或归一化序列上训练的评估前一定要反变换回去否则 RMSE 的数字会小得毫无意义答辩时被问一句“你这个 RMSE 单位是什么”就答不上来。另外MAPE 在股价预测里其实争议不小因为股价绝对值的变化幅度和百分比变化幅度不是一回事。我的做法是主指标用 RMSE 和 MAEMAPE 作为辅助并在论文里注明它的局限性这样反而显得你想得清楚。4.3 消融实验逐层加模块的对比表这套方案最有说服力的地方是它能做出一条清晰的逐级对比链。我一般会跑五组实验组模型说明A原始 XGBoost不做任何优化默认参数BGS-XGBoost加网格搜索调参CDWT-GS-XGBoost再加小波去噪DARIMA单独的时间序列基线EDWT-ARIMA-GSXGB完整混合模型在测试集上通常能看到这样一条趋势A 到 B 有提升但有限说明调参的边际收益在递减B 到 C 提升比较明显说明数据质量比模型复杂度更重要D 单独用 ARIMA 在中长期趋势上表现尚可但拐点处误差大E 综合了 C 和 D 的优点RMSE 和 MAE 都最低R² 最高。写论文时这张表后面必须跟一段解释说清楚每一级提升来自哪个机制而不是只把数字一贴了事。评委最爱问的就是“你这个提升是调参调出来的还是结构带来的”消融表就是标准答案。5. 踩坑与排查这套混合模型最容易翻车的五个地方下面这五条是我在实际跑这套流程时反复遇到的问题每条都按“现象→原因→解决”写照着排查能省不少时间。第一坑测试集指标好得离谱RMSE 低到不符合常识。现象是模型在测试集上的误差比训练集还小R² 接近 0.99。原因几乎一定是数据泄漏要么特征里混进了未来信息比如用了rolling().mean()却没shift(1)要么标准化时用了全量数据的均值方差要么随机打乱了时间顺序做交叉验证。解决办法是把特征构造函数单独拎出来逐列检查是否存在t时刻之后的信息标准化一律改成在训练集上fit、在测试集上transform。这一条排查完指标通常会“难看”一大截但那才是真实水平。第二坑ARIMA 定阶报错或者拟合不收敛。现象是auto_arima抛LinAlgError或者跑很久不结束。原因通常是序列不平稳、存在缺失值或者d阶数设得不对。解决方法是先做 ADF 检验确认平稳性缺失值用前向填充处理掉d从 1 开始试实在不行就手动指定(p,d,q)而不用自动定阶。另外auto_arima在长序列上很慢可以先在训练集上定阶拿到阶数后固定下来验证集和测试集不再重新定阶。第三坑小波重构后序列长度和原序列对不上。现象是waverec返回的数组比输入长一两个点后面做减法直接报ValueError。原因是离散小波变换的边界延拓机制导致的属于正常现象。解决办法就是代码里那句[:len(series)]或者用pywt.waverec之后配合pywt.pad做对齐。这个问题在换小波基或者换分解层数时会反复出现建议封装成一个函数别在每个脚本里各写一遍。第四坑XGBoost 的网格搜索跑了一整天还没结束。现象是参数组合数上了四位数每个组合还要做五折交叉验证时间直接爆炸。原因是搜索空间设得太宽而且没有利用早停。解决办法是改成两轮搜索第一轮粗搜控制在 100 组以内第二轮细搜控制在 50 组以内同时把n_estimators设大并开early_stopping_rounds实际训练的树数量会远小于设定值。另外可以用n_jobs开多核但注意别把机器跑死留两个核给自己干活。第五坑融合之后效果反而变差。现象是 DWT-ARIMA-GSXGB 的 RMSE 比单独的 DWT-GS-XGBoost 还高。原因通常是融合权重没调好或者 ARIMA 的残差里已经包含了 XGBoost 学不到的东西硬加反而引入噪声。解决办法是先看残差预测的均值如果明显偏离零说明 XGBoost 学出了系统性偏差需要检查特征里是否混入了和残差强相关的未来信息如果均值接近零但融合变差就把权重往 0 调甚至直接退回不使用 ARIMA 的版本。混合模型不是加得越多越好这一点要有心理准备。6. 答辩与验收怎么用三张表讲清这套模型的价值到了验收这一关评委不会逐行看你代码他们看的是你能不能把“为什么这么做”讲清楚。我的经验是准备三张表按顺序讲十五分钟内能讲完。第一张是结构表就是第 2 章那个模块职责表用来说明每个模块的输入输出和数据契约。讲的时候重点强调“每一层都能单独关掉”这句话能直接引出第二张表。第二张是消融表五组实验的 RMSE、MAE、R² 并排列出来每一行后面标注“相比上一组提升了多少”。讲这张表的关键不是念数字而是解释每一级提升的机制调参提升的是模型容量匹配度小波去噪提升的是输入数据质量ARIMA 融合提升的是对线性惯性的覆盖。评委听到机制解释才会认为你理解自己的模型而不是碰巧跑出一个好数字。第三张是误差分布表或图把测试集上的绝对误差按价格区间或者按波动率分组统计。这张表的作用是主动暴露局限比如在高波动区间的误差明显更大说明模型在极端行情下不稳。主动说出局限比被评委问出来要好得多而且能自然过渡到“后续可以怎么改进”。答辩时还有几个高频问题提前准备好答案为什么不用 LSTM样本量、训练成本、可解释性小波基为什么选 db4平滑性与局部性的平衡且是金融时序的常用选择网格搜索为什么不用贝叶斯优化组合数不大时网格搜索更直观且易于复现融合权重为什么在验证集上定避免在测试集上调参。这四个问题答顺了基本就稳了。我自己的习惯是每次答辩前把三张表打印出来对着讲三遍掐表控制在十二分钟留三分钟缓冲。第一次做这套方案的时候我在消融表上栽过跟头——只贴了数字没讲机制被追问了七八分钟场面很难看。从那以后我每次做对比实验都强制在表后面写一段“提升来自哪里”的说明哪怕只有两句话。希望这套结构能帮到你少走一点弯路。本文还有配套的精品资源点击获取