news 2026/10/11 13:25:26

LSTM股票预测大作业高分代码拆解:数据预处理、模型训练与评估回测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LSTM股票预测大作业高分代码拆解:数据预处理、模型训练与评估回测

简介:这是一份Python期末大型作业,主题为深度学习在股票分析预测中的应用,主要面向计算机相关专业学生与需要项目实战的自学者。项目在导师指导下完成并获评98分,源代码已完成本地编译与调试,可直接运行。包内共20个文件,包含6个Python脚本(如data_downloader、stock_indicator、prediction、backtest、strategy)、6张预测结果图片、3个CSV数据集以及README、requirements等说明文件,压缩包大小4.24MB。核心内容覆盖数据清洗、特征提取、模型构建、训练验证与回测评估等完整流程,使用LSTM等深度学习模型处理时间序列,并借助NumPy、Pandas、TensorFlow/Keras等工具实现分析预测。已有76人学习下载,适合希望系统掌握深度学习金融应用、提升Python实战能力的学习者参考。

1. 深度学习股票分析预测大作业:这份高分代码到底是怎么组织的

期末周拿到“深度学习应用于股票分析预测”这道题,大多数人的第一反应不是不会写,而是怕写完跑不起来。框架装了一下午、数据下载失败、Loss 不收敛、画出来的预测曲线和真实曲线错位一个周期——这些都是期末季最常见的翻车现场。我今天拆的这份高分作业代码,核心是用 LSTM 对股票收盘价做时间序列预测,完整走了一遍“数据获取 → 特征工程 → 建模训练 → 评估可视化”的主线流程,最后再用方向准确率做一轮回测验证。换句话说,它不是一个花架子 Demo,而是一个从数据到结论都能自洽的完整项目。适合三类人:第一次做深度学习大作业、想抄一个能交差框架的在校生;想快速把 LSTM 落地到行情数据上看看效果的从业者;以及需要一个 baseline 再做改进的量化方向初学者。

2. 数据源与特征工程:把 K 线转换成模型能吃的张量

2.1 复权价格与数据获取:选什么数据源,为什么复权

做股票预测第一步是取数。期末作业场景下,我见过三种常见来源:yfinance 免费接口、tushare pro(需要 token)、akshare。这份代码默认用的是 yfinance,因为不需要注册、不用填 token,pip 装好就能跑,最适合作为课程作业的默认方案。数据粒度上,代码默认取日线(interval='1d'),个股选的是 AAPL,如果你想换成 A 股标的,只需要改 ticker 符号,比如600519.SS(贵州茅台)或000001.SS(平安银行),yfinance 对美股和部分 A 股代码都支持。

import yfinance as yf import pandas as pd df = yf.download("AAPL", start="2020-01-01", end="2024-06-30", progress=False) df = df[["Open", "High", "Low", "Close", "Volume"]].copy() # 用 close 生成复权因子,简单做一次后向复权 df["Adj Close"] = df["Close"] / df["Close"].iloc[0] * 100

这里有个关键点:很多同学直接拿原始收盘价建模,遇到除权除息日,价格会出现一个向下的跳空缺口,模型会把这个缺口当做一个真实的下跌信号来学,导致预测结果在分红季前后明显失真。常见做法是使用复权价格(前复权或后复权),yfinance 返回的Adj Close就是复权收盘价,但这份代码的默认逻辑更简单——用一个基准日做归一化处理,用相对收益替代绝对价格。对于期末作业来说,只要你的论文里写清楚“本研究使用后复权收盘价”,这个处理就足够严谨了。参数progress=False是为了关掉下载进度条,避免在 Jupyter 里刷屏。

2.2 技术指标特征:MA、RSI、MACD 怎么算进矩阵

光有原始 OHLCV(开盘、最高、最低、收盘、成交量)五列数据,LSTM 也能训练,但预测效果通常比较“钝”。原因在于原始价格序列是非平稳的,直接输入会让模型花大量容量去拟合价格水平,而不是拟合价格变化。所以这份代码在特征工程上做了三件事:计算移动平均线 MA、相对强弱指标 RSI、MACD 指标,然后把它们和收益率拼成一个多维特征矩阵。

