
简介一份完整的基于长短期记忆网络LSTM的时序收益预测系统代码包面向金融数据分析初学者与深度学习入门者帮助掌握用Python实现循环神经网络变体对股票、大宗商品等收益序列进行建模与预测的方法。资源共55个文件压缩包仅2.27MB包含3个Python脚本、多种模型文件、训练数据、实验截图以及操作文档并配有数据、模型、日志等目录分别存放原始与预处理数据、训练好的模型和训练日志结构清晰便于对照学习。内容涉及长短期记忆网络的输入门、遗忘门、输出门机制TensorFlow或Keras建模数据标准化以及平均绝对误差和均方根误差评估等关键环节还提供了有色金属、化工、贵金属等多个品种的训练曲线与收益率拟合图可直观评估模型在不同标的上预测效果。已有2911人学习适合希望快速上手时序预测深度学习项目、需要完整可运行代码与操作说明的研究者或开发者。1. 基于LSTM的收益预测系统到底在预测什么把行情数据丢进LSTM训练完一测训练集loss漂亮得像捡到钱换到真实行情上预测值却基本趴在零附近——这是做时序收益预测的人最常撞上的墙。这套系统的价值不在“精确预测涨跌”而是用LSTM神经网络捕捉价格序列里的时间依赖输出未来一段时间的收益方向和相对强弱为仓位判断提供一个比单纯看技术指标更稳定的参考信号。它适合有Python基础、会操作Pandas、想用深度学习做量化尝试的从业者完整链路包括数据清洗、特征构建、样本切窗、模型训练、时序验证和回测闭环六个环节任何一个环节出错预测结果都会在实盘里现出原形。2. 数据清洗与样本构造K线怎么变成LSTM能吃下的训练集很多人拿到数据直接pct_change()一算就喂模型这是收益预测里第一个坑。LSTM学的是时间窗口内的模式喂进去的是“片段”每个片段由连续若干天的特征组成对应一个未来收益标签。这一步处理不好后面模型再先进也白搭。我一般会把整个数据准备分成四步原始数据处理、特征构建、标签构建、滑动窗口切分。2.1 原始数据预处理复权、停牌和交易频率用日线数据做预测第一件事是复权。如果直接用原始收盘价计算收益率除权除息日会出现人为的价格跳空模型会以为市场发生了剧烈波动实际上只是分红送股。常见做法是使用后复权价格后复权能保证历史价格连续且收益率计算准确前复权价格会随着最新价变动而整体平移历史训练数据每次都要重算维护成本高。另一个容易忽略的是停牌日处理。停牌期间成交量极低甚至为零价格不动这些样本既没有交易意义还会在滑动窗口切分时污染相邻样本。常规做法是直接删除不要用前向填充去补停牌价。import pandas as pd import numpy as np # 读取日线数据列至少包含date, open, high, low, close, volume df pd.read_csv(daily.csv, parse_dates[date]).sort_values(date) # 后复权复权因子由数据商提供这里用 factor 列示意 if factor in df.columns: df[close] df[close] * df[factor] df[open] df[open] * df[factor] df[high] df[high] * df[factor] df[low] df[low] * df[factor] # 停牌日剔除成交量为 0 或成交额为 0 的交易日直接丢弃 df df[(df[volume] 0) | (df[amount] 0)] # 只保留需要的列避免把日期字符串带进特征矩阵 df df[[date, open, high, low, close, volume]].reset_index(dropTrue)这段代码做了三件事解析日期并排序保证时间轴严格递增用复权因子修正价格过滤停牌交易日。要注意复权因子每家数据商给的形式不一样有的是factor列有的是按adj_factor计算只要保证最终算出来的收益率在除权日前后不出现异常跳变就行。2.2 特征工程用价格状态而不是原始价格训练LSTM对输入特征的尺度很敏感直接把收盘价当特征喂进去模型会花大量容量去拟合价格绝对值的变化而价格绝对值对未来的预测能力极弱。常见替代方案是用收益率、波动率、动量等技术指标作为特征它们描述的是市场状态而不是价格水平。特征不是越多越好收益预测场景下5个左右干净的特征往往比20个冗余特征效果更稳定。# 基础特征收益率、短期波动率、动量、均线偏离、量比 df[ret] df[close].pct_change() df[vol_5] df[ret].rolling(5).std() df[mom_10] df[close] / df[close].shift(10) - 1 df[ma_diff_20] df[close] / df[close].rolling(20).mean() - 1 df[volume_ratio] df[volume] / df[volume].rolling(20).mean() # 缺失值处理rolling 和 pct_change 会在序列开头产生 NaN df df.replace([np.inf, -np.inf], np.nan).dropna()特征计算的逻辑是ret表达昨日收益率vol_5表达最近5日的波动状态mom_10表达过去10日的累计动量ma_diff_20表达价格相对20日均线的偏离度volume_ratio表达当日成交量相对近期均量的倍数。这些都是t时刻“当下能拿到”的信息不包含任何未来数据。参数可以根据标的的波动特性调整做指数、做个股、做期货波动周期不同滚动窗口的周期也应该不同。对A股日线数据5日波动和20日均线偏离是比较通用的初始值。2.3 标签定义明确预测目标避免未来函数标签决定了模型学什么。最常见的标签定义有两种一是回归目标预测未来N日的累计收益率比如未来5个交易日涨跌幅二是分类目标预测未来N日收益率的正负输出“涨/跌”的二分类概率。二者各有优劣回归目标保留幅度信息但收益序列噪声极大模型很难在数值上精确拟合分类目标更贴近交易决策但会丢失“涨1%和涨5%差别很大”这层信息。我一般用回归目标训练评估时再算方向准确率而不是直接做分类因为分类任务在正负样本不均衡时容易让模型偷懒——全预测多数类也能拿到不错准确率。HORIZON 5 # 预测未来5个交易日的收益 # 标签t 时刻的标签是 t 到 tHORIZON 的累计收益 df[label] df[close].shift(-HORIZON) / df[close] - 1 # 移除末尾没有完整标签的行 df df.dropna().reset_index(dropTrue)注意shift(-HORIZON)和/ df[close]的写法特征窗口截止到t标签用的是t到tHORIZON的累计收益标签所在的位置与特征窗口错开了HORIZON个交易日这样才不会有“用今天的信息预测今天的收益”这种穿越。HORIZON是最重要的参数之一它决定模型预测的是短周期还是中周期走势。5对应一周左右的预测视野20对应一个月具体数值取决于你想做什么频率的交易。做日内T0的会把HORIZON设为1甚至用分钟级数据做趋势跟踪的会放到20以上。2.4 滑动窗口切分构造LSTM输入的样本对LSTM需要的输入是一个三维张量形状是(样本数, 序列长度, 特征数)每个样本就是一段长度为SEQ_LEN的连续行情。比如用过去20天的特征预测未来5天的收益每个样本就是20×5的矩阵。滑动窗口切分时最需要注意的是重叠问题相邻两个样本共享了19天数据这会造成训练集和验证集之间信息泄漏后面第四章会详细展开。先看构造代码SEQ_LEN 20 # 每个样本包含过去20个交易日 FEATURE_COLS [ret, vol_5, mom_10, ma_diff_20, volume_ratio] data df[FEATURE_COLS].values labels df[label].values X, y [], [] for i in range(SEQ_LEN, len(data)): X.append(data[i - SEQ_LEN:i]) # 特征窗口: [i-20, i) y.append(labels[i]) # 标签: 第 i 天对应的未来收益 X np.array(X, dtypenp.float32) y np.array(y, dtypenp.float32)这个循环从SEQ_LEN开始遍历每个样本的特征是data[i-SEQ_LEN:i]对应第i天之前20天的数据标签是labels[i]也就是第i天的未来收益。i在循环里充当了“样本锚点”保证每个样本的特征和标签在时间轴上严格对齐特征使用i时刻及之前的信息标签使用i时刻之后的收益。SEQ_LEN20对应一个月的交易日数量对日线级别的收益预测是个比较平衡的选择。太短则信息不足太长则引入太多与当前状态无关的旧信息。有人会把序列长度设到60如果数据量够大可以尝试但要留意训练样本量会随着序列长度增加而显著减少。3. LSTM模型设计PyTorch实现与参数选型的取舍数据准备好了接下来搭模型。LSTM在时序收益预测里受到偏爱核心原因是它的门控机制能选择性记忆和遗忘历史信息收益序列里既有长期趋势成分也有短期噪声LSTM能在训练中自己学习哪些历史状态值得保留、哪些应该丢弃。相比普通RNN它缓解了梯度消失问题能捕捉更长时间跨度的依赖相比Transformer它在中小规模时间序列数据上更容易收敛也不太需要海量数据支撑。但收益序列不同于自然语言或语音它的信噪比很低LSTM结构本身并不能保证预测有效合理的结构设计和参数选择更重要。3.1 输入输出形状先把张量维度想清楚LSTM在PyTorch中的输入形状是(seq_len, batch, input_size)设置batch_firstTrue后变成(batch, seq_len, input_size)。每个时间步的输入是一个长度为input_size的特征向量整个序列就是一个完整的特征窗口。输出是每个时间步的隐藏状态形状为(batch, seq_len, hidden_size)。对收益预测任务来说我们只关心最后一个时间步的输出——它汇集了整个窗口的信息。后面的全连接层把这个隐藏状态映射成标量收益预测值。3.2 模型结构从LSTM层到回归头的设计模型主体一般就三层LSTM层负责提取时序特征Dropout负责正则化全连接回归头负责输出预测值。回归头用一层带ReLU的隐藏层比直接线性输出效果略好但不要堆太多层数据量通常只有几万条复杂网络很容易过拟合。输出层是单节点激活函数为线性因为收益预测是回归任务输出值域是负无穷到正无穷用tanh或sigmoid都会限制预测范围。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size1, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0 ) # 回归头hidden_size - 32 - 1 self.regressor nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, output_size) ) def forward(self, x): # x 形状: (batch, seq_len, input_size) lstm_out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的隐藏状态 last_hidden lstm_out[:, -1, :] # (batch, hidden_size) return self.regressor(last_hidden).squeeze(-1)代码里有两个关键细节。第一dropout在nn.LSTM里只在num_layers 1时生效单层LSTM的dropout参数会被忽略所以单独在回归头里又加了一层Dropout确保单层结构也有正则化效果。第二lstm_out[:, -1, :]取的是最后一个时间步输出对这个预测任务来说整个窗口信息已经被压缩到这个向量里不需要像序列标注那样每个时间步都取。3.3 核心参数选型一份可以直接照抄的初始配置参数选型没有绝对最优但有比较稳妥的起点。下面的表格给出了一套我在日线收益预测场景下常用的初始参数可以在此基础上调整参数推荐值调整方向说明hidden_size32 ~ 64数据量大可增到128隐藏状态维度太小记忆容量不足太大会过拟合num_layers1 ~ 2最多不超过3层数增加带来的是训练难度和过拟合风险dropout0.2 ~ 0.3过拟合时增大随机丢弃部分神经元防止依赖单一特征learning_rate1e-3loss不降时减半AdamW配合初始1e-3通常足够batch_size256 ~ 512显存不够时减小日线数据样本量大大batch加速收敛SEQ_LEN2015 ~ 60 区间内调决定模型回溯多少天信息HORIZON51 ~ 20预测未来多少天的收益这里着重说两个容易踩的。第一hidden_size不是越大越好几千个样本下hidden_size设为128已经偏高LSTM参数量不小日线数据本身信息量有限隐藏单元过多时模型会把噪声当成模式记住训练集loss漂亮但验证集一塌糊涂。第二num_layers加深带来的收益远没有CNN那么明显两层LSTM是大多数时序预测场景的经验上限三层以上在金融数据上几乎必过拟合。3.4 损失函数与优化器回归任务的两个务实选择损失函数首选HuberLoss。它的特点是误差小时按均方误差更新误差大时按绝对误差更新对收益序列里的极端离群值有天然鲁棒性。普通MSE会被少数暴涨暴跌的样本主导梯度让模型花很大力气拟合这些不可预测的极端收益反而忽略了多数正常交易日的微弱规律。优化器用AdamW即可weight decay设为1e-5左右能起到额外正则化作用。如果发现loss一直降不下去先检查特征和标签是否对齐再考虑调学习率——绝大多数情况不是模型问题是数据问题。4. 训练与评估时序切分、walk-forward和三个核心指标模型训练里最常见的错误是把深度学习的标准切分方式直接搬过来。标准K折交叉验证在这条路上是行不通的滑动窗口导致相邻样本大量重叠随机切分后训练集和验证集里会出现几乎一摸一样的样本验证分数虚高换到新数据立刻打回原形。时序预测的验证必须让验证集严格排在训练集之后用“过去预测未来”的方式评估。4.1 时序切分三原则第一验证集必须晚于训练集。用前80%的交易日训练后20%验证这是最基础的划分。第二不能只做一次划分就下结论市场状态会变化某一段行情的验证结果可能只是运气好要做多段滚动验证。第三每段验证的目标是最小化与实盘环境的差异实盘时模型只见过当时之前的数据验证时也必须保证这一点任何全局操作都会破坏这个前提。4.2 Walk-Forward验证手工实现滚动评估Walk-Forward是时序预测里最常见的验证方式把数据按时间顺序分成多段第一次用第1段训练、第2段验证第二次用前2段训练、第3段验证依次类推。每一段验证集都在训练集之后且评估全过程中没有使用任何未来信息。实现方式def walk_forward_split(total_len, n_folds4): 返回 train_idx, val_idx 列表每轮验证集严格晚于训练集 fold_size total_len // (n_folds 1) for i in range(1, n_folds 1): train_idx range(0, i * fold_size) val_idx range(i * fold_size, (i 1) * fold_size) yield train_idx, val_idx # 使用示例 for train_idx, val_idx in walk_forward_split(len(X), n_folds4): X_train, y_train X[train_idx], y[train_idx] X_val, y_val X[val_idx], y[val_idx] # 在这里重新训练模型并记录验证指标把n_folds设为4相当于把数据切成5段第一轮用20%数据训练、下一段20%验证第二轮用前40%训练、再下一段20%验证依次滚动。每一轮都重新训练一次模型把验证指标记录下来最后看平均值。这样得到的评估结果远比单次切分可信。代价是训练时间变长但这是值得的它逼着模型在多个市场阶段上接受检验而不是只对某一段行情有效。4.3 评估指标MSE、方向准确率与IC收益预测不能只看MSE。收益序列噪声极大一个“只会输出零”的模型就能获得很低的MSE因为在均方误差看来预测0和真实值0.01之间的差距是可接受的。必须配合方向准确率和IC来评估。指标计算方式含义参考标准MSEmean((pred - true)^2)预测数值误差只做参考不作为主要判断方向准确率mean(sign(pred) sign(true))预测涨跌与真实涨跌一致的比例大于0.52算有正向作用ICspearman(pred, true)预测值与真实值的秩相关系数0.03以上有弱预测力0.05以上可用训练循环里每个epoch结束时计算这三个指标from scipy.stats import spearmanr def evaluate(model, X_val, y_val, criterion): model.eval() with torch.no_grad(): pred model(X_val) loss criterion(pred, y_val) # 方向准确率 direction_acc (torch.sign(pred) torch.sign(y_val)).float().mean().item() # IC: 秩相关系数 ic, _ spearmanr(pred.numpy(), y_val.numpy()) return loss.item(), direction_acc, icdirection_acc用符号比较判断预测方向和真实方向是否一致这对交易有直接意义方向对了才有赚钱的可能。IC看的是排序能力——预测值最高的样本是否真的涨得最多。如果一个模型的IC稳定在0.05以上说明它具备真实的排序能力用来构建多头组合才有意义。训练时用MSE或Huber作为梯度下降的优化目标评估时综合这三个指标决定是否采纳模型这是收益预测系统的普遍做法。训练过程中还需要设置早停机制每个epoch记录验证集loss连续10个epoch没有下降就停止训练并回退到验证集loss最低时保存的模型权重。不设早停的模型训练到后期几乎必然过拟合验证集指标先升后降最终保存的模型权重很可能不是最优的那一个。把这个逻辑嵌套在walk-forward循环里每一轮验证都做独立的早停判断。5. 避坑清单收益预测最常见的五个踩坑场景这一章的价值来自真实投入和反复回测后留下的记录。下面五个场景是我见过、也亲历过的高频问题每条按“现象→原因→解决”展开。5.1 训练集loss很低验证集预测值几乎全是0现象模型训练过程中loss一路下降训练集方向准确率高达65%验证集上预测值却全部在0附近波动方向准确率勉强50%。原因收益序列噪声极大回归模型在均方误差的驱动下学到的最优策略就是输出一个接近0的常数——因为大多数收益本来就接近0预测0的误差就是最小化。模型没有真的学到规律只是找到了一个廉价的最优解。解决把评估重点放到方向准确率和IC上MSE很低但没有方向区分度的模型没有意义另一个有效手段是改用HuberLoss减少离群样本主导梯度的问题如果仍无改善检查标签分布是否严重偏向0附近考虑使用分类目标做辅助监督。5.2 随机切分验证集时指标很高换新数据就失效现象用train_test_split随机切分验证集方向准确率做到58%IC超过0.08但把模型拿到新数据上跑方向准确率回落到50%以下。原因滑动窗口让相邻样本高度重叠随机切分后训练集和验证集里有大量共享数据的样本对模型“见过”验证集的信息验证指标是幸存者偏差。解决改用严格按时间顺序的walk-forward验证每轮验证集都晚于训练集并且验证集样本完全不与训练集重叠。如果walk-forward后指标显著低于随机切分这是正常现象说明随机切分时虚高的那部分水分被挤掉了。5.3 全量数据归一化导致未来信息泄漏现象训练时loss正常但换到新数据上预测值整体偏移方向准确率大幅下降一段时间后回测显示策略完全失效。原因对特征做MinMaxScaler时在全部数据上fitscaler记录了整个样本区间的最大值和最小值相当于把训练集之后的未来信息用到了训练前的归一化里。这种泄漏在数值上很隐蔽但会显著影响模型对新数据的适应性。解决归一化只在训练集上fit验证集和测试集使用训练集的scaler做transform。from sklearn.preprocessing import StandardScaler # 只对训练集拟合scaler验证集和测试集只做transform scaler StandardScaler() X_train_flat X_train.reshape(-1, X_train.shape[-1]) scaler.fit(X_train_flat) X_train scaler.transform(X_train_flat).reshape(X_train.shape) X_val_flat X_val.reshape(-1, X_val.shape[-1]) X_val scaler.transform(X_val_flat).reshape(X_val.shape)这里的实现要点是先把三维特征reshape成二维再量纲化之后reshape回原来的(样本数, 序列长度, 特征数)形状输入LSTM。注意fit操作只能执行一次就是用训练集的均值和方法做标准化不要在后面用全量数据重新fit。5.4 停牌、涨跌停日污染标签模型学到错误规律现象模型训练稳定但实盘中遇到一字涨停的标的预测收益方向完全正确却无法买入或者遇到复牌个股收益异常导致回测净值曲线剧烈波动。原因一字涨停或跌停时标的处于无法成交状态此时的“未来收益”不可交易停牌后复牌的个股往往有大幅补涨补跌这些收益被模型当作正常规律学习实际交易中根本无法获取。解决样本过滤时把标签对应的价格区间内存在一字涨停、一字跌停或停牌的样本剔除。比如HORIZON5时检查未来5个交易日内是否存在无法成交的交易日存在就直接删除该训练样本宁可少一个样本也不让模型学到虚假规律。5.5 推理时隐藏状态没有清零预测结果出现偏移现象同一个特征向量在上午和下午分别推理输出的预测值明显不同甚至符号都翻转模型在批量推理时表现正常单条推理时结果飘忽。原因PyTorch的nn.LSTM如果不传初始隐藏状态默认用全零初始化这本身没问题但如果之前已经推理过一个batch且h_n, c_n被保留下来并作为下一次推理的初始状态传入LSTM就会把上一批数据的末尾状态延续到新样本里破坏预测的独立性。解决推理时显式传入全零初始状态。def predict_single(model, x_one_seq): model.eval() batch torch.from_numpy(x_one_seq).unsqueeze(0) # (1, seq_len, input_size) h0 torch.zeros(model.lstm.num_layers, 1, model.lstm.hidden_size) c0 torch.zeros(model.lstm.num_layers, 1, model.lstm.hidden_size) with torch.no_grad(): lstm_out, _ model.lstm(batch, (h0, c0)) last_hidden lstm_out[:, -1, :] pred model.regressor(last_hidden).item() return pred每次推理都重新创建h0和c0全零张量保证每个样本都从头开始记忆。这是LSTM推理里最容易被忽略的细节批量评估时不易发现实盘单条推送时就会暴露。6. 从预测到仓位模型上线的回测闭环与监控技巧验证指标过关只是第一步模型要真正可用必须回答“预测值如何变成仓位”。我的做法是把预测值转成z-score再按阈值生成三档仓位信号z-score大于1赋多仓1小于-1赋空仓-1中间区间空仓。这样做的好处是只在高置信度时下单避免模型在预测模糊区间反复切换导致频繁交易。仓位对下一周期的真实收益相乘得到策略每期的收益序列然后计算夏普比率和最大回撤。# pred 为验证集或测试集的预测值future_ret 为对应的真实未来收益 pred_z (pred - pred.mean()) / pred.std() position np.where(pred_z 1.0, 1.0, np.where(pred_z -1.0, -1.0, 0.0)) # 信号必须在预测区间结束后才能与收益对齐 strategy_ret position * future_ret nav (1 strategy_ret).cumprod() sharpe strategy_ret.mean() / strategy_ret.std() * np.sqrt(252) max_drawdown (nav / np.maximum.accumulate(nav) - 1).min()这段代码里最容易出错的地方是position和future_ret的时间对齐预测是t时刻做出的对应的仓位只能从t1时刻开始持有收益也必须从t1时刻开始计算。如果直接把预测值和t时刻的同期收益相乘就会出现前视偏差回测利润虚高。我一般都把信号向后平移HORIZON个交易日后才与收益序列对齐这是回测闭环里最关键的校验点。模型上线后不要躺平。我习惯每天收盘后记录当天的预测值和实际收益率滚动计算近30个交易日的IC一旦IC的中轴从0.05掉到0以下就去检查是市场状态切换了还是特征参数过期了。数据分布漂移在金融时序里是常态稳定的预测系统需要定期重训。我现在每次训练都会把模型权重、特征版本和参数配置打个包存档方便模型失效时复盘是数据变了还是市场变了这个习惯帮我躲过了好几次收益回撤的翻车。希望帮到你。本文还有配套的精品资源点击获取