简介:基于机器学习的股票预测方法研究论文,是面向高校计算机、金融等专业毕业设计或课程设计的写作范本。资源包内为1个docx文档,体积仅35KB,便于下载阅读。目前已有586人学习浏览,受到相关领域学习者与毕业生关注。论文系统梳理了股票预测的两类主流研究路径:一类基于历史股票数据,涵盖分段线性表示、高斯过程分类、随机森林、支持向量机及深度递归神经网络等传统机器学习方法;另一类引入深度学习技术,涉及卷积神经网络提取短期特征、长短期记忆网络生成预测、生成对抗网络建模等。在此基础上,论文进一步探讨了融合金融新闻与股民评论的非结构化文本信息,如基于情感词典的情绪极性分析、事件结构化表示与神经张量网络等前沿思路,并总结了宏观因素与突发事件对预测的影响。全文结构完整、论述严谨,既有方法综述也有实例分析,适合初学者、工程师和在校师生借鉴,可为撰写相关毕设或课设论文提供专业知识、实验思路与写作参考。
1. 机器学习做股票预测,论文参考到底能抄到什么
毕设和课设里,“基于机器学习的股票预测方法研究”是出现频率最高的题目之一。拿到这个题目的同学,往往不是不知道怎么做,而是不知道主线怎么搭:从数据爬下来到模型出结果,中间隔着一堆看似能跑又说不清对错的步骤。这篇参考文档的价值,就是把这条线从头到尾串一遍——什么时候做特征、什么时候切数据、模型之间怎么对比、答辩老师最常追问哪个坑,都在路线里。
文档主线是标准的机器学习应用流程:先确定预测目标,再构造特征,然后选模型做对比实验,最后回测评估。适合需要在一个月内搭完实验并写完论文的本科毕设、课设学生;不适合指望拿一个模型直接实盘赚钱的人,这点先想清楚,后面所有实验设计都会围绕“论文可解释”而不是“绝对收益”。
2. 任务定义与特征构造:把股票预测拆成可训练的问题
拿到题目的第一步,先别急着爬数据。“股票预测”这个词太大,直接丢给机器学习模型,它只会还给你一个无法解释的结果。我一般会把标题里的“预测”拆成两个能落地的子问题:一是预测价格,二是预测方向。价格是连续值,方向是离散值,两者对应的模型选型和评估方式完全不同,写进论文里的思路也不一样。
这里涉及到一个很现实的问题:论文的“方法研究”部分到底要研究什么。如果只是把数据塞进模型里跑一遍,那论文没有灵魂,答辩时老师一问就露馅。所以这一章先把问题定义和特征构造讲透,这两件事决定了后面所有对比实验的走向,也决定了评审老师愿不愿意给你这个“方法研究”四个字打高分。
2.1 涨跌分类比收盘价回归更适合当论文主线
先说结论:我不建议把“预测明日收盘价的具体数值”作为论文主线。股票价格近似随机游走,你今天预测它明天收在 10.12 元还是 10.15 元,本质上和猜硬币没太大差别。用线性回归或神经网络直接拟合收盘价,出来的 RMSE 大得没法看,而且论文里完全解释不了误差来源。
更稳的做法是把预测目标定义成“明日涨跌方向”:明日收盘价高于今日收盘价记为 1,否则记为 0。这样一来,任务就从回归变成了分类,评估指标可以选准确率、F1、AUC,每一张图都能讲出故事。“预测涨跌”在金融上也有实际含义——它对应着仓位决策:预测涨就持有,预测跌就空仓或减仓。
这种做法在论文里很常见,也是一条成熟的“方法研究”主线。把涨跌分类作为主实验,把收盘价回归作为副实验放在后面做对比,既完整又不冒进。如果你连方向都预测不准,去预测具体数值只会让自己陷入被动。
2.2 候选机器学习算法盘点与选型理由
明确了分类主线之后,下一步是选模型。机器学习算法很多,但不能全塞进论文里跑一遍,那样既耗时间又让论文看起来像流水账。下面这张表是我给毕设同学做选型时的常用底稿,按“论文友好度”从高到低排列:
| 算法 | 适合的任务 | 论文里的优点 | 毕设风险 |
|---|---|---|---|
| 逻辑回归 | 涨跌二分类 | 系数可解释,能做特征重要性分析 | 对非线性关系拟合不足 |
| Lasso 回归 | 特征选择 | 自带稀疏性,可用来筛特征 | 输出是连续值,需要额外设阈值转分类 |
| RBF 核 SVM | 中小规模分类 | 决策边界直观,理论完备 | 样本量大时训练慢,核参数需要调 |
| 随机森林 | 表格数据分类 | 抗过拟合能力强,能输出特征重要性 | 树的数量和深度需要反复试 |
| XGBoost | 表格数据分类 | 精度上限高,竞赛验证过 | 调参维度多,容易陷入调参泥潭 |
| LSTM | 序列建模 | 贴合时间序列直觉,能写进对比章节 | 训练慢,数据少时效果还不如随机森林 |
如果论文只覆盖“机器学习”而不涉及深度学习,最稳妥的组合是:SVM 和随机森林做主角,XGBoost 做加分项,逻辑回归当 baseline。这套组合的优点在于,四个模型覆盖了“线性—核方法—树集成”三个方向,横向对比起来层次丰富,答辩时可以说清楚每个模型的差异。LSTM 不是不能加,但它属于深度学习模型,会引入训练时间、GPU、序列长度等一堆额外变量,时间紧的话建议放附录。
2.3 特征怎么构造:价格、成交量与技术指标的取舍
模型选好之后,决定预测效果上限的是特征。股票预测里常用的特征可以分成四组,每组在论文里都有对应的呈现方式:
| 特征组 | 典型字段 | 计算方式 | 注意点 |
|---|---|---|---|
| 基础价格 | 开盘价、最高价、最低价、收盘价 | 直接取原始行情 | 必须先做复权处理 |
| 衍生收益 | 日收益率、5 日收益率 | 收盘价 pct_change | 比原始价格更平稳,优先使用 |
| 均线类 | MA5、MA10、MA20、MA60 | 滚动均值 | 短中长周期各取一个即可 |
| 技术指标 | RSI、MACD、布林带位置 | 按公式计算 | 版本差异大,务必写明计算口径 |
| 量价类 | 成交量、量比、成交额 | 原始值或滚动均值 | 涨跌和放量缩量结合才有意义 |
一个常见误区是特征堆得越多越好。我见过有同学一上来构造了 80 维特征,随机森林倒是能跑,但 SVM 在 80 维小样本上直接过拟合,训练集准确率 98%,测试集 53%。对于股票日线数据这种典型的小样本场景,特征数量控制在 10 到 20 个比较合适。优先选你解释得清含义的特征,解释不清的单指标宁可不要。
另外提醒一句:技术指标算出来之后,前期数据必然有 NaN,需要统一丢弃,不能在后面训练时把 NaN 带进模型。这一点虽然基础,但在论文代码里翻车的概率非常高。
3. 数据、样本与标签:把原始行情变成可训练数据集
任务定义和特征方案定了,接下来就是把文字方案变成真实代码。这一章解决的是“数据从哪来、标签怎么打、样本怎么切”三个问题。做股票预测的论文,这一章的代码质量直接决定实验能否复现,也是答辩老师最常要求现场演示的部分。
3.1 数据来源与字段取舍
国内做 A 股日线数据,常见的做法是用免费的 Python 接口直接拉取,比如 akshare 或 tushare。akshare 不需要注册 token,对毕设来说最省事。下面这段代码拉取平安银行从 2020 年到 2023 年的日线数据:
import akshare as ak # 拉取平安银行(000001)日线,前复权 df = ak.stock_zh_a_hist( symbol="000001", period="daily", start_date="20200101", end_date="20231201", adjust="qfq", ) # 先打印列名,不同版本返回字段有差异 print(df.columns.tolist()) print(df.head())参数说明:symbol是股票代码,period="daily"表示日线,adjust="qfq"表示前复权。前复权的作用是消除分红送股造成的价格跳变,如果不复权,股价会产生虚假的暴跌,特征也会被污染。拿到数据后第一件事永远先打印列名和头部数据,确认下字段是不是“日期、开盘、最高、最低、收盘、成交量、成交额”这一套,再决定重命名逻辑。
建议拉取至少 5 到 10 年的数据。股票预测实验的样本量本身就小,日线一年只有 240 条左右,三年不到 800 条,再扣掉计算指标和生成标签时的 NaN 丢弃,最终能进模型的特征矩阵可能只有 700 行,这点数据量对 SVM 和随机森林来说非常紧张。
3.2 生成标签与特征:滑动窗口与滞后设计
数据拿回来以后,先重命名列,再生成标签和特征。这套流程我习惯写成一个函数,方便后面换股票或换时间区间时直接复用:
import pandas as pd def prepare_dataset(df): # 统一列名 df = df.rename(columns={ "日期": "date", "开盘": "open", "最高": "high", "最低": "low", "收盘": "close", "成交量": "volume" }) df = df[["date", "open", "high", "low", "close", "volume"]].copy() df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 标签:明日收盘价高于今日收盘价记为 1 # shift(-1) 取的是下一行的值,这正是我们要预测的未来信息 df["label"] = (df["close"].shift(-1) > df["close"]).astype(int) # 特征:收益、均线、量比 df["ret_1"] = df["close"].pct_change(1) df["ret_5"] = df["close"].pct_change(5) df["ma5"] = df["close"].rolling(5).mean() df["ma20"] = df["close"].rolling(20).mean() df["ma_ratio"] = df["ma5"] / df["ma20"] # 短期均线相对长期均线的位置 df["vol_ratio"] = df["volume"] / df["volume"].rolling(20).mean() # 丢弃计算产生的缺失值 df = df.dropna().reset_index(drop=True) return df这段代码里的关键点在注释里已经标出来了:shift(-1)生成标签时用到的确实是“未来一天”的收盘价,这在监督学习里是合法的,因为标签本来就是我们要预测的东西。但要注意,如果特征里也用了shift(-1),那模型在训练时就偷看了答案,这属于严重的未来函数泄漏,后面第 5 章会专门讲。
rolling(5).mean()会从第 5 行开始才有值,所以前 20 行左右的均线特征都是 NaN。dropna()把所有含 NaN 的行丢弃,最终得到的是干净整齐的特征矩阵。这里还有一个细节:vol_ratio用的是当日成交量除以过去 20 日平均成交量,大于 1 说明放量,小于 1 说明缩量,这个特征在量价分析里比只看绝对值稳定得多。
3.3 归一化与训练、验证、测试集划分
特征构造好之后,下一步是归一化和切分。股票数据是时间序列,不能用train_test_split直接随机打乱,否则模型看到的是“过去的未来”,测试集评估结果会虚高。我一般手动按时间顺序切分,训练集 70%、验证集 15%、测试集 15%:
from sklearn.preprocessing import StandardScaler feature_cols = ["ret_1", "ret_5", "ma5", "ma20", "ma_ratio", "vol_ratio"] X = df[feature_cols].values y = df["label"].values # 按时间顺序切分,绝对不能 shuffle train_end = int(len(df) * 0.70) val_end = int(len(df) * 0.85) X_train, y_train = X[:train_end], y[:train_end] X_val, y_val = X[train_end:val_end], y[train_end:val_end] X_test, y_test = X[val_end:], y[val_end:] # 归一化:只在训练集上 fit,再 transform 验证集和测试集 scaler = StandardScaler() scaler.fit(X_train) X_train = scaler.transform(X_train) X_val = scaler.transform(X_val) X_test = scaler.transform(X_test)这段代码最值得说的是scaler.fit(X_train)这一步。标准化需要计算均值和标准差,这两个统计量只能从训练集算,然后再用同样的参数去变换验证集和测试集。如果先对全量数据做标准化再切分,训练集里就包含了测试集的均值信息,这在论文里属于数据泄漏。虽然短期股价预测里影响不一定肉眼可见,但答辩老师问起来,答不上“为什么先 fit 再 transform”就露怯了。
如果想让论文显得更严谨,可以把手动切分布换成 sklearn 的TimeSeriesSplit,做 5 折时间序列交叉验证,用验证集的平均指标来选模型参数。但要注意,TimeSeriesSplit的每一折也是严格按时间顺序划分的,不会出现后一折作为前一折训练集的问题。这部分代码不需要写进正文,在论文的“实验设计”里描述清楚即可。
4. 模型训练与评估:用同一个切分跑平行对比实验
数据准备好了,模型对比就是水到渠成的事。论文里的“对比实验”最怕各跑各的——不同人写出来的代码用了不同的特征、不同的切分、不同的归一化方式,结果根本没有可比性。我的做法是固定一套数据管线,只更换模型对象,这样指标差异完全归因于模型本身的差异,写作时也省力。
4.1 统一评估函数与三类核心指标
机器学习分类任务里,准确率、F1 和 AUC 三件套基本够用。准确率直观,F1 对正负样本不平衡敏感,AUC 不依赖具体阈值,能从排序角度衡量模型的区分能力。下面定义统一的评估函数:
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score def evaluate_model(model, X_val, y_val): y_pred = model.predict(X_val) # 部分模型需要先取预测概率,再算 AUC if hasattr(model, "predict_proba"): y_prob = model.predict_proba(X_val)[:, 1] else: y_prob = model.decision_function(X_val) return { "accuracy": round(accuracy_score(y_val, y_pred), 4), "f1": round(f1_score(y_val, y_pred), 4), "auc": round(roc_auc_score(y_val, y_prob), 4), }参数说明:accuracy_score直接比较预测类别和真实类别;f1_score默认计算正类的 F1,这里正类就是 label 为 1 的“上涨日”;predict_proba取第 1 列,代表属于正类的概率。SVM 如果不带probability=True,就没有predict_proba属性,只能用decision_function的输出替代,这一点在调用时需要做兼容处理。
在论文里,这三个指标要一起放,不能只挑好看的写。AUC 能到 0.55 以上、准确率能到 52% 以上,在股票方向预测里已经是有效信息了,别拿图像分类的标准来要求金融时序,那是两个世界的任务。
4.2 主模型训练:SVM、随机森林与 XGBoost
模型训练部分,我会把要对比的模型放在一个字典里,循环训练并收集结果:
from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier models = { "svm_rbf": SVC(kernel="rbf", C=1.0, gamma="scale", probability=True, random_state=42), "random_forest": RandomForestClassifier( n_estimators=200, max_depth=8, min_samples_leaf=5, random_state=42 ), } results = {} for name, model in models.items(): model.fit(X_train, y_train) results[name] = evaluate_model(model, X_val, y_val) print(name, results[name])SVC这里开了probability=True,代价是训练时间变长,但换来的是可以直接算 AUC,对论文来说是值得的。C=1.0是正则化强度的默认值,越大越容易过拟合;gamma="scale"让核宽自动依据特征数量来设置,是一个比较安全的默认值。随机森林的n_estimators=200表示 200 棵树,max_depth=8限制每棵树的深度,min_samples_leaf=5强制每个叶子节点至少 5 个样本,这三组参数共同控制模型的复杂度,是表格数据里比较稳的起点。
如果你想让对比实验更饱满,可以再加一个 XGBoost:
from xgboost import XGBClassifier xgb = XGBClassifier( n_estimators=300, learning_rate=0.05, max_depth=4, subsample=0.8, colsample_bytree=0.8, random_state=42, eval_metric="logloss", ) xgb.fit(X_train, y_train) results["xgboost"] = evaluate_model(xgb, X_val, y_val) print("xgboost", results["xgboost"])XGBoost 的四个关键参数要能解释清楚:n_estimators决定树的数量,learning_rate是每棵树对最终预测的贡献权重,放小一点更抗过拟合;subsample每轮随机抽样比例,colsample_bytree每棵树随机选特征的比例,这两个都是防过拟合的手段。调参的时候不要一上来就 GridSearch,先用默认参数跑出结果,再固定一个参数单独调,否则搜索空间太大会把时间耗光。
4.3 论文结果表的呈现方式与写作建议
实验跑完,结果表建议按下面的模板组织,简单清晰:
| 模型 | 准确率 | F1 | AUC | 训练耗时 |
|---|---|---|---|---|
| 逻辑回归(baseline) | ||||
| RBF 核 SVM | ||||
| 随机森林 | ||||
| XGBoost |
训练耗时可以用 Python 的time.time()在 fit 前后打点记录,几秒钟还是几分钟会有肉眼可见的差别。论文里不要只写“随机森林效果最好”就结束,至少要补一句解释:为什么树模型在这个特征集上优于 SVM,常见的解释是特征间的非线性交互被树结构自动捕捉到了,而 SVM 需要人工设计核函数来拟合这种交互。
写作时有一个容易犯的毛病:把所有指标都加粗,突出自己最想说的那项。其实答辩老师不傻,别把验证集指标写成测试集指标,也别把“验证集调参”说成“测试集评估”,一旦被追问出矛盾,整篇论文的可信度都会打折扣。机器学习实战里最忌讳的就是实验过程不透明,宁可指标普通一点,也要保证每一步都经得起现场复现。
5. 论文里最容易被答辩老师追问的 5 个坑,先踩先改
机器学习股票预测这个方向的论文,技术栈不复杂,真正的技术含量全在细节。你踩过的坑,答辩老师大概率也踩过,他们问的问题基本集中在“数据有没有泄漏、标签怎么打的、评估到底可不可信”这几个方向。下面五个坑是我见过频率最高的,每个都按“现象—原因—解决”的顺序讲。
5.1 坑 1:标准化时把全样本统计量用进去了
现象:模型在训练集上 AUC 0.72,测试集上 AUC 只有 0.51,差距大得离谱,论文里交代不清。
原因:对全量数据统一调用scaler.fit_transform(X),训练阶段已经用到了测试集的均值和方差,相当于测试集信息提前进入了模型。
解决:严格按“先切分,再只对训练集 fit,再 transform 验证集和测试集”的顺序执行。代码格式见第 3.3 节,最重要的是把fit和transform分开写。
5.2 坑 2:随机打乱样本,时间序列被暗中作弊
现象:用train_test_split(X, y, test_size=0.2, random_state=42)切分,结果 AUC 稳定在 0.60 以上,看着很漂亮,但一改成时间顺序切分立刻掉到 0.53。
原因:随机打乱后,训练集里混入了“未来”的样本,模型在训练时见过同一段行情的邻近片段,测试时等于开卷考试。
解决:股票数据禁止随机洗牌,一律按时间序切分。也可以用TimeSeriesSplit做交叉验证,但本质上都必须保证训练集索引始终小于测试集索引。
5.3 坑 3:标签代码把当天信号当成了次日信号
现象:模型准确率异常高,比如到 68% 以上,但一画预测曲线发现所有预测都比真实信号“提前一天”。
原因:标签写成了(df["close"].shift(-1) > df["close"].shift(-2)),或者把当日涨跌幅直接当成了明日涨跌幅的标签。前者错位一天,后者拿当天的涨跌和未来的特征对齐,特征和标签时间窗口错位。
解决:标签统一用“下一根 K 线收盘价相对于当前收盘价的方向”,也就是df["close"].shift(-1) > df["close"]。写完代码后随机抽几行人工核对,看看 label 是否真的对应了明天的涨跌。
5.4 坑 4:只用准确率,被不均衡样本骗了
现象:预测结果准确率 55%,看起来很有效,但打印分类报告发现模型把所有样本全预测为“涨”,因为数据集里上涨日占比本来就 55%。
原因:股票数据中涨跌天数并非严格各半,当一方占多数时,模型学到的只是“无脑预测多数类”这个捷径。
解决:至少同时报告 AUC 和 F1。AUC 不受阈值影响,F1 对正类样本的捕捉能力敏感。如果正确率向多数类倾斜,优先调整分类阈值,用验证集上 F1 最高的阈值替代默认的 0.5。
5.5 坑 5:参数调到完美,换一段行情立刻失效
现象:在 2020—2023 年数据上调出来一组“完美参数”,测试集 AUC 0.58,换成 2024 年行情回测,立刻掉到 0.50,甚至不如随机猜。
原因:股价数据的信噪比极低,调参过程中模型可能把特定年份的行情特征当成了普适规律,比如把 2020—2021 年的白马股行情特征学进去了。
解决:论文里明确区分“调参集”和“最终测试集”。我一般会拿最近 10% 的样本做最后的留出集,调参期间完全不去碰它,所有参数确定之后才跑一次最终评估。如果最终结果和验证集差异过大,说明参数过拟合了行情,需要回到上一个台阶重新调。
6. 让实验结果更可信的两个进阶技巧
如果前面的主线实验都跑通了,再花半天时间做两个进阶验证,论文的说服力会明显上台阶,答辩时也有话可讲。
6.1 用滞后基线挡住“模型没学到东西”的尴尬
股票预测论文里最常被问到的问题是:你的模型比“什么都不做”强在哪?这时候你需要一个 baseline。最简单的基线是“惰性预测”:明天涨跌方向与今天相同,今天涨了预测明天也涨。实现只有一行:
base_line_acc = (df["close"].shift(1) < df["close"]).astype(int).mean()这个数值一般在 0.48 到 0.52 之间。你的模型准确率如果只有 51%,但基线已经 50.5%,那涨幅有限;反过来,如果模型 AUC 有 0.58、准确率 54%,而 baseline 只有 50%,结论就立住了。把这个数字写进实验设计章节,作为评估指标的参照系,比单独放一堆模型对比更有说服力。
6.2 考虑手续费和滑点的简单回测
分类指标只能说明预测能力,不能说明策略能不能赚钱。论文里加一个小节做简化回测,按预测结果模拟交易,同时扣除手续费,会让整体工作量看起来非常饱满。下面是一个极简回测框架:
def simple_backtest(close, pred, fee=0.0005, threshold=0.55): capital = 100000.0 # 初始资金 position = 0 # 持仓状态:1 持有,0 空仓 equity = [] # 每日净值 for i in range(1, len(close)): # 预测概率高于阈值才买入,避免频繁交易 if pred[i] >= threshold and position == 0: position = 1 capital *= (1 - fee) # 买入扣手续费 elif pred[i] < threshold and position == 1: position = 0 capital *= (close[i] / close[i - 1]) * (1 - fee) # 卖出扣手续费 if position == 1: capital *= close[i] / close[i - 1] equity.append(capital) return equity参数说明:fee=0.0005表示单边万五的手续费,threshold=0.55表示只有预测上涨概率超过 55% 才开仓,这是为了过滤掉那些“拿不准”的预测。回测的目的不是让你证明策略能赚钱,而是让你对比“带模型”和“随机入场”两条净值曲线的差异。如果两者走势几乎一样,说明预测信号没有实际交易价值,这时候回去调特征或阈值,比重新写模型更能解决问题。
我在做自己的课程设计时吃过一次亏:模型指标看着不错,但一加上手续费和滑点,年化收益直接变成负的。从那以后,我所有股票预测实验都强制带一次回测,哪怕结果是亏的,至少心里清楚亏在哪里。预测方向和实际交易之间隔着成本、滑点和仓位管理,把这些诚实写进论文里,反而是最能体现研究态度的部分。希望这篇笔记能帮你把实验主线搭得稳一点,少走点我当年走过的弯路。
本文还有配套的精品资源,点击获取