def add_technical_features(df): df = df.copy() # 简单移动平均:5 日和 20 日 df["MA5"] = df["Close"].rolling(window=5).mean() df["MA20"] = df["Close"].rolling(window=20).mean() # 对数收益率,消除量纲影响 df["Return"] = df["Close"].pct_change() * 100 # RSI(14):上涨均幅 / 下跌均幅 的衍生 delta = df["Close"].diff() gain = delta.clip(lower=0).rolling(window=14).mean() loss = (-delta.clip(upper=0)).rolling(window=14).mean() df["RSI"] = 100 - 100 / (1 + gain / loss) # MACD 快慢线差离值 ema12 = df["Close"].ewm(span=12, adjust=False).mean() ema26 = df["Close"].ewm(span=26, adjust=False).mean() df["MACD"] = ema12 - ema26 return df.dropna()

为什么用这些特征?简单解释一句就行:MA 刻画趋势方向,RSI 刻画超买超卖状态,MACD 刻画动量变化,这三个是传统技术分析里最常用的因子,拿到这里作为神经网络输入,本质上是用先验知识帮模型缩小搜索空间。注意dropna()会把前 20 行左右的数据丢空,因为 MA20 需要 20 个交易日才能算出第一个值。如果你的数据集比较短,可以把 MA20 换成 MA10,或者用min_periods参数放宽窗口限制。

2.3 归一化:为什么 MinMaxScaler 必须分两次 fit

这是全代码里最容易被忽略、但又最致命的环节。LSTM 默认使用 tanh 和 sigmoid 激活函数,输入范围最好在 0 到 1 附近,否则梯度很容易饱和。这份代码用了MinMaxScaler做归一化,如果你写的是下面这种“常规操作”,期末答辩时老师一眼就能看出问题:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() # 错误写法:全量数据 fit 后再 transform scaled = scaler.fit_transform(feature_df[["Close", "MA5", "MA20", "RSI", "MACD"]])

错在哪?fit_transform是在整个数据集(包括测试集)上计算 min 和 max,这等于把未来的价格区间信息泄漏给了训练过程。正确做法是对训练集单独fit,再用同一个训练集上得到的 min 和 max 去transform验证集和测试集:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() # 先按时间顺序切分,再在训练段上 fit train_size = int(len(feature_df) * 0.8) train_df = feature_df.iloc[:train_size] test_df = feature_df.iloc[train_size:] scaler.fit(train_df[feature_cols]) # 只用训练段求 min/max train_scaled = scaler.transform(train_df[feature_cols]) test_scaled = scaler.transform(test_df[feature_cols]) # 复用训练段的参数

这正是这份代码里做得规范的地方。train_size默认取前 80% 作为训练段,剩下的 20% 作为测试段。值得注意的细节是:这个 split 是按时间顺序硬切的,不能用train_test_split的默认随机模式,否则序列的上下文关系全部被打乱,模型就失去了时间序列的意义。

3. LSTM 模型构建与训练:时间步长、隐藏层与早停

3.1 为什么选 LSTM 而不是普通神经网络或 RNN

股票数据是典型的时间序列,前后的价格存在强相关性。如果用普通的前馈神经网络(MLP)去拟合,输入的每个时刻之间是相互独立的,模型只能看到一个窗口里的离散特征,抓不到时间维度上的依赖关系。而传统 RNN 理论上能处理序列,但存在梯度消失问题——当序列长度超过 20 步左右,反向传播的梯度就会指数级衰减,模型学不到“20 天前发生了什么”。

LSTM 通过三个门控机制(遗忘门、输入门、输出门)和一个记忆单元(cell state)来解决这个问题。遗忘门决定丢弃哪些历史信息,输入门决定记住哪些新信息,输出门决定输出什么给下一层。在课程作业的维度上,你不需要把门的数学公式完整写进代码,但要在论文里说明:LSTM 能选择性地记住长期趋势(比如 20 日前的支撑位),同时遗忘短期的噪声波动(比如一天的随机涨跌),这是它适合股票预测的主要原因。

