简介:这份资源面向对量化投资与机器学习感兴趣的开发者、金融数据分析初学者及具备一定Python基础的学习者,提供一套基于XGBoost算法预测股票未来n日价格的完整源码方案,可用于理解特征工程、模型训练与预测结果输出的全流程。压缩包共6个文件,包含3个Python脚本、2个文本说明与1个Excel结果文件,整体约18KB,其中脚本分别承担数据获取、工具函数封装与XGBoost建模预测等职责,文本文件用于环境依赖与使用说明,Excel则保存预测输出结果,结构紧凑便于快速上手。目前已有245人学习下载。读者可借此掌握从行情数据读取、特征构造到模型训练与结果落地的完整思路,并参考现有代码调整预测周期与参数,适合作为量化选股与价格趋势预测的入门实践素材。
1. 用 XGBoost 预测 n 日后股价:一个被低估的工程化落地点
很多人第一次听到「用 XGBoost 预测 n 日后的股价」,第一反应是「这不就是量化玄学吗」。但真正做过一轮的人会告诉你,这件事的价值不在预测准不准,而在于它逼着你把数据对齐、特征构造、标签定义、滚动回测这四件事一次性做扎实。XGBoost 在这里的角色不是水晶球,而是一台高效的「特征-标签映射机」:你喂给它过去 n 天的量价、波动、资金结构特征,它输出一个回归值或分类概率,告诉你未来第 n 日相对今天是涨还是跌。它适合两类人:一类是想入门量化但被深度学习劝退的工程师,另一类是想把已有因子体系快速验证一遍的从业者。下面这套流程,我按「能跑通、能复现、能排错」的标准拆开讲。
2. 先想清楚预测目标:回归还是分类,n 取多少
2.1 回归与分类的取舍,别一上来就做价格回归
最常见的翻车方式,是直接把「n 日后的收盘价」当标签做回归。股价绝对值受大盘、除权、停牌影响极大,模型会花大量精力去拟合一个跟你无关的价位中枢。我一般会改成两种更稳的标签:
- 收益率回归:
label = close.shift(-n) / close - 1,预测未来 n 日累计收益率。 - 方向分类:
label = 1 if 未来 n 日收益率 > 阈值 else 0,阈值常用 0 或略高于交易成本。
分类的好处是评估直观(AUC、准确率、召回),坏处是丢掉幅度信息。实操里我会两个都跑一遍,用分类定方向、用回归估幅度,最后合成一个打分。
2.2 n 的取值不是拍脑袋,要和你的持仓周期对齐
n 取 1、3、5、10、20 是常见档位。n 越小,信噪比越低,模型越容易学到噪声;n 越大,样本重叠越严重,训练集和验证集之间会「串味」。经验做法是:n 至少覆盖你一次完整调仓周期。如果你打算每周调一次仓,n 取 5 比较自然;做月度轮动,n 取 20。注意,n 越大,有效独立样本越少,这时必须用滚动窗口而不是随机切分。
import pandas as pd import numpy as np def make_label(df, n=5, mode="cls", threshold=0.0): # 未来 n 日累计收益率 df = df.copy() df["fwd_ret"] = df["close"].shift(-n) / df["close"] - 1 if mode == "reg": df["label"] = df["fwd_ret"] else: # 方向分类,阈值可设为交易成本 df["label"] = (df["fwd_ret"] > threshold).astype(int) # 末尾 n 行没有未来数据,必须丢掉,否则标签是 NaN 会污染训练 df = df.iloc[:-n] if n > 0 else df return df这段代码的关键在最后一行:shift(-n)会让最后 n 行变成 NaN,如果不丢弃,XGBoost 训练时会直接报错或静默丢弃,导致你误以为样本量对不上。threshold建议设成双边手续费加滑点,比如 0.0015,这样模型学到的「涨」才是扣费后还有意义的涨。
2.3 特征工程的三个层次:量价、波动、结构
量价特征是最基础的:过去 n 日的收益率、成交量变化率、换手率。波动特征包括滚动标准差、ATR、高低价差。结构特征稍微进阶:均线偏离度、RSI、MACD 柱值、过去 n 日创新高次数。我一般会构造 30 到 60 个特征,太多会稀释重要性,太少模型欠拟合。
def build_features(df, windows=(3, 5, 10, 20)): df = df.copy() df["ret1"] = df["close"].pct_change() for w in windows: df[f"ret_{w}"] = df["close"].pct_change(w) df[f"vol_std_{w}"] = df["ret1"].rolling(w).std() df[f"vol_ratio_{w}"] = df["volume"] / df["volume"].rolling(w).mean() df[f"ma_dev_{w}"] = df["close"] / df["close"].rolling(w).mean() - 1 # 高低价差与收盘位置 df["hl_spread"] = (df["high"] - df["low"]) / df["close"] df["close_pos"] = (df["close"] - df["low"]) / (df["high"] - df["low"] + 1e-9) return df.dropna()windows的选择要跟 n 呼应:如果 n=5,窗口里至少要有 3 和 5,10 和 20 用来提供中期背景。dropna()会砍掉前 20 行,这是正常代价。注意close_pos分母加了极小值,防止一字板导致除零。
3. 训练与验证:时间序列不能随机切分
3.1 滚动窗口切分,别用 train_test_split
这是血泪经验里排第一的坑。用train_test_split随机打乱,等于让模型在训练时「偷看」未来样本的邻居,验证 AUC 能到 0.7,实盘一上就废。正确做法是滚动窗口:用前 3 年训练、后 1 个月验证,然后窗口整体前移。
from sklearn.model_selection import TimeSeriesSplit def rolling_split(X, y, n_splits=5): tscv = TimeSeriesSplit(n_splits=n_splits) for train_idx, val_idx in tscv.split(X): yield train_idx, val_idxTimeSeriesSplit默认是扩展窗口,训练集越来越大。如果你想要固定长度窗口,可以自己写切片逻辑。验证集一定要在训练集之后,且两者之间留一个 n 日的间隔,避免标签重叠泄漏。
3.2 XGBoost 参数:先稳住学习率和深度
XGBoost 参数多,但真正影响过拟合的就那几个。我一般先用这套保守配置跑基线:
| 参数 | 建议值 | 作用 |
|---|---|---|
| learning_rate | 0.03~0.05 | 越小越稳,配合更多轮数 |
| max_depth | 3~5 | 金融数据信噪比低,深树必过拟合 |
| subsample | 0.7~0.8 | 行采样,抗噪 |
| colsample_bytree | 0.7~0.8 | 列采样,防特征共线 |
| min_child_weight | 5~10 | 叶子最小样本权重,越大越保守 |
| reg_lambda | 1~5 | L2 正则,压住极端值 |
| n_estimators | 300~800 | 配合 early_stopping |
import xgboost as xgb params = { "objective": "binary:logistic", # 分类用这个,回归换 reg:squarederror "learning_rate": 0.04, "max_depth": 4, "subsample": 0.8, "colsample_bytree": 0.8, "min_child_weight": 8, "reg_lambda": 2.0, "eval_metric": "auc", "tree_method": "hist", "seed": 42, } dtrain = xgb.DMatrix(X_train, label=y_train) dval = xgb.DMatrix(X_val, label=y_val) model = xgb.train( params, dtrain, num_boost_round=800, evals=[(dval, "val")], early_stopping_rounds=50, verbose_eval=100, )tree_method="hist"在大样本上比默认快很多。early_stopping_rounds=50是后悔药:验证集 AUC 连续 50 轮不涨就停,避免你手动猜轮数。seed固定住,否则每次跑出来的特征重要性排序会飘,没法对比。
3.3 特征重要性与 SHAP:别只看 gain
XGBoost 自带的feature_importances_用 gain 统计,容易偏向高基数特征。我习惯再跑一遍 SHAP,看每个特征对预测的边际贡献方向。如果发现某个特征重要性极高但方向反直觉,大概率是数据泄漏,比如用了未来才公布的财务字段。
import shap booster = model explainer = shap.TreeExplainer(booster) shap_values = explainer.shap_values(X_val) # 汇总每个特征的平均绝对贡献 shap.summary_plot(shap_values, X_val, plot_type="bar")SHAP 在树模型上是精确解,比近似方法可靠。跑完重点看两件事:一是排名前五的特征是否符合业务逻辑,二是是否存在某个特征单独主导预测,那通常意味着泄漏或过拟合。
4. 避坑与排查:五个真实踩过的坑
4.1 验证集 AUC 很高,实盘一塌糊涂
现象:离线 AUC 0.72,模拟盘胜率不到 50%。原因:随机切分导致标签重叠泄漏,或者特征里混入了未来信息(比如用当日收盘价算的指标去预测当日之后)。解决:改用滚动窗口,并在训练集和验证集之间留 n 日缓冲;逐个检查特征的计算时点,确保只用 t 日及之前的数据。
4.2 样本极度不平衡,模型全预测成「跌」
现象:分类标签里涨的只占 35%,模型输出全是 0。原因:默认阈值 0.5 偏向多数类。解决:用scale_pos_weight调权重,或者改用aucpr作为评估指标,再或者把阈值下调到 0.4 附近,用验证集 PR 曲线选最优点。
4.3 特征里有 NaN,XGBoost 不报错但结果诡异
现象:训练正常,但预测值分布异常集中。原因:XGBoost 默认把 NaN 当缺失处理并学一个默认方向,但如果某特征缺失率超过 30%,这个方向就是噪声。解决:先统计缺失率,超过 30% 的特征直接删;少量缺失用前向填充,别用均值填充,均值会抹掉时间结构。
4.4 换手率特征在停牌股上爆炸
现象:某些样本的换手率是几万倍。原因:停牌期间成交量为 0,复牌后除零或比值异常。解决:在特征构造前过滤掉停牌日,或者对成交量比值做 winsorize 缩尾,把 1% 和 99% 分位外的值截断。
4.5 模型每次跑结果都不一样
现象:同样的数据,两次训练 AUC 差 0.03。原因:XGBoost 默认并行建树,浮点求和顺序不固定;加上 subsample 和 colsample 的随机性。解决:固定seed和nthread=1做对比实验,确认稳定后再开并行。生产环境接受波动,但调参阶段必须可复现。
5. 把模型变成可用的打分器:滚动预测与阈值落地
5.1 滚动预测的工程写法
训练完不是终点,你要的是每天收盘后对全市场股票打分。做法是:用最近一段窗口重新训练,然后对最新截面做predict。注意预测时的特征必须和训练时同分布,所以每次都要重新计算滚动特征。
def daily_score(model, df_today, feature_cols): # df_today 是当日截面,已算好特征 X_today = df_today[feature_cols] dmat = xgb.DMatrix(X_today) df_today = df_today.copy() df_today["score"] = model.predict(dmat) # 按分数排序,取 top K return df_today.sort_values("score", ascending=False)feature_cols必须和训练时完全一致,顺序都不能变,否则 DMatrix 会按列名对齐失败或静默错位。我一般把特征列表存成 json,训练和预测共用同一份。
5.2 阈值不是 0.5,要用验证集 PR 曲线定
分类模型输出概率后,落地时要选一个阈值决定「买不买」。0.5 是默认值,但金融场景里正样本少,最优阈值往往在 0.55 到 0.65 之间。用验证集的 precision-recall 曲线,选 F1 最大或召回满足下限时精度最高的点。
from sklearn.metrics import precision_recall_curve prec, rec, thr = precision_recall_curve(y_val, val_prob) f1 = 2 * prec * rec / (prec + rec + 1e-9) best_thr = thr[f1.argmax()] print("best threshold:", best_thr)thr的长度比prec少 1,索引时注意对齐。选出来的阈值要拿到下一段滚动窗口再验证一次,防止过拟合到某一段行情。
5.3 一个容易被忽略的细节:分数标准化
不同时间窗口训练出的模型,输出概率的分布会漂移。今天 top 10 的分数是 0.62,下个月可能整体降到 0.55。如果直接用绝对阈值,会出现某段时间完全没信号。我的习惯是每天对全市场分数做横截面排序,取前 5% 或前 50 只,而不是卡固定概率。这样模型换了一茬,选股逻辑依然连续。
5.4 验证方法:除了 AUC,还要看分组收益
离线指标好看不代表能赚钱。我会把验证集按预测分数分成 5 组,看每组未来 n 日的平均收益是否单调。如果 top 组和 bottom 组收益差明显且单调,说明模型有区分力;如果中间组乱跳,说明模型只抓到了极端值,稳定性差。
| 分组 | 样本数 | 平均 n 日收益 | 胜率 |
|---|---|---|---|
| Q1(最低分) | 1200 | -0.8% | 44% |
| Q2 | 1200 | -0.2% | 48% |
| Q3 | 1200 | 0.1% | 50% |
| Q4 | 1200 | 0.5% | 53% |
| Q5(最高分) | 1200 | 1.3% | 57% |
这张表比 AUC 更能说明问题。如果 Q5 和 Q1 的收益差不到 0.5%,那这个模型基本没有实战价值,回去查特征和标签。
5.5 我最后保留的习惯
跑完任何一版模型,我都会把特征列表、参数、滚动窗口起止日期、验证集分组收益这四样东西存进一个版本目录,命名带日期。下次想对比新想法,直接翻旧记录,不用凭记忆猜「上次那个参数是多少」。这个习惯帮我省掉了无数次重复试错。XGBoost 预测股价这件事,模型本身不复杂,复杂的是数据纪律和验证纪律。把这两样守住,哪怕 AUC 只有 0.55,配合合理的仓位管理也能做出正期望;守不住,AUC 0.75 也是自欺欺人。希望帮到你。
本文还有配套的精品资源,点击获取