简介图神经网络GNN作为处理关系数据的强大工具其核心原理在于通过消息传递和聚合机制学习图中节点与边的复杂依赖关系。这种能力使其在社交网络、推荐系统等场景中展现出巨大价值。时空图神经网络STGNN进一步融合了时间序列建模能够同时捕捉空间关联与时间演化模式为交通预测、流行病传播等动态系统分析提供了统一框架。在金融科技领域尤其是量化交易中传统时序模型往往忽略资产间的关联性。STGNN通过将股票市场构建为动态关系图利用图注意力网络GAT自适应学习节点重要性并结合时间卷积网络TCN提取时序特征从而实现对股票间复杂相互作用与自身历史走势的联合建模为多维度走势预测和策略生成提供了新的技术路径。1. 项目概述当图神经网络遇见金融时间序列在量化交易这个领域预测股票走势一直是个充满诱惑又极具挑战的“圣杯”。传统的时序模型比如ARIMA、LSTM大多把每只股票看作一个孤立的序列来处理。但市场显然不是这样运作的股票之间存在着千丝万缕的联系同行业的公司会受相同政策影响供应链上下游的业绩会相互传导甚至市场情绪也会在板块间快速蔓延。这种复杂的、动态的关联性正是传统模型难以捕捉的“暗物质”。最近几年图神经网络GNN的兴起为我们打开了一扇新的大门。它天生擅长处理关系数据。而时空图神经网络STGNN更是将GNN与时间序列预测能力结合专门用来建模那些在空间或关系和时间两个维度上都存在依赖的动态系统——比如交通流量、社交网络传播当然也包括股票市场。这个“基于时空图神经网络STGNN的沪深300成分股多维度走势预测系统”项目核心目标就是利用STGNN将沪深300指数这300只核心股票构建成一个动态的金融关系图不仅分析每只股票自身的历史走势时间特征更要建模它们之间随时变化的相互影响空间特征从而实现对未来走势更精准的、多维度的预测。这个系统适合谁呢如果你是对量化交易感兴趣的开发者、金融科技从业者或者是对深度学习前沿应用特别是图神经网络有实践需求的研究者那么这个从数据构建、模型设计到策略回测的完整项目将是一个绝佳的练手和深化理解的案例。它不仅仅是一个模型调用更涉及如何将现实的金融问题抽象为图结构数据、如何设计和训练一个复杂的时空模型以及最终如何将预测信号转化为可执行的交易逻辑。接下来我将拆解整个系统的构建思路、核心实现细节以及那些只有亲手做过才会知道的“坑”。2. 核心思路与架构设计2.1 为什么是STGNN——从孤立序列到动态关系图传统的金融时间序列预测无论是用经典的统计模型还是RNN/LSTM其基本范式是for stock in stock_list: predict(stock.history)。这种方式隐含了一个强假设各序列独立同分布。这显然与金融市场事实相悖。STGNN的核心思想是打破这个假设将预测任务形式化为predict(Graph_Sequence.history)。这里Graph_Sequence是一个时间切片上的图序列每个切片G_t包含了t时刻所有股票的节点特征如价格、成交量、技术指标和节点之间的边表示关联关系。时空依赖的建模空间依赖通过图卷积层来捕捉。比如使用切比雪夫多项式近似或注意力机制的图卷积可以让一个节点的信息聚合其邻居节点的信息。在股票图中这意味着某只银行股的预测会考虑到其关联的其他银行股、保险股甚至地产股的信息。时间依赖通过时序卷积层TCN或门控循环单元GRU来捕捉。这负责学习股票特征随时间演化的模式和传统时序模型类似但这里的输入是已经融合了空间信息的节点特征。STGNN如STGCN、ASTGCN等经典模型通常采用“时空块”的堆叠结构一个时空块 一个时间卷积层捕捉时序模式 一个图卷积层捕捉空间关联 残差连接。多个这样的块堆叠就能同时、分层地提取复杂的时空特征。2.2 系统整体架构设计我们的系统不是一个单一的模型文件而是一个完整的工程流水线。下图展示了核心的数据流与模块组成[数据源] (Tushare/AkShare) | v [数据预处理模块] | - 股票基础数据获取日K线 | - 基本面/价量特征工程 | - 动态关系图构建基于滚动相关系数、行业分类 | v [特征与图结构存储] (PyTorch Geometric Dataset) | v [STGNN模型核心] | - 输入历史T天的图序列 (Node_Features, Edge_Index, Edge_Weight) | - 处理时空卷积块堆叠 | - 输出未来N天的节点特征预测如收益率 | v [预测后处理与信号生成] | - 将预测的收益率归一化或转化为涨跌概率 | - 生成多维度信号个股方向、行业强弱、市场情绪 | v [量化策略回测引擎] | - 基于信号的模拟交易考虑手续费、滑点 | - 计算绩效指标夏普比率、最大回撤、胜率 | v [可视化与评估] | - 预测值与真实值对比曲线 | - 资产净值曲线 | - 归因分析这个架构的关键在于动态图构建和多维信号生成。图不是静态的我们可能每过一段时间如每月就根据最近一段时间的收益率相关性重新计算边的关系甚至边的权重。信号也不仅仅是“A股明天涨还是跌”而是可以输出所有300只成分股未来几天的收益率预测向量进而可以衍生出行业轮动、强弱对冲等多种策略。3. 核心实现细节拆解3.1 动态金融关系图的构建这是项目的第一步也是最体现金融先验知识的一步。我们不能随意连接股票节点。节点特征对于每只股票i在时间t我们构建一个特征向量。这通常包括价量特征归一化的收益率、对数收益率、波动率、换手率、成交量移动平均比等。技术指标RSI相对强弱指数、MACD异同移动平均线、布林带位置等。注意这些指标需要处理未来函数问题确保在t时刻计算时只用到t及之前的数据。基本面特征可选如果频率是日线可以加入市盈率PE、市净率PB的滚动分位数。频率越高基本面数据可用性越差。边与边权定义股票之间的关联关系。常用方法有滚动相关系数计算每两只股票在过去M个交易日收益率序列的皮尔逊相关系数。取绝对值|ρ|作为边权并设置一个阈值如0.3只保留相关性较强的边。这种方法构建的图是动态的能捕捉到市场结构性变化。行业分类直接根据申万一级或二级行业分类将同行业的股票连接起来边权可以设为1或者根据市值加权。混合方法以行业分类为基础图再用滚动相关系数对行业内和行业间的边进行权重微调。实操心得滚动窗口M的选择很重要。太短如20天噪声大图结构不稳定太长如250天则对市场变化不敏感。一个折中的做法是使用60或120个交易日。另外计算全市场股票的相关矩阵是一个O(N²)的操作对于300只股票每次计算约9万对关系需要做好向量化优化避免循环。3.2 STGNN模型选型与PyTorch实现在众多STGNN变体中我们选择一种结构清晰、易于实现的架构作为基础Graph Convolutional Recurrent Network (GCRN)的思路或者使用纯卷积的STGCN。这里以一种结合了图注意力网络GAT和时间卷积网络TCN的模块为例因为注意力机制能自适应地学习节点间的重要性更适合关系复杂的金融市场。import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import GATConv class SpatialAttentionLayer(nn.Module): 空间注意力图卷积层 def __init__(self, in_channels, out_channels, heads4): super().__init__() self.gat_conv GATConv(in_channels, out_channels, headsheads, concatTrue, dropout0.2) # 注意GATConv输出维度是 out_channels * heads self.fc nn.Linear(out_channels * heads, out_channels) # 可选的投影层 def forward(self, x, edge_index, edge_weightNone): # x: [num_nodes, seq_len, in_channels] # 我们暂时忽略时间维在时间卷积后应用或者将seq_len视为batch # 更常见的做法是先时间卷积再图卷积。这里假设输入已通过时间层 batch_size, seq_len, num_nodes, feat_dim x.shape # 一种处理方式将 (batch, seq, nodes, feat) 转为 (batch*nodes, seq, feat) 做时间卷积 # 然后转为 (batch*seq, nodes, feat) 做图卷积 # 这里展示单步图卷积 x_spatial x.reshape(-1, feat_dim) # [batch*seq*nodes, feat] edge_index_batch ... # 需要将edge_index扩展到批处理版本这是一个难点 x_out self.gat_conv(x_spatial, edge_index_batch) x_out self.fc(x_out) return x_out.view(batch_size, seq_len, num_nodes, -1) class TemporalConvLayer(nn.Module): 时间卷积层使用因果膨胀卷积 def __init__(self, in_channels, out_channels, kernel_size3, dilation1): super().__init__() self.conv nn.Conv2d(in_channels, out_channels, kernel_size(kernel_size, 1), # 在时间维卷积节点维独立 padding((kernel_size-1)*dilation, 0), dilation(dilation, 1)) self.residual nn.Conv2d(in_channels, out_channels, kernel_size1) if in_channels ! out_channels else nn.Identity() def forward(self, x): # x: [batch, feat, seq, nodes] residual self.residual(x) x F.relu(self.conv(x) residual) return x class STAttentionBlock(nn.Module): 时空注意力块先时间卷积再空间图注意力 def __init__(self, in_feat, spatial_feat, temporal_kernel3, heads4): super().__init__() self.temp_conv TemporalConvLayer(in_feat, spatial_feat, kernel_sizetemporal_kernel) self.spatial_attn SpatialAttentionLayer(spatial_feat, spatial_feat, headsheads) self.layer_norm nn.LayerNorm(spatial_feat) def forward(self, x, edge_index): # x input: [batch, nodes, seq, feat] - 调整为 [batch, feat, seq, nodes] 适应Conv2d x x.permute(0, 3, 2, 1) # [batch, feat, seq, nodes] x_temp self.temp_conv(x) # [batch, spatial_feat, seq, nodes] x_temp x_temp.permute(0, 3, 2, 1) # [batch, nodes, seq, spatial_feat] batch, nodes, seq, feat x_temp.shape # 为空间层准备数据这里简化处理将batch和seq合并 x_spatial_input x_temp.reshape(batch * seq, nodes, feat) # 扩展edge_index以匹配批处理这是关键且易错点 # 假设edge_index是单张图的需要为每个时间步复制 edge_index_batch [] for i in range(batch * seq): offset i * nodes edge_index_batch.append(edge_index offset) edge_index_batch torch.cat(edge_index_batch, dim1) x_spatial_out self.spatial_attn.gat_conv(x_spatial_input, edge_index_batch) x_spatial_out x_spatial_out.view(batch, seq, nodes, -1).permute(0, 2, 1, 3) # [batch, nodes, seq, feat] x_out self.layer_norm(x_spatial_out x_temp) # 残差连接 return x_out注意事项PyTorch Geometric (PyG) 的批处理图数据需要将多个小图打包成一个“大图”节点索引会偏移。上述代码中edge_index_batch的构建是核心难点。更规范的做法是使用PyG的DataLoader和Batch对象来自动处理。对于时间序列图我们需要构建一个“时空图”数据集每个样本包含一个时间窗口内所有时间步的节点特征和共享的图结构。3.3 多维度走势预测的输出头设计模型最终的预测目标是什么我们不是简单地预测明天涨跌而是预测未来K天例如5天每只股票的多维度特征。一个有效的设计是多任务学习。主任务回归预测未来K天的收益率或对数收益率。这是一个回归任务输出层使用线性层损失函数用平滑L1损失Huber Loss它对异常值不如MSE敏感。self.regression_head nn.Linear(hidden_dim, predict_steps) # 为每个节点预测未来K步的值辅助任务1分类预测未来第K天的涨跌方向二分类。这可以帮助模型聚焦于方向性判断。损失函数用带权重的BCEWithLogitsLoss处理涨跌不平衡。self.classification_head nn.Linear(hidden_dim, 1) # 输出一个logit辅助任务2排名预测未来K天收益率的横截面排名用Softmax近似。这直接服务于“选股”需求损失函数可以用ListNet之类的列表排序损失或者简单的交叉熵将收益率分位数离散化。self.ranking_head nn.Linear(hidden_dim, num_quantiles) # 输出属于哪个收益分位数的概率总损失函数是这三个损失的加权和Loss λ1 * L_reg λ2 * L_cls λ3 * L_rank。通过多任务学习模型能同时学习绝对值、方向和相对强弱预测更加鲁棒和全面。4. 数据处理与模型训练实战4.1 使用PyG构建时空图数据集这是将原始数据转化为模型可消化形式的关键一步。我们需要自定义一个PyG的Dataset。from torch_geometric.data import Dataset, Data import pandas as pd import numpy as np class StockGraphDataset(Dataset): def __init__(self, root, feature_df, edge_index_dict, window_size30, predict_steps5, step1): feature_df: DataFrame, 索引为时间列为多层索引 (stock_code, feature_name) edge_index_dict: dict, key为时间点或时间段value为对应的edge_index (2, num_edges) super().__init__(root) self.feature_df feature_df self.edge_index_dict edge_index_dict self.window_size window_size self.predict_steps predict_steps self.step step self.dates sorted(feature_df.index.unique()) self.stock_codes sorted(feature_df.columns.get_level_values(0).unique()) self.num_nodes len(self.stock_codes) def len(self): return (len(self.dates) - self.window_size - self.predict_steps) // self.step 1 def get(self, idx): # 计算时间窗口 start_idx idx * self.step end_idx start_idx self.window_size predict_start end_idx predict_end predict_start self.predict_steps hist_dates self.dates[start_idx:end_idx] pred_dates self.dates[predict_start:predict_end] # 获取节点特征 [window_size, num_nodes, num_features] node_feats [] for date in hist_dates: # 获取该日期所有股票的所有特征并reshape feat_matrix self.feature_df.loc[date].values.reshape(self.num_nodes, -1) # (num_nodes, feat_dim) node_feats.append(feat_matrix) node_feats np.stack(node_feats, axis0) # (window_size, num_nodes, feat_dim) node_feats torch.FloatTensor(node_feats) # 获取图结构使用窗口中间或末尾的图 graph_date hist_dates[-1] # 使用最近的时间点定义的图 edge_index self.edge_index_dict[graph_date] edge_index torch.LongTensor(edge_index) # 获取预测目标例如未来predict_steps天的收益率 target_feats [] for date in pred_dates: # 这里假设我们要预测‘return’这个特征 target_matrix self.feature_df.loc[date][(slice(None), return)].values.reshape(self.num_nodes, -1) target_feats.append(target_matrix) target np.stack(target_feats, axis0) # (predict_steps, num_nodes, 1) target torch.FloatTensor(target).squeeze(-1).permute(1,0) # 转为 (num_nodes, predict_steps) # 构建PyG Data对象 data Data(xnode_feats, edge_indexedge_index, ytarget) # 注意这里x是3维的PyG的标准Data.x是2维。我们需要在模型前向传播中处理。 # 更常见的做法是将时间维视为批处理维或者使用自定义的数据结构。 # 一种改进将x reshape为 (window_size*num_nodes, feat_dim)并构建一个包含所有时间步连接的大图。 # 但这会极大增加图的大小。另一种标准做法是使用PyG Temporal的库。 return data踩坑实录直接使用上述Data对象标准的PyG卷积层无法处理3维节点特征。有两个主流解决方案1) 使用专门为时空图设计的库如torch_geometric_temporal2) 自己实现模型在forward函数中手动处理时间维。我们上面的STAttentionBlock就是一种手动处理的方式。强烈建议初学者先从torch_geometric_temporal的示例开始它提供了DynamicGraphTemporalSignal等现成的数据结构。4.2 模型训练的超参数与技巧训练STGNN模型不同于训练CNN或RNN有其特殊性。超参数设置学习率与优化器使用AdamW优化器初始学习率设置在1e-3到1e-4之间。由于金融数据噪声大学习率不宜过高。配合余弦退火或ReduceLROnPlateau调度器。批处理大小受限于图数据的复杂性批处理大小Batch Size可能只能设置得很小如4或8。可以使用梯度累积来模拟大Batch。正则化Dropout在图卷积层和全连接层后广泛使用Dropout0.2-0.5防止过拟合。图结构Dropout (Edge Dropout)随机丢弃一部分边相当于对图结构做数据增强能提升模型鲁棒性。标签平滑在分类任务中对涨跌标签进行平滑如0.9和0.1避免模型过于自信。损失函数权重多任务损失中的权重λ1, λ2, λ3需要仔细调优。通常回归任务权重最高λ11.0分类和排名任务权重较低如0.3和0.2。可以在验证集上根据任务重要性手动调整。训练技巧滚动训练与验证金融数据分布随时间变化非独立同分布。绝不能随机打乱所有样本再划分训练/验证集。必须使用时间序列交叉验证或滚动窗口法。例如用2008-2015年数据训练2016年验证2017年测试然后滚动到2009-2016训练2017验证2018测试以此类推。早停策略监控验证集损失。因为很容易过拟合到历史噪声一旦验证损失连续多个epoch不下降立即停止训练。特征标准化必须在滚动窗口内进行标准化即用当前训练窗口的均值和标准差来标准化该窗口及对应的验证/测试窗口绝对不能用未来数据的信息。5. 从预测到策略信号生成与回测模型输出了未来N天300只股票的预测收益率矩阵这只是一个中间产品。如何将其转化为真金白银的交易策略是更具挑战性的一步。5.1 多维度信号生成个股Alpha信号最直接的方式。对每只股票如果预测未来K天累计收益率高于阈值如0则产生买入信号低于阈值则产生卖出或做空信号。可以设置一个置信度分数例如预测收益率的绝对值大小只交易高置信度的股票。行业轮动信号将300只股票按行业分组计算每个行业所有股票预测收益率的均值。每天或每周买入预测收益率最高的前两个行业的股票等权或市值加权卖出或调出排名最低的行业。这利用了STGNN捕捉行业联动效应的能力。多空对冲信号做多预测收益率排名前10%的股票组合同时做空排名后10%的股票组合。这种市场中性策略旨在剥离大盘涨跌的影响纯粹获取模型选股能力的收益。集成情绪信号计算全市场300只股票预测收益率的均值、方差、正负比例等统计量作为对整个市场未来情绪的预测。可以单独作为一个择时指标或者与其他宏观指标结合。5.2 回测系统搭建要点回测不是简单的“信号买卖”必须考虑现实约束。交易成本必须包含佣金如万分之三和印花税卖出时千分之一。对于A股这是硬性成本。滑点假设订单会对价格产生冲击。可以用固定比例如0.1%或根据股票的流动性和订单大小动态估算。交易频率与仓位管理根据预测频率日度决定调仓频率。每日调仓交易成本极高可以考虑每周或每两周调仓。使用等权重、市值加权或基于预测置信度的权重分配仓位。禁止未来信息回测时任何在时间t使用的数据必须是t时刻及之前已经公开的信息。包括特征计算、图结构构建、模型预测需要训练时间窗口早于预测窗口。绩效评估不要只看总收益率。关键指标包括年化收益率 / 最大回撤衡量收益风险比。一个回撤过大的策略实盘时很难坚持。夏普比率衡量经风险调整后的收益。大于1通常算不错。胜率盈利交易次数占总交易次数的比例。信息比率对于对冲策略衡量超额收益的稳定性。月度收益分布检查收益是否集中在某几个月是否稳定。核心避坑指南回测中的过拟合是量化策略的“癌症”。你可能会通过反复调整模型参数、特征、信号阈值在历史数据上得到一个夏普比率高达3的完美曲线。但这极大概率是过度优化在实盘中会迅速失效。对抗过拟合的方法1)样本外测试严格划分训练、验证、测试时间段测试集数据在模型开发过程中绝对不能使用。2)简化策略逻辑策略规则越复杂过拟合风险越高。3)多市场、多周期验证如果条件允许用美股、港股等其他市场数据或者用不同时间周期30分钟、周线测试策略的普适性。6. 常见问题、调试与优化实录在实际构建这个系统的过程中你会遇到各种各样的问题。下面是我踩过的一些坑和解决方案。6.1 模型训练不稳定或发散现象训练损失剧烈震荡或者变成NaN。排查检查输入数据是否有NaN或无穷值特征标准化是否在正确的窗口内进行使用torch.isnan(x).any()检查。检查图结构edge_index中的节点索引是否超出范围边权重是否有异常值可以尝试先使用一个简单的、全连接的图进行测试。梯度爆炸在模型中加入梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。降低学习率。损失函数问题如果预测目标收益率范围很大回归损失容易爆炸。尝试对目标值进行缩放如除以一个波动率或者使用Huber Loss。优化从非常简单的模型开始比如只有一层GCN一层LSTM确保它能正常训练并过拟合一个小数据集。然后逐步增加复杂度。6.2 预测结果没有区分度近乎随机现象模型预测的所有股票收益率都差不多或者与一个简单的均值回归策略无异。排查特征有效性你输入的特征真的和未来收益率相关吗做一下IC信息系数分析计算每个特征与下期收益率的横截面RankIC。如果IC值接近0说明特征无效。标签噪声股票收益率本身噪声极大。尝试预测未来波动率调整后的收益率收益率/波动率或者预测未来一段时间的排名而非绝对数值。模型容量不足或过拟合增加模型层数或隐藏层维度小心过拟合或者相反如果模型已经很大可能是过拟合到了噪声需要加强Dropout、权重衰减等正则化。图结构太弱如果基于相关系数构建的图过于稀疏或稠密信息传递效果都会变差。调整相关系数阈值或尝试加入先验知识如行业、供应链。优化引入图注意力机制如GATv2让模型自己学习节点间的重要性权重而不是依赖预设的相关系数。6.3 回测结果很好但实盘模拟差现象在历史回测中表现优异的策略在实盘模拟交易Paper Trading中效果大打折扣。排查除前述过拟合外数据延迟与存活者偏差回测时使用的股票列表是今天的沪深300成分股。但历史上指数成分股会调整。你必须使用历史真实的成分股列表在回测中动态调整持仓剔除被调出的股票。这被称为“存活者偏差”忽略它会严重高估收益。交易可行性你的策略是否交易了流动性很差的股票回测假设可以按收盘价买卖但小盘股在实盘中可能无法以理想价格成交。在回测中加入流动性过滤如日均成交额门槛。市场状态变化模型在牛市、震荡市、熊市下的表现可能天差地别。进行市场状态划分基于波动率、趋势指标并分别查看策略在不同状态下的表现。一个只在牛市中有效的策略风险很高。优化实施在线学习或模型滚动更新。不要用一个固定历史数据训练的模型一直用下去。定期如每季度用最新的数据重新训练或微调模型让模型适应市场风格的变化。构建这样一个系统是一个典型的“数据工程模型实验策略开发”的闭环。它没有一劳永逸的银弹需要不断地迭代、诊断和优化。最大的体会是对数据的理解和清洗其重要性往往超过模型本身的复杂度。一个建立在错误或存在未来信息的数据上的复杂模型其危害远大于一个简单的线性模型。因此在兴奋地搭建STGNN这个“豪华引擎”之前请务必花足够的时间打好“数据地基”。本文还有配套的精品资源点击获取