3.2 滑动窗口切片:把连续序列切出“样本-标签”对

拿到归一化后的矩阵后,不能直接丢进 LSTM。LSTM 的输入 shape 是(batch_size, timesteps, features),其中timesteps是看多长的历史窗口,features是特征维度。这份代码用滑动窗口的方式构造样本:例如设定lookback=20,就是用第 1~20 天预测第 21 天,用第 2~21 天预测第 22 天,依次类推。

import numpy as np def create_sequences(data, lookback=20): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i, :]) # 过去 lookback 天的全部特征 y.append(data[i, 0]) # 当天收盘价作为预测目标 return np.array(X), np.array(y) lookback = 20 X_train, y_train = create_sequences(train_scaled, lookback) X_test, y_test = create_sequences(test_scaled, lookback)

lookback是这份代码里最重要的超参数,默认 20,对应大约一个月的交易日。这个值的设定有讲究:太短(比如 5),模型看不到中期趋势,对拐点的判断很迟钝;太长(比如 60),模型容易过拟合噪声,而且训练量变大,期末作业的实验时间会拉长。我一般会建议你用 20 和 30 各跑一组对比,把结果截图放进论文里,这就是一个很好的“参数敏感性分析”章节素材。注意切片后X_train.shape是(样本数, 20, 6),6 对应 Close、MA5、MA20、RSI、MACD、Return 这六列特征。

3.3 模型结构与训练参数:层数、神经元数、Dropout 与 EarlyStopping

这份代码的模型结构是两层 LSTM 加一层全连接输出。第二层 LSTM 设置了return_sequences=False,意思是只返回最后一个时间步的输出,而不是每个时间步都输出,这样才能接上Dense(1)做单值回归预测。模型编译用的损失函数是mse(均方误差),优化器是Adam,学习率设为0.001——这是回归任务的默认安全组合。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping model = Sequential() model.add(LSTM(units=64, return_sequences=True, input_shape=(X_train.shape[1], X_train.shape[2]))) model.add(Dropout(0.2)) model.add(LSTM(units=64, return_sequences=False)) model.add(Dropout(0.2)) model.add(Dense(units=1)) model.compile(optimizer="adam", loss="mse") model.summary() early_stop = EarlyStopping(monitor="val_loss", patience=10, restore_best_weights=True) history = model.fit(X_train, y_train, epochs=50, batch_size=32, validation_data=(X_test, y_test), callbacks=[early_stop], verbose=1)

第一层LSTM(units=64, return_sequences=True)返回每个时间步的隐藏状态,这样第二层 LSTM 才能继续序列建模。Dropout 层加在两轮 LSTM 之间,作用是随机丢掉 20% 的神经元输出,防止模型把训练集的噪声背下来。EarlyStopping监控验证集损失,如果连续 10 个 epoch 都没有下降,就提前终止训练,并且用restore_best_weights=True回滚到验证集最优的权重。batch_size 设置 32,epochs 设 50,配合早停通常二十来个 epoch 就能收敛,CPU 上跑十几分钟也能出结果。

4. 评估与可视化:RMSE 只是及格线,方向准确率才是分水岭

4.1 回归指标:RMSE、MAE 的计算与解释

LSTM 的原始输出是归一化后的数值,不能直接用来算误差。要还原成真实价格,必须先做inverse_transform的反归一化操作。这里最容易出的问题我已经在上面强调过:scaler.inverse_transform期望输入的列数必须和当初fit时的列数一致。这份代码把全部特征列一起缩放,所以反归一化时要切片取回收盘价那一列。

from sklearn.metrics import mean_squared_error, mean_absolute_error # 预测结果反归一化 y_pred_scaled = model.predict(X_test) y_test_inv = scaler.inverse_transform(np.concatenate([y_test.reshape(-1, 1), np.zeros((len(y_test), 5))], axis=1))[:, 0] y_pred_inv = scaler.inverse_transform(np.concatenate([y_pred_scaled, np.zeros((len(y_pred_scaled), 5))], axis=1))[:, 0] rmse = np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) mae = mean_absolute_error(y_test_inv, y_pred_inv) print(f"RMSE: {rmse:.2f}, MAE: {mae:.2f}")

