简介:这是一份面向计算机相关专业毕业设计、课程设计与期末大作业的深度学习实战项目,围绕股票价格预测与量化策略展开,包含 Python 源码、文档说明和答辩报告 PPT。项目由大四学生完成,经导师指导并获 99 分好评,代码完整可运行,适合需要快速上手完整项目流程的学习者,也适合作为毕业设计模板或实训案例。资源共 1409 个文件,压缩包约 16.2MB。其中以 Python 的 py/pyc 源码为主,辅以可执行文件、配置说明、CSV 数据文件和 PPT 报告,并包含虚拟环境相关配置,便于还原运行环境、查看预测结果与策略回测过程。文件类型覆盖源码、数据、说明文档和展示课件,结构清晰,可按模块检索学习。目前已有 247 人学习下载。通过这份资源,使用者可以获得一套完整的股票数据预处理、深度学习建模、价格预测和量化回测的实现思路,以及毕设文档和演示 PPT 的撰写参考,既能用于实战练习,也可为同类课题提供直接借鉴。
1. 股票预测项目拿到 99 分的底气不在模型,而在闭环
深度学习做股票价格预测,最常见的翻车不是网络不收敛,而是用 LSTM 预测收盘价时几乎原样复制前一天价格,预测曲线相比真实值整体滞后一根 K 线。这个“滞后复制”问题如果在答辩 PPT 里被当场指出来,项目基本就没有高分可能。这套基于深度学习的股票价格预测与量化策略研究 Python 源码能拿到 99 分,靠的不是把网络层数堆到多深,而是把数据清洗、序列建模、信号生成和量化回测整理成了一个能自证逻辑的闭环,文档说明和报告 PPT 也都围绕这一闭环做了展示。对正在做毕业设计的计算机专业学生,以及想用 Python 做量化入门实战的人来说,需要复现的正是这个闭环,而不是单独跑一个 LSTM。
2. LSTM 与 GRU 选型:从时间步建模到预测头设计
2.1 为什么普通 RNN 在日线行情上会失效
直接拿三层 RNN 预测日线收盘价,很容易发现验证集 loss 在前几个 epoch 仍然抖动不降。这个现象并不一定来自学习率,更多是梯度路径出了问题:当序列长度超过 30 步,RNN 的隐藏状态在反向传播时经过多轮连乘,较早时间步的梯度要么消失要么爆炸;而 A 股日线数据里值得记忆的形态往往散布在近 5 到 20 根 K 线中,普通 RNN 很难把这种跨时间的依赖保存下来。LSTM 用输入门、遗忘门、输出门维护一条细胞状态通路,梯度沿这条通路回传时受到稳定控制;GRU 把门控简化成两个门,参数更少,在小数据集上收敛更快。我在做这类毕设时,会同时保留 LSTM 和 GRU 两个等价模块,先跑 100 个 epoch 对比验证集 loss,再选默认实现。源码工程里同时看到两种结构并不奇怪,关键是要知道默认参数并不是最优。
2.2 用 PyTorch 搭一个可训练的 LSTMPredictor
下面给出一个能直接进入训练循环的预测网络。注意这里的输入 shape 使用 batch_first,方便和 DataLoader 返回的维度对齐。
import torch.nn as nn class LSTMPredictor(nn.Module): """将 LSTM 最后一个时间步的隐藏状态映射成未来收益预测。""" def __init__(self, n_features, n_hidden=64, n_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=n_hidden, num_layers=n_layers, batch_first=True, dropout=dropout, ) self.head = nn.Sequential( nn.Linear(n_hidden, 16), nn.ReLU(), nn.Linear(16, 1), ) def forward(self, x): out, _ = self.lstm(x) # out: [batch, seq_len, n_hidden] last = out[:, -1, :] # 只取最后一个时间步的特征 return self.head(last)这里的 n_features 取决于第 3 章里构造的特征宽度,不是只用收盘价。n_hidden 我一般从 64 起调,样本量只有几千条时超过 128 很容易过拟合;n_layers 设置为 2 是为了让模型能拟合到部分中长期依赖,3 层以上在日线场景下收益非常有限。head 部分先用一个 16 维全连接加 ReLU,再压缩到 1 个输出,比直接 Linear(n_hidden, 1) 的拟合能力更稳。如果希望同时预测未来 5 日最高价和最低价,把最后一个 Linear 的输出维度改成 2 或 3,并配合多任务损失,但这会明显加大调参量。
2.3 损失函数与评价指标:回归误差小不等于策略能赚钱
模型训练时最常用 MSE,但股票预测项目不能只看 MSE。下面这个表格可以帮你决定在不同阶段看哪个指标。
| 指标 | 计算口径 | 在毕设里的用途 |
|---|---|---|
| MSE | mean((y - y_hat)^2) | 训练主损失,放大大误差样本 |
| MAE | mean(abs(y - y_hat)) | 报告常用,单位直观 |
| MAPE | mean(abs((y - y_hat) / y)) | 无量纲对比,但接近 0 时会异常放大 |
| 方向准确率 | mean(sign(y_diff) == sign(y_hat_diff)) | 评估交易信号的核心指标 |
在训练日志里我会同时记录 MSE 和方向准确率:如果方向准确率长期低于 52%,说明模型没有学到比随机略好的规律,此时调大 lookback 或调整特征往往比加深网络更有效。MSE 优化出的模型会倾向于输出靠近均值的预测,天然容易把极端行情识别得不够充分,因此后续量化策略里对预测值做阈值切割时,不能直接套用回归评价结果。真正影响资金曲线的不是回归精度,而是信号切换频率和持仓周期,这一点在量化回测章节会更明显。
3. 数据预处理与滑动窗口:让模型“看见”可用的行情结构
3.1 前复权与特征拼装:别把除权跳空当成暴跌
很多数据接口默认返回未复权价格。未复权价格在分红送股当天会出现一个向下的价格跳空,模型会把它当成真实跌幅,从而学习到错误标签。在毕设里要干脆统一使用前复权价格,保证历史价格连续可回测。用 akshare 拉日线时,可以通过 adjust 参数控制复权方式:
import akshare as ak df = ak.stock_zh_a_hist( symbol="000001", period="daily", start_date="20180101", end_date="20231231", adjust="qfq", ) print(df.columns)不同版本的 akshare 返回列名可能不同,通常是中文列名。拿到原始数据后,把日期解析成 datetime 类型并排成升序,再用pd.to_datetime做索引。特征工程方面,可以加入过去 5 日收益率、成交量 5 日均线与 20 日均线的比值、最高最低价振幅、换手率;这些特征都是同一时刻能拿到的信息,不会引入未来值。对于常见做法,特征数量控制在 6 到 10 个即可,太多特征在几千条样本下会放大过拟合。
3.2 滑动窗口构造与标签设计:预测未来 K 根 K 线
有了特征表后,需要把二维行情数据切成模型能吃的三维序列。下面这个函数按时间顺序生成样本,标签是窗口结束后 horizon 天的累计收益率:
import numpy as np def make_sequences(data, feature_cols, label_col=0, lookback=30, horizon=5): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): t = i + lookback - 1 # 当前窗口最后一根K线的索引 X.append(data[i: t + 1, feature_cols]) future_ret = data[t + horizon, label_col] / data[t, label_col] - 1.0 y.append(future_ret) return np.array(X, dtype=np.float32), np.array(y, dtype=np.float32)这里 lookback 表示使用的历史长度,horizon 表示未来多少天。horizon 必须和策略的持仓周期保持一致:如果策略每 5 天调一次仓,就用 horizon=5;如果只是做次日涨跌判断,就设 horizon=1。标签使用未来第 horizon 天与当前窗口最后一天收盘价的比值减去 1,是为了避免模型预测绝对价格带来的量纲不稳定。注意最后 horizon 行的样本不存在完整标签,直接从训练集中丢弃,这是正确做法。窗口重叠是允许的,但会带来样本自相关性,论文里最好说明这一点;我通常把步长设为 horizon,减少重叠,测试集表现会稍微难看一点,但回测更诚实。
3.3 标准化与数据划分:防止未来函数泄漏
很多新手会把所有样本先算 mean/std 再做切分,然后在测试集上获得不错的表现,其实测试集的分布信息已经悄悄进入了标准化参数。正确做法是先按时间顺序切出训练集、验证集、测试集,再用训练集的数据去 fit 标准化器,最后 transform 三个集合。代码示例如下:
from sklearn.preprocessing import StandardScaler N, T, F = X_train.shape scaler = StandardScaler() scaler.fit(X_train.reshape(-1, F)) X_train = scaler.transform(X_train.reshape(-1, F)).reshape(N, T, F) # 验证集和测试集沿用同一 scaler,不能重新 fit提示:标准化器只能 fit 一次,切分顺序出错时回测会自动“偷看”未来,导致策略绩效虚高。
注意不能在整个数据集上按随机比例切分,时间序列必须保证验证集晚于训练集。下面是两种常见错误与正确做法的对比:
| 做法 | 是否可用 | 原因 |
|---|---|---|
| 全量数据标准化后切分 | 不可用 | 测试集信息泄漏到训练数据 |
| 随机 shuffle 后切分 | 不可用 | 破坏时间依赖,产生未来函数泄漏 |
| 按时间顺序切分后再 fit scaler | 可用 | 符合真实交易时只能看到历史信息 |
| 用滚动窗口重训模型 | 推荐 | 模拟真实市场,代价是训练成本上升 |
数据泄露会在回测阶段表现为“预测曲线和真实走势高度重合”,但实盘完全失效。后面第 5 章会专门介绍一种诊断这种假学习的检查脚本,现在只需要记住:标准化器只能 fit 一次,时间顺序不能破坏。
4. 模型输出到量化策略:信号生成、回测引擎与绩效指标
4.1 连续预测值如何变成交易信号:阈值、差分与持仓周期
模型输出的是未来 5 日累计收益率,不能简单拿大于 0 就开多,小于 0 就开空。因为预测误差大,靠近 0 的信号频繁翻转会吃满手续费。常见做法是设置一个阈值,只有预测收益超过阈值才建仓,低于负阈值才反向。阈值可以取交易成本的 2 到 3 倍。信号生成代码可以写成这样:
cost = 0.001 # 单边手续费加滑点 threshold = cost * 2 # 0.002,约 20 个基点 pred = pd.Series(...) # 模型输出的未来5日累计收益率 target = pd.Series(0, index=pred.index) target[pred > threshold] = 1 target[pred < -threshold] = -1 signal = target.shift(1) # 信号在次日开盘执行,避免用当天收盘价偷看未来这里信号序列 target 是在收盘后计算的,所以执行时点必须向后平移一天。如果预测周期是 5 天,那么 signal 应该保持 5 天不变,而不是每天都按当日预测结果调仓;一个简单实现是对 target 进行reindex().ffill(limit=horizon-1),或者直接对每日收益率向量按持仓周期采样。A 股还有 T+1 限制,这里只做简化演示,论文里要单独说明。
4.2 向量化回测引擎:计算资金曲线与换手成本
回测引擎不推荐用 for 循环逐日买卖,用 pandas 向量化更高效且不易出错:
import pandas as pd close = df["close"] daily_ret = close.pct_change().fillna(0.0) position = signal.fillna(0.0) turnover = position.diff().abs().fillna(0.0) strategy_ret = daily_ret * position - turnover * cost equity = (1 + strategy_ret).cumprod()position.diff()统计的是相邻两天持仓变化,变化一次就计一次单边交易成本。这里的成本是简化模型,实际回测还可以按成交价滑点、印花税分别建模。要注意 position 必须在前一天收盘时已经确定,也就是说daily_ret和position在索引上要对齐但执行上错位 1 天,上面的target.shift(1)已经处理了这个错位。如果回测资金曲线出现 45 度上行后断崖回落,一般就是没有考虑换手成本或信号前视。
4.3 绩效归因:夏普比率、最大回撤与卡玛比率
给出一套基础绩效函数:
def sharpe_ratio(returns, rf=0.0, periods=252): return np.sqrt(periods) * (returns.mean() - rf) / (returns.std() + 1e-8) def max_drawdown(equity): peak = equity.cummax() return ((peak - equity) / peak).max()sharpe_ratio 中加 1e-8 是防止 std 为 0 导致除零;max_drawdown 返回的是回撤比例,数值越小越好。在报告 PPT 中,建议用下面这个标准化表格模板对齐策略表现:
| 指标 | 公式 | 高分毕设里应呈现的水平建议 |
|---|---|---|
| 年化收益率 | 所有交易日的累计收益折算年化 | 结合样本区间说明,不要单独写 |
| 年化波动率 | 日收益标准差乘 sqrt(252) | 与基准比较 |
| 夏普比率 | (年化收益-无风险)/年化波动 | 1.2 以上算合格 |
| 最大回撤 | 峰值到谷底的亏损幅度 | 尽量控制 15% 以内 |
| 卡玛比率 | 年化收益 / 最大回撤 | 与夏普配合使用 |
另外,项目压缩包里出现的 fitness_dll.dll、oputils.dll,从文件命名看估计是策略参数寻优时使用的动态库。常见做法是回测中把适应度函数用 C/C++ 封装成 DLL,再通过 ctypes 在 Python 中调用,批量参数扫描可以提速几倍到几十倍;如果直接在 Python 里写 for 循环跑网格搜索,几千组参数可能要等半小时。这一层的优化不影响策略逻辑,但能把答辩现场演示做得更流畅。
5. 验证 y_hat 是否为“滞后复制”:种子固定与结果重放的三个技巧
5.1 用相关关系定位“假学习”模型
模型训练完成后,通常会导出 y_hat.csv、y_hat2.csv 这类预测结果。我拿到后第一件事不是看 loss,而是算两个相关系数。如果模型在复制上一天价格,那么预测序列与真实序列的同步相关系数可能很高,但和真实序列滞后一日数据的相关系数更高。下面用 pandas 快速检查:
import pandas as pd pred = pd.read_csv("y_hat.csv", index_col=0)["pred"] actual = pd.read_csv("y_true.csv", index_col=0)["close"] sync_corr = pred.corr(actual) lag_corr = pred.corr(actual.shift(-1)) print(f"sync={sync_corr:.3f}, lag={lag_corr:.3f}")当lag_corr明显高于sync_corr,说明模型学到的是“今天等于昨天”的退化解。正确模型应该同步相关更高,且滞后相关接近同步相关或更低。如果预测目标是收益率,把这个检查应用到收益率序列上同样有效。这个诊断技巧在导师提问“你怎么证明模型没有未来函数”时可以直接回答。
5.2 固定随机种子与模型保存,确保答辩可复现
神经网络每次运行结果不同,评阅人复现时如果跑出差异,很容易被认为结论不稳定。训练入口处统一固定三个随机源:
import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic = True set_seed(42) model = LSTMPredictor(n_features=F) torch.save(model.state_dict(), "best_model.pt")开了 cudnn.deterministic 后 PyTorch 的卷积和 LSTM 计算会切换到确定算法,运行变慢但结果可复现。预测完成后把结果写回 y_hat.csv,同真实标签一起画图放进 PPT。最容易打动答辩老师的图组合是:训练集与验证集 loss 曲线、真实收盘价与预测收益率的对比曲线、回测资金曲线和回撤区间标注。不要只写模型准确率,写“方向准确率 56%、夏普 1.3、最大回撤小于 15%”更能经受连续追问。如果换机器复现,项目里自带的 activate.bat、pyvenv.cfg 是 venv 生成的虚拟环境配套文件,先检查 requirements.txt 是否完整,再在 Python 安装目录下用python -m venv venv重建环境即可。
本文还有配套的精品资源,点击获取