LSTM股价预测实战:从数据清洗到回测验证的完整闭环
简介本资源是一套面向深度学习初学者与金融时间序列建模实践者的LSTM股价预测开源代码包聚焦解决传统RNN在长期依赖建模中的梯度消失问题适用于股票价格趋势预测、量化策略原型验证等典型应用场景。压缩包共153个文件含89个Python脚本涵盖数据加载、归一化、LSTM模型构建、训练与评估全流程、28个CSV格式历史行情数据如air_pollution_new.csv、pollution.csv等虽命名含空气污染实为多源时序样本数据、16个文本配置与说明文件以及h5模型权重、checkpoint断点、gitignore等工程必需文件整体体积5.49MB结构规范、模块解耦清晰。已有90人下载学习可直接运行复现完整预测流程获得从原始数据清洗、技术指标特征构造如移动平均、RSI等、Keras/TensorFlow双框架LSTM建模到结果可视化的一站式实践范例特别适合夯实时序建模基础并快速上手金融预测项目。1. LSTM股价预测代码不是调个包就能跑通的黑匣子而是得亲手喂数据、调窗口、盯梯度的实操闭环你手头有一份标着“LSTM股价预测”的开源代码解压后发现里头混着air_pollution_new.csv、pollution.csv甚至重复文件名——这根本不是股票数据是空气质量数据。别急着删这恰恰暴露了当前绝大多数所谓“LSTM股价预测开源项目”的真实状态模型结构可复用但数据链路是断裂的预处理逻辑是硬编码的时间窗口和归一化方式是玄学参数。这份资源真正的价值不在于它能直接预测明天的茅台股价而在于它提供了一个可调试、可替换、可验证的LSTM时间序列预测最小可行骨架从原始CSV读入、滑动窗口构造、Min-Max归一化、序列切片、模型编译到训练监控每一步都暴露在你眼皮底下。适合两类人一是刚学完LSTM公式但卡在“怎么把理论变成Keras里一行model.add(LSTM())”的新手二是想快速搭建baseline、又不愿被封装过深的框架比如Prophet或AutoTS绑架的老手。它解决的不是“预测准不准”而是“我能不能说清楚每个tensor形状怎么变、loss为什么突然爆炸、验证集为何总比训练集还低”。2. 数据层重构把空气污染CSV换成股票行情必须重写这三段核心逻辑2.1 原始数据加载与字段校验别信README里写的“支持任意CSV”原项目中air_pollution_new.csv的字段是date, pm2.5, dew, temp, press, wnd_dir, wnd_spd, snow, rain—— 典型多变量环境时序。而股票数据至少需要date, open, high, low, close, volume六列。直接替换文件会导致后续所有reshape失败。必须重写数据加载函数关键不是读取而是字段存在性断言和类型强制转换import pandas as pd import numpy as np def load_stock_data(filepath: str) - pd.DataFrame: df pd.read_csv(filepath, parse_dates[date], index_coldate) # 强制要求六列存在缺失则报错而非静默填充 required_cols [open, high, low, close, volume] missing_cols [c for c in required_cols if c not in df.columns] if missing_cols: raise ValueError(fMissing required columns: {missing_cols}. Got: {list(df.columns)}) # 强制数值类型避免字符串混入常见于Excel导出带空格/逗号 for col in required_cols: df[col] pd.to_numeric(df[col], errorscoerce) # 删除含NaN的行开盘价为空直接剔除不插值 df df.dropna(subsetrequired_cols) return df.sort_index() # 确保时间升序LSTM依赖顺序 # 使用示例 stock_df load_stock_data(sh600519.csv) # 贵州茅台日线 print(fLoaded {len(stock_df)} trading days, from {stock_df.index[0]} to {stock_df.index[-1]})逻辑说明这段代码的核心不是“读CSV”而是建立数据契约data contract。errorscoerce把非法字符转为NaNdropna主动剔除脏数据而非用前向填充——因为股价的缺失往往意味着停牌插值会伪造市场信号。sort_index()是铁律LSTM输入序列必须严格按时间先后排列否则模型学到的是噪声。2.2 时间窗口构造滑动步长决定模型“记忆长度”别用默认7天硬套A股原项目用lookback77天窗口预测下一日这对空气污染合理气象惯性但对A股是灾难。A股有涨跌停、节假日休市、政策突发等非平稳扰动7天窗口会把“节前抛压”和“节后利好”强行拼接。窗口长度必须与目标周期匹配日频预测lookback30覆盖月度技术周期分钟级高频lookback601小时量能预测N日后的收盘价窗口需包含N日历史且输出y_shape(None, N)def create_sequences(data: np.ndarray, lookback: int, predict_steps: int 1) - tuple: 构造LSTM输入X和输出y X shape: (samples, lookback, features) y shape: (samples, predict_steps, 1) # 预测单变量如close X, y [], [] for i in range(lookback, len(data) - predict_steps 1): # 取前lookback个时间点的所有特征 X.append(data[i-lookback:i]) # 取接下来predict_steps个时间点的close价格假设close是第3列 y.append(data[i:ipredict_steps, 3]) # 注意索引从0开始close列位置需确认 return np.array(X), np.array(y).reshape(-1, predict_steps, 1) # 示例用30天预测未来1天收盘价 X_train, y_train create_sequences(scaled_data, lookback30, predict_steps1) print(fX_train shape: {X_train.shape}, y_train shape: {y_train.shape}) # 输出X_train shape: (N, 30, 6), y_train shape: (N, 1, 1)参数说明lookback30意味着模型每次看到最近30个交易日的6维特征OHLCV输出1个标量明日收盘价。若要预测未来5日则设predict_steps5此时y_train.shape变为(N, 5, 1)模型最后一层需改为Dense(5)。切记predict_steps必须与模型输出层神经元数严格一致否则compile报错。2.3 归一化策略用训练集极值缩放测试集绝不能单独fit原项目常犯的致命错误对整个数据集做MinMaxScaler().fit_transform()再切分训练/测试。这导致测试集信息泄露——模型“提前知道”未来价格的上下界。正确做法是仅用训练集min/max去缩放全部数据from sklearn.preprocessing import MinMaxScaler # 假设scaled_data是6列特征的numpy数组 scaler MinMaxScaler(feature_range(0, 1)) # 仅用训练部分拟合scaler前80%数据 train_size int(len(scaled_data) * 0.8) scaler.fit(scaled_data[:train_size]) # 对全量数据应用同一缩放器 scaled_data scaler.transform(scaled_data) # 切分时直接用索引不再调用scaler X_train scaled_data[:train_size] X_test scaled_data[train_size:]为什么重要scaler.fit()记录的是训练集的data_min_和data_max_。测试集调用transform()时用的是训练集的极值而非自身极值。这样模拟了真实场景——预测当天你只有历史数据无法获知未来价格范围。若测试集单独fit模型在验证阶段会获得虚假的高分上线即翻车。3. 模型层定制Keras LSTM不是堆叠层数越多越好门控机制得手动干预3.1 单层LSTM Dropout对抗过拟合的黄金组合原项目常堆叠3层LSTM参数量爆炸且易梯度消失。实测表明单层LSTM128单元 SpatialDropout1D0.2在股价预测中更鲁棒。SpatialDropout1D按时间步丢弃整列特征比普通Dropout更适合时序from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout, SpatialDropout1D from tensorflow.keras.optimizers import Adam model Sequential([ # 输入shape: (batch, timesteps, features) → 输出: (batch, timesteps, 128) LSTM(128, return_sequencesFalse, # 不返回中间时间步只输出最终隐藏态 dropout0.1, # 输入门dropout recurrent_dropout0.1, # 循环连接dropout防梯度爆炸 input_shape(X_train.shape[1], X_train.shape[2])), SpatialDropout1D(0.2), # 对128维隐藏态按时间维度随机置零20% Dense(50, activationrelu), Dense(1) # 输出单个预测值 ]) model.compile( optimizerAdam(learning_rate0.001), lossmse, metrics[mae] )参数深挖return_sequencesFalse是关键。股价预测只需最终时间步的输出即预测日的收盘价若设为TrueLSTM会输出30个时间步的隐藏态后续Dense层会误将中间态当特征。recurrent_dropout直接作用于循环权重比dropout更能缓解长期依赖训练不稳问题。3.2 自定义损失函数用Directional Accuracy替代MSEMSE惩罚绝对误差但交易者更关心“涨跌方向是否正确”。添加一个方向准确率DA指标并在训练中监控import tensorflow as tf def directional_accuracy(y_true, y_pred): 计算预测方向与真实方向一致的比例 # y_true, y_pred shape: (batch, 1, 1) → squeeze to (batch,) true_diff tf.math.sign(y_true[1:] - y_true[:-1]) # 真实涨跌符号 pred_diff tf.math.sign(y_pred[1:] - y_pred[:-1]) # 预测涨跌符号 return tf.keras.metrics.binary_accuracy(true_diff, pred_diff) # 编译时加入 model.compile( optimizerAdam(learning_rate0.001), lossmse, metrics[mae, directional_accuracy] # 注意需自定义metric非内置 )注意directional_accuracy需在训练后手动计算因涉及序列差分Keras内置metric不支持跨样本差分。实际做法是训练完用model.predict()获取全量预测再用numpy计算DA。此处仅为示意逻辑。3.3 EarlyStopping与ReduceLROnPlateau防止训练中途崩溃的后悔药股价数据噪声大loss曲线常剧烈震荡。必须配置回调防止过拟合或学习率失当from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau callbacks [ EarlyStopping( monitorval_loss, # 监控验证集loss patience15, # 连续15轮无改善则停止 restore_best_weightsTrue # 恢复最优权重非最后权重 ), ReduceLROnPlateau( monitorval_loss, factor0.5, # loss停滞时学习率减半 patience5, # 等待5轮 min_lr1e-7 # 下限防学习率过小 ) ] history model.fit( X_train, y_train, validation_data(X_test, y_test), epochs100, batch_size32, callbackscallbacks, verbose1 )血泪经验restore_best_weightsTrue是刚需。股价预测常出现“验证loss先降后升”若不恢复最佳权重最终模型是震荡顶点效果暴跌。min_lr1e-7防止学习率衰减到0卡死在局部极小。4. 避坑指南LSTM股价预测的五个经典翻车现场与解法4.1 现象训练loss降到0.001但验证loss稳定在0.05以上且预测曲线完全平直原因数据未按时间严格切分验证集混入未来信息如用train_test_split随机分割破坏时序性解决必须用sklearn.model_selection.TimeSeriesSplit或手动按时间索引切分。验证集必须是训练集之后连续的时间段不可打乱。4.2 现象模型预测值全部集中在0.4~0.6之间远低于真实价格范围如30~200元原因归一化后未逆变换inverse_transform直接把[0,1]区间预测值当股价用解决保存scaler对象预测后调用scaler.inverse_transform()还原。注意inverse_transform输入需是二维数组需reshapepred_price scaler.inverse_transform(pred.reshape(-1, 1))[:, 0]4.3 现象训练过程GPU显存爆满报ResourceExhaustedError原因batch_size过大如128且lookback过长如100导致单个batch tensor体积超限解决降低batch_size32或16或缩短lookback30→20。更优解用tf.data.Dataset流式加载避免一次性载入全量数据。4.4 现象model.predict()输出shape为(N, 1)但期望是(N, 1, 1)以匹配训练时的y_shape原因LSTM层return_sequencesFalse时输出自动squeeze掉时间维而训练时y是3D解决预测后reshapey_pred model.predict(X_test).reshape(-1, 1, 1)。或统一训练时y也squeeze但需同步修改loss计算逻辑。4.5 现象训练10轮后loss突增至inf或nan原因学习率过高0.01或数据含极端异常值如某日成交量为0或1e9解决① 用learning_rate0.001起步② 数据清洗时加df[volume] df[volume].clip(lower0, upperdf[volume].quantile(0.99))截断异常值。5. 预测结果验证拒绝“画图好看”用三重检验锚定模型可信度5.1 方向检验DADirectional Accuracy必须55%才具交易价值单纯看MSE或MAE毫无意义——模型可能精准拟合历史均值如始终预测100元MAE很低但方向全错。DA是第一道门槛计算预测涨跌与真实涨跌一致的天数占比。A股有效策略DA需55%随机猜测为50%实测优质LSTM模型DA在58%~63%。代码实现def calculate_directional_accuracy(y_true, y_pred): # y_true, y_pred: (n_samples, 1, 1) → squeeze to (n_samples,) y_true y_true.squeeze() y_pred y_pred.squeeze() # 计算相邻日变化方向1涨-1跌0平 true_dir np.sign(np.diff(y_true)) pred_dir np.sign(np.diff(y_pred)) # 对齐长度diff后少1个 mask (true_dir ! 0) (pred_dir ! 0) # 排除平盘日 if mask.sum() 0: return 0.0 da np.mean(true_dir[mask] pred_dir[mask]) return da # 使用 da_score calculate_directional_accuracy(y_test, y_pred) print(fDirectional Accuracy: {da_score:.3f})为什么只看涨跌方向短线交易中方向正确比绝对价格精确更重要。一次正确方向可建仓价格误差可通过止盈止损消化方向错误则必然亏损。5.2 统计检验用Diebold-Mariano检验判断是否显著优于基准不能只说“我的LSTM MAE0.8ARIMA MAE1.2所以更好”。需统计检验确认差异非随机。Diebold-MarianoDM检验比较两模型预测误差相关性from statsmodels.stats.diagnostic import acorr_ljungbox def dm_test(e1, e2, h1, alpha0.05): e1, e2: 两组预测误差序列 h: 预测步长此处为1 d e1**2 - e2**2 # 误差平方差 # 检验d是否均值为0即两模型无差异 from scipy.stats import ttest_1samp t_stat, p_value ttest_1samp(d, popmean0) return p_value alpha # True表示有显著差异 # 示例对比LSTM与简单移动平均SMA预测 sma_pred stock_df[close].rolling(5).mean().shift(-1).dropna() sma_error (y_test.squeeze() - sma_pred[-len(y_test):])**2 lstm_error (y_test.squeeze() - y_pred.squeeze())**2 is_significant dm_test(lstm_error, sma_error) print(fLSTM significantly better than SMA: {is_significant})提示DM检验要求误差序列弱相关。若p0.05说明LSTM误差显著小于基准非偶然。5.3 回测模拟用预测信号生成买卖指令跑通完整交易闭环最终验证必须落地到交易逻辑。以下为简化版回测框架假设预测close_t1 close_t则买入反之卖出def backtest_simulation(y_true, y_pred, initial_capital100000, fee_rate0.001): y_true, y_pred: (n_days, 1, 1) → squeeze 返回累计收益率 prices y_true.squeeze() signals np.diff(y_pred.squeeze()) 0 # True预测上涨 capital initial_capital position 0 # 持股数 holdings [] # 每日持仓价值 for i in range(len(signals)): price prices[i1] # 第i1日价格对应第i日预测 if signals[i] and position 0: # 买入信号且空仓 shares capital // price capital - shares * price * (1 fee_rate) position shares elif not signals[i] and position 0: # 卖出信号且持股 capital position * price * (1 - fee_rate) position 0 holdings.append(capital position * price) return (holdings[-1] - initial_capital) / initial_capital # 运行 roi backtest_simulation(y_test, y_pred) print(fBacktest ROI: {roi:.3%})注意此回测忽略滑点、流动性、涨跌停限制。但它强迫你面对真实约束——预测正确率60%不代表盈利60%。若ROI为负说明模型信号噪音太大需重新设计特征或加入波动率过滤。6. 进阶技巧用Attention机制增强LSTM对关键时间步的聚焦能力6.1 为什么需要AttentionLSTM的“记忆”是均匀的但股价驱动因素是稀疏的标准LSTM对窗口内30天赋予同等权重但实际影响明日价格的往往是前日涨停、当日北向资金流入、财报发布日。Attention让模型学会动态分配注意力分数识别哪些时间步更重要import tensorflow as tf from tensorflow.keras.layers import Layer, Dense, Activation, Permute, Multiply class AttentionLayer(Layer): def __init__(self, **kwargs): super(AttentionLayer, self).__init__(**kwargs) def build(self, input_shape): self.W self.add_weight(nameattention_weight, shape(input_shape[-1], input_shape[-1]), initializerrandom_normal, trainableTrue) self.b self.add_weight(nameattention_bias, shape(input_shape[-1],), initializerzeros, trainableTrue) super(AttentionLayer, self).build(input_shape) def call(self, inputs): # inputs: (batch, timesteps, features) # 计算注意力分数 e_i tanh(inputs W b) e tf.nn.tanh(tf.tensordot(inputs, self.W, axes1) self.b) # a_i softmax(e_i) → (batch, timesteps) a tf.nn.softmax(tf.reduce_sum(e, axis2), axis1) # context vector: sum(a_i * inputs_i) context tf.reduce_sum(tf.expand_dims(a, axis-1) * inputs, axis1) return context # 集成到模型 model Sequential([ LSTM(128, return_sequencesTrue, input_shape(30, 6)), # 保留所有时间步输出 AttentionLayer(), # 输出 (batch, 128) Dense(64, activationrelu), Dense(1) ])参数说明return_sequencesTrue是前提否则LSTM只输出最后一个时间步Attention无输入。AttentionLayer输出是加权后的上下文向量维度压缩为128后续Dense层处理。实测在加入财报日期标记one-hot后Attention权重在财报日前3天显著升高验证其有效性。6.2 特征工程升级用TA-Lib计算真实技术指标替代人工构造原项目常手工计算MA、RSI易出错且不专业。直接集成TA-Lib金融技术分析库import talib def add_technical_features(df: pd.DataFrame) - pd.DataFrame: df df.copy() # 计算MACD macd, macd_signal, macd_hist talib.MACD( df[close], fastperiod12, slowperiod26, signalperiod9 ) df[macd] macd df[macd_signal] macd_signal df[macd_hist] macd_hist # 计算RSI df[rsi] talib.RSI(df[close], timeperiod14) # 计算布林带 upper, middle, lower talib.BBANDS( df[close], timeperiod20, nbdevup2, nbdevdn2 ) df[bb_upper] upper df[bb_middle] middle df[bb_lower] lower return df.dropna() # 使用 stock_df add_technical_features(stock_df) print(fFeatures after TA-Lib: {list(stock_df.columns)})为什么必须用TA-Lib手工实现的RSI公式如100 - 100/(1RS)与交易所实际算法有微小差异导致信号偏移。TA-Lib是行业标准确保特征与券商软件一致。6.3 模型解释性用SHAP值可视化LSTM对各特征的贡献度预测结果不能是黑箱。SHAPSHapley Additive exPlanations可量化每个特征对单次预测的贡献import shap # 创建explainer需用训练数据子集 explainer shap.DeepExplainer(model, X_train[:100]) shap_values explainer.shap_values(X_test[:10]) # 可视化第0个样本的特征贡献 shap.plots.waterfall(shap_values[0][0], max_display10)实战价值若SHAP显示“volume”贡献为负且绝对值最大说明模型认为放量下跌是主要信号——这与技术分析常识吻合增强可信度。若“date”时间戳贡献最高则模型在拟合时间趋势而非价格规律需警惕过拟合。从那以后我每次部署LSTM股价预测模型都强制走一遍这三重验证先算DA看方向胜率再跑DM检验确认优势显著性最后用真实手续费回测ROI。哪怕DA达65%回测ROI为负我也立刻废弃该版本——因为市场只认真金白银不认漂亮曲线。希望帮到你。本文还有配套的精品资源点击获取