上面的代码块用了一个小技巧:np.concatenate把预测值拼上五列 0,凑成和原始特征矩阵一致的结构,再做反归一化,最后取第一列收盘价。y_test 的逆变换会用到同一个 scaler,但顺序上要注意:inverse_transform得到的矩阵,第一列才是收盘价对应的缩放,第二列是 MA5 等特征对应的缩放,不能直接取整个矩阵。RMSE 的量纲是价格单位(比如美元或分),如果你想做一个与价格无关的评估,可以除以测试期间的平均价格,得到百分比误差。期末答辩时,老师更关心的是你知不知道这两个指标各自的局限——RMSE 对大误差敏感,MAE 更鲁棒;所以这份代码两个都算了,这是加分项。

4.2 预测曲线可视化:一张图把论文的“实验结果”撑起来

期末大作业的评分很大程度取决于结果图好不好看、能不能讲出故事。这份代码画了两条曲线:真实收盘价和 LSTM 预测收盘价,横轴是时间,纵轴是价格。从视觉上,最常见的预测曲线形态是“滞后一拍”——预测线和真实线几乎平行,但总是晚一天。这在股票预测领域几乎是必然的,因为 LSTM 在最小化 MSE 时的最优策略是“预测值 ≈ 昨天的真实值”,因为股价变化接近随机游走,昨天的价格就是今天价格的无偏估计。

import matplotlib.pyplot as plt plt.figure(figsize=(12, 6)) plt.plot(test_df.index[-len(y_test_inv):], y_test_inv, label="Real Close", color="black") plt.plot(test_df.index[-len(y_pred_inv):], y_pred_inv, label="LSTM Predict", color="steelblue", linestyle="--") plt.xlabel("Date") plt.ylabel("Close Price (USD)") plt.legend() plt.title("LSTM Prediction on Test Set") plt.show()

画图的参数里值得说一句的是横坐标索引:由于滑动窗口切掉了前lookback个样本,预测结果对应的时间点应该是test_df.index[lookback:]而不是test_df.index[:],否则曲线会整体向左偏移一个窗口,这在答辩时很容易被老师追问。如果你发现预测线整体比真实线低很多,另一个常见错误是反归一化时用错了 scaler,或者训练集和测试集的价格区间相差过大(比如测试区间发生了暴涨暴跌),导致缩放后的预测值在反向变换时发生偏移。

4.3 方向准确率:比 RMSE 更能说明问题的指标

回归误差只说明“预测价格偏离了多少”,但股票预测实际更关心方向判断得对不对——明天是涨还是跌。一个模型如果 RMSE 偏大,但方向准确率超过 55%,在应用层面就比特单纯预测更准的马模型更有价值。所以这份代码额外计算了“方向命中率”:比较sign(真实收益)和sign(预测收益)是否一致。

# 真实下一日涨跌方向与预测下一日涨跌方向 true_next = np.diff(y_test_inv) # 真实:明天减今天 pred_next = np.diff(y_pred_inv) # 预测:明天减今天 true_direction = np.sign(true_next) pred_direction = np.sign(pred_next) accuracy = np.mean(true_direction == pred_direction) print(f"Direction Accuracy: {accuracy:.2%}")

此处有个细节要注意:np.diff会让结果长度比原始数组少 1,所以计算准确率时两个方向序列的长度要确保一致。方向准确率的合理基线是 50%——相当于抛硬币;如果模型低于 55%,说明它的预测基本没有实际参考价值,但作为大作业,能跑到 52%~58% 已经属于正常水平。你可以把这组数字和 RMSE 一起写进论文结论,比单纯展示一张“拟合得很漂亮”的图更能说明问题。

5. 避坑与常见问题排查:期末周最常见的五个翻车现场

