简介这份资源是一套基于深度学习神经网络的通用股票预测模型Python实现面向计算机、人工智能、金融商贸等方向的学生与开发者可用于课程设计、毕业设计或量化预测入门实践。项目整合了LSTM、RNN、Transformer及BERT等多种时序建模思路配套数据下载、预处理、训练与预测脚本帮助读者理解从行情数据获取到模型评估的完整链路。压缩包共33个文件约5MB其中13个py源码文件承载模型训练与数据处理逻辑13个png图片展示损失曲线与预测对比结果另有3个md说明文档、1个txt依赖清单及许可证等辅助文件结构清晰便于按模块查阅。目前已有104人学习下载。读者可据此掌握数据爬取、特征构建、模型调参与指标评估等关键环节并可在现有代码基础上修改以适配其他预测任务适合作为学习进阶与项目立项的参考范例。1. 通用股票预测模型为什么“一套代码跑所有票”是个伪命题很多人第一次接触股票预测脑子里想的都是同一件事搞一个深度学习神经网络输入历史价格输出明天涨跌最好还能给个买入卖出信号。标题里说的“通用股票预测模型”听起来就像一把万能钥匙能开所有股票的门。但真正上手跑过几轮的人都知道这事儿没那么简单。所谓“通用”不是指一个模型直接套用到任何标的上都能赚钱而是指一套可复用的工程框架——数据管道、特征工程、模型结构、训练流程、回测逻辑——能快速适配不同标的和不同频率。Python 生态里做这件事的工具链已经相当成熟从 pandas 到 PyTorch从 LSTM 到 Transformer代码层面没有太大门槛。真正的门槛在于你得先接受一个反直觉的结论——股票预测模型的瓶颈从来不在网络结构有多深而在标签怎么定义、特征怎么构造、以及你怎么判断它是不是在自欺欺人。这篇内容面向的是想用深度学习做量化预测的 Python 开发者不管你是刚学完 BP 神经网络想找个项目练手还是已经在跑 LSTM 但回测结果忽好忽坏想找原因下面的拆解都能让你少走几段弯路。2. 从 OHLCV 到模型输入数据管道的四个关键决策2.1 为什么原始价格不能直接喂给神经网络把收盘价序列直接丢进 LSTM 是最常见的起手式也是最容易翻车的地方。原始价格是非平稳序列均值方差随时间漂移模型很容易学到“最近涨所以继续涨”这种伪规律。我一般会先做三件事一是把价格转成对数收益率二是做滚动窗口的 Z-Score 标准化三是把成交量取对数后差分。这样处理之后输入特征的分布相对稳定模型收敛也快得多。import pandas as pd import numpy as np def build_features(df: pd.DataFrame, window: int 20) - pd.DataFrame: df 需包含 open/high/low/close/volume 列索引为日期 window: 滚动标准化窗口默认 20 个交易日 out pd.DataFrame(indexdf.index) # 对数收益率平稳化处理 out[ret] np.log(df[close] / df[close].shift(1)) # 高低价差相对幅度 out[hl_range] (df[high] - df[low]) / df[close] # 成交量对数差分 out[vol_chg] np.log(df[volume] 1).diff() # 滚动 Z-Score用过去 window 天做标准化避免未来信息泄露 for col in [ret, hl_range, vol_chg]: roll_mean out[col].rolling(window).mean() roll_std out[col].rolling(window).std() out[f{col}_z] (out[col] - roll_mean) / (roll_std 1e-8) # 额外加一个动量特征过去 5 天累计收益 out[mom5] out[ret].rolling(5).sum() return out.dropna()这段代码的核心逻辑是所有特征都只依赖当前及历史数据滚动统计量用rolling计算不会引入未来信息。参数window控制标准化回看长度20 是日频数据的常用值做周频可以调到 12 左右。注意vol_chg里加了 1 再取对数是为了处理成交量为零的情况。如果你用的是分钟级数据这个窗口要相应缩小否则标准化会过于平滑丢掉短期突变信号。2.2 标签构造分类、回归还是排序标签怎么定直接决定模型在学什么。常见做法有三类一是预测下一期收益率做回归二是预测涨跌方向做二分类三是做多标的排序学习。我一般会先跑分类因为方向预测的评估更直观而且容易和基准对比。但分类有个坑涨跌阈值设成 0 的话标签噪声极大稍微改一点阈值准确率能差出十几个百分点。比较稳的做法是用未来 N 日累计收益的中位数做动态阈值或者直接预测收益率然后自己设交易规则。def make_label(features: pd.DataFrame, horizon: int 5, threshold: float 0.0): horizon: 预测未来几个交易日的累计收益 threshold: 分类阈值0 表示涨跌方向 future_ret features[ret].shift(-1).rolling(horizon).sum().shift(-(horizon - 1)) label (future_ret threshold).astype(int) return label.dropna()这里horizon5表示预测未来一周的累计收益方向threshold0是最简单的设定。实际用的时候我会把threshold设成过去 60 天收益率标准差的 0.5 倍这样标签分布更均衡。注意shift的方向写反了就是拿未来数据预测过去回测能跑出 90% 以上的准确率实盘一上就废。2.3 训练集/验证集/测试集的时间切分随机切分是股票预测里最隐蔽的坑。用train_test_split打乱顺序模型会学到跨期的模式回测虚高。正确做法是按时间顺序切前 70% 训练中间 15% 验证调参最后 15% 测试。如果要做滚动回测就用 expanding window 或 sliding window每轮用历史数据训练、下一段数据测试。def time_split(features, labels, train_ratio0.7, val_ratio0.15): n len(features) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) X_train, y_train features[:train_end], labels[:train_end] X_val, y_val features[train_end:val_end], labels[train_end:val_end] X_test, y_test features[val_end:], labels[val_end:] return (X_train, y_train), (X_val, y_val), (X_test, y_test)这个切分逻辑简单但有效。注意特征和标签的索引要对齐dropna之后重新reset_index容易把时间顺序搞乱我一般保留原始日期索引切分时用位置切片而不是标签切片。2.4 序列样本构造滑动窗口的尺寸怎么定LSTM 和 Transformer 都需要三维输入(样本数, 时间步, 特征数)。时间步就是回看多少天这个参数没有理论最优值只能试。日频数据我一般从 20 开始试再到 60、120。太短抓不到趋势太长梯度容易消失而且样本量会急剧减少。构造的时候用sliding_window_view或者自己写循环都行关键是别把标签对齐搞错。def make_sequences(features: np.ndarray, labels: np.ndarray, seq_len: int 60): X, y [], [] for i in range(seq_len, len(features)): X.append(features[i - seq_len:i]) y.append(labels[i]) return np.array(X), np.array(y)seq_len60对应约三个月的交易日。如果你做的是分钟级预测这个值要降到 30 甚至 10。注意labels[i]对应的是第 i 个时间点的标签而输入是i-seq_len到i-1的特征这样才没有用到未来信息。3. 网络结构选型LSTM、TCN 还是 Transformer3.1 LSTM 作为基线的三个必调参数LSTM 是股票预测里最常用的结构不是因为它最好而是因为它最稳。我一般先用单层 LSTM 加一个全连接输出跑基线隐藏层维度从 64 开始试dropout设 0.2 到 0.5 之间batch_size用 32 或 64。优化器选 Adam学习率 1e-3 起步如果 loss 震荡就降到 1e-4。这些参数没有魔法但组合起来能覆盖大部分日频预测场景。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers1, dropout0.3): super().__init__() self.lstm nn.LSTM( input_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0 ) self.fc nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) # 二分类输出 logit ) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ self.lstm(x) return self.fc(out[:, -1, :]) # 取最后一个时间步hidden_dim64是起点数据量大可以加到 128 或 256。num_layers1先跑通再加到 2 层并配合dropout。注意out[:, -1, :]取的是最后一个时间步的隐藏状态如果你用batch_firstTrue这个维度是对的。训练时用BCEWithLogitsLoss它把 sigmoid 和交叉熵合在一起数值更稳定。3.2 TCN 为什么在金融序列上有时比 LSTM 稳TCN时序卷积网络用因果卷积加膨胀卷积感受野可以做得很大而且并行计算比 LSTM 快。在股票预测里TCN 的优势是梯度传播路径短不容易出现 LSTM 那种长序列遗忘。我试过在相同特征和标签下TCN 的验证集 loss 曲线比 LSTM 平滑但差距不大。如果你的数据量超过十万条样本TCN 的训练速度优势会很明显。class TCNBlock(nn.Module): def __init__(self, in_ch, out_ch, kernel_size, dilation, dropout0.2): super().__init__() padding (kernel_size - 1) * dilation self.conv nn.Conv1d(in_ch, out_ch, kernel_size, paddingpadding, dilationdilation) self.chomp nn.ConstantPad1d((0, -padding), 0) # 因果卷积去掉右侧多余填充 self.relu nn.ReLU() self.dropout nn.Dropout(dropout) def forward(self, x): out self.conv(x) out self.chomp(out) return self.dropout(self.relu(out))dilation按 1、2、4、8 递增kernel_size一般取 3。chomp那一步是因果卷积的关键保证 t 时刻只看 t 及之前的数据。输入需要转成(batch, channels, seq_len)和 LSTM 的维度顺序不同写的时候注意 transpose。3.3 Transformer 编码器的小数据陷阱Transformer 在 NLP 里大杀四方但在股票预测这种低信噪比、小样本场景下很容易过拟合。自注意力机制参数量大没有足够数据根本训不动。我的经验是日频数据少于 5000 条样本时别上 Transformer如果非要用把层数压到 1 层、头数降到 2 或 4、d_model设成 32并且加很强的 dropout 和权重衰减。否则验证集 loss 会先降后升典型的过拟合曲线。class TransformerPredictor(nn.Module): def __init__(self, input_dim, d_model32, nhead4, num_layers1, dropout0.3): super().__init__() self.input_proj nn.Linear(input_dim, d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforwardd_model * 2, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.fc nn.Linear(d_model, 1) def forward(self, x): x self.input_proj(x) x self.encoder(x) return self.fc(x[:, -1, :])d_model32是最小可用配置nhead4要求d_model能被整除。dim_feedforward设成d_model*2而不是默认的 4 倍也是为了控制参数量。训练时用AdamW加weight_decay1e-4早停 patience 设 5 到 10 个 epoch。3.4 多标的通用模型共享权重还是独立建模“通用”这个词在工程上最实际的落地方式是用同一套网络结构对每个标的单独训练一套权重但共享特征工程和训练流程。另一种做法是加一个标的 embedding让模型学不同标的的差异。前者简单可控后者适合标的数量多、单标的样本少的场景。我一般先跑独立建模如果某些标的样本太少再考虑用 embedding 做迁移。class MultiAssetLSTM(nn.Module): def __init__(self, input_dim, num_assets, hidden_dim64, emb_dim8): super().__init__() self.asset_emb nn.Embedding(num_assets, emb_dim) self.lstm nn.LSTM(input_dim emb_dim, hidden_dim, batch_firstTrue) self.fc nn.Linear(hidden_dim, 1) def forward(self, x, asset_id): # x: (batch, seq_len, input_dim) emb self.asset_emb(asset_id).unsqueeze(1).repeat(1, x.size(1), 1) x torch.cat([x, emb], dim-1) out, _ self.lstm(x) return self.fc(out[:, -1, :])emb_dim8是经验值标的数量超过 100 可以加到 16 或 32。asset_id是每个标的的整数索引训练时和序列一起喂进去。注意 embedding 层也会参与梯度更新如果某个标的样本极少它的 embedding 会欠拟合这时候可以冻结 embedding 只训 LSTM。4. 训练与回测把模型从“能跑”推到“能用”4.1 损失函数和评估指标的选择二分类用BCEWithLogitsLoss回归用MSELoss或HuberLoss。但股票预测里准确率不是唯一指标甚至不是最重要的指标。我一般同时看三个数验证集 AUC、按预测概率排序后的多空组合收益、以及最大回撤。AUC 高但多空收益为负的情况很常见说明模型学到了方向但没学到幅度。这时候要回去检查标签阈值和特征里有没有混入噪声。from sklearn.metrics import roc_auc_score def evaluate(model, loader, devicecpu): model.eval() preds, targets [], [] with torch.no_grad(): for x, y in loader: x, y x.to(device), y.to(device) logits model(x).squeeze() preds.extend(torch.sigmoid(logits).cpu().numpy()) targets.extend(y.cpu().numpy()) auc roc_auc_score(targets, preds) return auc, np.array(preds), np.array(targets)roc_auc_score对类别不平衡不敏感适合涨跌样本比例接近 1:1 的场景。如果涨跌比例悬殊改用average_precision_score。返回的preds可以进一步做分组回测按预测概率从高到低分 5 组看 top 组和 bottom 组的收益差。4.2 滚动回测用时间序列交叉验证代替单次切分单次切分的测试集结果波动很大换一段数据可能完全不一样。滚动回测用多个时间窗口每个窗口训练一次、测试一次最后把测试结果拼起来。这样得到的评估更稳定也能看出模型在不同市场环境下的表现差异。def walk_forward(features, labels, n_splits5, seq_len60): n len(features) fold_size n // (n_splits 1) results [] for i in range(n_splits): train_end fold_size * (i 1) test_end min(train_end fold_size, n) X_train, y_train make_sequences( features[:train_end], labels[:train_end], seq_len) X_test, y_test make_sequences( features[train_end - seq_len:test_end], labels[train_end - seq_len:test_end], seq_len) # 这里插入模型训练和评估代码 results.append((X_train, y_train, X_test, y_test)) return resultsn_splits5表示做 5 轮滚动每轮训练集逐步扩大。注意测试集的序列构造要从train_end - seq_len开始取保证第一个测试样本的输入窗口完整。这个逻辑写错的话测试集第一个样本的输入会缺数据预测结果不可靠。4.3 交易成本与滑点回测里最容易被忽略的减法很多回测曲线漂亮得不像话一加上交易成本就原形毕露。A 股单边手续费加印花税大约千分之一到千分之三滑点按千分之一到千分之五估算。如果模型每天换仓一年 250 个交易日光成本就能吃掉 25% 到 75% 的收益。我一般会在回测里设cost_per_trade0.002然后看扣费后的净值曲线。如果扣费后收益归零说明模型在赚噪声的钱。def backtest_with_cost(preds, returns, threshold0.5, cost0.002): positions (preds threshold).astype(float) - (preds 1 - threshold).astype(float) positions positions[:-1] # 对齐下一期收益 gross_ret positions * returns[1:] turnover np.abs(np.diff(positions, prepend0)) net_ret gross_ret - turnover * cost cumulative (1 net_ret).cumprod() return cumulative, net_retthreshold0.5表示预测概率超过 0.5 做多低于 0.5 做空。cost0.002是单边成本估计。turnover计算仓位变化每次变化扣一次成本。这个回测逻辑假设按收盘价成交实际交易中还有冲击成本保守一点可以把cost调到 0.005。4.4 模型集成把多个弱信号拼成一个稳信号单个模型的预测噪声很大把 LSTM、TCN、Transformer 的预测概率做平均或者用验证集 AUC 做加权平均通常能提升稳定性。我一般训 3 到 5 个不同结构的模型取预测均值作为最终信号。如果某些模型在验证集上 AUC 低于 0.52直接剔除不参与集成。def ensemble_predict(models, loaders, weightsNone): all_preds [] for model, loader in zip(models, loaders): _, preds, _ evaluate(model, loader) all_preds.append(preds) all_preds np.array(all_preds) if weights is None: weights np.ones(len(models)) / len(models) return np.average(all_preds, axis0, weightsweights)weights可以按验证集 AUC 归一化后赋值。注意所有模型的测试集预测必须对齐同一个时间索引否则平均会错位。集成之后再用backtest_with_cost跑一遍对比单模型和集成的扣费收益。5. 避坑与排查五个让回测虚高的隐蔽问题5.1 特征里混入了未来信息现象验证集 AUC 超过 0.75回测曲线几乎不回撤。原因某个特征在计算时用了未来数据比如用全样本均值做标准化或者shift方向写反。解决逐列检查特征生成代码确保每个值只依赖当前及之前的数据。滚动统计量用rolling而不是expanding或全样本统计。5.2 标签和特征的时间对齐错位现象训练 loss 正常下降但测试集预测和实际收益相关性接近零。原因make_sequences里标签索引和特征窗口错了一位模型学到的是错位后的伪关系。解决打印前几个样本的日期和标签人工核对。特征窗口的最后一天应该是标签日期的前一天。5.3 验证集被反复用于调参现象验证集 AUC 很高测试集一塌糊涂。原因根据验证集结果反复调整超参数验证集实际上变成了训练集的一部分。解决留一个最终的测试集只在最后评估时用一次。调参用验证集但不要根据测试集结果回头改模型。5.4 样本量太少导致过拟合现象训练集准确率 90%验证集 50%。原因日频数据只有几百到几千条模型参数量远大于样本量。解决减少网络层数和隐藏维度加 dropout 和权重衰减或者改用更简单的模型如逻辑回归加手工特征做基线。5.5 回测没有考虑停牌和涨跌停现象回测里某些交易日收益异常高实盘无法成交。原因A 股有涨跌停和停牌回测假设按收盘价成交但涨停时买不进、跌停时卖不出。解决在回测里加过滤条件涨停日不建仓跌停日不平仓停牌日跳过。这个细节对短线策略影响很大。6. 一个可复现的最小训练脚本与参数速查把前面的模块串起来一个最小可跑的训练脚本大概长这样。数据用yfinance或本地 CSV 都行这里假设你已经有了一个包含 OHLCV 的 DataFrame。import torch from torch.utils.data import DataLoader, TensorDataset def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for x, y in loader: x, y x.to(device), y.float().to(device) optimizer.zero_grad() logits model(x).squeeze() loss criterion(logits, y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(loader) # 假设 features 和 labels 已经构造好 # X, y make_sequences(features.values, labels.values, seq_len60) # dataset TensorDataset(torch.FloatTensor(X), torch.FloatTensor(y)) # loader DataLoader(dataset, batch_size64, shuffleFalse) # 时间序列不能 shuffle # model LSTMPredictor(input_dimX.shape[-1], hidden_dim64, dropout0.3) # optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-5) # criterion torch.nn.BCEWithLogitsLoss()注意shuffleFalse时间序列训练不能打乱顺序否则 batch 内的样本会跨期BN 层和梯度都会出问题。clip_grad_norm_设 1.0 是防止 LSTM 梯度爆炸的常规操作。weight_decay1e-5是很轻的正则数据量小可以加到 1e-4。参数速查表参数日频常用值分钟频常用值说明seq_len6030回看窗口越长样本越少hidden_dim64-12832-64LSTM 隐藏层维度dropout0.2-0.50.1-0.3防止过拟合batch_size32-6464-128时间序列不 shufflelr1e-31e-3Adam 默认震荡时降 10 倍horizon510预测未来几个周期cost_per_trade0.0020.001单边交易成本估计最后说一个我自己的习惯每次跑完回测先不看收益曲线先看扣费后的净值曲线和换手率。如果换手率超过 50% 而扣费后收益归零这个模型就不值得继续调。股票预测这件事少亏比多赚重要活得久比跑得快重要。希望帮到你。本文还有配套的精品资源点击获取