简介本资源是一套面向量化金融从业者、深度学习研究者及高校金融工程方向学生的实战型项目代码包聚焦于利用时空图神经网络STGNN解决沪深300成分股多维度走势预测这一典型非平稳金融时间序列建模难题。资源共109个文件涵盖32个核心Python训练/推理脚本、24个预处理后的.npy特征数据、4个.pkl模型参数、1个.pth模型权重、1个.ipynb可视化分析示例及1个.docx附赠说明文档完整复现从图构建基于行业/市值/相关性、时空特征提取到多步价格预测与策略信号生成的全流程压缩包仅9.38MB轻量易部署。目前已有54人学习下载代码基于PyTorch实现结构清晰、模块解耦含data_loader、stgnn_model、trainer、backtest等独立目录附带CSI300距离矩阵CSV及标准化处理逻辑可直接用于量化策略原型开发或学术研究复现。1. 为什么股票预测要从个体建模转向关系建模1.1 沪深300成分股天然是一张动态关联网络做股票预测的人最先接触的往往是单股的时间序列预测。LSTM、Transformer、TCN一个个单独建模把每只股票当成独立的样本输入过去N天的价格和成交量输出未来M天的走势。这套做法我用了很久直到一个很现实的问题反复出现模型在回测里表现不错一到实盘就频频失效尤其是市场风格切换的关口单股模型的预测精度会突然崩塌。后来我意识到问题的根源不在模型结构而在数据建模方式。沪深300成分股不是一个互相独立的股票集合而是一张动态变化的关联网络。银行股的异动会传导到地产锂矿价格会传导到电池厂商茅台的走势会影响整个白酒板块的估值锚。这种行业间的联动、龙头对板块的带动、资金在个股之间的轮动在单股时间序列里是观察不到的因为单股模型只能看到自身历史看不到别人家发生的事。这也是我转向时空图神经网络STGNN的最直接原因。STGNN把股票之间的关联关系建模成图结构节点是成分股边是股票间的相关性或行业归属关系然后在图上同时提取空间特征股票间相互影响和时间特征价格序列本身的变化规律最终输出多维度的走势预测。比起传统单股序列模型它多了一个关系视角而这恰恰是A股市场最不能忽略的维度。1.2 传统时序模型丢失的结构信息行业传导与资金联动用一个实际例子说明结构信息有多重要。2023年人工智能主题行情启动的时候光模块板块率先上涨随后带动云服务、数据要素、算力租赁等关联板块轮动。如果只看个股历史K线小市值算力股在启动前是典型的下跌趋势单股模型大概率给出看空信号。但从图结构角度看光模块龙头节点的上涨会通过边把热度传导到算力节点的特征表示里模型就能捕捉到这种跨股票的信息。这就是图神经网络和传统序列模型的本质区别传统模型假设样本是独立同分布的图模型显式刻画样本之间的依赖关系。行业归属是A股市场最容易获得、也最稳定的边关系。同属一个行业板块的股票往往共享相似的宏观驱动力、原材料成本和政策风险。另一种边关系是历史行情相关性过去60个交易日的收益率相关性可以反映资金层面的联动但缺点是市场风格切换后相关性结构会发生变化需要用滑动窗口持续重建。还有一类是自适应学习得到的隐式关系不依赖人工定义让模型自己从数据中学习边权重。这三种边关系我后来都试过。行业边的解释性最强行情相关边的预测效果最稳定自适应边在训练数据充足时上限最高但过拟合风险也最大。在实际项目中我们采用的是融合方案具体细节放在数据章节详细展开。1.3 GCN/GAT在图结构数据上的建模逻辑图神经网络的核心操作是消息传递。每个节点不仅用自己的特征做预测还会聚合邻居节点的特征更新自己的表示。这个过程可以类比成开会讨论每个参会者节点先陈述自己的看法自身特征然后听取其他人的观点邻居特征聚合综合之后形成新的判断节点表示更新。图卷积网络GCN做的是加权平均的邻居特征聚合权重由邻接矩阵决定一层GCN让每个节点看到一跳邻居的信息两层GCN就能看到两跳邻居。图注意力网络GAT更进一步给每条边学习一个注意力权重让模型自动决定开会时谁的发言更值得参考。具体到股票预测场景GCN的聚合方式天然契合行业传导的逻辑。某个股票在未来一段时间的走势受到自身基本面、技术面特征的影响同时也受到同行业股票、产业链上下游股票的带动。这种自身因素环境因素的叠加就是时空图神经网络要建模的完整信息。2. 把行情数据变成图节点、边与滑动窗口的构造细节2.1 节点特征工程基础特征与衍生特征做图神经网络和做普通深度学习有一个很大的不同输入不再是单调的二维矩阵而是带有拓扑结构的图数据。第一个要解决的就是节点特征怎么定义。以沪深300成分股为例节点就是这300只股票。每个节点在时间步t的特征我最终采用了三个维度的拼接基础行情特征过去20个交易日的日收益率、对数收益率、成交量的移动平均、换手率标准分。这些是预测走势的基本原料简单但不冗余。技术指标特征RSI相对强弱指标、MACD柱值、布林带位置占比。建议只保留3到5个经典指标堆太多指标会导致节点特征维度过高模型训练变慢还容易过拟合。横截面排名特征将每个指标在当日全部成分股中进行百分位排名。这一步非常关键因为沪深300成分股之间的股价绝对数值差异巨大直接用原始数值建模高价股的方差会主导损失函数。排名之后每个特征都变成0到1之间的横截面相对值模型学习的是这只股票目前在全体成分股中处于什么位置而不是它值多少钱。节点特征维度的最终设定是20个交易日的基本特征 5个技术指标 6个横截面排名特征 31维。输入到模型里的张量形状是[300, 31]一个时间步对应一个全量快照。2.2 边权重设计的三种方案行业、相关性与自适应图神经网络能不能发挥作用边权重的设计比节点特征更关键。我对比了三种构造方式实测结果差异很大。第一种行业归属构造稀疏邻接矩阵用申万一级行业分类作为边关系。同一行业的两只股票邻接矩阵对应位置置1否则置0。这个矩阵是静态的训练前构造一次整个训练过程不再变化。优点是稳定、可解释缺点是过于粗糙同属食品饮料的海天味业和贵州茅台业务关联度天差地别行业边却一视同仁。第二种动态相关性矩阵每20个交易日滚动计算一次两两股票之间的收益率Pearson相关系数然后做稀疏化处理只保留相关性大于0.6的边其余置0。这个方案能捕捉到资金层面的真实联动但存在一个问题高相关性的股票往往集中在同一个板块结果行业边和相关性边高度重叠信息增益有限。第三种自适应邻接矩阵不预设任何先验结构让模型自己学习节点之间的关系。常见做法是初始化一个可训练的节点嵌入矩阵用嵌入向量的内积作为边的权重。STGCN原论文里的自适应邻接矩阵就是这种思路公式为A_adp SoftMax(ReLU(E1 * E2^T))其中E1和E2是两个可学习的节点嵌入矩阵维度为[300, c]c是嵌入维数。训练过程中模型会自动发现哪些股票的关系对预测更有利。最终方案的取舍逻辑是这样的静态行业边提供稳定的先验结构动态相关边引入市场波动信息自适应边补充模型从数据中挖掘的隐式关联。三个矩阵做加权融合后作为图卷积的邻接矩阵。这个融合方案比单独使用任何一种都更稳在验证集上的RankIC提升了大约12%而额外代价只是多了一组可学习参数。2.3 滑动窗口切分与防泄漏划分图神经网络吃的是时间窗口数据。每个训练样本的构造方式是取连续T天的股价特征序列作为图上的动态输入对应未来H天的走势作为预测目标。我采用的默认配置是历史窗口60个交易日预测未来5个交易日T60, H5。输入张量形状为[T, N, F]即[60, 300, 31]。这里的N300是成分股数量F31是特征维度。数据集划分是最容易出问题的地方。普通机器学习项目随机划分训练集和测试集但金融时序数据绝不能随机划分后一天的数据信息会泄露到前一天的训练样本中。我的做法是严格按时间顺序划分2018年到2021年底作为训练集2022年全年作为验证集2023年作为测试集。这样模型在验证和测试阶段面对的是完全没见过的市场环境评估结果才有说服力。提示这里有一个很隐蔽的细节训练集内部仍然存在数据时效性问题。2020年的样本和2018年的样本之间隔着两年市场结构早已变化。所以实际训练时我们只取最近500个交易日的样本做滚动训练每周末用最新数据增量更新一次模型避免旧数据对当前市场的干扰。3. STGNN模型结构与PyTorch实现的核心逻辑3.1 时空块设计图卷积与时间卷积的交替常说的STGNN不是指某一个固定模型而是一类模型的统称。STGCN、DCRNN、ASTGCN、Graph WaveNet都属于这个范畴。我项目里最终选用的结构是STGCN的改进版本核心是时空卷积块。一个时空卷积块由两部分组成时间维卷积 空间维图卷积交替堆叠。时间维卷积在T维度上做1D卷积提取股票自身历史走势的时间模式。卷积核大小为3相当于每个时间步会聚合前后各一天的信息。空间维图卷积在N维度上做图卷积聚合邻居节点的信息。上一步的时间卷积已经在每股内部做了平滑这一步让每只股票的表示融入关联股票的信息。整体的前向传播可以写成# 伪代码交替堆叠的时空卷积块 def forward(x, adj, device): # x: [batch, T, N, F] # adj: [N, N] x temporal_conv1(x) # 时间维度卷积 [B, T, N, F_1] x graph_conv1(x, adj) # 空间维度图卷积 [B, T, N, F_2] x temporal_conv2(x) # 第二次时间卷积 [B, T, N, F_3] x graph_conv2(x, adj) # 第二次图卷积 return x两个时空卷积块堆叠后网络感受野能覆盖大约8到10个交易日的跨股票联动。实测这个深度在沪深300的预测任务上效果最好再加深模型参数量暴涨但RankIC的提升变得非常有限。3.2 从代码角度拆解维度变化很多人看STGNN论文能看懂公式但一到写代码就卡在维度变换上。这里用PyTorch代码完整展示一次前向传播的维度变化过程。import torch import torch.nn as nn import torch.nn.functional as F class TemporalConv(nn.Module): 时间维卷积提取每个节点自身的时间变化特征 def __init__(self, in_channels, out_channels, kernel_size3): super().__init__() self.conv nn.Conv2d( in_channels, out_channels, kernel_size(kernel_size, 1), padding(kernel_size // 2, 0), ) def forward(self, x): # x: [B, F, T, N]注意这里把特征维度放到了通道位置 return self.conv(x) class GraphConv(nn.Module): 空间维图卷积聚合邻居节点的信息 def __init__(self, in_features, out_features, adj): super().__init__() self.weight nn.Parameter(torch.FloatTensor(in_features, out_features)) self.adj adj # [N, N] nn.init.xavier_normal_(self.weight) def forward(self, x): # x: [B, F, T, N] B, F, T, N x.shape x x.permute(0, 2, 3, 1).contiguous() # [B, T, N, F] x torch.einsum(btnf,nm-btmf, x, self.adj) # 邻居聚合 x torch.matmul(x, self.weight) # 特征变换 x x.permute(0, 3, 1, 2).contiguous() # [B, F, T, N] return x class STBlock(nn.Module): 时空卷积块时间卷积 图卷积 残差 def __init__(self, in_channels, out_channels, adj): super().__init__() self.temporal TemporalConv(in_channels, out_channels) self.graph GraphConv(out_channels, out_channels, adj) self.relu nn.ReLU() self.residual ( nn.Conv2d(in_channels, out_channels, kernel_size1) if in_channels ! out_channels else nn.Identity() ) def forward(self, x): out self.relu(self.temporal(x)) out self.relu(self.graph(out)) return out self.residual(x)这段代码最需要理解的是维度排列。PyTorch的Conv2d默认输入格式是[B, C, H, W]这里把输入整理成[B, F, T, N]C位置放特征维度FH位置放时间维度TW位置放节点维度N。TemporalConv的时间卷积本质上是在H轴时间轴上做卷积GraphConv的图卷积通过einsum和邻接矩阵做矩阵乘法沿N轴聚合邻居信息。整个前向过程清晰可控这也是我从论文公式到可运行代码之间花时间最多的地方。3.3 训练超参与优化器选择模型结构确定之后训练环节有几个参数对结果影响很大我逐个说明。优化器AdamW初始学习率0.001weight_decay设为0.0001。AdamW相比Adam在权重衰减处理上更规范对图神经网络的训练稳定性更好。学习率调度CosineAnnealingLR最小学习率设为初始学习率的1/10。金融时序数据的损失曲面通常比较崎岖余弦退火能帮助模型跳过局部最小值收敛更平滑。Batch Size32。输入样本形状是[32, 60, 300, 31]在单张24GB显存的GPU上刚好可以训练。如果显存不够优先减少batch size而不是降低历史窗口长度因为窗口长度直接影响模型能看到的时序范围。Dropout0.3加在时空卷积块的输出和最后的全连接层之前。图神经网络比普通深度网络更容易过拟合因为每个节点的特征经过了多次邻居传播信息重复度高。4. 损失函数与评估指标用排序视角代替回归视角4.1 为什么单纯的均方误差不够用模型输出层用全连接网络将时空特征映射为预测值默认损失函数是MSE这本身没有错。但做量化选股的人会发现一个问题预测误差最小的模型选股收益不一定最好。原因在于MSE是回归视角目标是让预测值尽量接近真实值而实际选股策略核心是点击概率排序——我选出预测涨幅最高的前50只股票并不关心具体涨5%还是5.5%。只要排序正确涨幅略偏也能赚钱。如果一味追求MSE最小化模型会把大量参数用来精确拟合已经确定的趋势反而忽略了横截面上不同股票之间的相对强弱这恰恰是选股最看重的信息。4.2 用RankIC、TopK命中率和多头超额收益来评估我最终用四套指标评估模型分别从不同角度判断它是否真的能用于实战。指标计算方式预期范围说明IC预测值与真实收益率的Pearson相关系数0.03以上可接受0.05以上较好整体预测能力RankIC预测值与真实收益率的Spearman秩相关系数0.04以上可接受排序能力比IC更稳健TopK命中率预测TopK股票中上涨(跑赢基准)的比例55%以上选股胜率多空收益差做多TopK做空BottomK的月度平均超额收益月度2%以上策略实际获利能力RankIC是评估质量最重要的指标。它衡量的是预测值和真实值之间排序上的一致性不关心绝对数值。沪深300成分股的短周期走势噪声很大绝对涨跌幅的预测非常难但排序预测要容易得多。模型在这套评估体系下的测试结果我印象最深的一组数据是训练集RankIC约0.071验证集RankIC约0.058测试集RankIC约0.046。测试集的RankIC明显低于训练集说明存在一定程度的过拟合但降幅在可接受范围内预测信号仍然有效。4.3 损失函数改造MSE与排序损失的结合为了兼顾数值精度和排序能力我在最终版本里把损失函数改成了组合形式# 组合损失MSE 排序损失 def combined_loss(pred, target, alpha0.7): mse_loss F.mse_loss(pred, target) # 排序损失对预测值和真实值分别排序计算排序差异 pred_rank pred.argsort(dim1).argsort(dim1).float() target_rank target.argsort(dim1).argsort(dim1).float() rank_loss F.mse_loss(pred_rank / pred.shape[1], target_rank / target.shape[1]) return alpha * mse_loss (1 - alpha) * rank_lossalpha设为0.7意味着主力还是MSE但排序损失会持续引导模型关注横截面相对强弱。这个改造让测试集RankIC从0.046提升到了0.052而MSE几乎没有变差。损失函数的组合方式是一个低成本的优化方向比盲目调模型结构有效得多。5. 实测中踩过的坑数据泄漏、动量陷阱与过拟合5.1 最常见的坑用全样本做归一化前期我做过一版模型回测结果相当漂亮测试集RankIC高达0.09。后来仔细一查代码发现特征归一化用的是全样本均值和方差也就是把未来数据的信息偷偷用进去了。这属于典型的数据泄漏做金融深度学习的项目时极容易犯而且极难发现。正常做法是用训练集的统计量归一化训练集、验证集和测试集# 训练集上计算均值和方差 scaler StandardScaler() train_scaled scaler.fit_transform(train_features) # 验证集和测试集用同一组统计量变换 val_scaled scaler.transform(val_features) test_scaled scaler.transform(test_features)这个顺序不能颠倒否则模型看到的历史分布实际上包含了未来信息。市场均值和方差每年都在变化指数上涨年份均值偏高下跌年份均值偏低。一旦用全样本统计量模型很容易产生一种错觉它似乎总能提前知道未来市场的涨跌但这种能力在实盘中完全消失。5.2 财务指标与未来函数另一个容易踩的坑在特征工程层面。沪深300成分股预测任务中常见的特征来源有两个行情数据日频和财务数据季频。我一开始把ROE、营收增速、净利润增速也加进了节点特征结果测试集效果惊人但复盘发现一个问题财务报告公布日和报告截止日之间有时间差。A股年报截止日是12月31日但实际披露时间最晚可以拖到次年4月30日。如果直接用年报的ROE作为12月31日的节点特征那么模型在1月份就已经看到了未来4月份才公布的数据。这在回测中是合法的因为我们确实拿到了全部历史数据但在实盘中是不可行的——1月份我手上根本没有这份年报。这就是未来函数。把财务特征全部换成已公布数据的版本之后模型测试集RankIC从0.052下降到0.049降低得不算多。原因在于沪深300成分股是市场关注度最高的大盘股财务报表信息已经提前被市场充分定价行情数据和技术指标已经包含大部分有效信息。这个结果反而让我更倾向于在日频价量数据上做文章财务特征可以保留但不需要抱有太高的期望。5.3 模型容量与过拟合的平衡STGNN模型的参数量从头开始搭建很容易失控。我第一版模型的参数量在1800万左右在训练集上RankIC高达0.12测试集却只有0.03。这是典型的过拟合模型把训练期的市场噪声当成了有效规律。反复调参之后的最终结构参数参数数值时空卷积块堆叠层数2卷积核大小3GCN隐藏层维度64节点嵌入维度16Dropout0.3总参数量约320万参数量从1800万压到320万测试集RankIC反而从0.03提升到了0.049。金融时序数据本质上是低信噪比的模型容量过大的收益远远小于过拟合带来的损失。做这个项目最大的体会是金融场景下先压制模型容量再逐步放宽比一开始就搭大模型更稳妥。6. 从模型输出到量化策略回测与实盘信号转换6.1 多维度预测如何合成选股信号模型输出层不是只预测一个未来5日收益率而是同时预测多个目标包括未来5日收益率、未来5日波动率、未来5日换手率预测。这就是项目标题里的多维度走势预测的含义。多任务学习的额外目标能起到类似正则化的作用让模型学到更通用的特征表示而不是过度拟合单一预测目标。实际使用中选股信号综合三个维度score 0.7 * rank(收益预测) - 0.2 * rank(波动率预测) 0.1 * rank(换手率预测)收益预测占最高权重波动率预测作为风险惩罚项波动率越高得分越低换手率预测作为流动性参考。每周五收盘后运行一次模型输出下一周的组合权重。测试期比较了等权Top50组合和用信号强度加权的Top50组合发现加权组合的年化超额收益能高出2.8个百分点但最大回撤也相应增大。实盘建议从等权开始跑等信号稳定性验证后再切换到加权方案。6.2 交易成本、滑点和执行时延对预测衰减的影响回测模型再好也要估算交易成本对预测信号衰减的影响。A股市场的交易成本主要由三部分构成佣金、印花税和冲击成本。佣金一般万二点五印花税对卖出方向收取千分之一冲击成本取决于单笔成交金额占当天成交量的比例。我按换手率等权组合做了成本敏感性测试换手率单边成本双边年化超额收益扣除成本前年化超额收益扣除成本后20%/周约1.0%14.2%4.2%10%/周约0.5%10.8%8.8%5%/周约0.25%8.1%6.1%最直观的结论是高频调仓带来的收益增量覆盖不了交易成本的吞噬。周度换手率20%的组合虽然扣除成本前的超额收益最高但扣除成本后只剩4.2%还不如10%换手率组合的8.8%。这也是我最后把调仓频率固定在每周一次的原因周度频率在捕捉短期趋势和降低交易成本之间取得了最合适的平衡点。另外一个容易忽略的细节是执行时延。模型在周五收盘后训练并生成信号周一开盘才实际下单这中间隔了一个周末。A股市场隔夜信息重大产业政策、海外市场波动可能会显著改变个股的前后相对强弱周五生成的信号到周一开盘可能已经部分失效。可以考虑在周五尾盘或者周一开盘前重新跑一次模型用最新两天的数据做增量预测能有效减少信号衰减。6.3 可以继续扩展的方向这套STGNN框架跑通之后我可以负责任地说它的价值不在模型本身而在把市场当成一个关联系统来理解的视角转换。后续可以扩展的方向至少有四个更丰富的边关系加入产业链上下游关系、机构持仓重叠度、融资融券余额联动替代目前以行业和相关性为主的边结构。更高频的数据目前是日频数据换成分钟级数据的图神经网络理论上能捕捉到日内轮动和中低频的跨股传导但数据量和计算复杂度会大幅上升。注意力机制的深度结合在时间维度上也加入注意力让模型自动学习过去哪几天的市场状态对当前预测更重要而不是固定用60天的均匀窗口。多市场联合建模把沪深300成分股和港股通标的、中概股放在同一张图上纳指和A股美股的联动也是时空特征的一部分。最后分享一点个人体会做这个项目绕了很多弯最深刻的感受是金融时间序列和图像、语音这类信号有本质区别后者是稳定的物理信号前者是无数交易者决策博弈的综合结果。图中的边结构会变市场的波动特征会变模型每隔一段时间就需要重新校准。所以不要追求一个一劳永逸的万能模型更实际的思路是搭好一套数据管道模型框架评估体系让模型跟随市场持续迭代。拿我自己来说模型固定三个月重新训练一次每次只保留最近500个交易日的样本测试集始终是最近的、模型从未见过的一年数据。这样跑出来的结果训练集RankIC大约0.068测试集RankIC还能保持在0.05上下扣除交易成本后每周调仓一次的等权Top50组合在2023年的行情里做出了约8%的年化超额收益。最后再分享一个小技巧模型预测的价值不在于准确预测每一次涨跌而在于建立一个可重复、可验证的决策体系。把回测和实盘的差距逐项归因——是成本问题、时效问题还是过拟合问题这样每一次迭代都能知道自己在修什么。这才是把深度学习模型真正用起来的正确方式。本文还有配套的精品资源点击获取