5.1 现象:训练集 Loss 很低,测试集 Loss 高得离谱

原因:标准化时在全量数据上 fit 了 MinMaxScaler,或者滑窗切分时没有按时间顺序严格切分,导致测试集的分布信息提前泄漏进训练过程。模型的“高分”只是记住了价格区间,而不是学到了规律。解决:先把数据按时间顺序切成 train 和 test 两段,只对训练段fit标准化器,再分别transform。每次跑新实验前,检查scaler.min_和scaler.scale_是否只基于训练段计算。

5.2 现象:Loss 能降到很低,但预测曲线是一条近似水平的直线

原因:股价序列本身接近随机游走,模型在 MSE 优化下的最优解就是预测“等于上一个时刻的真实值”。如果你看到预测线几乎贴着真实线但滞后一天,这其实是正常现象,不用惊慌。但如果预测线完全水平且偏离真实值,说明 LSTM 的输入特征失效了,可能原因是特征列里混入了太多一律性数值(比如常数列),或者学习率太大导致模型没有收敛。解决:把lookback调小到 10~15 天,并检查特征列是否存在 NaN 填充错误的异常值。

5.3 现象:yfinance 下载 A 股数据时,部分股票返回空 DataFrame

原因:yfinance 对 A 股代码的格式要求是600519.SS(上交所)或000001.SZ(深交所),如果写成600519或600519.SZ,接口会查不到数据。另一个隐蔽问题是:连续停牌的股票(比如 ST 股)在区间内没有交易记录,下载结果可能只有一行或为空。解决:用df.dropna(axis=0, how="any")提前清洗,并打印df.head()确认数据区间是否正确;换标的时建议先用 yfinance 的Ticker.history(period="1mo")做一次连通性测试。

5.4 现象:LSTM 训练十几个 epoch 后 val_loss 震荡不降

原因:最常见的是学习率偏大,Adam 在接近局部最优时出现震荡;其次是滑窗序列样本之间存在高度重合,训练集和验证集的分布几乎相同,导致验证集 Loss 无法真实反映泛化能力。解决:先把学习率降到0.0005或0.0003重跑;如果依然震荡,增加 Dropout 比例到 0.3,并把patience从 10 降到 5,让 EarlyStopping 更早触发。注意每次改超参都要记录,别靠手感瞎调——期末答辩时老师说“你这个参数为什么取 64”,你得答得上来。

5.5 现象:预测结果整体比真实价格低(或高)一个固定比例

原因:反归一化时张量结构对不上。比如当初对 6 列特征做了标准化,但inverse_transform时只传入了 1 列(收盘价),sklearn 会报维度错误,于是很多人改成“先用 scaler 处理 1 列的预测值”,结果缩放参数与训练时不一致。解决:不管预测几列,都用np.concatenate拼接成和原始特征维度一致的矩阵,再切片取收盘价那一列。这个坑在这份代码里已经处理好了,但如果你改成只预测Close一列,要记得重写整个标准化流程,不能混用。

6. 验证的进阶玩法:从“跑通代码”到“能解释结果”

6.1 把方向准确率扩展成一次最简回测

方向准确率只是一个统计值,期末作业想拿高分,最好把它翻译成“策略收益”。你可以做这样的最简回测:从测试集开始,每天按 LSTM 预测方向做多或做空(或者只做多头),计算累计收益。代码逻辑很简单,不需要引入回测框架:

capital = 1.0 positions = [] for i in range(len(pred_direction)): daily_ret = true_next[i] / y_test_inv[i] # 当日真实收益率 if pred_direction[i] > 0: capital *= (1 + daily_ret) # 预测涨,持有 elif pred_direction[i] < 0: capital *= (1 - daily_ret) # 预测跌,空仓或反向 positions.append(capital) buy_hold = np.cumprod(1 + true_next / y_test_inv[:-1]) # 基准:一直持有

这个回测没有考虑交易费,逻辑上只是把“预测方向”与“真实收益”相乘,得到一个策略净值曲线,再和“一直持有”做对比。答辩时你能讲清楚“模型方向准确率 54%,回测曲线跑赢基准”,就已经超过九成只贴 MSE 的同学了。

