机器学习股票预测:从数据准备到回测评估的完整指南 简介基于机器学习的股票预测与分析系统Python毕业设计项目内含完整源码与配套文档说明主要面向计算机相关专业学生以及需要项目实战练习的课程设计、期末大作业学习者。项目完整覆盖股票历史数据获取、数据清洗与预处理、机器学习建模、模型评估及结果可视化等环节有助于理解线性回归、决策树、支持向量机、CNN与LSTM等算法的落地流程。资源包共2000个文件总体积约693MB其中包含156个CSV行情数据、112个NPZ特征数据、39个PTH模型权重、9个Python源文件以及大量PNG可视化图表和MD/XML说明文档目录分类清晰。该项目已有55人学习下载源码与文档经过严格调试、确保可直接运行读者既可快速跑通股票预测流程也能参考其中的数据划分、特征处理、模型调参与误差分析模块用于扩展自己的实验或毕业设计。1. 机器学习股票预测回测曲线再漂亮也要先回答数据泄漏在毕业设计里股票预测是机器学习方向最常见也最容易翻车的题目。它看起来足够省事行情数据免费可得pandas 读进来就能造特征套一个分类器得到涨跌结果最后画一条漂亮的资金曲线。可一旦答辩追问数据切分是否破坏了时间顺序、标签有没有引用未来信息、回测是否扣除了手续费大部分方案的指标就会明显下滑。真正拉开“优质毕业设计”和“普通代码”差距的不是模型有多新而是数据处理和评估是否经得起追问。这篇内容按照一套完整可运行的流程展开数据获取与清洗、特征工程与切分、模型选型与参数、回测评估以及最终文档说明的组织方式。代码以 Python 为环境模型从逻辑回归、LightGBM 到 LSTM 都有覆盖。适合想把机器学习知识落到真实数据上的学生也适合想快速验证一个策略思路是否值得继续投入的从业者。2. 用 Python 准备股票数据行情获取、特征工程与时间序列切分模型再复杂也无法弥补数据准备阶段的错误。股票预测和图像分类、文本分类有一个明显差异样本具有时间顺序改变行之间的前后关系会改变整个问题的语义所以不能把数据当普通二维表处理。这一部分先讨论怎么把行情数据洗成机器学习能消费的数据集再做特征和切分。2.1 数据获取本地 CSV 保底akshare 在线补充我一般的做法是探索性阶段用在线接口取数正式实验版本以固定 CSV 文件为准。股票日线数据的最低字段是 date、open、high、low、close、volume 六个。选择 akshare 做在线补充是因为它覆盖 A 股和 ETF日线接口不需要额外申请 token答辩现场演示起来不容易被网络或权限问题卡住。import pandas as pd # 方式一本地 CSV字段至少包含 date, open, high, low, close, volume df pd.read_csv(data/stock_daily.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 方式二A 股日线探索阶段常用 # import akshare as ak # df ak.stock_zh_a_hist(symbol600519, perioddaily, # start_date20180101, end_date20251231, adjustqfq)代码里两个细节不能省parse_dates把日期文本转成时间类型sort_values保证时间升序。数据源返回的顺序不一定是时间顺序跳过排序会让后续shift(-1)这类滞后操作错位到难以排查的状态。adjustqfq表示前复权把分红和送转折算进历史价格避免模型把除权除息当天的大幅价格变化当成可预测规律。数据量建议至少 5 到 8 年日频数据约 1200 到 2000 个样本。再少的话测试集只有两三百条记录回测收益率的随机性太强很难支撑结论。数据量也不是越多越好对日频任务来说超过 10 年的数据会包含市场制度变化这些结构性中断会让模型学到片段式记忆测试期结论更难泛化。2.2 特征工程滞后收益、移动平均与波动率的窗口设置def build_features(df: pd.DataFrame, windows(5, 20, 60)) - pd.DataFrame: df df.copy() df[ret_1] df[close].pct_change(1) # 前 1 日收益率 for w in windows: df[fma_{w}] df[close].rolling(w).mean() # 均线 df[fstd_{w}] df[close].rolling(w).std() # 波动率 df[fret_{w}] df[close].pct_change(w) # w 日累计收益 df[volume_ratio] df[volume] / df[volume].rolling(20).mean() df[high_low_range] (df[high] - df[low]) / df[close] # 日内振幅 return df.replace([float(inf), -float(inf)], float(nan)) feature_df build_features(df)这段代码的窗口参数值得单独解释。pct_change(1)计算百分比收益而不是价格差避免高价股天然在特征里权重过大rolling(20)需要至少 20 个有效值所以前 60 行会因为窗口未蓄满而产生 NaN。处理这些空值不要直接用fillna(0)股票序列开头的缺失意味着没有历史信息填成 0 会让模型学到“开始阶段就是平稳期”的错误规则正确做法是后续统一dropna()。特征名计算窗口含义与设计意图ret_11 日短期动量捕捉昨日收益惯性ma_2020 日中期趋势方向std_2020 日波动率刻画风险区间volume_ratio20 日量能放大或收缩high_low_range当日日内振幅振幅过大常伴随反转特征的个数不需要追求多。移动平均类特征在趋势市里互相之间高度相关比如 ma_5 和 ma_20 几乎同向变化。我一般控制在 10 到 20 个可解释特征答辩时能讲清楚每个特征为什么被选入就足够超过 40 个再做相关性筛选边际收益很低解释成本却很高。2.3 时间序列切分训练集、验证集、测试集必须保持顺序valid_cutoff int(len(feature_df) * 0.70) test_cutoff int(len(feature_df) * 0.85) train_df feature_df.iloc[:valid_cutoff] valid_df feature_df.iloc[valid_cutoff:test_cutoff] test_df feature_df.iloc[test_cutoff:] print(ftrain {len(train_df)}, valid {len(valid_df)}, test {len(test_df)})这里不要调用默认的train_test_split。它的shuffleTrue会打乱行顺序打乱后的股票数据会让训练集里混入测试区间的“未来”模型等于提前看过答案测试准确率虚高。如果硬要用 sklearn需要写成train_test_split(X, y, test_size0.2, shuffleFalse)。我更习惯直接按行号切语义上接近“第 N 天之前训练、第 N 到 M 天验证、M 天之后测试”。标准化也要放在切分之后。StandardScaler只能从训练集上fit再对验证集和测试集做transform。如果在全量数据上先算均值和方差验证集的信息已经进入训练流程属于一种隐式泄漏。把这个细节写进文档说明答辩被问到也有话可答。提示特征全部来自当天收盘前可得的数据标签来自下一个交易日这是股票预测项目里必须守住的一条边界。3. 股票预测模型实现从基线逻辑回归到 LSTM 的参数要点数据集准备好后进入模型环节。这一章先把任务定义为监督学习再给出基线模型、梯度提升和 LSTM 的具体实现。每段代码后面给出参数范围和建议方便替换成自己的数据后直接运行。3.1 先定义预测任务分类标签与回归标签的不同股票预测的标签通常有两种定义。第一种是二分类下一个交易日收盘价是否高于当日收盘价上涨记为 1下跌记为 0。第二种是回归预测下一交易日的收益率数值例如明日收益 close.shift(-1) / close - 1。分类结果直观和“涨跌方向”的汇报口径一致回归保留幅度信息但评估噪声更大。feature_df[target_cls] (feature_df[close].shift(-1) feature_df[close]).astype(int) feature_df[target_ret] feature_df[close].shift(-1) / feature_df[close] - 1 feature_df feature_df.dropna()shift(-1)把收盘价序列整体向前移动一位所以第 t 行的target_cls表示 t1 日的方向。注意这个操作必须在dropna()之前做否则平移产生的 NaN 会被提前删掉导致最后一行样本标签丢失。这也是时序任务里第一个要检查的细节标签是否对齐到下一时刻而不是当前时刻。我一般会同时做两个方向主实验用分类模型延伸实验把分类模型输出的概率当作排序得分用回归来验证幅度相关性。两种标签互相印证能说明预测能力不依赖单一度量方式。3.2 基线模型用 scikit-learn 实现逻辑回归from sklearn.linear_model import LogisticRegression from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler feature_cols [c for c in feature_df.columns if c not in (date, open, high, low, close, volume, target_cls, target_ret)] X feature_df[feature_cols] y feature_df[target_cls] train_mask feature_df.index valid_cutoff valid_mask (feature_df.index valid_cutoff) (feature_df.index test_cutoff) base_model make_pipeline(StandardScaler(), LogisticRegression(C1.0, max_iter1000)) base_model.fit(X[train_mask], y[train_mask]) print(valid acc:, base_model.score(X[valid_mask], y[valid_mask]))管道make_pipeline保证标准化只依赖训练折内的数据不会偷看验证集。逻辑回归的C是正则化强度的倒数C1.0约束适中特征之间相关性较高时C调到 0.1 会更稳定。基线模型的作用不是追求最高成绩而是给后续模型提供一个最低门槛。如果树模型连逻辑回归都打不过大概率是标签或回测环节出了问题不用急着换更复杂的网络。3.3 LightGBM 梯度提升树学习率、叶子数与早停梯度提升树是这类表格数据的主力模型。相比随机森林它对异常值和缺失值处理更直接训练速度也快。下面是一组我常用的 LGBMClassifier 配置。import lightgbm as lgb from sklearn.metrics import roc_auc_score lgb_model lgb.LGBMClassifier( n_estimators500, learning_rate0.05, num_leaves31, max_depth5, min_child_samples20, subsample0.8, subsample_freq1, colsample_bytree0.8, random_state42, ) lgb_model.fit(X[train_mask], y[train_mask], eval_set[(X[valid_mask], y[valid_mask])], callbacks[lgb.early_stopping(50, verboseTrue)]) print(valid auc:, roc_auc_score(y[valid_mask], lgb_model.predict_proba(X[valid_mask])[:, 1]))先理解再调参。learning_rate0.05是每棵树的权重贡献学习率调小需要的树数n_estimators就要增加两者联合决定迭代范围。num_leaves31控制叶子数叶子太多会记忆单一样本max_depth5限制树深度防止从高维特征里强行划分出没有泛化能力的规则。subsample0.8表示每棵树只用 80% 的样本行colsample_bytree0.8表示每棵树只用 80% 的列两个参数共同降低方差。early_stopping(50)在验证集指标连续 50 轮不提升时中断训练避免迭代次数过大造成过拟合。训练后立即查看特征重要性这是后续文档和答辩最重要的素材之一。importance pd.Series(lgb_model.feature_importances_, indexfeature_cols) print(importance.sort_values(ascendingFalse).head(10))LightGBM 的feature_importances_默认是分裂次数计数不是信息增益。文档里要把“分裂次数最高的特征”表述成“该特征最常参与决策划分”不要直接说成“对预测贡献最大”措辞不严谨容易在答辩时被追问。3.4 LSTM 的适用边界什么时候值得加一层循环网络模型选型存在明显权衡我常用下面这张表放在论文的模型对比部分模型优点缺点在毕业设计中的定位逻辑回归参数少、可解释难以刻画非线性基线与对照LightGBM训练快、对特征尺度不敏感需要防过拟合主结果来源LSTM能表达时间依赖数据少时训练不稳定延伸对比实验LSTM 通过门控结构把历史信息保留在隐藏状态中理论上能学到比固定窗口更长的依赖。但日频数据只有一千多个样本LSTM 动辄几万到几十万个参数过拟合风险远大于数据增强收益。如果要用建议把结构做得轻量。import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def make_sequences(data, labels, seq_len20): xs, ys [], [] for i in range(seq_len, len(data)): xs.append(data[i - seq_len:i]) ys.append(labels[i]) return np.array(xs), np.array(ys) seq_len 20 X_seq, y_seq make_sequences(X.values, y.values, seq_len) rnn_model Sequential([ LSTM(64, return_sequencesTrue, input_shape(seq_len, X.shape[1])), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1, activationsigmoid), ]) rnn_model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy])return_sequencesTrue让第一层输出完整时间步序列方便第二层 LSTM 继续读取。Dropout(0.2)随机丢弃 20% 的神经元连接是控制过拟合的主要手段。训练时 batch_size 取 64epoch 控制在 50 以内同时监控验证集 loss。实际项目中经常 20 个 epoch 后验证 loss 就开始回升说明模型进入过拟合。seq_len取 20 对应约一个月的交易日日频数据窗口超过 3 个月后有效性收益很小计算成本却涨得很快。4. 回测与评估准确率失真、滚动验证与样本外衰减训练完模型只完成一半另一半是用严谨的评估体系判断模型有没有实际意义。这一部分把评估拆成四块滚动验证的写法、指标的选择、包含交易成本的回测、以及样本外衰减的排查方式。4.1 用 TimeSeriesSplit 做滚动验证避免一次性切分的运气成分固定的一次性切分只产生一个验证集结论容易被行情阶段左右。假如验证集恰好是普涨行情任何偏多的模型都会有不错的表现。更稳妥的做法是滚动时间序列交叉验证。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, gap5) auc_list [] for fold, (tr_idx, va_idx) in enumerate(tscv.split(X), start1): m lgb.LGBMClassifier(n_estimators200, learning_rate0.05, num_leaves31, max_depth5) m.fit(X.iloc[tr_idx], y.iloc[tr_idx]) auc roc_auc_score(y.iloc[va_idx], m.predict_proba(X.iloc[va_idx])[:, 1]) auc_list.append(auc) print(ffold {fold}: AUC {auc:.4f}) print(mean AUC:, np.mean(auc_list))这里关键参数是gap5。相邻交易日的收益率存在短期自相关验证集紧贴训练集末尾时模型容易“沿用最近几天惯性”做出预测成绩虚高。留出 5 天不参与训练的缓冲期可以减少短期记忆带来的偏差。n_splits5把数据集切成五段每次用前面一段训练、后面一段验证五轮平均指标比单次切分可靠得多。4.2 用 IC、AUC 和方向准确率联合评估预测能力股票预测的样本天然存在类别不平衡上涨天数通常超过一半单独看准确率有误导性。机器学习三大假设要求样本独立同分布股票收益序列恰恰不满足这一点。用单指标评价模型很容易把“多数类优势”误判成“模型优势”。指标计算方式在股票预测里的作用准确率(TPTN)/总样本直观但会被多数类拉高精确率/召回率针对上涨类统计判断上涨信号的可靠性AUC正负样本排序得分与阈值无关检验排序能力IC预测分数与真实收益的秩相关衡量预测与收益的线性相关度推荐至少同时记录 AUC 和 IC。AUC 由 sklearn 一行计算IC 用 Spearman 秩相关更稳健。from scipy.stats import spearmanr pred_score base_model.predict_proba(X[valid_mask])[:, 1] true_ret feature_df.loc[valid_mask, target_ret] ic, p_value spearmanr(pred_score, true_ret) print(fIC: {ic:.4f}, p: {p_value:.4g})spearmanr输出两个值IC 相关系数和 p 值。日频任务里 IC 绝对值超过 0.05 算有一定区分度p 值低于 0.05 才说明相关性不是随机波动。如果模型 AUC 在 0.55 以上但 IC 接近 0说明模型只能排序少数极端样本对大部分日子的预测没有实际意义。4.3 回测模拟把手续费和滑点写进交易循环模型输出的概率本身不产生收益必须经过回测才能变成资金曲线。下面是最简回测函数适合直接嵌入毕业设计。def run_backtest(df, signal, init_cash100000, fee_rate0.0003, slippage0.0002): cash init_cash position 0 equity [] for i in range(len(df)): price df[close].iloc[i] if i 0: equity.append(cash) continue if signal[i] and position 0: position cash / (price * (1 fee_rate slippage)) cash 0 elif not signal[i] and position 0: cash position * price * (1 - fee_rate - slippage) position 0 equity.append(cash position * price) return pd.Series(equity, indexdf[date])signal是布尔序列1 表示持仓0 表示空仓。fee_rate0.0003近似双边佣金成本slippage0.0002表示成交价偏离收盘价的比例两项合计约 0.05%这个假设在回测里已经比很多报告保守。函数没有处理涨跌停无法成交的情况也不考虑最小交易单位但作为核心回测逻辑已经够用。真正的重点是把成本放进每一次买卖循环而不是在最终收益里事后扣减一次。4.4 样本外衰减从数据、特征、评估三层排查训练集 AUC 0.62、验证集 AUC 0.60、测试集 AUC 0.53这是典型的样本外衰减路径。排查先从数据层开始股票池是否只包含当前还在上市的股票退市股票在历史数据里缺失会留下幸存者偏差。再看特征层基本面特征有没有使用财务报告发布日之后才可得的数值成交量特征是否经过复权调整。最后看评估层测试期是否集中在某一段趋势行情里如果是需要把测试期拆成上涨、震荡、下跌三段分别报告。这三层核对完仍然衰减剩下的才是市场规律本身的不稳定部分也是任何股票预测项目都绕不开的结论。5. 把股票预测项目整理成优质毕业设计的文档说明代码能跑只是底线文档决定了答辩时老师能否快速理解工作。最后这章说工程目录、实验记录和答辩材料的具体整理方式。5.1 工程目录与 requirements换一台机器也能跑通项目根目录建议固定成以下结构stock_ml/ ├── data/ │ ├── raw/ # 原始行情 CSV │ └── processed/ # 清洗后特征表 ├── src/ │ ├── data.py # 数据读取与预处理 │ ├── features.py # 特征工程函数 │ ├── models.py # baseline、lgb、lstm │ ├── evaluate.py # 指标与回测 │ └── config.py # 所有参数字典 ├── reports/ # 图表和实验记录 ├── README.md └── requirements.txtrequirements.txt建议固定版本号例如pandas2.1.4、scikit-learn1.3.2而不是只写pandas。这样换机器后执行pip install -r requirements.txt能一遍成功。机器学习环境隔离也建议提前做python -m venv .venv创建虚拟环境再安装依赖避免和系统 Python 的包互相覆盖。config.py把所有训练参数集中管理跑多组实验时只改参数文件不动主逻辑源码。5.2 实验记录表格与图表导出给论文直接引用文档说明里保留一张可追加的实验记录表固定模板如下版本特征集模型验证 AUC测试 AUC回测年化最大回撤baseline价格、均线逻辑回归0.5210.5126.2%17%v2加量比、RSILightGBM0.5510.5328.7%14%v3加 LSTM 特征LightGBMLSTM0.5480.5217.5%15%这张表在论文里可以直接引用答辩老师也能一眼看到实验是逐步推进的。不要只写最好的结果保留中间版本反而更能体现完整的工作量。图表输出要自动化用 matplotlib 生成测试集资金曲线、特征重要性 Top10、验证集预测分数分布三张固定图统一保存到reports/目录。import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(10, 4)) ax.plot(test_df[date], (1 test_df[ret_1]).cumprod(), label买入持有) ax.plot(test_df[date], backtest_equity, label模型策略) ax.set_title(测试集资金曲线) ax.legend() fig.savefig(reports/backtest_curve.png, dpi150)dpi150是为了满足论文插图清晰度图例和标题用中文时需要额外设置字体参数否则保存出来的中文会变成方块建议在构建图前统一配置plt.rcParams[font.sans-serif]。5.3 答辩时重点讲特征重要性与局限答辩演示不用逐行讲代码。先展示逻辑回归基线说明数据处理正确再用 LightGBM 提升的结果说明泛化能力最后强调回测已经包含手续费和滑点。特征重要性部分挑选 top 3 解释即可例如“ret_5 排在前面说明近 5 日动量是模型的有效特征而 std_20 的引入是为了过滤高波动区间”。这样既体现数据观察也体现金融逻辑。被问到“为什么准确率这么高却没有实盘价值”时正面应答比辩护更有效测试集收益没有考虑冲击成本和涨跌停无法成交的延迟样本外的市场结构也会漂移。把模型定位成辅助研究工具而不是自动交易机器毕业论文的结论才站得住。本文还有配套的精品资源点击获取