Python实战:基于LSTM的股票价格预测模型构建与评估
1. 从直觉到算法为什么用LSTM预测股票股票预测这事儿听起来就像是在寻找金融市场的“圣杯”。每个交易者无论是新手还是老手心里都或多或少有过一个念头要是能提前知道明天的股价就好了。传统的技术分析看K线、数波浪、画趋势线本质上是在用过去的价格形态去推测未来的价格行为这背后隐含的假设是“历史会重演”。但市场是由无数参与者的情绪、信息和决策构成的复杂系统其非线性、高噪声和时变性的特点让简单的线性模型或基于固定规则的模型常常力不从心。这恰恰是循环神经网络RNN特别是其变体长短期记忆网络LSTM可以大显身手的地方。LSTM的设计初衷就是为了解决传统RNN在处理长序列数据时的“梯度消失”或“梯度爆炸”问题。简单来说当模型需要学习很久以前的信息来影响当前决策时普通RNN会“忘记”早期的信息。而LSTM通过精巧的“门控”机制输入门、遗忘门、输出门像一个有选择性的记忆细胞可以决定记住什么、忘记什么、输出什么。对于股票价格序列这种典型的、前后高度相关的时间序列数据LSTM能够捕捉到其中跨越较长时间周期的依赖关系比如一个持续数周的上涨趋势或者一种特定的“周一效应”模式。所以当我们谈论用Python实现基于LSTM的股票预测时我们做的不是占卜而是构建一个数据驱动的、能够从历史序列中学习复杂模式的统计模型。它的目标不是百分百准确那是不可能的而是尝试从海量的、看似随机的价格波动中提取出一些相对稳定的、可被模型学习的统计特征从而对短期走势做出概率上更有优势的判断。这个过程融合了金融、数据科学和机器学习是一个极具挑战也充满乐趣的实践项目。2. 实战第一步数据获取与核心特征工程没有高质量的数据再精巧的模型也是空中楼阁。对于股票预测数据是地基。我们的目标不仅是拿到价格数据更要构造出对模型“友好”且富含信息的特征。2.1 数据源选择与获取对于A股数据akshare库是一个免费且强大的选择。它接口稳定数据全面。我们以预测“贵州茅台”sh600519的收盘价为例。import akshare as ak import pandas as pd import numpy as np # 获取贵州茅台历史日线数据 stock_code “sh600519” df ak.stock_zh_a_hist(symbolstock_code, period“daily”, start_date“20100101”, end_date“20231231”, adjust“qfq”) print(df.head()) print(df.info())这里有几个关键点adjust“qfq”获取前复权数据。这是必须的因为除权除息会导致价格出现断层使用复权数据可以保证价格序列的连续性这对时间序列模型至关重要。获取的数据通常包含日期、开盘、最高、最低、收盘、成交量、成交额等字段。我们最关心的是“收盘”价。注意金融数据存在“幸存者偏差”。我们用的是茅台这只长期走牛股的历史数据做演示。在实际研究中务必考虑样本选择偏差你的模型在明星股上表现好不一定能在其他股票上复现。2.2 构建更有预测力的特征集只使用原始收盘价序列是远远不够的这相当于只给了模型一道简单的看图题。特征工程的目标是把这道题变成包含更多线索的阅读理解。我们构建以下几类特征技术指标这是从传统技术分析中借鉴的量化特征。移动平均线MA比如5日线MA5、10日线MA10、20日线MA20。它们代表了不同时间窗口下的平均持仓成本金叉死叉是经典信号。相对强弱指数RSI衡量价格变动速度和幅度判断超买超卖状态。布林带Bollinger Bands由中轨MA20和上下轨标准差通道组成反映价格波动率和相对位置。移动平均收敛发散MACD由快线DIF、慢线DEA和柱状图MACD组成用于判断趋势的强度和转折。可以使用ta库方便地计算这些指标。import ta # 计算RSI df[‘RSI_14’] ta.momentum.RSIIndicator(closedf[‘收盘’], window14).rsi() # 计算布林带 bollinger ta.volatility.BollingerBands(closedf[‘收盘’], window20, window_dev2) df[‘BB_upper’] bollinger.bollinger_hband() df[‘BB_middle’] bollinger.bollinger_mband() df[‘BB_lower’] bollinger.bollinger_lband() # 计算MACD macd_indicator ta.trend.MACD(closedf[‘收盘’]) df[‘MACD’] macd_indicator.macd() df[‘MACD_signal’] macd_indicator.macd_signal()滞后特征这是时间序列预测的核心。我们用过去N天的数据来预测未来。for lag in range(1, 6): # 创建过去1到5天的滞后特征 df[f‘close_lag_{lag}’] df[‘收盘’].shift(lag) df[f‘volume_lag_{lag}’] df[‘成交量’].shift(lag)波动率与统计特征滚动收益率的标准差波动率。过去N日的最高价、最低价。df[‘returns’] df[‘收盘’].pct_change() # 日收益率 df[‘volatility_5’] df[‘returns’].rolling(window5).std() df[‘high_5’] df[‘最高’].rolling(window5).max() df[‘low_5’] df[‘最低’].rolling(window5).min()目标变量定义我们不是预测明天的绝对收盘价而是预测明天的涨跌方向分类或收益率回归。这里以预测“次日收盘价是否高于今日收盘价”作为一个二分类任务示例。df[‘target’] (df[‘收盘’].shift(-1) df[‘收盘’]).astype(int) # 1表示涨0表示跌或平完成特征构建后务必处理缺失值由于滞后和滚动计算前几行会有NaN。df.dropna(inplaceTrue) feature_columns [‘收盘’, ‘RSI_14’, ‘BB_upper’, ‘BB_middle’, ‘BB_lower’, ‘MACD’, ‘MACD_signal’, ‘volatility_5’, ‘high_5’, ‘low_5’] [f‘close_lag_{i}’ for i in range(1,6)] [f‘volume_lag_{i}’ for i in range(1,6)] X df[feature_columns].values y df[‘target’].values3. 数据预处理与序列构建为LSTM准备“食粮”LSTM的输入不是一行行的独立样本而是一个个三维的小批次序列。我们需要把整理好的特征数据转换成LSTM能消化的格式。3.1 标准化让特征站在同一起跑线不同特征如价格、RSI、成交量的量纲和数值范围差异巨大。直接输入网络会导致模型被数值大的特征如价格主导。标准化就是将每个特征缩放到均值为0、标准差为1的分布。这里有一个至关重要的坑必须使用训练集的均值和标准差来缩放训练集和测试集绝不能在整个数据集上做标准化后再划分否则就造成了“数据泄露”模型会通过未来的信息来“预测”过去导致回测结果严重虚高。from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split # 首先按时间顺序划分训练集和测试集股票数据不能随机打乱 split_ratio 0.8 split_idx int(len(X) * split_ratio) X_train_raw, X_test_raw X[:split_idx], X[split_idx:] y_train, y_test y[:split_idx], y[split_idx:] # 初始化并拟合训练集的缩放器 scaler StandardScaler() scaler.fit(X_train_raw) # 只在训练集上拟合 # 用训练集的参数转换训练集和测试集 X_train_scaled scaler.transform(X_train_raw) X_test_scaled scaler.transform(X_test_raw)3.2 构建时间序列样本假设我们设定时间步长time_steps 20这意味着模型将根据过去20天的特征数据来预测第21天的目标涨跌。我们需要将数据重塑成(samples, time_steps, features)的形状。def create_sequences(data, targets, time_steps): X_seq, y_seq [], [] for i in range(len(data) - time_steps): X_seq.append(data[i:(i time_steps)]) # 取第i到i19天的特征作为输入 y_seq.append(targets[i time_steps]) # 取第i20天的标签作为输出 return np.array(X_seq), np.array(y_seq) time_steps 20 X_train, y_train create_sequences(X_train_scaled, y_train, time_steps) X_test, y_test create_sequences(X_test_scaled, y_test, time_steps) print(f“训练集形状: X_train {X_train.shape}, y_train {y_train.shape}”) print(f“测试集形状: X_test {X_test.shape}, y_test {y_test.shape}”) # 输出示例训练集形状: X_train (2000, 20, 20), y_train (2000,) # 表示有2000个样本每个样本是20天的时间窗口每天有20个特征。4. LSTM模型构建、训练与关键调参现在数据已经准备就绪我们可以搭建LSTM网络了。这里使用TensorFlow/Keras框架。4.1 模型架构设计一个基础的LSTM预测模型可以这样构建from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout, BatchNormalization from tensorflow.keras.optimizers import Adam from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau model Sequential([ # 第一层LSTM设置return_sequencesTrue以将完整序列输出给下一层 LSTM(units50, return_sequencesTrue, input_shape(time_steps, X_train.shape[2])), BatchNormalization(), # 批标准化加速训练并有一定正则化效果 Dropout(0.2), # Dropout层随机丢弃20%的神经元防止过拟合 # 第二层LSTM LSTM(units50, return_sequencesFalse), # 最后一层LSTM只返回最后时间步的输出 Dropout(0.2), # 全连接层用于输出最终预测 Dense(units25, activation‘relu’), Dense(units1, activation‘sigmoid’) # 二分类使用sigmoid激活函数输出0-1的概率 ]) model.compile(optimizerAdam(learning_rate0.001), loss‘binary_crossentropy’, # 二分类交叉熵损失 metrics[‘accuracy’]) model.summary()为什么这么设计两层LSTM第一层提取较低层的时间模式第二层在更高层次上整合这些模式。return_sequences参数的设置是关键。Dropout这是应对金融数据过拟合的利器。LSTM参数多容易记住噪声。Dropout在训练时随机“关闭”一部分神经元迫使网络学习更鲁棒的特征。BatchNormalization稳定并加速训练过程允许使用更高的学习率。输出层因为是二分类涨/跌所以用sigmoid激活和binary_crossentropy损失。4.2 训练策略与回调函数股票数据噪声大直接训练很多轮很容易过拟合。我们需要一些技巧来指导训练过程。# 早停当验证集损失在连续若干轮patience不再下降时停止训练防止过拟合。 early_stop EarlyStopping(monitor‘val_loss’, patience10, restore_best_weightsTrue, verbose1) # 学习率衰减当验证集指标停滞时自动降低学习率有助于模型在后期精细调优。 reduce_lr ReduceLROnPlateau(monitor‘val_loss’, factor0.5, patience5, min_lr1e-6, verbose1) history model.fit(X_train, y_train, epochs100, # 设置一个较大的epoch靠早停来实际控制 batch_size32, validation_split0.2, # 从训练集中再分20%作为验证集 callbacks[early_stop, reduce_lr], verbose1)4.3 核心超参数调优思路模型的表现很大程度上取决于超参数。手动调参效率低可以借助KerasTuner或Optuna进行自动化搜索。以下几个参数是调优重点LSTM单元数units通常从50、100、150开始尝试。单元数太少模型容量不足太多则容易过拟合。LSTM层数1到3层。层数增加模型表达能力增强但训练难度和过拟合风险也剧增。对于股票数据2层通常是较好的起点。Dropout比率0.2到0.5。这是控制过拟合最直接的手段。可以在LSTM层之间和之后都加Dropout。时间步长time_steps10、20、30、60。它决定了模型“看”多长的历史。太短可能看不到趋势太长会引入过多噪声并增加计算负担。可以尝试不同的值观察验证集效果。学习率learning_rate这是最重要的优化器参数。可以从0.001开始配合ReduceLROnPlateau回调动态调整。实操心得在金融数据上防止过拟合比追求训练集高精度重要得多。一个在训练集上准确率高达90%但在测试集上只有52%的模型是毫无用处的它只是记住了噪声。你的调优目标应该是最大化验证集/测试集的性能并时刻关注训练集和验证集损失曲线的差距gap。5. 模型评估、回测与结果分析模型训练完成后我们不能只看测试集的准确率Accuracy就下结论。在股票预测中一个把“涨”永远预测为“跌”的模型如果市场恰好是熊市准确率也可能很高但它没有任何交易价值。5.1 超越准确率综合评估指标from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score import matplotlib.pyplot as plt import seaborn as sns # 在测试集上预测 y_pred_prob model.predict(X_test).flatten() # 预测概率 y_pred_class (y_pred_prob 0.5).astype(int) # 以0.5为阈值分类 print(“分类报告:”) print(classification_report(y_test, y_pred_class)) print(“\n混淆矩阵:”) print(confusion_matrix(y_test, y_pred_class)) print(f“\nROC-AUC分数: {roc_auc_score(y_test, y_pred_prob):.4f}”)需要重点关注的指标精确率Precision在所有预测为“涨”的样本中真正上涨的比例。高精确率意味着你的“买入信号”可靠性高。召回率Recall在所有实际上涨的样本中被你成功预测出来的比例。高召回率意味着你抓住了大部分上涨机会。F1-Score精确率和召回率的调和平均数是综合衡量。ROC-AUC这个指标非常重要。它衡量模型将“涨”样本排在“跌”样本前面的能力。AUC越接近1越好0.5相当于随机猜测。一个AUC在0.55-0.65的模型在学术上可能就认为具有一定的预测能力了。5.2 简单的策略回测评估模型最终要落到“能不能赚钱”上。我们设计一个最简单的回测策略当模型预测明天上涨概率0.5时就在今天收盘时买入并在明天收盘时卖出假设可以T1且不考虑手续费和滑点。我们对比这个策略与“始终持有”和“随机预测”的收益曲线。# 假设我们回测起始资金为1 capital 1.0 capital_curve [1.0] hold_curve [1.0] # 始终持有的基准 random_curve [1.0] # 随机预测基准以0.5概率随机买卖 # 获取测试集对应的实际收盘价需要对齐因为构建序列时开头有time_steps个数据被消耗了 test_dates df.index[split_idx time_steps:] # 对齐后的测试集日期 test_prices df[‘收盘’].iloc[split_idx time_steps:].values test_returns df[‘returns’].iloc[split_idx time_steps:].values for i in range(len(y_pred_class) - 1): # 留出最后一天用于卖出 # 模型策略 if y_pred_class[i] 1: # 预测涨买入 capital * (1 test_returns[i1]) # 享受下一天的收益率 capital_curve.append(capital) # 始终持有策略 hold_curve.append(hold_curve[-1] * (1 test_returns[i1])) # 随机策略模拟 import random if random.random() 0.5: random_curve.append(random_curve[-1] * (1 test_returns[i1])) else: random_curve.append(random_curve[-1]) # 预测跌不操作 # 绘制资金曲线 plt.figure(figsize(12, 6)) plt.plot(capital_curve, label‘LSTM Strategy’) plt.plot(hold_curve, label‘Buy Hold’) plt.plot(random_curve, label‘Random Guess’, alpha0.7) plt.xlabel(‘Trading Day’) plt.ylabel(‘Portfolio Value’) plt.title(‘Strategy Backtest Comparison’) plt.legend() plt.grid(True) plt.show() # 计算年化收益率、最大回撤等略需补充计算逻辑结果分析要点看曲线更要看指标观察策略曲线是否稳定在基准线上方。计算夏普比率、最大回撤、年化收益等量化指标。过拟合检验如果策略在训练集时间段内表现惊人在测试集却一塌糊涂基本可以断定是过拟合。交易成本上述回测忽略了手续费和买卖价差滑点。在A股频繁交易的手续费会侵蚀大量利润。一个预测准确率55%的策略扣掉成本后可能根本无法盈利。风险回测曲线漂亮不代表实盘就能赚钱。市场结构会变过去有效的模式未来可能失效。这就是为什么实盘前必须经过严格的样本外测试和模拟盘验证。5.3 常见问题与模型改进方向如果你发现模型表现不佳比如测试集准确率在52%以下AUC接近0.5可以从以下几个方面排查和改进特征不够有效股价运动是否真的能被这些技术指标和滞后特征所刻画可以考虑加入更多元的数据如基本面数据市盈率、市净率、财报数据季度/年度。市场情绪数据新闻情感分析利用NLP分析财经新闻、社交媒体热度、搜索指数。宏观数据利率、CPI、PMI等但这些数据频率低需要做对齐处理。其他资产数据相关板块指数、期货数据、汇率等。数据量不足或质量差LSTM是数据饥渴型模型。对于日线数据至少需要几千条样本。如果股票上市时间短数据量少可以考虑使用更高频率的数据如小时线、分钟线或者使用“横截面”数据同时训练多只股票来增加样本多样性。模型结构或参数不当尝试更深的网络3层LSTM、引入注意力机制Attention、使用更先进的变体如GRU参数更少训练更快。调整time_steps可能有意想不到的效果。预测目标定义问题预测“绝对涨跌”非常困难。可以尝试预测“涨跌幅度是否超过一个阈值”过滤小幅波动或者将其转化为回归问题直接预测收益率然后根据预测收益率的大小来决定交易信号强度。标签噪声股票价格本身噪声极大今天的特征和明天的涨跌之间因果关系很弱。可以考虑平滑标签例如使用未来N日的平均收益率作为目标或者只对波动大的日子进行预测。最后的忠告基于LSTM或任何机器学习模型的股票预测是一个极具探索性的领域但绝非“印钞机”。它更像是一个复杂的“概率游戏”。这个项目的最大价值不在于构建出一个稳赚不赔的策略而在于完整地实践一遍从数据获取、特征工程、模型构建到回测评估的量化研究流程。在这个过程中你对金融市场、时间序列分析和深度学习的理解会深刻得多。保持理性重视风险管理和模型的可解释性比追求高预测准确率更重要。我的个人体会是把这次实践当作一个学习和研究的起点用它来验证想法、理解市场而不是直接用于实盘决策。