
简介基于机器学习的股票预测方法研究论文文档面向毕业设计、课程设计及金融科技初学者是一份写作规范、逻辑紧密的论文参考。内容系统梳理传统机器学习如分段线性表示、随机森林、支持向量机与深度学习卷积神经网络、LSTM、生成对抗网络在股市预测中的应用并涵盖金融新闻事件结构化表示、情感极性分析、文本向量化等结合非结构化数据的预测思路同时比较历史数据驱动与多源信息融合两类方法的优缺点论述突发事件影响、宏观经济因素等关键问题可作为论文写作框架、技术综述或开题参考。资源共1个docx文件约35KB便于直接阅读与二次编辑适合在校师生、工程师及毕业生在毕设或课设中借鉴思路但需注意不可完全照搬。已有586人学习下载。1. 基于机器学习的股票预测毕设选题怎么做才不只是“跑个LSTM画条线”“基于机器学习的股票预测”是近几年机器学习相关毕业设计里最热门的题目也是最容易做成“黑匣子展示”的题目之一。我见过不少学生的第一版成果tushare 拉数据、LSTM 一跑、画个预测图然后就没有然后了。答辩老师只问三个问题你的预测目标到底是什么凭什么说它有效换一段行情还成立吗这篇文章就把这三个问题拆成一条可落地的技术路线——数据怎么取、标签怎么定、模型怎么选、回测怎么做才算数以及论文里最容易被抓的五个细节。适合正在准备机器学习方向毕设或课设的学生也适合想验证“机器学习做股票预测到底靠不靠谱”的从业者。这篇不是包赚的攻略而是一条能在一个学期内交得出代码、讲得清逻辑的路线。2. 先定预测目标再碰数据标签构造与特征工程2.1 预测目标涨跌方向、收益率还是价格决定整个论文的走向很多初学者一上来就把目标写成“预测收盘价”把第 t1 天的价格作为回归 target。这个做法有个根本性问题价格序列非平稳。2015 年的 50 元与 2024 年的 50 元不是同一个量级的信号模型拟合出来的“精准预测价”既没有统计意义也产生不了可执行的买卖点——就算告诉你明天收盘价是 10.52 元你仍然不知道这算贵还是便宜。我一般建议把预测目标改成“未来 N 日收益率的符号或等级”让模型输出一个方向或类别而不是一个绝对值。常见有三种二分类label 1 表示未来 5 日收益率大于 0否则为 0。三分类label 1显著上涨未来 5 日收益率 1%0横盘在 -1% 到 1% 之间-1显著下跌收益率 -1%。回归直接预测未来 5 日收益率数值再用阈值转成信号。从毕设论文的角度三分类能讲的内容最多可以画混淆矩阵、按类别分析 Precision/Recall还能讨论“模型为什么总是预测横盘”。但类别阈值不能拍脑袋得先看数据。正确做法是统计研究区间内收益率的分位数比如取 30% 和 70% 分位点作为上下阈值。这个“先看分布再定阈值”的过程写进论文就是研究设计直接写“设定 ±1%”容易被答辩老师视为调参玄学。2.2 tushare 取数接口选择、复权处理与增量更新国内做机器学习股票预测tushare Pro 是毕设里的绝对主流。它的一次性 token 机制对新手友好免费额度足够拿到日线级别的沪深股票和 ETF 历史数据。有个使用前提pro 接口对调用频率有限制批量拉全市场会触发限流所以初版代码最好只拉一只指数或几只主流 ETF跑通全流程之后再扩股票池。akshare 也可以它不需要 token数据字段更杂但接口跟随网页改版频繁稳定性差一点不适合论文这种需要“可复现”的场景。给一段可以直接跑通的取数代码import tushare as ts import pandas as pd # 初始化token 在 tushare pro 个人主页获取 ts.set_token(你的token) pro ts.pro_api() # 拉取沪深300日线 df pro.index_daily( ts_code000300.SH, # 指数代码 start_date20150101, # 左闭区间 end_date20241231 # 右闭区间 ) # 统一列名、排序、转日期格式 df df.rename(columns{trade_date: date}) df df.sort_values(date).reset_index(dropTrue) df[date] pd.to_datetime(df[date]) print(df.head()) print(df.shape)逻辑说明index_daily返回的是指数日线字段包括 open、high、low、close、vol、amount和个股的daily接口字段基本一致方便后续写通用特征函数。ts_code的写法有讲究上交所股票是600000.SH深交所股票是000001.SZETF 要用fund_daily接口代码如510300.SH。日期参数是字符串格式YYYYMMDD不要传 datetime 对象新版 tushare 对类型校验较严格。个股数据还要多一步复权处理。tushare 的daily接口返回的是不复权价格遇到分红送股会出现价格跳空直接算收益率会产生假信号。常见做法是调用adj_factor接口拿复权因子然后自己做前复权或后复权。前复权适合回测因为它保持最新价格不变历史价格按因子调整后复权适合长期趋势分析。建议在论文里写明“本实验采用前复权数据”并解释复权的目的这也是一处查重友好的原创内容。增量更新是个容易被忽略的环节。毕设数据可能横跨五年到十年中途要补充最新行情。不要每次重新全量拉取而是在已有 DataFrame 末尾按日期追加新数据并去重# 增量更新只拉 last_date 之后的数据 last_date str(df[date].max().date()).replace(-, ) new pro.index_daily(ts_code000300.SH, start_datelast_date) df pd.concat([df, new], ignore_indexTrue) df df.drop_duplicates(subsetdate).sort_values(date).reset_index(dropTrue)2.3 构造标签shift(-fwd) 算未来收益注意末端 NaN标签构造是整个流程中最容易写错、又最不容易被发现的一步。给一个通用函数def make_label(df, fwd5, up_th0.01, down_th-0.01, modeternary): 根据未来 fwd 日收益率构造分类标签。 df 必须按日期升序排列。 df df.copy() # 未来收益 fwd 日之后的收盘价 / 当前收盘价 - 1 df[fwd_ret] df[close].shift(-fwd) / df[close] - 1.0 if mode binary: df[label] (df[fwd_ret] 0).astype(int) elif mode ternary: df[label] 0 df.loc[df[fwd_ret] up_th, label] 1 df.loc[df[fwd_ret] down_th, label] -1 # 末尾 fwd 行没有未来数据标记为缺失 df.loc[df[fwd_ret].isna(), label] np.nan return df # 使用示例 df make_label(df, fwd5, up_th0.01, down_th-0.01) print(df[label].value_counts(dropnaFalse))参数说明fwd是预测窗口日线数据取 5 表示“未来一周”取 20 表示“未来一个月”。这个值应该和你的调仓频率一致——如果你打算每周评估一次策略就用 5如果按月调仓就用 20不要随意填。shift(-fwd)的意思是把未来第 fwd 个交易日的收盘价“挪”到当前行与当前收盘价做除法得到区间收益。用区间收益而不是单日涨跌作为标签是因为单日信噪比太低且受涨跌停和隔夜跳空影响模型难以学习到稳定模式。注意函数末尾的处理数据集最后 fwd 行的未来收益为空这些样本需要标记为缺失并在训练前剔除。很多学生忘记这一步把 NaN 标签直接喂给模型sklearn 会报错Keras 则可能静默学习一堆无意义样本。2.4 特征工程慢变量优先所有特征只能用“当天及之前”的信息特征工程的原则可以用一句话概括慢变量优先快变量作补充。所谓慢变量是指动量、波动率、均线偏离度这类基于一段时间窗口计算的统计量它们天然平滑与未来收益的相关性更稳定快变量是当日涨跌幅、当日成交量变化等噪声大但有时能捕捉短期情绪。给一组可直接复用的特征代码import numpy as np def add_features(df, window10): df df.copy() # 动量过去5/10日收盘价变化 df[mom_5] df[close] / df[close].shift(5) - 1.0 df[mom_10] df[close] / df[close].shift(10) - 1.0 # 波动率对数收益率的滚动标准差 df[log_ret] np.log(df[close]).diff() df[vol_10] df[log_ret].rolling(window, min_periods5).std() # 均线偏离度当前价格相对10日均线的位置 df[ma_10] df[close].rolling(window).mean() df[dev_ma_10] df[close] / df[ma_10] - 1.0 # 成交量变化相对过去10日均量的比值 df[vol_chg] df[vol] / df[vol].rolling(window).mean() - 1.0 # 删除开头因滚动窗口产生的缺失行 df df.dropna(subset[mom_5, mom_10, vol_10, dev_ma_10, vol_chg]) return df逻辑说明shift(5)取的是前 5 个交易日的价格保证了特征里没有任何未来信息rolling窗口默认只包含当前及之前的行这是 pandas 滚动窗口的天然属性。min_periods5允许窗口内至少 5 个有效值就计算避免数据开头产生过多缺失。特征数量建议控制在 5 到 8 个。初学者容易犯的毛病是构造三五十个技术指标结果树模型的特征重要性高度分散LSTM 则因为特征维度太高而过拟合。更实际的做法是统一让滚动窗口等于预测窗口先做一个 5 特征的小模型再逐步加特征对比效果。论文里可以放一张表格对比“5 特征 vs 10 特征 vs 20 特征”的验证集准确率这比单纯堆特征更能体现研究逻辑。3. 模型选型从逻辑回归、XGBoost 到 LSTM 的落地路径3.1 先跑基线模型逻辑回归和 XGBoost 的代码与参数一上来就上 LSTM 是毕设初稿最常见的翻车点没有之一。LSTM 对数据预处理、序列长度和训练稳定性要求高出了问题极难排查。我先说一条不花哨但能救命的经验无论最终模型是什么先跑两个基线——逻辑回归和 XGBoost。基线的价值有两条。第一验证特征和标签之间有没有真实关系如果逻辑回归连训练集准确率都只有 52%说明特征构造方向有问题这时候换 LSTM 只是把“无效模型”包装得更复杂。第二答辩时形成对比论文里写出“基线模型 XGBoost 准确率 56%我们提出的 LSTM 准确率 58%”比只放一个 LSTM 结果可信得多。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from xgboost import XGBClassifier from sklearn.metrics import classification_report feature_cols [mom_5, mom_10, vol_10, dev_ma_10, vol_chg] # 剔除标签缺失的行 train_df df.dropna(subset[label]).reset_index(dropTrue) X train_df[feature_cols].values y train_df[label].values.astype(int) # 时间顺序切分前80%训练后20%测试 split int(len(X) * 0.8) X_train_raw, X_test_raw X[:split], X[split:] y_train, y_test y[:split], y[split:] # 数值特征标准化 scaler StandardScaler() X_train scaler.fit_transform(X_train_raw) X_test scaler.transform(X_test_raw) # 基线1逻辑回归 lr LogisticRegression(max_iter1000, multi_classmultinomial) lr.fit(X_train, y_train) print(LR train acc:, lr.score(X_train, y_train)) print(LR test acc:, lr.score(X_test, y_test)) # 基线2XGBoost xgb XGBClassifier( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, eval_metricmlogloss, random_state42 ) xgb.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) print(XGB test acc:, xgb.score(X_test, y_test)) print(classification_report(y_test, xgb.predict(X_test), digits3))参数说明split int(len(X) * 0.8)是按时间顺序切分绝不能用 sklearn 默认的train_test_split那是随机切分会把时间序列打乱造成数据泄露第 5 章细讲。StandardScaler只在训练集上fit再用同一套参数transform测试集这一步顺序错位是常见的隐藏 bug。逻辑回归的multi_classmultinomial表示多分类用 softmaxXGBoost 的multi:softprob由XGBClassifier自动设置mlogloss是多分类对数损失用来观察训练过程是否在过拟合。n_estimators300配合learning_rate0.05树多、步长小模型容量够用且不易振荡max_depth4对这种 5 维特征足够了过深会显著过拟合。3.2 LSTM 序列建模滑窗造样本与网络结构参数基线的验证集准确率如果略高于随机水平比如三分类超出 33% 一些就可以考虑 LSTM。LSTM 的定位不是“更厉害的预测器”而是“能利用连续 n 天特征序列的时序模型”。它的输入是三维张量[样本数, 时间步, 特征数]所以第一步是把二维表格改造成滑窗序列def make_sequence(X, y, seq_len10): xs, ys [], [] for i in range(seq_len, len(X)): xs.append(X[i - seq_len:i]) # 取前 seq_len 天的特征 ys.append(y[i]) # 标签是第 i 天的结果 return np.array(xs), np.array(ys) seq_len 10 X_seq, y_seq make_sequence(X, y, seq_len) # 按时间切分 split int(len(X_seq) * 0.8) X_train_seq, X_test_seq X_seq[:split], X_seq[split:] y_train_seq, y_test_seq y_seq[:split], y_seq[split:] print(训练集形状:, X_train_seq.shape)这里有个容易绕晕的对应关系滑窗用前 10 天的特征来预测第 11 天的标签而第 11 天的标签是“第 11 天之后 5 日的收益率”。整个链条是前 10 天特征 → 第 11 天收盘 → 未来 5 日收益标签。只要特征严格取自当天收盘前标签严格指向未来时间上是闭合的不存在信息穿越。网络结构给一个稳妥的 Keras 实现from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model Sequential([ LSTM(64, input_shape(seq_len, X_train_seq.shape[2]), return_sequencesTrue), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(16, activationrelu), Dense(3, activationsoftmax) ]) model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model.fit( X_train_seq, y_train_seq, validation_data(X_test_seq, y_test_seq), epochs100, batch_size64, callbacks[early_stop], verbose1 )参数说明第一层LSTM(64, return_sequencesTrue)输出完整时间步是为了把序列传给第二层 LSTM如果只堆一层return_sequences必须设为False否则输出形状对不上。Dropout(0.2)放在 LSTM 层之后是常规防过拟合手段也可以直接在 LSTM 层里传dropout0.2, recurrent_dropout0.2但后者训练更慢。损失函数用sparse_categorical_crossentropy因为标签是整数 0/1/2如果你把标签做了 one-hot就改成categorical_crossentropy。patience10表示验证损失连续 10 个 epoch 不下降就停止并回滚到最优权重。batch_size64在几千条日线数据上是稳妥初值样本量上万可以调到 128。我通常会加一句提醒LSTM 不是一用就灵。如果 XGBoost 的验证集准确率已经能到 56%LSTM 跑出来却只有 52%这在金融时间序列上完全正常因为序列模型更需要大量数据。论文里可以如实报告这个负结果然后分析原因这比强行调参把 LSTM 调到过拟合更有价值。3.3 训练纪律固定随机种子、归一化与早停机器学习训练里最容易被忽视的是“可复现性”。同一份代码跑三次结果不一样答辩时老师让你现场重跑你翻车了场面会很尴尬。解决方法是全局固定随机种子import numpy as np import random import tensorflow as tf def set_seed(seed42): np.random.seed(seed) random.seed(seed) tf.random.set_seed(seed) set_seed(42)固定种子之后XGBoost 还要显式传random_state42LSTM 的初始化权重也会随种子固定。论文里写一句“实验使用固定随机种子以保证结果可复现”答辩效果好过你在那里解释半天。第二个纪律是归一化必须先 fit 训练集。前面代码里scaler.fit_transform(X_train_raw)只用了训练集的数据计算均值和方差这是对的。如果先对整个数据集做归一化再切分测试集的分布信息已经泄漏进 scaler等于变相预知了测试集的均值和波动范围。LSTM 对输入量级更敏感特征不归一化时 loss 可能直接是 NaN。第三个纪律是早停。深度学习训练中val_loss不再下降就应该停硬要跑满 100 个 epoch 的结果必然是过拟合。EarlyStopping 的restore_best_weightsTrue很关键不加这个参数的话停止时保留的是最后一个 epoch 的权重而不是验证集最优的那一份。这个“小坑”会直接拉低测试集表现而且初学者很难察觉。把三个模型放在一张对比表里是论文结果章节最常见也最实用的呈现方式模型输入形态训练速度过拟合风险可解释性逻辑回归二维特征向量极快低高XGBoost二维特征向量快中中LSTM三维序列张量慢高低4. 评估体系回测收益、信息系数与稳定性检验4.1 用 walk-forward 滚动验证替代单次切分单次 80/20 时间切分只能证明“在这个时点之后的一段行情里有效”说服力有限。更被量化领域认可的做法是 walk-forward 滚动验证把数据切成多段每一段都用之前的若干天训练、之后若干天验证窗口向后滚动。它模拟的是“每隔一段时间重新训练一次”的真实使用方式。def walk_forward(df, model_fn, feature_cols, train_len800, test_len200, step200): model_fn: 接收 (X_train, y_train) 返回已训练模型 data df.dropna(subset[label]).reset_index(dropTrue) preds_all, y_all [], [] for start in range(0, len(data) - train_len - test_len, step): train data.iloc[start:start train_len] test data.iloc[start train_len:start train_len test_len] m model_fn(train[feature_cols].values, train[label].values) pred m.predict(test[feature_cols].values) preds_all.append(pred) y_all.append(test[label].values) return np.concatenate(y_all), np.concatenate(preds_all) # 调用示例 def xgb_fn(X_train, y_train): m XGBClassifier(n_estimators200, learning_rate0.05, max_depth4, subsample0.8, colsample_bytree0.8, random_state42) m.fit(X_train, y_train) return m y_all, pred_all walk_forward(df, xgb_fn, feature_cols) print(walk-forward 总准确率:, (y_all pred_all).mean())逻辑说明这个循环每次用过去 800 天训练、未来 200 天验证窗口一次向后滑动 200 天。step200意味着相邻两段验证期是首尾相接的不会重叠能覆盖完整测试区间。训练多次模型在日线数据上代价很小XGBoost 训练一次不到一秒LSTM 稍微慢些也可以接受。论文里可以画一条“各验证段准确率”的折线图直观展示模型在不同市场阶段的表现差异。4.2 把预测概率转成仓位手续费、换手率与最大回撤预测准确率不等于策略收益这个道理论文里要讲透。一个准确率 58% 的模型如果每次只在预测上涨概率超过 70% 时才买入加上手续费后可能仍然是亏的反之一个准确率只有 52% 的模型配合仓位管理和严格的止损可能反而有正收益。所以评估体系必须包含从预测到持仓的转换。一个简单但有效的仓位规则是用“上涨概率减下跌概率”构造一个在 -1 到 1 之间的仓位信号再设阈值决定是否持仓而不是对每个样本硬取 argmax 分类结果。# pred_proba 是模型输出的三分类概率列顺序 [跌, 横盘, 涨] proba xgb.predict_proba(X_test) pos proba[:, 2] - proba[:, 0] # 信号强度涨概率 - 跌概率 # 阈值大于0.5做多小于-0.5做空A股默认空仓 pos np.where(pos 0.5, 1.0, np.where(pos -0.5, -1.0, 0.0)) # 获取与测试集日期对齐的每日对数收益率 # 注意这里假设 df 最后 len(pos) 行就是测试集对应区间 ret df[log_ret].iloc[-len(pos):].values daily_pnl pos * ret # 交易成本换手率 * 单边费率 turnover np.abs(np.diff(pos, prepend0.0)) cost turnover * 0.0015 net_pnl daily_pnl - cost # 累计净值与最大回撤 cum np.cumprod(1 net_pnl) max_drawdown (cum / np.maximum.accumulate(cum) - 1).min() print(累计收益:, cum[-1] - 1) print(最大回撤:, max_drawdown) print(日均换手:, turnover.mean())参数说明pos proba[:, 2] - proba[:, 0]用的是softmax输出的概率差模型越确定仓位绝对值越大模型不确定时仓位自动趋近 0这比硬性阈值更稳健。turnover用持仓变化量的绝对值近似换手率成本系数 0.0015 是双边手续费加滑点的经验值——ETF 实际更低个股更高论文里写明假设即可。回撤计算用np.maximum.accumulate(cum)求历史峰值再算当前净值相对峰值回落的幅度。有一个细节必须提醒测试集预测的样本区间和df中的日期要对齐。代码里用iloc[-len(pos):]是简写严谨做法是给df加一列pred_date回测时按日期 merge避免因为中间dropna导致行数错位。4.3 三个检测过拟合的指标Rank IC、分段准确率衰减、换手率除了准确率和累计收益论文里要有更硬的客观指标来回答“模型到底有没有在背答案”。我常用三个第一个是 Rank IC。计算每个验证时段的预测值与未来实际收益的 Spearman 秩相关系数再取均值。IC 绝对值大于 0.05 就说明预测包含微弱但真实的信息如果接近 0 或为负说明预测方向和实际收益基本无关准确率高可能是运气。from scipy.stats import spearmanr # fwd_ret 是未来5日实际收益 y_fwd df[fwd_ret].iloc[-len(pos):].values ic, p_value spearmanr(proba[:, 2], y_fwd) print(Rank IC:, ic, p-value:, p_value)第二个是分段准确率衰减。把测试集按时间切三段分别算准确率。如果前段 62%、中段 55%、后段 49%说明模型只在离训练集最近的时段有效本质上是在记忆而非泛化。这个衰减趋势要写进论文的结果分析老师问“模型对新行情是否有效”时这段数据就是答案。第三个是换手率。如果模型昨天看多、今天看空、明天又看多信号不稳定换手率高交易成本会吞噬收益。回测里把turnover.mean()报出来如果日均换手超过 0.5基本可以判定信号噪声太大需要加大预测窗口或调高信号阈值。5. 常见问题与避坑排查毕设/课设论文里的五个翻车细节5.1 训练集 96%测试集 50%数据泄露的三种来源现象训练准确率接近满分测试集却只有随机水平或者测试集准确率极高但换一段行情立刻崩盘。原因数据泄露最常见有三种。第一种是随机切分把时间序列切碎后训练集和测试集分布在重叠时间段内模型间接看到了测试集的行情模式。第二种是特征里混入了未来信息比如用了当日收盘价预测当日收盘后的收益。第三种是归一化在全数据集上 fit测试集的统计信息提前进入了模型。解决严格按时间顺序切分特征只用shift()之后的历史数据归一化只在训练集上 fit。这三个问题单独出现还好排查最怕的是两个叠加新手往往会怀疑模型结构不对去换网络浪费大量时间。排查口诀是先查切分再查特征最后查预处理。5.2 换随机种子结果差一倍可复现性设置现象同一份代码只改了random_state测试集准确率从 58% 掉到 51%回测收益从正变负。原因树模型的特征抽样、LSTM 的权重初始化、Dropout 的随机丢弃都会受随机种子影响。在样本量只有几千条的金融数据上这个波动会被放大得更明显。解决用一个set_seed()函数在程序入口固定所有库的种子XGBoost 显式传random_stateLSTM 固定tf.random.set_seed。论文里写明种子值并把“重复运行 5 次的准确率均值±标准差”报告出来。这既证明了结果的稳定性也防止答辩现场重跑时结果不一致的尴尬。5.3 LSTM 损失不降或直接 NaN学习率与归一化现象训练初期 loss 一直在 1.1 附近不动或者某一步变成 inf/nan。三分类的交叉熵初始值应该在log(3)≈1.0986附近如果初始 loss 远大于这个值或者出现 NaN基本可以断言特征或模型配置出了问题。原因最常见的是特征量级差异过大。LSTM 内部使用 tanh 激活输入动辄上百的值会直接让梯度爆炸其次是学习率过高Adam 默认 0.001 对某些数据并不合适还有可能是序列长度过长梯度在时间步上连续传播后消失。解决先做标准化先把每个特征缩放到均值 0、方差 1如果 loss 仍然不稳定把学习率降到 0.0005 或 0.0001序列长度从 10 开始不要一上来就试 60。还要强调不要用归一化之前的原始数据跑 LSTM这是新手最容易踩的坑。5.4 回测“赚钱”但成交价不对未来函数与信号延迟现象回测净值曲线漂亮得惊人年化收益 100% 以上但你知道实盘不可能做到。原因成交时间与信号生成时间错位。最常见的是用当日收盘价计算当日特征、当日收盘价成交但特征里的“收盘价”要到收盘那一刻才知道信号真正可用是在收盘后最早只能次日开盘成交。如果回测里当天信号当天成交其实就是偷看了当天的收盘价。解决强制信号延迟一天。当天收盘后产生目标仓位次日 open 成交。手动实现就是一句话# 今日收盘产生的仓位明日才执行 exec_pos pos_series.shift(1).fillna(0.0) daily_pnl exec_pos * next_day_ret回测代码里加这一行收益可能明显下降但这才是真实可交易的结果。论文里明确写“所有信号均滞后一天成交”这句话对回测可信度极其重要。5.5 答辩被问“怎么落地”答不上来加决策规则与样本外验证现象论文里写了“预测准确率 60%”老师问“那你能靠它赚钱吗”“你怎么实操”学生只答“根据预测概率决定买卖”再被追问阈值、止损、交易成本然后卡壳。原因预测模型和交易决策之间缺了一层“决策逻辑”。论文通篇在讲分类准确率但从没说明准确率如何转化为一个可模拟执行的策略。解决在论文里单独写一节“决策规则与样本外验证”。决策规则要具体预测上涨概率大于阈值才买入持仓超过固定天数强制平仓空仓时不参与。样本外验证则是把最近 6 个月或最近 20% 的数据完全隔离整个建模过程不触碰最后才跑一遍展示这段数据的预测曲线与策略净值。这是整个毕设里最具说服力的一组实验。参考文献方面周志华《机器学习》的模型评估与选择章节以及关于时间序列预测的综述文献是支撑“为什么用样本外验证”的标准引用。6. 让预测变成决策信号滚动再训练与可解释输出前面五章讲完了从数据到评估的完整链路但真正让这套系统有研究价值的是滚动再训练和可解释输出。滚动再训练是指每隔一段固定时间就把最新数据纳入训练集重新训练模型再预测未来一段。在 walk-forward 的框架上做一个小改动——每训练完一段把验证期数据并入下一次训练集就完成了“滚动”的闭环。实际操作里XGBoost 在几千条日线上重训一次不到一秒LSTM 也就一分钟以内完全可接受。论文中给出“每季度重训”与“不重训”的对比结果往往能发现重训版本的样本外表现更稳定因为这能自适应市场风格的变化比如从低波动切换到高波动行情。可解释输出方面除了给出类别还要给出置信度。XGBoost 的predict_proba和 LSTM 的 softmax 输出都是天然的概率。我习惯把所有预测结果整理成一个标准表日期、预测概率、实际未来收益、历史特征快照然后画三张图——预测概率与实际收益的分组柱状图、预测概率时间序列、不同阈值下的收益曲线。这三张图基本可以撑起论文实验章的大半篇幅同时让整个系统不再是“黑匣子”。我个人的一个习惯是“先拒绝后接受”每产生一个预测信号强制先看这个预测的概率和依据概率低于阈值的信号一律丢弃宁可不做也不错做。这个习惯在论文里体现为“阈值敏感性分析”在实践中能过滤掉大量噪声信号。做股票预测重点从来不是模型多复杂而是样本外稳定性、成本结构和可解释性。守住固定种子、严格按时间切分、如实报告负结果就已经超过大部分为了“跑个模型”而做的毕设了。希望帮到你。本文还有配套的精品资源点击获取