简介:这份PDF为兰州理工大学软件工程硕士学位论文,聚焦基于XGBoost的混合模型在股票价格预测中的应用,适合金融数据分析、机器学习与模式识别方向的学习者和研究者参考。资源包共1个PDF文件,大小约3.48MB,内容完整呈现论文封面、目录、绪论、理论基础、模型设计与实验分析等章节。论文从股票预测的背景与难点切入,梳理集成学习中的决策树、CART、GBDT与XGBoost原理,并介绍AR、MA、ARMA、ARIMA等时间序列模型,进而讨论混合模型的构建思路、特征工程与参数调整。实验部分采用MSE、MAE和R²等指标对比不同模型表现,分析非线性关系捕捉、异常点处理与预测稳定性,同时提及过拟合等局限及后续改进方向。已有661人学习,可为相关课题的模型选型、实验设计与论文写作提供较完整的参考框架。
1. 先别急着上深度学习:这套 XGBoost 混合模型把股票预测拆成了三段
很多人做股票预测第一反应是上深度学习,但我拆完这份 XGBoost 混合模型资料后,反而先把 LSTM 放一边。它把问题拆成三段:先用网格搜索把 XGBoost 的参数压到可控区间,再用离散小波变换给价格序列降噪,最后用 ARIMA 抓线性趋势、XGBoost 啃残差。整套实验从单模型基线一路对比到 DWT-ARIMA-GSXGB,评价指标、数据切分和参数寻优都落在同一套流程里。适合想复现金融时序混合模型、补特征工程和实验对比的人,也适合拿它当论文复现或二次开发底座。别把它当交易信号,它更像一张可执行的实验路线图。
2. 从论文目录到可复现流水线:数据、特征、模型、评估四层怎么拆
2.1 资源提供的五层实验结构:不是单个模型,而是一条递进对比链
这份资料最值钱的地方不是某一个模型公式,而是它把实验做成了递进链。第一层是问题定义和理论基础,讲清楚股票价格序列为什么难预测:非平稳、噪声大、线性和非线性混在一起。第二层是 GS-XGBoost,用网格搜索给 XGBoost 找参数组合,先把单模型基线打稳。第三层是 DWT-GS-XGBoost,在进入模型之前用离散小波变换做多尺度分解和降噪,再重构回价格序列。第四层是 DWT-ARIMA-GSXGB,把 ARIMA 对线性趋势的捕捉能力和 XGBoost 对残差非线性关系的拟合能力拼起来。第五层是统一评价和对比,把 RMSE、MAE、MAPE、R² 放在同一张表里看相对排序。
这种结构对二次开发很友好,因为每次只加一个变量。你先跑原始 XGBoost,再跑 GS-XGBoost,能看出网格搜索带来的增益;再加 DWT,能看出降噪有没有用;最后加 ARIMA 残差修正,能看出线性部分被剥离后 XGBoost 是否更专注。很多人一上来就把三个模型揉在一起,结果指标涨了也不知道是谁的功劳,指标跌了更不知道拆哪一块,这种黑匣子式实验在论文复现里很容易翻车。
技术栈方面,常见组合是 Python 打底,pandas 和 numpy 做数据整理,scikit-learn 做切分、网格搜索和评价,xgboost 做回归主模型,statsmodels 做 ARIMA,PyWavelets 做离散小波变换,matplotlib 出对比图。这套栈的好处是每个环节都能单独替换,不绑死某个深度学习框架,跑在普通笔记本上也能完成中小规模实验。XGBoost 对表格特征、缺失值和特征交互比较友好,ARIMA 对线性自相关有明确解释,DWT 能把价格序列里的高频噪声压下去,网格搜索虽然暴力,但胜在可复现,参数空间写进配置文件就能反复跑。
| 层次 | 常见工具 | 在项目里的职责 | 改动建议 |
|---|---|---|---|
| 数据层 | pandas、numpy | 读取行情、对齐日期、构造目标列 | 换数据源时先统一列名和频率 |
| 特征层 | pandas、numpy | 滞后特征、滚动统计量、收益率 | 所有特征先 shift,再进模型 |
| 分解层 | PyWavelets | 小波分解、阈值降噪、重构 | 先固定小波基和层数做消融 |
| 模型层 | xgboost、statsmodels | XGBoost 回归、ARIMA 线性建模 | 先基线后混合,别一次堆满 |
| 评估层 | scikit-learn、matplotlib | RMSE、MAE、MAPE、R²、对比图 | 切分方式和口径写进配置 |
2.2 目录结构:按数据、特征、模型、评估四层拆
我一般会把这类实验拆成四层目录,不把所有逻辑塞进一个 notebook。notebook 适合看数据分布和画图,真正要反复跑的流程放进 src,参数放进 configs,输出放进 outputs。这样你改小波层数、改网格范围、换评价指标时,不用满文件找变量。下面这份目录清单是按论文里的三组模型整理出来的复现骨架,文件职责写得很直白,照着建就能跑。
| 路径 | 职责 | 备注 |
|---|---|---|
| data/raw/ | 原始行情文件 | 只读,不在这里改列名 |
| data/processed/ | 清洗后的训练数据 | 保留时间戳和原始价格 |
| configs/params.yaml | 网格、窗口、小波参数 | 实验口径统一放这里 |
| src/features.py | 滞后特征、滚动统计量 | 所有 shift 集中管理 |
| src/dwt.py | 小波分解、阈值、重构 | 注意长度对齐 |
| src/arima_model.py | ARIMA 拟合和残差输出 | 阶数可配置 |
| src/xgb_model.py | XGBoost 训练和网格搜索 | 基线模型也走这里 |
| src/train.py | 串起数据、特征、模型、评估 | 命令行入口 |
| src/evaluate.py | 指标计算和对比表 | 口径只写一次 |
| outputs/metrics.csv | 每次实验的指标记录 | 建议加时间戳 |
| outputs/figures/ | 预测对比图、残差图 | 论文插图来源 |
配置文件不要写得太花,能把关键参数固定住就行。下面这段 YAML 是常见写法,model控制跑哪一档实验,split控制时间序列切分,xgb_grid控制网格搜索范围,dwt控制小波基和层数。你后面做消融实验时,只改一个块,其他块不动,指标才有可比性。
# configs/params.yaml model: gs_xgb # 可选:xgb / gs_xgb / dwt_gs_xgb / dwt_arima_gsxgb target_col: close split: n_splits: 5 test_size: 20 xgb_grid: n_estimators: [200, 400] max_depth: [3, 5] learning_rate: [0.03, 0.1] subsample: [0.8] colsample_bytree: [0.8] dwt: wavelet: db4 level: 3 threshold_scale: 1.0 arima: order: [2, 1, 2]这段配置的逻辑是把“实验条件”从代码里挪出来。model是实验开关,方便你先跑基线再跑混合模型;n_splits和test_size决定时间序列交叉验证的折数和每折测试长度;xgb_grid是网格搜索的空间,先放小一点,跑通后再扩;dwt里的wavelet和level直接影响降噪强度,threshold_scale是阈值缩放系数,调大压噪更狠,但也可能把有效波动一起削掉;arima.order是 ARIMA 的 p、d、q,先固定一组能收敛的阶数,再谈优化。
2.3 环境准备与最小运行命令
环境不用追求花哨,能稳定复现就行。我一般先建独立虚拟环境,再装基础包,最后用一条命令跑基线。下面这条命令先跑gs_xgb,确认数据读取、特征构造、网格搜索、指标输出整条链路是通的,再去跑dwt_arima_gsxgb。很多人一开始就冲最复杂的混合模型,结果报错堆在 ARIMA 索引对齐上,连基线有没有跑通都不知道,这种顺序很吃亏。
python -m venv .venv source .venv/bin/activate pip install -U pip pip install pandas numpy scikit-learn xgboost statsmodels PyWavelets matplotlib pyyaml # 先跑 GS-XGBoost 基线 python -m src.train --config configs/params.yaml --model gs_xgb # 基线通了再跑 DWT-ARIMA-GSXGB python -m src.train --config configs/params.yaml --model dwt_arima_gsxgb命令里的--config指向参数文件,--model覆盖配置里的实验开关,这样同一份代码可以跑四档模型。失败时先看三件事:PyWavelets的包名是PyWavelets,导入时写pywt;xgboost和scikit-learn的版本要能对上,网格搜索的 scoring 名称在不同版本里有差异;ARIMA 的预测索引是按训练序列长度往后推的,start和end写错会直接报越界。先把gs_xgb跑通,再逐层加 DWT 和 ARIMA,排查成本会低很多。
提示:每次实验把
outputs/metrics.csv另存一份带时间戳的文件,别让新结果覆盖旧结果。混合模型的对比结论全靠这些记录撑着。
3. GS-XGBoost 参数寻优:网格搜索和时间序列交叉验证的落地细节
3.1 为什么先做 GS-XGBoost:基线不稳,后面混合模型都是空中楼阁
GS-XGBoost 是整份资料的第一个可运行模型,也是后面所有对比的锚点。XGBoost 本身参数不少,学习率、树深、采样比例、正则项都会影响拟合能力。手工拍参数不是不行,但论文复现最怕“参数靠感觉”,今天跑一个结果,明天换台机器又对不上。网格搜索的价值不是一定找到全局最优,而是把参数搜索过程固定下来,让每次实验都能复现同一组候选组合和同一套评价口径。
参数空间不要一上来铺满。常见做法是先粗后细:第一轮只调max_depth、learning_rate、n_estimators,把模型容量定在一个合理区间;第二轮再动subsample、colsample_bytree、min_child_weight;第三轮才看gamma、reg_alpha、reg_lambda这些正则项。股票价格序列样本量通常不大,树太深、学习率太小、树太多,训练集 loss 会很好看,测试集一塌糊涂。网格搜索如果一次铺几千组,跑得慢不说,还容易在验证集上过拟合。
| 参数 | 作用 | 常用范围 | 改动建议 |
|---|---|---|---|
| n_estimators | 树的数量 | 200~800 | 先固定中等值,配合学习率调 |
| max_depth | 单棵树深度 | 3~7 | 股票数据先从 3 或 4 起 |
| learning_rate | 学习率 | 0.01~0.1 | 越小越稳,但树要更多 |
| subsample | 行采样比例 | 0.7~0.9 | 防过拟合,别低于 0.6 |
| colsample_bytree | 列采样比例 | 0.7~0.9 | 特征不多时别压太狠 |
| min_child_weight | 叶子最小样本权重 | 1~5 | 样本少时往上加 |
| gamma | 分裂最小损失下降 | 0~0.3 | 指标抖就加一点 |
| reg_alpha / reg_lambda | L1 / L2 正则 | 0~0.1 / 1~5 | 先动 lambda,再动 alpha |
3.2 参数网格与时间序列交叉验证代码
下面这段代码是 GS-XGBoost 的核心骨架。TimeSeriesSplit保证每折训练集都在测试集前面,GridSearchCV负责遍历参数组合,XGBRegressor用平方误差做回归。注意scoring用负 RMSE,因为 scikit-learn 的网格搜索默认是分数越大越好,误差指标要取负号。refit=True表示找到最优参数后自动用全量训练集重训一次,后面直接拿search.best_estimator_预测。
import numpy as np from xgboost import XGBRegressor from sklearn.model_selection import GridSearchCV, TimeSeriesSplit def train_gs_xgb(X_train, y_train, n_splits=5, test_size=20): # 时间序列交叉验证:训练集始终在测试集之前 tscv = TimeSeriesSplit(n_splits=n_splits, test_size=test_size) # 先给小网格,跑通后再扩 param_grid = { "n_estimators": [200, 400], "max_depth": [3, 5], "learning_rate": [0.03, 0.1], "subsample": [0.8], "colsample_bytree": [0.8], "min_child_weight": [1, 3], } base = XGBRegressor( objective="reg:squarederror", random_state=42, n_jobs=-1, ) search = GridSearchCV( estimator=base, param_grid=param_grid, scoring="neg_root_mean_squared_error", cv=tscv, n_jobs=-1, verbose=1, refit=True, ) search.fit(X_train, y_train) return search.best_estimator_, search.best_params_, search.best_score_这段代码的逻辑是先用TimeSeriesSplit把训练集切成“前面训练、后面验证”的滚动折,再用GridSearchCV对每个参数组合做交叉验证,最后按负 RMSE 选最优。n_splits控制折数,折数太少结果不稳,折数太多每折训练样本变短;test_size控制每折验证长度,要和你的预测周期匹配,做 5 日预测就别把test_size设成 1。param_grid里先只放 6 个参数,跑通后再加gamma、reg_alpha、reg_lambda。random_state=42固定随机性,n_jobs=-1用满 CPU,verbose=1能看到搜索进度。
注意:别用
KFold(shuffle=True)做股票时序交叉验证。随机打乱会把未来样本混进训练集,指标会虚高,后面上真实回测会直接翻车。
3.3 训练、预测与评价指标口径
模型训完只是第一步,指标口径不统一,后面三组模型没法比。下面这段评价函数把 RMSE、MAE、MAPE、R² 放在一起算,safe_mape对真实值接近零的样本做了保护。股票价格水平值一般不会为零,但如果你把目标换成收益率或差分序列,零值就会出现,直接算 MAPE 会炸出无穷大。R² 在非平稳价格序列上容易虚高,因为价格水平本身带趋势,模型只要跟着趋势走就能拿到不错的拟合优度,所以它只能作为辅助指标,不能单独拿来下结论。
import numpy as np from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score def safe_mape(y_true, y_pred, eps=1e-8): y_true = np.asarray(y_true, dtype=float) y_pred = np.asarray(y_pred, dtype=float) mask = np.abs(y_true) > eps if mask.sum() == 0: return np.nan return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 def evaluate(y_true, y_pred): rmse = np.sqrt(mean_squared_error(y_true, y_pred)) mae = mean_absolute_error(y_true, y_pred) mape = safe_mape(y_true, y_pred) r2 = r2_score(y_true, y_pred) return {"RMSE": rmse, "MAE": mae, "MAPE": mape, "R2": r2}这段代码里,RMSE对大误差更敏感,MAE更直观,MAPE是百分比误差,方便跨价格量级比较,R²看解释方差比例。eps是零值保护阈值,别设太大,否则会把正常小值也过滤掉。真正做模型对比时,我一般会同时记录价格水平和一阶差分两套指标:价格水平看整体拟合,差分看方向变化。只报价格水平 R²,很容易把“跟着趋势走”误判成“模型学到了规律”。
3.4 实验结果怎么读:别只看 R²
GS-XGBoost 的实验结果要放在同一张表里读,不能只看单个数字。第一列写模型,第二列写切分方式,第三列写预测目标,后面才是 RMSE、MAE、MAPE、R²。原始 XGBoost 和 GS-XGBoost 的差别只在参数有没有搜过,其他条件必须完全一致:同一段数据、同一个特征集、同一个训练测试切分、同一个随机种子。否则你看到指标涨了,可能只是测试集换了一段行情。
| 模型 | 切分方式 | 预测目标 | RMSE | MAE | MAPE | R² | 备注 |
|---|---|---|---|---|---|---|---|
| XGBoost | TimeSeriesSplit | 收盘价 | 记录 | 记录 | 记录 | 记录 | 手工参数 |
| GS-XGBoost | TimeSeriesSplit | 收盘价 | 记录 | 记录 | 记录 | 记录 | 网格搜索 |
| GS-XGBoost | TimeSeriesSplit | 一阶差分 | 记录 | 记录 | 记录 | 记录 | 看方向变化 |
读结果时先看相对排序,再看绝对数值。如果 GS-XGBoost 在价格水平上提升明显,但一阶差分上没提升,说明网格搜索主要优化了趋势拟合,对短期波动帮助有限。如果训练集误差很低、测试集误差很高,先别怀疑数据不够,先查特征有没有泄漏、切分有没有打乱、标准化有没有在全量数据上做。参数搜索的最优组合只是验证集上的最优,不是未来行情上的最优,这一点在股票时序里尤其要认。
4. DWT 与 ARIMA-GSXGB 混合:降噪、残差修正和特征防泄漏
4.1 DWT-GS-XGBoost 的数据流:先分解再重构,别把未来信息混进来
DWT 这一层的作用是把价格序列拆成不同频率的分量,低频保留趋势,高频压噪声,再用阈值处理细节系数,最后重构回一条更平滑的序列。常见做法是用pywt.wavedec做多层分解,对细节系数做软阈值,再用pywt.waverec重构。小波基选db4比较常见,层数一般 2 到 4 层,层数太多会把有效波动也当成噪声削掉。模式用periodization可以保证重构长度和原序列对齐,省掉后面截断的麻烦。
import numpy as np import pywt def dwt_denoise(series, wavelet="db4", level=3, threshold_scale=1.0): arr = np.asarray(series, dtype=float) # periodization 模式便于重构后长度对齐 coeffs = pywt.wavedec(arr, wavelet=wavelet, level=level, mode="periodization") # 用最高频细节系数估计噪声水平 sigma = np.median(np.abs(coeffs[-1])) / 0.6745 uthresh = threshold_scale * sigma * np.sqrt(2 * np.log(len(arr))) # 近似分量保留趋势,细节分量做软阈值 coeffs[1:] = [pywt.threshold(c, value=uthresh, mode="soft") for c in coeffs[1:]] rec = pywt.waverec(coeffs, wavelet=wavelet, mode="periodization") return rec[: len(arr)]这段代码的关键参数是wavelet、level和threshold_scale。wavelet换小波基会改变分解形状,db4是常用起点;level控制分解层数,层数越多低频越平滑;threshold_scale控制阈值强度,调大压噪更狠,调小保留更多波动。sigma用最高频细节系数的中位数估计噪声,uthresh是通用阈值。注意这段代码是在整条序列上估计阈值,严格来说有前视偏差,做论文复现可以先用它跑通,做真实回测时要改成滚动窗口:每个窗口只用窗口内数据估阈值,或者训练集估一次阈值,测试集沿用训练阈值。
4.2 ARIMA 抓线性趋势,XGBoost 啃残差:DWT-ARIMA-GSXGB 的组合方式
DWT-ARIMA-GSXGB 的组合思路不复杂:先用 DWT 降噪,再用 ARIMA 拟合线性部分,把 ARIMA 的残差交给 GS-XGBoost 去学非线性关系,最后把 ARIMA 预测和 XGBoost 残差预测相加。这样 ARIMA 负责它擅长的自相关和趋势,XGBoost 负责它擅长的特征交互和残差修正。下面这段代码是核心骨架,order先用配置文件里固定的一组阶数,跑通后再做阶数搜索。arima.predict的start和end是按训练序列长度往后推的,测试集预测从len(y_train)开始。
import numpy as np from statsmodels.tsa.arima.model import ARIMA from xgboost import XGBRegressor def fit_arima_xgb_hybrid(y_train, y_test, X_train, X_test, order=(2, 1, 2)): # ARIMA 拟合线性部分 arima = ARIMA(y_train, order=order).fit() arima_train_pred = arima.predict(start=0, end=len(y_train) - 1) arima_test_pred = arima.predict( start=len(y_train), end=len(y_train) + len(y_test) - 1, ) # 残差交给 XGBoost resid_train = np.asarray(y_train) - np.asarray(arima_train_pred) xgb = XGBRegressor( objective="reg:squarederror", n_estimators=400, max_depth=4, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, random_state=42, n_jobs=-1, ) xgb.fit(X_train, resid_train) resid_test_pred = xgb.predict(X_test) # 线性预测 + 非线性残差修正 final_pred = np.asarray(arima_test_pred) + np.asarray(resid_test_pred) return final_pred, arima_test_pred, resid_test_pred这段代码里,order=(2,1,2)是 ARIMA 的 p、d、q,d=1表示做一阶差分,适合非平稳价格序列;arima_train_pred是样本内拟合值,用来算训练残差;arima_test_pred是样本外预测;resid_train是 ARIMA 没解释掉的部分;xgb用特征矩阵去拟合残差;final_pred是两部分相加。这里最容易出错的是索引对齐:X_train和y_train长度必须一致,X_test和y_test长度必须一致,DWT 重构后的序列要截到原长度,否则 ARIMA 和 XGBoost 的残差对不上。跑失败时先打印四个长度,别急着调参。
4.3 特征工程与防泄漏:滞后特征、滚动统计量怎么造
混合模型再复杂,特征泄漏一样白搭。股票时序最常见的泄漏是拿当天收盘价预测当天收盘价,或者用全量数据算滚动均值再切分。我一般会把所有特征统一放在features.py里,价格类特征一律先shift(1),滚动统计量也先 shift 再 rolling,确保第 t 行特征只能看到 t-1 及以前的信息。下面这段代码给了滞后价格、滚动均值、滚动标准差和一日收益率,列名清楚,后面换数据源也好改。
import pandas as pd def make_lag_features(df, target_col="close", lags=(1, 2, 3, 5, 10), windows=(5, 10, 20)): out = df.copy() for lag in lags: out[f"lag_{lag}"] = out[target_col].shift(lag) # 先 shift 再 rolling,避免把当前值算进窗口 for w in windows: out[f"roll_mean_{w}"] = out[target_col].shift(1).rolling(w).mean() out[f"roll_std_{w}"] = out[target_col].shift(1).rolling(w).std() out["ret_1"] = out[target_col].pct_change().shift(1) return out.dropna()这段代码的lags控制回看天数,windows控制滚动窗口长度,shift(1)是所有防泄漏动作的核心。dropna()会删掉前面因为滞后产生的空行,所以特征构造完之后再切训练测试集,别先切再各自构造,否则两边行数对不上。滚动标准差可以当波动率代理,收益率特征可以缓解价格非平稳。特征不是越多越好,滞后阶数铺太大,样本量会被吃掉,XGBoost 也容易在噪声特征上过拟合。
4.4 三组模型怎么排同一张表
对比表要把“每次只加一个变量”写清楚。原始 XGBoost 是纯非线性基线;GS-XGBoost 只加网格搜索;DWT-GS-XGBoost 在 GS-XGBoost 前面加小波降噪;DWT-ARIMA-GSXGB 再加 ARIMA 残差修正。表格里除了指标,还要写输入序列、线性建模、非线性建模和关键参数,这样别人看表就知道差异在哪。
| 模型 | 输入 | 线性部分 | 非线性部分 | 关键参数 | 评价重点 |
|---|---|---|---|---|---|
| XGBoost | 原始价格+特征 | 无 | XGBoost | 手工参数 | 基线 |
| GS-XGBoost | 原始价格+特征 | 无 | GS-XGBoost | 网格搜索 | 参数增益 |
| DWT-GS-XGBoost | 小波降噪后价格+特征 | 无 | GS-XGBoost | 小波基、层数、阈值 | 降噪增益 |
| DWT-ARIMA-GSXGB | 小波降噪后价格+特征 | ARIMA | GS-XGBoost 拟合残差 | ARIMA 阶数、XGB 参数 | 混合增益 |
读这张表时先看消融顺序。如果 DWT 加进去后 RMSE 反而变大,先别否定小波,检查层数是不是太深、阈值是不是太狠、重构长度有没有截对。如果 ARIMA 残差修正后指标变差,检查 ARIMA 阶数是不是拍脑袋定的,残差是不是还有明显自相关。混合模型不是堆得越多越好,每一层都要在验证集上证明自己有用,不然就是给自己增加排查成本。
5. 避坑与排查:股票时序项目最容易翻车的五个地方
5.1 数据切分与泄漏类
现象:交叉验证分数很漂亮,一上样本外测试就崩,RMSE 突然翻倍,预测曲线明显滞后。
原因:用了随机KFold或shuffle=True,把未来样本混进训练集;或者在全量数据上做标准化、算滚动统计量、估小波阈值,测试集信息提前漏进训练过程。
解决:时间序列一律用TimeSeriesSplit或自己写 walk-forward,所有统计量只在训练窗口内 fit,测试窗口只 transform。小波阈值也一样,训练集估一次,测试集沿用,别在整条序列上重新估。下面这段 walk-forward 骨架可以直接替换随机切分。
import numpy as np def walk_forward_splits(n_samples, train_size, test_size): start = train_size while start + test_size <= n_samples: yield np.arange(0, start), np.arange(start, start + test_size) start += test_size这段代码按时间顺序滚动产出训练索引和测试索引,train_size是初始训练窗口,test_size是每次向前推进的测试窗口。它不保证每折训练集等长,但保证训练永远在测试之前。你可以把它包成cv传给网格搜索,也可以在外层循环里手动训练和评估。排查泄漏时先打印每折的训练最大日期和测试最小日期,确认没有重叠。
5.2 小波与 ARIMA 的模型类问题
现象:DWT 降噪后模型反而更差,或者 ARIMA 残差图里还有明显周期。
原因:小波层数太深,把有效波动当噪声削掉;阈值缩放系数太大,细节系数被压平;ARIMA 阶数靠猜,残差不是白噪声,XGBoost 拟合的是还有结构的残差。
解决:小波先固定db4,层数从 2 开始,一层一层加,每加一层看验证集 RMSE;阈值系数先设 1.0,再试 0.5 和 1.5,别一次跳太大。ARIMA 先用 ADF 检验看差分阶数,再看 ACF/PACF 定 p 和 q,残差画自相关图,残差还有峰就加阶数或换模型。混合模型里 ARIMA 不是越复杂越好,残差越接近白噪声,XGBoost 越容易学到东西。
现象:网格搜索跑了几百组,最优参数在验证集上很好,测试集一塌糊涂。
原因:参数空间铺太大,验证集被反复使用,等价于在验证集上过拟合;树太深、学习率太小、树太多,训练集拟合过度。
解决:先小网格后大网格,每轮只动 2 到 3 个参数;加early_stopping_rounds或直接限制n_estimators;固定random_state,每次实验只改一个变量。别把“网格越大越专业”当真理,股票时序样本少,参数搜索的边际收益很快会掉下来。
5.3 工程复现与指标口径类
现象:换一台机器跑,指标对不上;或者 MAPE 突然变成inf。
原因:随机种子没固定,数据列顺序变了,特征构造顺序变了;MAPE 分母出现零或接近零的值,收益率序列里很常见。
解决:配置文件里固定random_state,数据读进来先按日期排序再按列名排序,特征构造函数只在一个文件里改。MAPE 用safe_mape过滤接近零的真实值,同时记录 RMSE 和 MAE,别只盯一个百分比指标。每次实验把配置文件、数据版本、指标结果一起存档,后面写对比表才不会对不上账。
提示:把“切分方式、预测目标、评价指标”写进实验记录模板,比事后回忆可靠。股票时序实验最怕口径漂移,跑着跑着就不知道在比什么了。
6. 二次开发:把三模型流水线改成自己的实验台
这套资料最适合二次开发的地方,是它已经把“基线—调参—降噪—残差混合”拆成了可替换模块。你可以先换数据源,把收盘价目标换成收益率或对数差分,观察模型在平稳序列上的表现;再换分解方法,把 DWT 换成经验模态分解或变分模态分解,比较不同降噪路径对残差的影响;最后换基模型,把 XGBoost 换成 LightGBM 或 CatBoost,保持 ARIMA 残差框架不变。每次只动一个模块,其他模块锁死,指标才有解释力。
验证方法我一般用 walk-forward 加固定口径。下面这段骨架把训练窗口、测试窗口和模型训练串起来,外层滚动,内层只做时间序列验证。你可以把它包成函数,传入不同的模型工厂,跑同一张对比表。
def run_walk_forward(X, y, model_factory, train_size=200, test_size=20): records = [] for train_idx, test_idx in walk_forward_splits(len(y), train_size, test_size): X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y.iloc[train_idx], y.iloc[test_idx] model = model_factory() model.fit(X_train, y_train) pred = model.predict(X_test) records.append(evaluate(y_test, pred)) return records这段代码的model_factory是模型工厂,返回任何带fit和predict的对象;train_size和test_size控制滚动窗口;evaluate沿用前面的统一指标函数。改造时最容易犯的错是只改模型不改切分,结果新旧模型不在同一口径上比较。我的习惯是先把切分和评价写进配置,再
本文还有配套的精品资源,点击获取