6.2 滚动向前验证:把“一次性切分”升级成更可信的评估方式

静态切分最大的问题是:结果对“在哪个日期切分”非常敏感,切在牛转熊的节点和切在熊转牛的节点,效果是天壤之别。滚动向前验证的做法是:固定训练窗口长度(比如 300 天),每次向前滚动 20 天,重新训练一次模型并预测未来 20 天,然后把多次预测结果拼起来。整个过程会跑很多轮模型,每个模型只重训一两次,训练开销大,但对期末作业来说,你只需要跑 3 次滚动(3 折),就能画出一条比静态切分更有说服力的预测曲线。

6.3 多说一句

从那以后我每次拿到时间序列大作业,都会先检查三个地方:数据切刀有没有按时间排序、Scaler 是不是只在训练集上 fit、反归一化有没有补齐维度。这套流程走一遍,项目基本不会出大问题。这份代码把这三件事都处理干净了,你拿到手先跑通再改参,很快就能看到一条能解释的预测曲线。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 13:25:24

券商研报量化复现:PDF因子提取、标准化与回测闭环

简介&#xff1a;本资源是一套面向量化投资初学者与金融工程学习者的Python实战复现教程&#xff0c;聚焦券商金工研报核心方法论落地&#xff0c;适用于计算机、人工智能、金融工程等相关专业学生及从业者入门进阶。资源包含211个文件&#xff0c;主体为40个可执行Python脚本、…

作者头像 李华
网站建设 2026/10/11 13:25:07

PyTorch入门必跑MNIST:从解压到Grad-CAM的完整实践指南

简介&#xff1a;本资源是一套基于PyTorch框架实现MNIST手写数字识别的完整实践项目&#xff0c;面向计算机、电子信息工程及数学等专业的本科生&#xff0c;适用于课程设计、期末大作业或毕业设计参考。项目包含可直接运行的训练与测试源码&#xff08;.py&#xff09;、预训练…

作者头像 李华
网站建设 2026/10/11 13:21:32

Flutter跨平台校园服务平台开发:架构设计与鸿蒙适配实践

校园生活服务平台&#xff0c;是我这几年见过最考验项目落地能力的一类应用。它不像纯社交App那样只靠几个核心流程走天下&#xff0c;也不像工具类App那样功能单一&#xff0c;而是要把课表、校园卡、公告、报修、二手集市、社团活动这些场景全部卷进同一个应用里&#xff0c;…

作者头像 李华
网站建设 2026/10/11 13:21:06

Go 写 Agent 框架是异端还是未来?Unreal Agent 引爆的技术栈之争

Go 写 Agent 框架是异端还是未来&#xff1f;Unreal Agent 引爆的技术栈之争 【免费下载链接】unreal-agent Async-first agent harness 项目地址: https://gitcode.com/gh_mirrors/un/unreal-agent 2026 年 9 月&#xff0c;一个名为 Unreal Agent 的开源项目出现在 Gi…

作者头像 李华
网站建设 2026/10/11 13:20:03

影刀RPA新手教程:调试三板斧——日志、断点与单步执行

影刀RPA新手教程&#xff1a;调试三板斧——日志、断点与单步执行 流程跑一半报错&#xff0c;日志里只有一行看不懂的英文&#xff0c;你盯着几十条指令不知道从哪查起——这是每个影刀RPA新手都会卡住的地方。我自己也是非技术出身&#xff0c;第一年做采集流程时&#xff0c…

作者头像 李华
网站建设 2026/10/11 13:18:11

多关键字排序实战:从奖学金题学透排序规则与自定义比较函数

某天我在一个在线题库里整理题单的时候&#xff0c;又看到了这道编号1106的老朋友——《奖学金》。说它是"老朋友"&#xff0c;是因为这类多关键字排序的题目在信息学竞赛入门阶段太常见了&#xff0c;几乎每本教材、每个模拟赛里都会换着花样出现一次。第一次见到它…

作者头像 李华