资讯动态

基于时空图神经网络STGNN的沪深300成分股多维度走势预测系统

发布时间:2026/10/27 3:34:57 来源:尧图企业网站定制
简介这份资源是面向金融量化研究者与深度学习开发者的沪深300成分股走势预测系统源码包基于时空图神经网络STGNN构建用于解决股票市场非平稳时间序列预测与量化交易策略开发问题。包内共109个文件以32个py源码、31个pyc编译文件、24个npy数据数组、4个pkl模型文件为主另含pth权重、h5模型、ipynb实验笔记、csv距离矩阵与docx说明文档压缩包约9.38MB结构完整便于复现。系统利用PyTorch实现STGNN对成分股历史价格进行时空特征提取捕捉价格变动的时序规律与市场间空间关联性并可为量化策略提供数据驱动决策支持。目前已有56人学习下载适合具备一定深度学习与金融时间序列基础、希望深入图神经网络实战的读者参考可据此理解模型搭建、数据处理与策略验证的完整链路。1. 从一张股票关系图说起STGNN 到底在预测什么沪深300成分股不是300个孤岛。银行股异动会顺着资金链传导到地产新能源龙头跳水会带崩整条产业链这种「一只票的走势被它邻居牵着走」的现象用传统 LSTM 逐只建模根本抓不住。基于时空图神经网络 STGNN 的沪深300成分股多维度走势预测系统核心就是把「时间维度的序列依赖」和「空间维度的股票关联」塞进同一个模型里联合训练。它解决的是单票时序模型看不见横截面联动的问题适合已经跑通过 LSTM 预测、想进一步引入图结构做量化交易策略开发的从业者。读完你能拿到一套可复现的建图、训练、回测路径以及参数怎么设、坑在哪。2. 把沪深300建成图节点、边和特征怎么定2.1 为什么必须用图而不是把300只票拼成一个大向量最朴素的做法是把300只股票的特征在时间步上拼成一个 300×F 的矩阵直接喂给 LSTM 或 Transformer。这条路我踩过翻车点很明确拼接后的向量维度固定一旦成分股调整、停牌、新股纳入整个输入结构就崩了而且模型学不到「谁和谁相关」只能隐式地靠注意力去猜。图结构的好处是节点集合可以动态增删边表达的是显式关系模型在消息传递时天然知道信息该往哪流。STGNN 的「时空」拆开看空间维度靠图卷积GCN、GAT 或 GraphSAGE在节点间聚合邻居信息时间维度靠 GRU、TCN 或时间卷积在序列上滑动。两者交替堆叠就是 STGNN 的典型骨架。常见做法是「时间层 → 图层 → 时间层」的 sandwich 结构先抽局部时序再做跨股票聚合再抽一次时序。2.2 三种建图方式行业、相关性、资金流节点就是沪深300的成分股难点全在边。我一般按下面三种方式建图实际项目里会融合建图方式边定义优点缺点行业图同申万一级行业连边稳定、可解释太粗行业内分化大相关性图滚动窗口收益率 Pearson 相关 阈值连边数据驱动、动态阈值敏感、易受极端行情污染资金流/供应链图北向持仓、龙虎榜共现、上下游关系捕捉真实传导数据获取成本高相关性图是最容易落地的。用过去60个交易日的日收益率算相关矩阵取绝对值 Top-K 作为邻居避免阈值法在震荡市里连出一堆噪声边。K 一般取 510太大图会稠密GCN 过平滑严重。import numpy as np import pandas as pd def build_corr_graph(returns: pd.DataFrame, topk: int 8, window: int 60): returns: index日期, columns股票代码, values日收益率 返回邻接矩阵 A (N x N), 对称, 无自环 recent returns.tail(window) corr recent.corr().abs() # 取绝对值, 负相关也是强关联 np.fill_diagonal(corr.values, 0) # 去掉自环 N corr.shape[0] A np.zeros((N, N)) for i in range(N): # 每只票只保留相关性最高的 topk 个邻居 idx np.argsort(corr.values[i])[-topk:] A[i, idx] corr.values[i, idx] A np.maximum(A, A.T) # 对称化, 保证无向图 return A逻辑说明先算滚动窗口相关矩阵取绝对值是因为负相关比如多空对冲的两只票同样是强关联信号。np.fill_diagonal置零去掉自环自环信息由 GCN 的权重矩阵自己学。Top-K 而非阈值是为了保证每只票的度相对均匀避免出现孤立节点导致梯度传不进去。最后对称化因为资金传导是双向的。参数说明window取 60 是经验值太短相关矩阵噪声大太长会掩盖近期结构变化topk取 8 左右沪深300 里每只票平均 8 个强关联邻居已经能覆盖主要传导链。如果做高频window 要缩到 20 以内。2.3 节点特征多维度到底指哪些维度标题里的「多维度」不是虚词节点特征至少覆盖四类价量开高低收、成交量、换手率、技术指标MACD、RSI、布林带位置、资金面北向净流入、大单净额、情绪面新闻情感分、股吧热度。每类做 z-score 标准化注意是按时间截面标准化而不是按整段历史否则会引入未来信息。def normalize_cross_section(features: np.ndarray): features: (T, N, F) 时间 x 股票 x 特征 按每个时间截面对 N 只股票做标准化, 避免未来函数 mean features.mean(axis1, keepdimsTrue) std features.std(axis1, keepdimsTrue) 1e-8 return (features - mean) / std这里有个血泪经验很多人图省事用sklearn.StandardScaler对整段数据 fit结果回测收益虚高实盘一上就亏。截面标准化才是对的因为它模拟的是「当天你只知道当天所有票的相对强弱」。3. STGNN 模型结构时间层和图层怎么交替堆3.1 一个能跑通的最小 STGNN 骨架不追求 SOTA先要能复现。下面这个结构是「两层时间卷积 一层图卷积 输出头」输入 (T, N, F)输出每只票的涨跌概率或收益率。import torch import torch.nn as nn import torch.nn.functional as F class STGNN(nn.Module): def __init__(self, n_feat, hidden64, n_nodes300, dropout0.3): super().__init__() # 时间层: 沿时间维做一维卷积, 抽局部时序模式 self.tcn1 nn.Conv1d(n_feat, hidden, kernel_size3, padding1) self.tcn2 nn.Conv1d(hidden, hidden, kernel_size3, padding1) # 图层: 用可学习的邻接权重做消息传递 self.gcn_w nn.Linear(hidden, hidden, biasFalse) self.dropout nn.Dropout(dropout) # 输出头: 每只票一个标量(预测收益或logit) self.head nn.Linear(hidden, 1) def forward(self, x, A): # x: (B, T, N, F) - 合并 B,N 做时间卷积 B, T, N, F_ x.shape x x.permute(0, 2, 3, 1).reshape(B * N, F_, T) x F.relu(self.tcn1(x)) x F.relu(self.tcn2(x)) x x.mean(dim-1).reshape(B, N, -1) # 时间维池化 - (B,N,H) # 图卷积: A_norm 是归一化后的邻接矩阵 (N,N) h self.gcn_w(x) h torch.einsum(nn,bnh-bnh, A, h) # 邻居聚合 h self.dropout(F.relu(h)) return self.head(h).squeeze(-1) # (B, N)逻辑说明时间卷积用Conv1d在时间轴上滑动padding1保证长度不变。把 B 和 N 合并是为了让同一套时间卷积权重作用在所有股票上参数共享避免过拟合。时间维用mean池化而不是取最后一个时间步是因为金融序列尾部噪声大平均更稳。图卷积这里用einsum做最朴素的邻居加权求和A需要提前做对称归一化D^-1/2 A D^-1/2否则度数大的节点数值爆炸。参数说明hidden取 64 是沪深300 数据量下的稳妥值超过 128 在小样本上必过拟合dropout0.3 是金融时序的常用起点震荡市可以加到 0.5kernel_size3对应 3 个交易日想抓更长周期就堆层或换膨胀卷积。3.2 邻接矩阵归一化不做这一步梯度会炸上面代码里A不能直接用原始相关矩阵。必须做对称归一化def normalize_adj(A: np.ndarray): A A np.eye(A.shape[0]) # 加自环, 保留自身信息 d A.sum(axis1) d_inv_sqrt np.power(d, -0.5) d_inv_sqrt[np.isinf(d_inv_sqrt)] 0 # 孤立节点保护 D np.diag(d_inv_sqrt) return D A D不加自环节点在聚合时会把自己的信息丢掉不做 D^-1/2 归一化高度数节点聚合后的数值量级会碾压低度数节点训练时 loss 直接 NaN。孤立节点停牌、新股的度数为 0np.power会出 inf必须置 0 保护这是最容易忽略的一行。3.3 训练目标回归收益还是分类涨跌两种都行但我更推荐多任务主任务回归未来 5 日收益率辅任务分类未来 5 日涨跌方向。回归给连续信号用于排序选股分类给概率用于阈值过滤。损失函数用 HuberLoss回归 BCEWithLogitsLoss分类加权loss huber(pred_ret, true_ret) 0.5 * bce(pred_logit, (true_ret 0).float())HuberLoss 对极端收益涨停、跌停比 MSE 鲁棒金融数据里肥尾是常态。权重 0.5 是让两个任务量级接近实际调参时看哪个任务的梯度范数大就压哪个。4. 训练与回测从数据切分到策略落地4.1 时序切分绝对不能随机 shuffle金融时序最致命的错误是随机划分训练测试集。必须按时间切前 70% 训练中间 15% 验证最后 15% 测试。而且训练集内部做样本时标签窗口不能跨越切分点否则未来信息泄漏。def time_split(dates, train0.7, val0.15): n len(dates) i1, i2 int(n * train), int(n * (train val)) return dates[:i1], dates[i1:i2], dates[i2:]标签用未来 5 日收益意味着训练集最后一个样本的标签用到了切分点之后的数据。稳妥做法是在切分点前留 5 个交易日的 gap这段数据只做特征不做标签。4.2 回测别只看 IC要看扣费后的净值模型指标看 IC信息系数和 RankIC但真正决定策略生死的是扣费净值。沪深300 成分股流动性好双边成本按万分之三算加上印花税千分之一卖出一次完整交易成本约 0.13%。如果模型预测的 5 日收益均值只有 0.2%扣费后基本白干。def backtest(pred, true_ret, topk30, cost0.0013): # 每天选预测收益最高的 topk 只, 等权持有 ranks pred.argsort(axis1)[:, -topk:] daily np.zeros(pred.shape[0]) for t in range(pred.shape[0]): daily[t] true_ret[t, ranks[t]].mean() # 换手率近似: 与昨日持仓的重合度 turnover 1 - (np.isin(ranks[1:], ranks[:-1]).sum(axis1) / topk) daily[1:] - turnover * cost return np.cumprod(1 daily)逻辑说明每天按预测收益排序取 Top-K 等权这是最朴素的截面选股。换手率用相邻两日持仓重合度近似np.isin逐行比较。成本只在换手部分扣因为没换的仓位不产生交易费用。参数说明topk30是沪深300 里兼顾分散和收益的常用值太小波动大太大趋近指数cost0.0013是 A 股双边成本的保守估计做高频要往上调。4.3 多维度特征怎么融合进节点前面说的四类特征不是简单 concat 就完事。价量和技术指标是高频的日频资金面和情绪面可能是低频或不规则的。我一般把低频特征做前向填充到日频再统一进normalize_cross_section。如果某类特征缺失严重比如北向数据某些票没有用行业中位数填充而不是全局均值保留行业结构。5. 避坑与排查STGNN 在股票上最容易翻车的五件事5.1 现象验证集 IC 很高实盘一上就亏原因用了未来信息。最常见的是标准化用了全样本统计量或者标签窗口跨越了切分点。另一个隐蔽来源是相关性图用了未来窗口的收益率算边。解决所有统计量均值、方差、相关系数只在当前时间点之前的数据上计算。建图时严格用returns[:t]的滚动窗口绝不碰returns[t:]。写完代码后做一次「打乱未来数据」测试把测试集之后的收益随机打乱如果模型指标不变说明没泄漏。5.2 现象训练 loss 正常下降但预测值全部趋同原因图卷积过平滑。堆了太多图层或者邻接矩阵太稠密所有节点的表示被平均成同一个向量。解决图层不超过 2 层topk控制在 10 以内。加残差连接h h gcn(h)保留节点自身信息。监控节点表示的方差如果趋近 0 就是过平滑了。5.3 现象某些交易日预测全是 NaN原因停牌股票的收益率是 NaN传播到相关矩阵和特征里再进模型就污染全局。解决数据清洗阶段把停牌日的前向填充做掉收益率算不出来就置 0。建图时对停牌节点做 mask聚合时跳过。normalize_adj里的孤立节点保护那行就是干这个的。5.4 现象回测收益曲线漂亮但换手率高得离谱原因模型预测值日间波动大导致每天持仓剧烈变化。STGNN 对输入噪声敏感尤其是相关性图每天变邻居一换预测就跳。解决对邻接矩阵做时间平滑比如A_t 0.7 * A_t 0.3 * A_{t-1}让图结构缓慢演化。预测值也做移动平均或者加换手率惩罚项进损失函数。5.5 现象GPU 显存爆了batch 调到 1 还是 OOM原因沪深300 的图是 300×300如果 batch 里每个样本都存一份邻接矩阵显存是B × 300 × 300 × 4字节。B32 就是 11MB看着不大但中间激活值尤其是 einsum 的输出会放大几十倍。解决邻接矩阵在 batch 内共享同一时间截面所有样本用同一张图用torch.einsum时确保 A 不参与梯度。如果还爆把图卷积换成稀疏矩阵乘法torch.sparse.mm300 节点的稀疏图能省一半以上显存。6. 让 STGNN 真正跑赢基准的一个技巧动态图 行业约束前面所有内容能让你跑通但跑通不等于跑赢。我试过最有效的改进是动态图加行业约束相关性图每天变但纯数据驱动的边在极端行情里会连出跨行业的伪关联比如某天所有票一起跌相关矩阵全是高相关。加一层行业 mask只允许同行业或已知供应链关系的股票连边噪声边能砍掉一大半。def masked_graph(A, industry_mask): industry_mask: (N, N) bool, 同行业或已知关联为 True 只保留 mask 内的边, 其余置零后重新归一化 A A * industry_mask return normalize_adj(A)industry_mask从申万行业分类构建同行业为 True再手动加几条已知的上下游关系比如锂矿和电池。这个 mask 是静态的和动态的相关性图相乘既保留数据驱动的动态性又用先验知识过滤噪声。验证这个改进是否有效别只看整体 IC要分行情看震荡市里行业约束的提升最明显单边牛市里反而可能拖累因为普涨时跨行业联动才是主导。我一般会做一个行情分层回测把测试期按沪深300 指数波动率分成高波动和低波动两段分别看策略表现。如果高波动段提升、低波动段持平说明约束起作用了。还有个习惯每次改完模型先跑一遍「只用价量特征 行业图」的 baseline再逐步加特征和动态边。任何一步如果 IC 没提升超过 0.005我就回退因为金融数据里这点提升很可能只是噪声。这个规矩帮我省了无数次白干。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