☰
通用股票预测模型实战:从LSTM到Transformer的Python量化避坑指南
2026/9/28 5:37:31 网站建设 项目流程

简介:这份资源是一套基于深度学习神经网络的通用股票预测模型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(index=df.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()

这里horizon=5表示预测未来一周的累计收益方向,threshold=0是最简单的设定。实际用的时候我会把threshold设成过去 60 天收益率标准差的 0.5 倍,这样标签分布更均衡。注意shift的方向,写反了就是拿未来数据预测过去,回测能跑出 90% 以上的准确率,实盘一上就废。

2.3 训练集/验证集/测试集的时间切分

随机切分是股票预测里最隐蔽的坑。用train_test_split打乱顺序,模型会学到跨期的模式,回测虚高。正确做法是按时间顺序切:前 70% 训练,中间 15% 验证调参,最后 15% 测试。如果要做滚动回测,就用 expanding window 或 sliding window,每轮用历史数据训练、下一段数据测试。

def time_split(features, labels, train_ratio=0.7, val_ratio=0.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_len=60对应约三个月的交易日。如果你做的是分钟级预测,这个值要降到 30 甚至 10。注意labels[i]对应的是第 i 个时间点的标签,而输入是i-seq_len到i-1的特征,这样才没有用到未来信息。

3. 网络结构选型:LSTM、TCN 还是 Transformer

3.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_dim=64, num_layers=1, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout 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_dim=64是起点,数据量大可以加到 128 或 256。num_layers=1先跑通,再加到 2 层并配合dropout。注意out[:, -1, :]取的是最后一个时间步的隐藏状态,如果你用batch_first=True,这个维度是对的。训练时用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, dropout=0.2): super().__init__() padding = (kernel_size - 1) * dilation self.conv = nn.Conv1d(in_ch, out_ch, kernel_size, padding=padding, dilation=dilation) 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_model=32, nhead=4, num_layers=1, dropout=0.3): super().__init__() self.input_proj = nn.Linear(input_dim, d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=d_model * 2, dropout=dropout, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_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_model=32是最小可用配置,nhead=4要求d_model能被整除。dim_feedforward设成d_model*2而不是默认的 4 倍,也是为了控制参数量。训练时用AdamW加weight_decay=1e-4,早停 patience 设 5 到 10 个 epoch。

3.4 多标的通用模型:共享权重还是独立建模

“通用”这个词在工程上最实际的落地方式是:用同一套网络结构,对每个标的单独训练一套权重,但共享特征工程和训练流程。另一种做法是加一个标的 embedding,让模型学不同标的的差异。前者简单可控,后者适合标的数量多、单标的样本少的场景。我一般先跑独立建模,如果某些标的样本太少,再考虑用 embedding 做迁移。

class MultiAssetLSTM(nn.Module): def __init__(self, input_dim, num_assets, hidden_dim=64, emb_dim=8): super().__init__() self.asset_emb = nn.Embedding(num_assets, emb_dim) self.lstm = nn.LSTM(input_dim + emb_dim, hidden_dim, batch_first=True) 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_dim=8是经验值,标的数量超过 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, device='cpu'): 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_splits=5, seq_len=60): 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 results

n_splits=5表示做 5 轮滚动,每轮训练集逐步扩大。注意测试集的序列构造要从train_end - seq_len开始取,保证第一个测试样本的输入窗口完整。这个逻辑写错的话,测试集第一个样本的输入会缺数据,预测结果不可靠。

4.3 交易成本与滑点:回测里最容易被忽略的减法

很多回测曲线漂亮得不像话,一加上交易成本就原形毕露。A 股单边手续费加印花税大约千分之一到千分之三,滑点按千分之一到千分之五估算。如果模型每天换仓,一年 250 个交易日,光成本就能吃掉 25% 到 75% 的收益。我一般会在回测里设cost_per_trade=0.002,然后看扣费后的净值曲线。如果扣费后收益归零,说明模型在赚噪声的钱。

def backtest_with_cost(preds, returns, threshold=0.5, cost=0.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, prepend=0)) net_ret = gross_ret - turnover * cost cumulative = (1 + net_ret).cumprod() return cumulative, net_ret

threshold=0.5表示预测概率超过 0.5 做多,低于 0.5 做空。cost=0.002是单边成本估计。turnover计算仓位变化,每次变化扣一次成本。这个回测逻辑假设按收盘价成交,实际交易中还有冲击成本,保守一点可以把cost调到 0.005。

4.4 模型集成:把多个弱信号拼成一个稳信号

单个模型的预测噪声很大,把 LSTM、TCN、Transformer 的预测概率做平均,或者用验证集 AUC 做加权平均,通常能提升稳定性。我一般训 3 到 5 个不同结构的模型,取预测均值作为最终信号。如果某些模型在验证集上 AUC 低于 0.52,直接剔除,不参与集成。

def ensemble_predict(models, loaders, weights=None): 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, axis=0, weights=weights)

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_norm=1.0) optimizer.step() total_loss += loss.item() return total_loss / len(loader) # 假设 features 和 labels 已经构造好 # X, y = make_sequences(features.values, labels.values, seq_len=60) # dataset = TensorDataset(torch.FloatTensor(X), torch.FloatTensor(y)) # loader = DataLoader(dataset, batch_size=64, shuffle=False) # 时间序列不能 shuffle # model = LSTMPredictor(input_dim=X.shape[-1], hidden_dim=64, dropout=0.3) # optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) # criterion = torch.nn.BCEWithLogitsLoss()

注意shuffle=False,时间序列训练不能打乱顺序,否则 batch 内的样本会跨期,BN 层和梯度都会出问题。clip_grad_norm_设 1.0 是防止 LSTM 梯度爆炸的常规操作。weight_decay=1e-5是很轻的正则,数据量小可以加到 1e-4。

参数速查表:

参数日频常用值分钟频常用值说明
seq_len6030回看窗口,越长样本越少
hidden_dim64-12832-64LSTM 隐藏层维度
dropout0.2-0.50.1-0.3防止过拟合
batch_size32-6464-128时间序列不 shuffle
lr1e-31e-3Adam 默认,震荡时降 10 倍
horizon510预测未来几个周期
cost_per_trade0.0020.001单边交易成本估计

最后说一个我自己的习惯:每次跑完回测,先不看收益曲线,先看扣费后的净值曲线和换手率。如果换手率超过 50% 而扣费后收益归零,这个模型就不值得继续调。股票预测这件事,少亏比多赚重要,活得久比跑得快重要。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询