拓冰建站拓冰建站
首页 / 资讯中心 / 正文

通用股票预测模型实战:深度学习网络构建与滚动回测

简介这是一份基于深度学习神经网络的通用股票预测模型Python源码包面向金融科技、人工智能方向的在校学生与开发者可用于个股价格走势预测研究及毕业设计。压缩包内共33个文件含13个Python脚本、13张训练与预测可视化图片、3个Markdown说明文档、1个依赖源码包及许可证文件等整体约5MB。代码模块覆盖数据采集、数据预处理、模型训练与预测评估支持通过行情数据下载或使用自定义数据源集成了LSTM、RNN、BERT、Transformer等常见深度学习网络并附有训练损失、预测效果以及MAPE、MSE等评估指标的图像便于横向对比不同模型的表现。项目目录结构清晰脚本与图表分类存放可快速复现完整流程并在此基础上二次开发从而深入理解深度学习方法在金融时序数据中的应用。目前已有104人学习/下载适合课程设计、毕业设计或入门进阶参考。1. 通用股票预测模型为什么先落在深度学习神经网络上股票预测最常被泼的冷水是“市场接近随机游走深度学习凭什么找到规律”但实际做过项目的人会告诉你问题不在深度学习本身而在数据构造和验证方式。基于深度学习神经网络的通用股票预测模型要解决的并不是“精确预知明天涨多少”而是从高噪声的量价序列里稳定提取上涨形态、波动收敛、量能配合这类局部模式再用时序结构判断当前行情处于什么阶段。对已经会写 Python 的工程师来说最有价值的不是把网络层数堆得更高而是把“通用”二字落实成可执行的工程方案特征怎么算、标签怎么设、训练集怎么切、模型怎么验证。下面按一个可运行的 Python 源码项目顺序展开数据处理、模型定义、训练配置、回测评估再到换股票前必须做的检查。2. 数据与标签构造先定义神经网络要学什么、不学什么2.1 数据源与复权处理后复权数据才是回测的正确口径第一步最容易出错的地方不在模型而在拿到的是哪种复权价格。常见做法是使用 akshare、tushare 这类开源数据接口拉取日线但参数里对复权类型的选择往往被忽略。前复权按最新股本把历史价格整体折算历史价格会随着时间点变化而变化同一个日期在不同日期下载数值完全不同后复权以首日为基准历史价格固定不变。对模型训练来说固定不变的价格序列意味着训练阶段下载的数据换一台机器重新下载后仍然一致特征分布不会因为除权除息而整体平移。import akshare as ak import pandas as pd def load_daily(symbol: str, start: str, end: str) - pd.DataFrame: # adjusthfq 指定后复权避免因未来分红送转而回改历史价格 raw ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart, end_dateend, adjusthfq, ) raw.columns [date, open, close, high, low, volume, amount, amplitude, pct_change, change, turnover] return raw[[date, open, high, low, close, volume]].copy()参数说明start、end使用20200101这样的字符串adjust可以直接写死成hfq不要在配置文件里留切换开关否则训练脚本某次跑前复权、某次跑后复权两次结果根本没有可比性。代码里把原始列名一次性重命名是为了后面所有特征函数都只认这一套字段名。加载函数最后还应打印最新行情日期判断数据源是否更新到位避免在旧数据上训练完还浑然不觉。提示如果数据源不支持后复权退一步的做法是用不复权数据但需要剔除除权除息日前后若干样本防止跳空缺口被误当成真实趋势。2.2 特征窗口与预测标签三分类比回归更贴近交易决策原始量价数据不能直接送入网络。我给通用模型设定的输入是一个形状为[seq_len, feature_dim]的二维窗口seq_len取 20 到 60feature_dim通常控制在 8 到 15。窗口内每一行是一个交易日的特征例如当日收益率、日内振幅、量比、收盘价相对 20 日均线的偏离度。标签我一般不用“预测未来第 N 日收盘价”而是做三分类未来horizon日收益率大于 0.5% 是上涨小于负 0.5% 是下跌中间是持平。分类目标把回归目标里那种无休止的噪声消化掉一部分梯度信号更集中也更接近实际交易中“买、卖、不动”的决策逻辑。import numpy as np import pandas as pd def build_features(df: pd.DataFrame) - pd.DataFrame: df df.sort_values(date).reset_index(dropTrue) df[ret] df[close].pct_change() # 当日收益率 df[range_pct] (df[high] - df[low]) / df[close] # 日内振幅 df[vol_ma5] df[volume].rolling(5).mean() df[vol_ratio] df[volume] / df[vol_ma5] # 放量缩量比例 df[ma20_bias] df[close] / df[close].rolling(20).mean() - 1.0 df df.replace([np.inf, -np.inf], np.nan) return df def build_label(df: pd.DataFrame, horizon: int 5, threshold: float 0.005) - pd.Series: future_ret df[close].shift(-horizon) / df[close] - 1.0 label np.where(future_ret threshold, 2, np.where(future_ret -threshold, 0, 1)) return pd.Series(label, indexdf.index).fillna(1)build_features里的vol_ratio表示当日成交量相对 5 日均量的倍数刻画的是一根 K 线背后有没有增量资金ma20_bias表示价格相对中期成本线的偏离帮助网络判断当前位置是在趋势中段还是已经超买。build_label中的threshold0.005对应 0.5%这是针对日线数据比较稳的阈值。序列最后horizon行无法计算未来收益fillna(1)先把它们填成持平后续构建训练集时直接丢弃不使用dropna()是为了保持索引对齐。2.3 数据集划分股票数据不能随机打散我见过不少项目把train_test_split的shuffle参数保持默认直接用在日线样本上结果验证集指标高得离谱实盘一跑就崩。原因是股票样本之间有先后依赖第 t 天的特征窗口包含第 t-1 天的信息随机打散等于把第 t-1 天的样本放进测试集、第 t 天的样本放进训练集模型在训练时已经间接接触过测试阶段的价格信息。通用模型的划分必须按日期切分前 70% 训练中间 15% 验证最后 15% 测试。验证集参与调参和早停测试集只能在全部流程定稿后跑一次跑完就不能回头再调。划分方式是否推荐原因随机打散划分不可用未来信息泄漏指标虚高简单按时间切前 70% 训练后 30% 测试不推荐单用缺少验证集调参会污染测试结论训练/验证/测试按时间依次划分推荐验证集承担调参测试集作最终盲测walk-forward 滚动窗口推荐且更严格模拟多次重训成本更高def split_by_time(df: pd.DataFrame, train_ratio0.7, val_ratio0.15): n len(df) train_end int(n * train_ratio) val_end int(n * (train_ratio val_ratio)) train df.iloc[:train_end] val df.iloc[train_end:val_end] test df.iloc[val_end:] return (train.dropna(), val.dropna(), test.dropna())train_ratio0.7、val_ratio0.15测试集自动占剩余 15%。这里有一个容易踩的细节滚动均值和标签都在完整序列上先计算再按位置切分而不是切分后每个子集单独算。rolling(mean)只看过去数据不会引入未来信息标签shift(-horizon)虽然用到未来价格但它只是预测目标不是模型输入。在完整序列上先算、后切分既能保证训练集末尾的样本不缺滚动特征又能保证标签口径一致。3. 网络结构选型LSTM 抓时序CNN 抓局部形态注意力补权重3.1 为什么通用模型很少只用单一网络如果只用纯 CNN模型能提取局部量价形态但感受野经过池化压缩后覆盖不了 20 天以上的趋势依赖如果只用纯 LSTM模型能记住阶段趋势但输入序列中高频率的行情噪声会不断干扰门控结构训练也更容易震荡。通用股票预测模型里更稳的做法是把两者串联先用一维卷积核对量价序列做局部形态提取再把卷积输出按时间顺序送入 LSTM 记忆阶段趋势最后引入注意力层显式告诉模型哪些时间步对当前预测更重要。这个结构和行情数据的三个层次是呼应的单日 K 线形态、数日趋势走向、长时间窗口里的主次节奏。3.2 可运行的混合网络PyTorch 实现下面这个模型类可以直接用在训练脚本里输入形状为[batch_size, seq_len, feature_dim]输出三分类 logits。import torch import torch.nn as nn class StockHybridNet(nn.Module): def __init__(self, input_dim: int, seq_len: int, hidden_dim: int 128, num_layers: int 2, num_classes: int 3, dropout: float 0.3): super().__init__() self.conv nn.Sequential( nn.Conv1d(input_dim, 64, kernel_size5, padding2), nn.BatchNorm1d(64), nn.ReLU(), nn.Conv1d(64, 64, kernel_size3, padding1), nn.ReLU(), ) self.lstm nn.LSTM( 64, hidden_dim, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.attn nn.MultiheadAttention( hidden_dim, num_heads4, batch_firstTrue, ) self.fc nn.Sequential( nn.LayerNorm(hidden_dim), nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, num_classes), ) def forward(self, x): x x.transpose(1, 2) # [batch, feature_dim, seq_len] x self.conv(x) # [batch, 64, seq_len] x x.transpose(1, 2) # [batch, seq_len, 64] lstm_out, _ self.lstm(x) # [batch, seq_len, hidden_dim] attn_out, _ self.attn(lstm_out, lstm_out, lstm_out) last attn_out[:, -1, :] # 取最后一个时间步的加权输出 return self.fc(last)参数说明是重点。Conv1d的kernel_size5表示卷积核在时间维度上覆盖连续 5 个交易日输入维度是input_dim而不是 1因为我们要在多个量价维度上同时做模板匹配。padding2保证卷积后序列长度不变否则后续 LSTM 的时间对齐会乱。nn.MultiheadAttention的num_heads4把 hidden_dim 拆成 4 个子空间各自学习不同时间尺度的依赖。取attn_out[:, -1, :]而不是直接取 LSTM 最后一个时间步是因为注意力输出已经把全部时间步按权重融合过末尾向量携带的是整段序列的加权信息。结构感受能力训练稳定性适用场景单层 LSTM阶段趋势一般序列规律明显的品种CNN LSTM形态 趋势较好默认通用方案CNN LSTM Attention形态 趋势 关键日较好噪声大、信号稀疏的日线数据纯 Transformer全局依赖较差数据量不足时容易过拟合3.3 kernel_size、num_layers 与 hidden_dim 的取值边界卷积核尺寸对应“一个形态持续几天”。1 到 2 天的形态噪声太大7 天以上会让卷积核退化成捕捉月线级别走势和 LSTM 的功能高度重叠。kernel 取 5 是一个比较稳的起点刚好覆盖一个交易周。LSTM 层数在股票数据上很少需要超过 3我一般固定 2 层因为层数增加只会让梯度在时间维度上传递更困难而日线序列里的趋势记忆并不需要多层抽象。hidden_dim 取 64 到 256 之间训练数据量不到 10 万条时不要碰 512参数越多模型越容易陷入记忆训练集、忘记泛化的状态。验证时如果发现训练集准确率远高于验证集优先把 hidden_dim 减半、把 dropout 提高而不要先加数据增强或换更重的注意力结构。4. 训练配置与超参数调节把 loss 下降变成模型可用4.1 训练循环骨架与梯度裁剪训练循环本身没有太多神秘之处但有两个细节直接影响成败梯度裁剪和正确的评估时机。股票数据偶尔会出极端行情某一天收益率异常放大会让该 batch 的 loss 瞬间飙升反向传播得到的梯度范数随之爆炸后续 loss 直接变成 nan。只降低学习率解决不了这个问题必须对梯度做显式裁剪。def train_one_epoch(model, loader, optimizer, criterion, device, clip1.0): model.train() total_loss 0.0 total_correct 0 total 0 for x, y in loader: x, y x.to(device), y.to(device) optimizer.zero_grad() logits model(x) loss criterion(logits, y) loss.backward() # LSTM 沿时间步展开后梯度容易指数膨胀裁剪是必加保护 nn.utils.clip_grad_norm_(model.parameters(), max_normclip) optimizer.step() total_loss loss.item() * x.size(0) total_correct (logits.argmax(dim1) y).sum().item() total x.size(0) return total_loss / total, total_correct / totalclip1.0的含义是把全模型梯度范数限制在 1.0 以内。如果训练中 loss 频繁跳变优先把 clip 降到 0.5如果训练速度慢且 loss 下降平缓可以放宽到 2.0。criterion用nn.CrossEntropyLoss()它内部自带 softmax所以网络最后一层不需要手动加 softmax否则推理阶段要多做一次归一化处理。argmax(dim1)取预测类别对应三分类标签。4.2 超参数联动学习率、batch size、序列长度怎么配这些参数是耦合的。batch size 决定梯度估计的噪声水平噪声反过来决定需要多大的学习率。股票数据量小batch size 从 64 开始比较合适不要一上来就 256因为一个 epoch 迭代次数太少学习率下降多少很难观察。seq_len 决定模型每次往前看多长历史20 天相当于一个月交易窗口40 天相当于两个月训练集足够时可以让 seq_len 慢慢加长但加长后训练时间会同步增加。参数常见起始值调参信号learning_rate2e-4loss 不降就先降学习率batch_size64验证指标波动大就适量调大seq_len20训练集 loss 低、验证集 F1 低考虑加长hidden_dim128训练集准确率过高而验证集低先减半num_layers2增加层数后验证集无提升就退回我一般固定 seq_len20、hidden_dim128学习率从 2e-4 起训练 30 个 epoch。如果训练 loss 一开始不降大概率是学习率过大或过小先降到 5e-5 观察如果 loss 前期快速下降但验证指标很差这是过拟合的迹象问题不在学习率而在参数量和正则强度。4.3 早停、学习率调度与 checkpoint 保存股票训练集通常只有几万条样本epoch 太多一定过拟合。常见做法是用验证 loss 做早停配合学习率衰减保存最优模型。scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience4, ) best_loss float(inf) bad_epochs 0 max_epochs 200 for epoch in range(max_epochs): train_loss, train_acc train_one_epoch( model, train_loader, optimizer, criterion, device ) val_loss, val_acc evaluate(model, val_loader, criterion, device) scheduler.step(val_loss) # 依据验证 loss 调整学习率 if val_loss best_loss - 1e-4: best_loss val_loss bad_epochs 0 torch.save(model.state_dict(), stock_model_best.pth) else: bad_epochs 1 if bad_epochs 12: breakfactor0.5表示验证 loss 连续 4 个 epoch 没有创新低时学习率乘 0.5而不是粗暴乘 0.10.1 会让后续训练几乎无法继续收敛。早停的bad_epochs耐心值是 12若模型连续 12 个 epoch 没有比历史最佳低超过1e-4就终止训练。这里的best_loss - 1e-4不是可有可无如果直接写val_loss best_loss浮点噪声会让 checkpoint 在同一水平上反复被覆盖最后保存的版本未必比中间版本更好。evaluate函数内部要用torch.no_grad()包裹前向计算并切换model.eval()否则 Dropout 和 BatchNorm 在验证阶段的统计量会漂移。5. 滚动回测与评估验证“通用”而不是“记忆”5.1 用 walk-forward 滚动窗口模拟连续重训单次训练/测试划分只能证明模型在某个时间段有效。通用模型的验证目标是换时间段、换股票池后仍然稳定所以需要 walk-forward 滚动验证。做法是训练集取过去 500 个交易日验证集取随后 60 天测试集再取 60 天一段跑完后窗口整体后移 60 天重新训练、重新评估。整个过程相当于每次都用历史数据训练然后预测未来一个季度的行情再把窗口向后滚动。最终把所有测试段预测结果拼接起来得到一个贴近真实使用体验的评估结果。def walk_forward(df, train_days500, val_days60, test_days60): pred_list, label_list [], [] start 0 while start train_days val_days test_days len(df): train df.iloc[start: start train_days] val df.iloc[start train_days: start train_days val_days] test df.iloc[start train_days val_days: start train_days val_days test_days] model, scaler train_pipeline(train, val) test_x, test_y build_samples(test, scaler) preds model.predict(test_x) pred_list.append(preds) label_list.append(test_y) start test_days # 窗口后移一个测试段长度 return np.concatenate(pred_list), np.concatenate(label_list)这里steptest_days窗口之间不重叠运行速度快如果样本量少可以让step30做重叠滚动评估结果更平滑代价是同一段测试数据会被重复评估。代码里build_samples(test, scaler)的scaler只在训练集上fit测试集只transform。如果测试集重新计算均值和标准差等于把未来分布信息泄漏进训练过程这样的评估指标没有参考意义。5.2 三分类指标准确率不够F1 和上涨类 AUC 一起报类不平衡的三分类数据上准确率很容易骗人。假设上涨样本只占 30%模型把所有样本都预测成下跌或持平准确率也能到 70% 左右但毫无交易价值。评估时我必看三个数macro F1、上涨类 AUC、训练准确率与验证准确率的差距。macro F1 对三个类别一视同仁避免多数类主导指标上涨类 AUC 单独衡量模型把“会上涨”和“不会上涨”区分开的能力。from sklearn.metrics import f1_score, roc_auc_score def evaluate_metrics(true_labels, pred_labels, pred_proba): macro_f1 f1_score(true_labels, pred_labels, averagemacro) up_auc roc_auc_score( (true_labels 2).astype(int), # 上涨类作为正样本 pred_proba[:, 2], # 模型对上涨类的概率 ) return {macro_f1: round(macro_f1, 4), up_auc: round(up_auc, 4)}pred_proba必须是 softmax 归一化后的概率。训练时CrossEntropyLoss直接吃 logits不会保存 softmax 结果推理阶段需要显式调用torch.softmax(logits, dim-1)。up_auc大于 0.5 才有区分度0.52 到 0.55 属于实用区间超过 0.6 时要回头检查代码是否发生了数据泄漏。指标参考区间说明macro F10.30 以上三个类别预测的总体均衡度up_auc0.52 以上上涨类与非上涨类的区分能力训练验证准确率差8% 以内差距过大说明过拟合5.3 预测延迟一天排除未来函数的干扰回测里一个隐蔽错误是假设当天收盘就能用当天的预测结果下单实盘做不到。模型特征包含当日收盘数据时预测结果最早要到次日开盘才能执行。正确做法是预测结果整体向后平移一天第 T 日收盘后生成预测第 T1 日开盘按信号下单评估时与 T1 日至 T1horizon 日的实际走势对齐。文档里的行数最好也按这种思路设计把 date、close、pred 输出到同一张表里用shift(1)把预测列整体后移再和真实时序做比较。训练指标比实际可执行指标高出一截通常就是少了这一步对齐。6. 换股票、换周期前必须做的三个检查6.1 先看新样本的特征分布是否在训练区间内通用不代表无脑套用。把新股票的vol_ratio、range_pct直方图和训练集对比如果新股票日均振幅 3%训练集里大多是 1%卷积核学到的形态尺度就失真了。处理办法重新拟合归一化参数或者把训练池扩大到覆盖目标波动区间。换品种后先跑一周的滚动回测而不是直接上线是最保守的做法。6.2 用置信度过滤交易信号模型输出的三分类概率比 argmax 结果更有价值。上涨类概率超过阈值才触发买入信号低于阈值一律按空仓处理。阈值在验证集上画精确率-召回率曲线选取取精确率相对高且召回率没有骤降的点0.6 只是一个常见起点不是固定值。牺牲一部分信号覆盖率换取更低的假阳性实盘表现会更接近回测。6.3 监控概率均值漂移提前触发重训模型上线后通常不是突然失效而是预测分布缓慢漂移。每天记录推理结果中上涨类概率的均值如果连续 5 个交易日该均值相对训练期间均值偏移超过 0.1说明当前行情风格已经脱离模型认知范围应触发重新训练。为了实现这个监控模型封装时就要预留统一接口推理模块只输出 logits外部统一做 softmax这样监控、回测和交易系统共用同一套代码路径。换预测周期时也要注意把 horizon 从 5 改成 1卷积核 kernel_size 应相应缩小到 3否则短周期预测会遇到感受野过大的问题模型会把一个月的形态强加在一天级别的信号上。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门