简介:本资源是一份面向计算机及相关专业本科生的毕业设计实战项目,聚焦使用Python与LSTM(长短期记忆网络)构建股票及基金价格预测模型,解决金融时间序列建模与预测这一典型机器学习应用场景。资源包共11个文件,含5个Excel数据集(涵盖训练/测试/预测结果等多维度基金行情数据)、3个核心Python脚本(实现数据获取、LSTM建模与可视化)、1份Markdown说明文档、1张模型效果对比图及1个编译缓存文件,整体仅155KB,轻量易部署。已有49人学习下载,适合毕设开题、课程设计或机器学习入门实践者快速上手。用户可直接运行源码完成端到端流程:从基金净值数据采集(get_funds_LSJZ_1.py)、标准化预处理、LSTM网络搭建(lstm_model.py)到预测结果可视化(Figure_1.png),并基于README.md理解整体架构与调参逻辑,具备完整复现性与教学参考价值。
1. 项目概述:当量化投资遇见LSTM
最近几年,身边不少做量化研究的朋友,都开始把目光从传统的技术指标、统计套利,转向了机器学习,尤其是时间序列预测。我自己也花了相当长的时间,在股票和基金的预测模型上做各种尝试。今天想和大家深入聊聊的,就是其中应用最广泛、也最让人“又爱又恨”的一个模型——LSTM(长短期记忆网络)。用Python搭建一个LSTM模型来预测股价或基金净值,听起来很酷,像是打开了财富密码,但实际操作过的人都知道,这里面坑太多了,从数据清洗到模型调参,每一步都可能让你前功尽弃。
这个项目本质上,是利用历史金融时间序列数据(比如开盘价、收盘价、成交量),训练一个能够学习数据中长期依赖关系的神经网络,从而对未来走势进行预测。它适合有一定Python和机器学习基础,对金融市场感兴趣,并且愿意投入时间进行大量实验和调试的开发者或量化爱好者。别指望看完一篇教程就能稳定盈利,但这个过程能让你深刻理解机器学习在非平稳、高噪声数据上应用的挑战与乐趣。接下来,我会拆解整个流程,分享我踩过的坑和总结出的一些有效策略。
2. 核心思路与模型选型:为什么是LSTM?
在动手写代码之前,搞清楚“为什么”比“怎么做”更重要。金融时间序列数据有几个让人头疼的特点:非平稳性(均值、方差随时间变化)、高噪声(受无数因素影响,信号微弱)、长期依赖(今天的价格可能和一周前、甚至一个月前的某个事件相关)。传统的线性模型(如ARIMA)很难捕捉这些复杂的非线性关系。
2.1 从RNN到LSTM的进化
循环神经网络(RNN)是处理序列数据的天然选择,它理论上可以记住之前的信息。但标准RNN有个致命缺陷:梯度消失或爆炸。当序列很长时(比如用过去100天的数据预测明天),网络在反向传播时,梯度会指数级地减小或增大,导致无法学习到长期的依赖关系。这就像让你回忆一个月前早餐吃了什么,细节早就模糊了。
LSTM作为RNN的一种变体,通过精巧的“门控”结构解决了这个问题。你可以把LSTM单元想象成一个有选择性的记忆细胞。它包含三个门:
- 遗忘门:决定从细胞状态中丢弃哪些信息(比如,忘记一周前的某个无关紧要的微小波动)。
- 输入门:决定哪些新信息需要被存入细胞状态(比如,记住昨天发生的重大政策公告)。
- 输出门:基于当前的细胞状态,决定输出什么信息给下一个时间步。
这个机制使得LSTM能够有选择地保留长期信息,过滤短期噪声,非常适合金融序列这种需要“记住”重要事件(如财报发布、政策转向)影响的应用场景。
2.2 项目整体设计思路
我们的目标不是预测绝对价格(这几乎不可能),而是预测价格的变化趋势或收益率。因此,整个项目的Pipeline可以这样设计:
- 数据获取与预处理:获取干净的、一致的历史数据,并进行归一化、构建特征和标签。
- 序列数据构建:将数据整理成LSTM需要的
[样本数, 时间步长, 特征数]格式。 - 模型构建与训练:搭建LSTM网络,划分训练集/验证集/测试集,进行训练和验证。
- 预测与回测:用训练好的模型在未见过的数据(测试集)上进行预测,并将预测结果与实际值对比,进行简单的回测分析。
- 模型评估与迭代:分析模型表现,思考改进方向(特征工程、模型结构、超参数调优)。
这里有一个关键心法:在金融预测中,防止过拟合比追求训练集上的高精度重要得多。一个在历史数据上拟合得完美的模型,大概率在实盘中会亏得很惨。
3. 实战环境搭建与数据准备
工欲善其事,必先利其器。我们先来把环境和数据准备好。
3.1 Python环境与核心库配置
我强烈建议使用Anaconda来管理Python环境,它能很好地处理科学计算库的依赖。创建一个独立的环境是个好习惯:
conda create -n stock_lstm python=3.9 conda activate stock_lstm接下来安装核心库。除了经典的pandas,numpy,matplotlib,本项目的主角是:
yfinance:从雅虎财经获取股票/基金历史数据的利器,免费且方便。scikit-learn:用于数据预处理(如归一化)和模型评估。tensorflow/keras:构建和训练LSTM模型的主流深度学习框架。对于新手,从keras开始接口更友好。
安装命令如下:
pip install yfinance pandas numpy matplotlib scikit-learn # 安装TensorFlow,根据你的硬件选择版本 pip install tensorflow # CPU版本 # 或者 pip install tensorflow-gpu # GPU版本,训练速度更快注意:如果安装
tensorflow遇到问题,可以先尝试安装一个稍旧的稳定版本,如pip install tensorflow==2.10.0。确保你的Python版本与TensorFlow兼容。
3.2 数据获取与初步探索
我们以预测某只股票(例如,沪深300ETF,代码510300.SS)的收盘价为例。使用yfinance获取数据非常简单。
import yfinance as yf import pandas as pd import numpy as np import matplotlib.pyplot as plt # 定义股票代码和时间范围 ticker = ‘510300.SS‘ # 沪深300ETF start_date = ‘2018-01-01‘ end_date = ‘2023-12-31‘ # 下载历史数据 df = yf.download(ticker, start=start_date, end=end_date) print(df.head()) print(df.info())下载的数据框(DataFrame)通常包含Open,High,Low,Close,Adj Close,Volume等列。Adj Close(复权收盘价)考虑了分红、拆股等因素,是更常用的价格指标。
首先,我们可视化一下收盘价走势,对数据有个直观感受:
plt.figure(figsize=(14,5)) plt.plot(df.index, df[‘Adj Close‘], label=‘Adjusted Close Price‘) plt.title(f‘{ticker} Price History‘) plt.xlabel(‘Date‘) plt.ylabel(‘Price (CNY)‘) plt.legend() plt.grid(True) plt.show()3.3 数据预处理与特征工程
原始数据不能直接喂给模型。预处理的目标是让数据变得“规整”且对模型友好。
1. 处理缺失值:金融数据在节假日等非交易日会有缺失。通常采用前向填充(用前一天的数据填充)或直接删除。
df.fillna(method=‘ffill‘, inplace=True) # 前向填充2. 构建预测目标:如前所述,我们更关心变化。通常我们会构建收益率序列作为预测目标。
df[‘Return‘] = df[‘Adj Close‘].pct_change() # 日收益率 df.dropna(inplace=True) # 第一行收益率是NaN,删除3. 特征选择与构建:除了价格和收益率,我们还可以加入其他可能具有预测能力的特征。这里可以简单点,也可以复杂点。
- 简单版:只使用历史价格序列。例如,用过去N天的收盘价预测下一天的收盘价。
- 增强版:加入技术指标。例如,移动平均线(MA)、相对强弱指数(RSI)、布林带(Bollinger Bands)等。可以用
ta库(Technical Analysis)方便地计算。pip install taimport ta # 添加简单移动平均线 df[‘SMA_20‘] = ta.trend.sma_indicator(df[‘Close‘], window=20) # 添加相对强弱指数 df[‘RSI_14‘] = ta.momentum.rsi(df[‘Close‘], window=14) # 再次处理因计算指标产生的NaN值 df.dropna(inplace=True)
4. 数据归一化/标准化:这是关键一步!LSTM等神经网络对输入数据的尺度非常敏感。我们必须将不同特征缩放到相似的尺度(通常是0-1或-1到1之间)。这里使用MinMaxScaler。
from sklearn.preprocessing import MinMaxScaler # 假设我们选择这些特征 feature_columns = [‘Adj Close‘, ‘Volume‘, ‘SMA_20‘, ‘RSI_14‘] target_column = [‘Return‘] scaler_features = MinMaxScaler(feature_range=(0, 1)) scaler_target = MinMaxScaler(feature_range=(0, 1)) scaled_features = scaler_features.fit_transform(df[feature_columns]) scaled_target = scaler_target.fit_transform(df[target_column])实操心得:一定要将特征和目标分开进行缩放,并且分别保存它们的缩放器(scaler)!在后续用模型预测出新值后,你需要用
scaler_target.inverse_transform将其反变换回原始的收益率或价格尺度,才能进行有意义的评估和回测。这是一个常见的踩坑点。
4. 构建LSTM模型与训练流程
数据准备好了,现在进入核心环节——构建模型。
4.1 构建监督学习序列
LSTM的输入是一个三维张量:[样本数, 时间步长, 特征数]。我们需要把一长条时间序列数据,切成许多个滑动窗口。每个窗口包含连续look_back天的数据(特征),用来预测第look_back+1天的目标值(收益率)。
def create_dataset(feature_data, target_data, look_back=60): X, y = [], [] for i in range(look_back, len(feature_data)): X.append(feature_data[i-look_back:i, :]) # 取过去look_back天的所有特征 y.append(target_data[i, 0]) # 预测第i天的目标(收益率) return np.array(X), np.array(y) look_back = 60 # 使用过去60天的数据 X, y = create_dataset(scaled_features, scaled_target, look_back) print(f‘X shape: {X.shape}‘) # 应为 (样本数, 60, 特征数) print(f‘y shape: {y.shape}‘) # 应为 (样本数,)4.2 划分训练集、验证集和测试集
绝对不能随机打乱时间序列数据!必须按时间顺序划分。通常用前80%的数据训练,中间10%验证,最后10%测试。
train_size = int(len(X) * 0.8) val_size = int(len(X) * 0.1) test_size = len(X) - train_size - val_size X_train, X_val, X_test = X[:train_size], X[train_size:train_size+val_size], X[train_size+val_size:] y_train, y_val, y_test = y[:train_size], y[train_size:train_size+val_size], y[train_size+val_size:]4.3 定义LSTM模型结构
使用Keras的Sequential API可以快速搭建模型。一个经典的LSTM结构如下:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout, Input from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau model = Sequential() # 第一层LSTM,需要指定input_shape model.add(Input(shape=(look_back, len(feature_columns)))) # 明确输入形状 model.add(LSTM(units=50, return_sequences=True)) # 50个神经元,返回完整序列供下一层使用 model.add(Dropout(0.2)) # 丢弃20%的神经元,防止过拟合 # 第二层LSTM model.add(LSTM(units=50, return_sequences=False)) # 不返回序列,只输出最后一个时间步的结果 model.add(Dropout(0.2)) # 全连接层,将LSTM输出映射到最终预测值 model.add(Dense(units=1)) # 编译模型 model.compile(optimizer=‘adam‘, loss=‘mean_squared_error‘) # 回归问题常用MSE损失 model.summary()- units=50:LSTM层中神经元的数量。这是一个超参数,可以从50开始尝试,增加可能提升模型容量但也可能导致过拟合。
- return_sequences:当后面还要接LSTM层时,需要设为
True;如果是最后一层LSTM或后面接Dense层,则设为False。 - Dropout:在训练过程中随机“关闭”一部分神经元,是防止过拟合的强有力工具。比例通常在0.2到0.5之间。
- 优化器与损失函数:
adam优化器自适应学习率,效果通常不错。对于预测连续值(收益率),均方误差(MSE)是标准的损失函数。
4.4 训练模型与使用回调
训练时,使用验证集来监控模型是否过拟合,并应用回调函数来优化训练过程。
# 定义回调函数 early_stopping = EarlyStopping(monitor=‘val_loss‘, patience=10, restore_best_weights=True) reduce_lr = ReduceLROnPlateau(monitor=‘val_loss‘, factor=0.5, patience=5, min_lr=1e-6) # 训练模型 history = model.fit( X_train, y_train, epochs=100, # 训练轮数,可能被早停回调提前结束 batch_size=32, # 每次梯度更新使用的样本数 validation_data=(X_val, y_val), callbacks=[early_stopping, reduce_lr], verbose=1 )- EarlyStopping:当验证集损失在连续
patience个epoch内不再下降时,停止训练,并恢复最佳权重。这能有效避免无效训练。 - ReduceLROnPlateau:当验证损失停滞时,自动降低学习率。有助于模型在后期精细调整。
训练完成后,绘制损失曲线来诊断训练过程:
plt.figure(figsize=(12,4)) plt.plot(history.history[‘loss‘], label=‘Training Loss‘) plt.plot(history.history[‘val_loss‘], label=‘Validation Loss‘) plt.title(‘Model Loss‘) plt.xlabel(‘Epoch‘) plt.ylabel(‘Loss (MSE)‘) plt.legend() plt.grid(True) plt.show()理想的曲线是训练损失和验证损失都稳步下降,并最终趋于平稳。如果训练损失持续下降而验证损失开始上升,那就是明显的过拟合信号。
5. 模型预测、评估与回测分析
模型训练好了,是骡子是马,得拉出来溜溜。
5.1 进行预测与结果反归一化
首先在测试集上进行预测,然后将缩放后的预测值转换回原始的收益率尺度。
# 在测试集上预测 y_pred_scaled = model.predict(X_test) # 将预测值和真实值反归一化 # 注意:为了正确反归一化,我们需要构建一个临时的数据框 # 因为scaler_target期望的输入形状是 [n_samples, n_features],我们这里只有1个特征(收益率) y_pred_original = scaler_target.inverse_transform(y_pred_scaled) y_test_original = scaler_target.inverse_transform(y_test.reshape(-1, 1))5.2 可视化预测效果
将预测的收益率序列和真实的收益率序列进行对比。
plt.figure(figsize=(14,5)) plt.plot(y_test_original, label=‘Actual Return‘, alpha=0.7) plt.plot(y_pred_original, label=‘Predicted Return‘, alpha=0.7) plt.title(‘Stock Return Prediction vs Actual (Test Set)‘) plt.xlabel(‘Time Step‘) plt.ylabel(‘Return‘) plt.legend() plt.grid(True) plt.show()通常,你会看到预测曲线紧紧跟随真实曲线,但存在滞后,并且无法预测剧烈的波动(尖峰)。这完全正常,甚至可以说是“正确”的。LSTM学到的是历史序列中的主要模式和趋势,对于市场中的突发黑天鹅事件,模型无法预测。
5.3 构建简单的交易策略进行回测
单纯的预测收益率曲线好看与否意义不大,最终要落到“能否赚钱”上。我们设计一个最简单的策略:
- 规则:如果模型预测下一天的收益率为正,则在当天收盘时买入,并在下一天收盘时卖出(假设可以T+1)。如果预测为负,则空仓。
- 初始资金:假设为1。
- 不考虑:交易费用、滑点、涨停跌停限制(这是一个简化回测)。
# 根据预测信号生成交易信号 signal = np.where(y_pred_original > 0, 1, 0) # 预测为正则买入信号为1,否则为0 # 计算策略收益率:信号 * 实际收益率。注意要错位一天,因为用今天的数据预测明天的收益率。 strategy_returns = np.zeros_like(y_test_original) strategy_returns[1:] = signal[:-1].flatten() * y_test_original[1:].flatten() # 计算累积收益率 cumulative_actual_returns = np.cumprod(1 + y_test_original.flatten()) - 1 cumulative_strategy_returns = np.cumprod(1 + strategy_returns.flatten()) - 1 # 绘制累积收益率曲线 plt.figure(figsize=(14,5)) plt.plot(cumulative_actual_returns, label=‘Buy & Hold (Actual)‘) plt.plot(cumulative_strategy_returns, label=‘LSTM Strategy‘) plt.title(‘Cumulative Returns: LSTM Strategy vs Buy & Hold‘) plt.xlabel(‘Time Step on Test Set‘) plt.ylabel(‘Cumulative Return‘) plt.legend() plt.grid(True) plt.show()5.4 关键绩效指标计算
用几个量化指标来评估策略表现:
# 计算年化收益率、波动率、夏普比率等(简化版) total_days = len(strategy_returns) annual_factor = 252 # 假设一年有252个交易日 # 策略总收益率 total_return = cumulative_strategy_returns[-1] # 年化收益率 annual_return = (1 + total_return) ** (annual_factor / total_days) - 1 # 年化波动率 annual_volatility = np.std(strategy_returns) * np.sqrt(annual_factor) # 夏普比率(假设无风险利率为0) sharpe_ratio = annual_return / annual_volatility if annual_volatility != 0 else 0 print(f‘策略总收益率: {total_return:.2%}‘) print(f‘策略年化收益率: {annual_return:.2%}‘) print(f‘策略年化波动率: {annual_volatility:.2%}‘) print(f‘策略夏普比率: {sharpe_ratio:.2f}‘) # 对比买入持有策略 bh_total_return = cumulative_actual_returns[-1] print(f‘\n买入持有总收益率: {bh_total_return:.2%}‘)6. 模型优化方向与常见陷阱
第一次构建的模型表现很可能不尽如人意,甚至跑不过简单的买入持有。这才是常态。以下是几个关键的优化方向和必须避开的坑。
6.1 特征工程:模型的“燃料”
原始价格和成交量信息有限。可以考虑:
- 更多技术指标:MACD, OBV, ATR, 各种移动平均线的组合等。
- 基本面数据:市盈率(PE)、市净率(PB)等,但需要处理发布频率不一致的问题(日频 vs 季频)。
- 市场情绪数据:新闻情感分析、社交媒体热度等(难度较高)。
- 其他资产数据:相关指数、汇率、大宗商品价格等,作为外部特征。
- 特征构造:例如,计算收益率的不同时间窗口(1日、5日、20日收益率),计算价量关系(价格*成交量的变化率)。
注意事项:特征不是越多越好。高度相关的特征(共线性)可能让模型训练不稳定,无关的噪声特征会干扰模型学习。可以使用相关性分析、主成分分析(PCA)或基于模型的特征重要性评估来进行特征筛选。
6.2 模型结构与超参数调优
- 网络结构:
- LSTM层数:1-3层通常足够,更深可能难以训练。
- 每层神经元数:从50开始,尝试增加或减少。可以使用网格搜索或随机搜索。
- Dropout率:0.2到0.5之间调整。
- 序列长度 (
look_back):这是最重要的超参数之一。太短(如10天)模型看不到长期趋势;太长(如250天)可能包含太多噪声,且训练更慢。可以尝试30, 60, 90, 120等不同值。 - 损失函数:除了MSE,可以尝试平均绝对误差(MAE),它对异常值不那么敏感。对于方向预测,可以尝试自定义损失函数,例如更看重预测符号(涨跌)的正确性。
- 优化器:可以尝试调整Adam优化器的初始学习率(
learning_rate),默认的0.001有时可能偏大。
6.3 过拟合与泛化:永恒的难题
金融数据模式会随时间“漂移”,过去有效的模式未来可能失效。对抗过拟合的方法:
- 使用Dropout和正则化:如上所述。
- 简化模型:在验证集上表现开始变差时,宁愿选择一个更简单的模型。
- 交叉验证(时间序列版本):使用“TimeSeriesSplit”,确保验证集总是在训练集之后,防止信息泄露。
- 集成学习:训练多个LSTM模型(使用不同的初始权重、不同的数据子集),然后对它们的预测进行平均(Bagging),可以降低方差。
- 更频繁的再训练:不要指望一个模型能用一辈子。定期(如每季度或每月)用最新的数据重新训练或微调模型。
6.4 常见问题排查清单
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 训练损失不下降 | 学习率太高/太低;网络结构太简单;数据未归一化 | 调整学习率;增加LSTM层或神经元;检查数据预处理流程 |
| 验证损失远高于训练损失(过拟合) | 模型太复杂;训练数据太少;没有使用Dropout | 增加Dropout率;简化模型;尝试获取更多数据或使用数据增强(需谨慎) |
| 预测结果几乎是一条直线 | 模型学到了数据的平均值;目标变量缩放不当;激活函数问题 | 检查最后一层是否使用了正确的激活函数(回归问题通常不用激活函数或线性激活);检查数据标签是否包含足够的变化;尝试预测价格差分而非价格本身 |
| 预测结果滞后于真实数据 | 模型倾向于学习平滑的趋势,对突变反应迟钝;这是时间序列预测的常见问题 | 尝试加入波动率特征(如ATR);关注方向预测而非精确值;考虑结合其他能快速反应的指标 |
| 在测试集上表现极差 | 数据划分错误,导致信息泄露;市场状态发生结构性变化 | 绝对检查数据划分代码,确保时间顺序!;考虑在更近的时间段内划分测试集 |
7. 从实验到实盘的思考
最后,分享几点我从无数次实验和失败中得出的体会。
第一,降低期望,明确目标。用LSTM预测金融市场,目标不应该是“精准预测明天价格”,而是“探索数据中可能存在的、超越随机性的微弱模式”,或者“构建一个能提供略优于基准(如买入持有)信号的辅助工具”。把它当作一个概率游戏。
第二,重视回测,但不要迷信回测。回测过拟合(在历史数据上过度优化策略)是量化领域的头号杀手。你的模型可能只是恰好拟合了历史上某段特定行情。一定要做样本外测试和向前滚动验证,用最新的、模型从未见过的数据来最终评估。
第三,风险控制永远第一位。即使模型给出了强烈的买入信号,也必须设置止损位。机器学习模型会犯错,而且可能连续犯错。一个没有风控的策略,无论模型多精巧,最终都可能归零。
第四,持续学习与迭代。市场在变,有效的因子和模式也在变。这个项目不是一个一劳永逸的工程,而是一个需要持续维护、监控和更新的系统。关注模型预测效果的衰减,定期用新数据重新训练。
这个项目最大的价值,不在于最终能否做出一个“圣杯”模型,而在于整个过程中你对金融数据、机器学习模型以及两者结合时产生的各种微妙问题的深刻理解。这些经验,远比一个在历史数据上夏普比率很高的策略曲线宝贵得多。
本文还有配套的精品资源,点击获取