深度学习股票预测系统:LSTM+MLP混合模型与Python量化回测完整实战
发布时间:2026/10/1 13:03:22来源:尧图网络
简介面向高年级本科生的基于深度学习的股票分析预测系统Python实现是一份Python期末大作业的完整源码适用于课程设计、毕业设计及人工智能入门进阶。系统采用多层感知机与LSTM混合架构覆盖技术指标提取、特征工程、时序预测、回测与策略执行既能体现机器学习基础又涉及时间序列分析与神经网络建模。压缩包共25个文件、约6.34MB包含6个Python核心模块、6张预测结果图、3个CSV行情数据文件以及备份配置、说明文档和依赖清单数据下载、指标计算、预测、回测、策略展示等流程均有对应脚本支撑。已有96人学习下载。代码经学术导师审核并获98分评价结构完整、注释详尽可直接运行复现实验结果同时提供了清晰的数据预处理流程、模型训练策略和超参数优化思路方便学习者在此基础上做二次开发与功能扩展。1. 深度学习股票分析预测系统这套 Python 期末项目为什么能拿 98 分期末大作业能拿高分通常不是因为模型多炫而是因为「链路完整、能复现、能讲清楚」。这套基于深度学习的股票分析预测系统就是典型代表——它在 Python 课程设计里拿到 98 分不是靠单点模型创新而是把数据下载、技术指标提取、MLPLSTM 混合模型训练、策略生成、回测验证这五件事全部打通了。压缩包里同时有预测结果图、沪深 300 成分股列表、上证指数数据和训练好的预测 CSV解压之后按 README 走一遍就能看到从「原始股价」到「买卖信号」再到「收益曲线」的完整过程。适合正在做课程设计或毕业设计的计算机专业学生也适合想用深度学习做时间序列入门的人——这套代码最大的价值不是那个模型而是「原来一个完整的量化研究流程是这么组织起来的」。2. 数据下载与特征工程把日K线变成 LSTM 能吃的时间序列2.1 数据链路与文件职责先搞清楚每个脚本在干什么拿到压缩包先别急着跑prediction.py先把文件职责捋清楚。项目里的分工很明确data_downloader.py负责拉数据stock_indicator.py负责算技术指标prediction.py负责训练模型和生成预测图strategy.py把预测结果转成交易信号backtest.py做历史回测。train_data/目录下是清洗好的个股日K线 CSV比如sh.600000.csv、sh.600009.csvstocks/sh.000300.csv是沪深 300 指数数据hs300_stocks.csv是成分股列表。这套设计是典型的量化研究流水线——每一步都产出一个中间文件下一步只依赖上一步的结果排错非常方便。常见的数据字段包括date、open、close、high、low、volume、amount。如果你想换成别的股票或者别的周期只需要保证 CSV 里有这几列就行。数据下载的常见做法是用akshare或tushare拉日K线项目里data_downloader.py干的就是这件事核心逻辑大概是import akshare as ak import pandas as pd df ak.stock_zh_a_hist( symbol600009, perioddaily, start_date20150101, end_date20231231, adjustqfq ) # 统一列名保证下游代码只认这套字段 df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount }) df df[[date, open, close, high, low, volume, amount]] df.to_csv(train_data/sh.600009.csv, indexFalse)这段代码做了两件事拉取前复权日K线然后统一列名。注意adjustqfq这个参数很关键——如果选不复权历史价格会因为除权除息出现跳空模型会把这些跳空当成真实价格波动来学预测结果会非常离谱。项目里所有训练数据都用了前复权这是第一个值得借鉴的细节。2.2 技术指标提取MA、MACD、RSI 怎么算才不踩坑stock_indicator.py负责把原始行情转成技术指标。常见做法是计算均线MA、指数平滑异同移动平均线MACD、相对强弱指标RSI、布林带BOLL然后把原始价格和技术指标拼接成一张宽表。均线部分用 Pandas 的rolling窗口函数就能实现def add_indicators(df: pd.DataFrame) - pd.DataFrame: df[ma5] df[close].rolling(window5).mean() df[ma20] df[close].rolling(window20).mean() df[ma60] df[close].rolling(window60).mean() # RSI: 14 日相对强弱指标 delta df[close].diff() gain delta.clip(lower0).rolling(window14).mean() loss (-delta.clip(upper0)).rolling(window14).mean() rs gain / loss.replace(0, 1e-10) df[rsi14] 100 - (100 / (1 rs)) # 涨跌幅和成交量变化率 df[pct_change] df[close].pct_change() df[volume_ratio] df[volume] / df[volume].rolling(window20).mean() # 去掉前 60 行 NaN保证进入模型的数据没有空值 df df.dropna().reset_index(dropTrue) return df这段代码有三个细节值得注意RSI 计算里分母遇到零用1e-10兜底避免infvolume_ratio用 20 日均量做对比能反映放量缩量最后统一dropna()并把索引重置因为后续构建时间序列时索引必须是从 0 开始的连续整数否则切片会错位。很多人在这一步翻车——忘记处理 NaN模型训练时报ValueError: Input contains NaN或者索引不连续导致时间窗口错位。2.3 时间序列窗口构建lookback 怎么选数据怎么切LSTM 处理股票数据不能拿单条样本去预测需要把每股价格组织成「过去 N 天 → 未来 M 天」的样本对。这个 N 就是lookback项目里常见取值是 20、30、60。窗口构建的代码逻辑如下def make_sequences(df: pd.DataFrame, lookback60, predict_days5): features df.drop(columns[date]).values X, y [], [] for i in range(lookback, len(df) - predict_days): X.append(features[i - lookback:i]) # 过去 60 天的全部特征 y.append(df[close].iloc[i predict_days]) # 未来第 5 天的收盘价 return np.array(X), np.array(y) X, y make_sequences(df, lookback60, predict_days5)这里predict_days5意味着模型预测的是「未来第 5 天的收盘价」而不是「明天」的收盘价。这样做的好处是给交易留出了缓冲时间——你不可能今天收盘后看到信号明天就买卖A 股 T1 制度下信号至少要提前几天才可操作。数据集切分必须按时间顺序不能用train_test_split随机切——股票数据是时间序列随机切会把未来数据混进训练集测试结果会虚高。项目里的标准比例是训练集 70%、验证集 15%、测试集 15%切分时直接按索引位置切train_end int(len(X) * 0.7) val_end int(len(X) * 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:]这个切法能保证模型永远只用过去的数据做预测不会偷看未来。3. MLPLSTM 混合模型prediction.py 的架构拆解与训练参数3.1 为什么用混合架构LSTM 管时序MLP 管指标单一模型处理股票预测有两个痛点纯 LSTM 对技术指标的敏感度不够纯 MLP 又完全没有时序记忆能力。这个项目的方案是把两条线并联——LSTM 分支吃原始价格序列MLP 分支吃技术指标矩阵最后拼接在一起输出预测值。这样设计的好处是LSTM 负责捕捉价格走势的形态规律比如双底、头肩顶这些形态MLP 负责学习指标之间的静态映射关系比如 RSI 超卖 MACD 金叉组合对后续涨跌的影响。两个分支的输入维度不一样。LSTM 分支吃的是原始行情序列open、close、high、low、volume形状是(batch_size, lookback, 5)MLP 分支吃的是技术指标宽表形状是(batch_size, num_indicators)。这两个输入在模型内部先分别过各自的特征提取层然后拼在一起这才是「混合」二字的含义。3.2 模型定义与编译Keras Functional API 怎么写项目里prediction.py的模型定义用 Keras Functional API 最顺手因为要处理两个不同形状的输入。核心代码如下from tensorflow.keras.layers import Input, LSTM, Dense, Dropout, Concatenate from tensorflow.keras.models import Model # LSTM 分支输入 60 天 x 5 维行情 seq_input Input(shape(60, 5), nameseq_input) lstm_out LSTM(units64, return_sequencesFalse, dropout0.2)(seq_input) # MLP 分支输入技术指标个数视特征工程而定 ind_input Input(shape(12,), nameind_input) mlp_out Dense(32, activationrelu)(ind_input) mlp_out Dropout(0.2)(mlp_out) # 拼接两个分支 merged Concatenate()([lstm_out, mlp_out]) merged Dense(16, activationrelu)(merged) output Dense(1, activationlinear)(merged) model Model(inputs[seq_input, ind_input], outputsoutput) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()几个关键参数的选型理由units64是 LSTM 隐层维度对于日K线这种低频数据 64 足够再大容易过拟合dropout0.2加在 LSTM 输出和 MLP 输出上防止模型死记训练集价格形态lossmse因为这是回归任务预测的是具体价格而不是概率。return_sequencesFalse表示只取最后一个时间步的输出因为预测目标是未来某天的收盘价不需要输出序列。这两个分支的维度需要自己核对——LSTM 输出是 64 维技术指标分支如果指标太多建议在Dense(32)之前先降维。3.3 训练策略早停、学习率衰减、归一化泄漏模型编译好之后训练过程里最容易出问题的是数据归一化。股票价格动不动就是几十到几百的跨度必须做归一化但MinMaxScaler的fit只能放在训练集上做等验证集和测试集都用同一套参数去transform。要是把全量数据拿去fit测试集的信息就泄进训练过程了回测结果会好看到失真from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_shape X_train.shape X_train scaler.fit_transform(X_train.reshape(-1, X_train.shape[-1])).reshape(train_shape) # 测试集只用 transform不重新 fit test_shape X_test.shape X_test scaler.transform(X_test.reshape(-1, X_test.shape[-1])).reshape(test_shape)训练时的早停和学习率衰减是必备操作prediction.py里是这么配置的from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) lr_scheduler ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-6) history model.fit( [X_train_seq, X_train_ind], y_train, validation_data([X_val_seq, X_val_ind], y_val), epochs200, batch_size32, callbacks[early_stop, lr_scheduler], verbose1 )patience10表示连续 10 个 epoch 验证损失不下降就停止训练早停后模型自动恢复到验证集上最好的权重。ReduceLROnPlateau在验证损失不降时把学习率减半避免后期震荡不收敛。这里的batch_size32是经验值如果你的显存小可以降到 16如果数据量大可以升到 64但注意 batch_size 变大后训练震荡会加剧早停的patience也要相应调大。训练完成后模型会把测试集的预测结果和真实价格画在一起生成sh.600009_predict.jpg之类的对比图方便肉眼判断拟合质量。判断标准不是「曲线重合得好」而是「预测曲线是否跟随真实值的拐点」——滞后是正常的但整体趋势不能偏。4. 策略生成与回测从预测结果到可回测的交易信号4.1 从预测结果到买卖信号阈值怎么定模型输出的是未来第 5 天的预测收盘价但直接拿预测价格做交易没有意义要把价格转换成信号。strategy.py里做的是这样一件事比较预测价格和当前价格的差异差异超过阈值就买入或卖出。阈值可以是固定百分比也可以是滚动标准差。常见做法是固定阈值加信号平滑def generate_signals(pred_close, actual_close, buy_threshold0.03, sell_threshold-0.03): pred_close: 模型预测的未来收盘价 actual_close: 当前实际收盘价 返回: 1 买入, -1 卖出, 0 持有 pred_pct_change (pred_close - actual_close) / actual_close signals np.where(pred_pct_change buy_threshold, 1, 0) signals np.where(pred_pct_change sell_threshold, -1, signals) # 平滑连续 3 天都出现买入信号才真的买入过滤噪音 buy_signal pd.Series(signals).rolling(3).sum() final_signal np.where(buy_signal 3, 1, 0) return final_signal阈值0.03意味着模型预测未来 5 天涨幅超过 3% 才触发买入低于 -3% 触发卖出。这个阈值不是随便拍的——A 股 5 日内平均波动在 2% 到 4% 之间设太小会被噪音反复触发设太大可能一年都没几次交易。滚动窗口过滤那行是关键连续 3 天都出现买入信号才执行能干掉单日预测噪声。实际用的时候可以在3%和5%之间来回试回测里哪个夏普比率高就用哪个。4.2 回测指标收益率、最大回撤、夏普比率backtest.py负责把信号序列映射到真实收益上计算核心指标def run_backtest(close_prices, signals, fee_rate0.001): position 0 # 0 空仓, 1 满仓 equity 100000.0 # 初始资金 10 万 records [] for i in range(len(close_prices)): if signals[i] 1 and position 0: position 1 buy_price close_prices[i] * (1 fee_rate) # 买入手续费 elif signals[i] -1 and position 1: position 0 sell_price close_prices[i] * (1 - fee_rate) # 卖出手续费 equity equity * sell_price / buy_price records.append(equity) elif position 1: records.append(equity * close_prices[i] / buy_price) else: records.append(equity) # 最大回撤从历史峰值往下最深的一跌 equity_curve pd.Series(records) drawdown equity_curve / equity_curve.cummax() - 1 max_drawdown drawdown.min() # 年化收益率假设 250 个交易日 total_return equity / 100000 - 1 annual_return (equity / 100000) ** (250 / len(close_prices)) - 1 # 夏普比率超额收益 / 波动率 daily_returns equity_curve.pct_change().dropna() sharpe daily_returns.mean() / daily_returns.std() * (250 ** 0.5) return { total_return: total_return, annual_return: annual_return, max_drawdown: max_drawdown, sharpe: sharpe }fee_rate0.001是单边手续费假设A 股实际佣金加印花税成本比这稍高回测里取 0.1% 是比较保守的口径。最大回撤表示从历史净值最高点到跌落谷底的最大幅度这个指标比收益率更能暴露策略风险——一个年化 30% 但回撤 40% 的策略实际操作起来根本拿不住。回测指标对照夏普比率大于 1 算合格大于 2 算优秀最大回撤控制在 20% 以内比较稳。这个项目回测出来的result.jpg里会画出净值曲线和回撤曲线打开图片一眼能看出策略在哪个时间段亏钱。4.3 预测结果可视化预测图里画了什么sh.600009_predict.jpg这类图片是把测试集的真实收盘价和模型预测的收盘价画在一张图上用 Matplotlib 就能生成import matplotlib.pyplot as plt plt.figure(figsize(12, 6)) plt.plot(test_dates, y_test, label真实收盘价, linewidth2) plt.plot(test_dates, y_pred, label预测收盘价, linewidth1.5, linestyle--) plt.axvline(xtest_dates[len(test_dates)//2], colorgray, linestyle:) plt.xlabel(日期) plt.ylabel(收盘价 (元)) plt.title(600009 测试集预测效果对比) plt.legend() plt.grid(alpha0.3) plt.savefig(sh.600009_predict.jpg, dpi150)看预测图有一个技巧不要只看曲线贴合程度要看拐点位置。如果预测曲线的峰值总比真实曲线晚一天出现说明模型在学「昨天涨今天大概率涨」的惯性这种情况下策略信号会整体滞后。纵向的虚线把测试集切成前后两半方便对比模型在最近一段时间的表现是否退化。5. 避坑与排查依赖冲突、时间戳错位与过拟合的翻车现场5.1 环境依赖的坑requirements.txt 为什么变成了 .zbak现象解压后看到requirements.txt.zbak、.gitignore.zbak这种备份文件直接装环境时报ImportError或版本不兼容。原因.zbak是作者改名的备份文件——说明原项目在某个环境里跑出过问题作者当时的解决办法是改了依赖版本。最常见的坑是numpy版本过高导致pandas运行异常或者是tensorflow和keras版本不匹配。深度学习相关的 Python 包互相依赖很重requirements.txt里的版本号被锁定是有原因的不能随便pip install -U全升到最新版。解决先创建一个干净的虚拟环境再按 requirements.txt 安装。建议用 Python 3.8 或 3.9太新的 Python 版本比如 3.12对老版本 TensorFlow 兼容性很差。装完后跑一遍data_downloader.py做冒烟测试确认 pandas 和 akshare 能正常读数据。5.2 停牌、涨跌停与时间戳错位数据对不上怎么回事现象训练时模型 loss 正常但回测时信号日期和真实价格对不上或者预测图整体向右偏移。原因A 股有停牌日停牌当天没有行情记录如果直接按 CSV 行号切时间窗口模型会以为停牌日也在交易造成时间戳错位。涨跌停同理——涨停时买不进跌停时卖不出如果信号在那一天触发但实际成交不了回测收益会虚高。解决处理数据时用交易日历对齐先拿到完整交易日序列再对原数据做reindex缺失日期直接丢弃不要插值。在信号生成时判断涨跌停df[limit_up] (df[close] df[high]) (df[pct_change] 0.095) df[limit_down] (df[close] df[low]) (df[pct_change] -0.095) # 涨停不追买跌停不杀跌 signals np.where(df[limit_up], 0, signals) signals np.where(df[limit_down], 0, signals)0.095对应 10% 的涨跌停板阈值ST 股是 5% 需要单独处理。加了这层过滤后回测结果会真实很多但这层逻辑很多课程设计都会漏掉。5.3 过拟合与归一化泄漏训练集好看测试集拉胯现象模型在训练集上的 MAE 很低验证集也还行但一上测试集就飘预测曲线比真实曲线平滑很多呈明显的「跟着走」形态。原因两个问题叠加。第一归一化时把全量数据放进MinMaxScaler.fit()测试集的最大最小值泄漏到训练过程中导致模型在测试集上「作弊」第二LSTM 隐层维度太大或 dropout 太小模型记住了训练集的噪声。解决归一化严格按照训练集 fit、验证集和测试集只 transform 的顺序执行。过拟合用早停兜底已经能解决大半问题如果再不行就降低 LSTMunits把 64 降到 32或者调大dropout到 0.3。还有一个判断技巧如果训练集 MAE 是测试集的一半以下高概率过拟合优先调正则化而不是加数据。5.4 回测结果失真手续费和滑点没算进去现象回测年化收益率 50%实盘完全达不到甚至亏损。原因backtest.py里默认费率0.001只算了单边佣金但实际交易还有印花税和滑点。模型在第 5 天预测涨 3% 触发买入实际买入时价格可能已经涨到 3.5%这就是滑点成本。解决回测参数建议设置双边成本合计 0.15%小额资金还要考虑每笔最低佣金 5 元。把这个成本加进去跑一遍收益率会缩水一截但那个数字才真正可信。股票预测系统的难点从来不是模型拟合而是把交易摩擦算清楚之后还能不能赚钱。6. 进阶改造换股票、调参、加自己指标的验证流程先把项目跑通再动手改造这个顺序不能乱。换股票是最简单的验证方式去data_downloader.py把symbol改成你感兴趣的代码比如600519贵州茅台生成新的 CSV 放进train_data/然后跑stock_indicator.py生成指标接着跑prediction.py训练并出预测图最后strategy.py和backtest.py出回测结果。整套流程跑下来如果换的股票是不同行业的预测误差会有明显差异——波动大的票更难预测周期股和消费股的特征也不一样。调参不要乱调我建议按这个顺序来。第一步固定lookback60观察预测效果然后试 30 和 90对比测试集 MAE——窗口太小模型看不到中期趋势窗口太大计算量暴涨收益却有限。第二步调 LSTM 的units从 32 试到 128每次只改一个参数并记录回测的夏普比率。第三步调买卖阈值在 2% 到 5% 之间扫一遍观察交易次数和胜率的关系。所有实验的指标记录到一张表格里哪组参数夏普高、回撤小就用哪组。加新指标也是常见的改造方向。比如传统技术指标 CCI顺势指标在很多软件里都有现成公式实现起来不难def add_cci(df, window20): tp (df[high] df[low] df[close]) / 3 ma_tp tp.rolling(window).mean() mean_dev tp.rolling(window).apply(lambda x: np.abs(x - x.mean()).mean()) df[cci] (tp - ma_tp) / (0.015 * mean_dev) return dfCCI 默认阈值是 ±100超买超卖信号比 RSI 更敏感加进特征矩阵后记得同步修改prediction.py里 MLP 分支的Input(shape(12,))的维度否则模型构建直接报错。加指标后要重新做一遍全流程验证判断指标到底有没有贡献的标准是加了指标之后测试集 MAE 有没有下降回测夏普有没有提升——如果变化不大说明这个指标和现有特征高度相关留着只增加过拟合风险。最后说一个我自己跑这类项目养成的习惯拿到别人的源码包永远先把文件里的.zbak、备份文件.zip这些旧版本导出来对比一下看看作者改了什么——那次修改大概率就是最大的坑点。改完参数重新训练时把模型权重和预测图存档到单独文件夹里按日期命名版本。从那以后我每次拿到新的股票预测项目都强制走一遍「数据对齐检查 → 归一化泄漏检查 → 回测成本校准」这三步能省下大半天排错时间。希望这套流程帮你也少踩几个坑。本文还有配套的精品资源点击获取
网站建设高端定制企业官网