图神经网络在量化投资中的应用:从股票关联图到GNN模型实战

📅 发布时间:2026/10/20 20:54:34
图神经网络在量化投资中的应用:从股票关联图到GNN模型实战
1. 从K线到图结构为什么金融数据天生适合图神经网络1.1 传统量化模型的盲区在哪里做了几年量化的人都会有一个共同的感受因子挖了一大堆回测曲线看着漂亮一上实盘就拉胯。问题出在哪儿很多时候不是因子本身不行而是我们看待市场的方式太“扁平”了。传统多因子模型也好时序深度学习模型也罢本质上都把每只股票当成一个独立的样本。你给某只股票喂进去过去60天的量价数据、财务指标、动量因子模型吐出来一个涨跌预测。这套逻辑跑了很多年但它忽略了一件极其关键的事情——股票之间是有关系的。同一条产业链上的上下游公司股价会联动同属一个板块的股票资金会轮动大票和小票之间存在明显的溢出效应。这些关系在传统因子框架里很难被有效表达。你当然可以手动构造一些“行业哑变量”或者“板块动量因子”但这些都是非常粗糙的近似而且需要大量人工经验去定义。图神经网络GNN之所以在金融领域越来越受关注核心原因就一个它天然就是用来处理“实体关系”这种结构数据的。股票是节点股票之间的关系是边整个市场就是一张动态演化的图。这个建模思路比把每只股票孤立看待要合理得多。1.2 图神经网络到底在做什么事如果之前没接触过GNN我用一个生活化的类比来解释。想象你是一个刚转学来的学生想快速了解班里每个同学的性格。你会怎么做你肯定不会挨个聊天问一遍而是先看看谁跟谁玩得好然后通过社交关系去推断。比如A和B是好朋友B和C是好朋友那A和C大概率也有相似之处。GNN做的事情本质上就是这个——通过节点之间的连接关系让每个节点“吸收”邻居的信息从而更新自己的表示。放到金融场景里某只银行股发布了超预期的财报这个消息不仅影响它自己还会通过“同行业”这条边传导到其他银行股再通过“上下游”这条边传导到相关的金融科技公司。GNN的消息传递机制恰好能捕捉这种传导效应。具体到技术实现GNN的核心操作叫消息传递Message Passing分三步走消息生成每个节点根据自己的特征生成一条“消息”发给邻居消息聚合每个节点收集所有邻居发来的消息用某种方式求和、平均、注意力加权聚合成一个向量特征更新节点把自己的原始特征和聚合后的邻居信息结合起来更新自己的表示这个过程可以重复多轮每多一轮节点就能“看到”更远的邻居。两轮之后一个节点就能感知到“邻居的邻居”的信息。1.3 金融图的几种典型构建方式GNN在金融里能不能用好八成取决于图怎么建。模型结构反而是次要的。我见过太多人一上来就调模型参数结果图建得一塌糊涂怎么调都没用。常见的金融图构建方式有这么几类图的类型节点边的定义适用场景行业关联图个股同属一个行业/概念板块板块轮动、行业中性化供应链图个股上下游供应关系产业链传导、基本面联动股东关联图个股共同股东/交叉持股资金联动、抱团效应相关性图个股历史收益率相关系数统计套利、配对交易新闻共现图个股同一新闻中共同出现事件驱动、情绪传导动态时序图个股滚动窗口内的关系变化捕捉关系结构的时变性我个人的经验是刚开始做的时候不要贪多先把行业关联图跑通。原因很简单行业分类数据容易获取逻辑清晰而且效果通常已经比不用图要好。等这套流程跑顺了再逐步叠加供应链、股东关系等更复杂的边。2. 动手之前必须想清楚的几个设计决策2.1 节点特征怎么选节点特征就是每只股票的“个人简历”。GNN再强如果喂进去的特征没有信息量也白搭。常用的节点特征分几大类量价特征过去N天的收益率、换手率、波动率、成交量变化等。这是最基础也是最重要的基本面特征市盈率、市净率、ROE、营收增速、毛利率等技术指标MACD、RSI、布林带位置等资金流特征大单净流入、北向资金持仓变化等这里有个坑要特别注意特征的时间对齐。财务数据有披露延迟你在一季度末能拿到的财务数据实际上反映的是去年四季度或者更早的情况。如果不对齐就会引入未来信息回测结果会严重高估。我的做法是所有特征统一按“可获取时间”打标签。比如某公司4月25日发布年报那这份年报数据从4月25日之后才能进入特征。这个细节看起来小但对回测结果的真实性影响巨大。2.2 边权重要不要最简单的做法是边只有0和1——属于同一行业就是1不属于就是0。但现实中关系的强弱是有区别的。比如两只股票历史相关系数0.8和0.3显然不应该同等对待。这时候可以给边赋权重权重可以是相关系数、可以是供应链上的营收占比、可以是共同股东的持股比例等。但要注意边权重需要做归一化处理。不同来源的权重尺度差异很大相关系数在-1到1之间营收占比可能是0到100%如果不归一化GNN聚合的时候就会被大数值的边主导。2.3 模型选GCN还是GAT这是被问得最多的问题之一。简单说一下我的理解GCN图卷积网络的聚合方式是固定的——邻居特征加权求和权重由节点度决定。优点是简单、参数少、不容易过拟合。缺点是它给所有邻居相同的待遇没法区分哪个邻居更重要。GAT图注意力网络引入了注意力机制每个节点可以自动学习给不同邻居分配不同权重。优点是表达能力强能捕捉“某些邻居比其他邻居更重要”这种情况。缺点是参数多小数据集上容易过拟合。在金融场景下我的建议是如果图比较稠密每个节点有很多邻居优先用GAT如果图比较稀疏GCN就够了。另外金融数据信噪比低模型越简单越稳健这一点要时刻记在心里。3. 从零搭建一个股票关联图GNN的完整流程3.1 数据准备与图构建假设我们有一批A股股票的数据目标是预测未来5日的收益率排序。整个流程从数据准备开始。第一步确定股票池。通常用中证500或者沪深300的成分股流动性好数据质量高。假设我们选了300只股票。第二步构建邻接矩阵。以行业关联为例import numpy as np import pandas as pd # 假设有300只股票每只有行业标签 # industry_labels: shape (300,), 值为行业编号 n_stocks 300 industry_labels np.random.randint(0, 20, sizen_stocks) # 20个行业 # 构建邻接矩阵 adj_matrix np.zeros((n_stocks, n_stocks)) for i in range(n_stocks): for j in range(n_stocks): if i ! j and industry_labels[i] industry_labels[j]: adj_matrix[i][j] 1.0 # 归一化对称归一化类似GCN的做法 degree adj_matrix.sum(axis1) degree_inv_sqrt np.power(degree, -0.5) degree_inv_sqrt[np.isinf(degree_inv_sqrt)] 0 adj_norm np.diag(degree_inv_sqrt) adj_matrix np.diag(degree_inv_sqrt)这段代码做了一件核心的事情对称归一化。为什么要归一化因为如果不做度数大的节点邻居多的股票在聚合后会得到数值很大的特征向量而度数小的节点特征值很小导致训练不稳定。归一化之后每个节点的特征尺度就统一了。第三步准备节点特征矩阵。假设每只股票有20个特征取过去60个交易日的数据# features: shape (n_stocks, n_features) # 这里用随机数据模拟实际中需要从数据库读取 n_features 20 features np.random.randn(n_stocks, n_features) # 标签未来5日收益率排名做截面排序 labels np.random.randn(n_stocks)3.2 模型定义与关键参数用PyTorch Geometric来搭建一个两层GCNimport torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import GCNConv class StockGCN(nn.Module): def __init__(self, n_features, hidden_dim, n_output): super(StockGCN, self).__init__() self.conv1 GCNConv(n_features, hidden_dim) self.conv2 GCNConv(hidden_dim, hidden_dim) self.fc nn.Linear(hidden_dim, n_output) self.dropout nn.Dropout(0.3) def forward(self, x, edge_index, edge_weightNone): # 第一层GCN ReLU x self.conv1(x, edge_index, edge_weight) x F.relu(x) x self.dropout(x) # 第二层GCN ReLU x self.conv2(x, edge_index, edge_weight) x F.relu(x) x self.dropout(x) # 输出层 x self.fc(x) return x几个关键参数的选择逻辑隐藏层维度通常设64或128。太小了表达不够太大了过拟合。我一般从64开始试Dropout率金融数据噪声大Dropout是必须的。0.3是一个比较安全的起点如果过拟合严重可以加到0.5层数2层是绝大多数情况下的最优选择。层数太多会导致过平滑over-smoothing所有节点的表示变得趋同反而丢失区分度注意GNN的层数不是越多越好。在金融场景下2层GNN已经能让每个节点感知到“邻居的邻居”这通常足够了。3层以上几乎必然过平滑。3.3 训练策略与损失函数金融预测和图像分类最大的区别在于我们关心的是排序不是绝对数值。所以损失函数的选择很关键。常用的几种选择MSE损失直接回归收益率。简单但对异常值敏感IC损失最大化预测值和真实收益率的截面相关系数。更贴近实际需求Ranking损失 pairwise ranking loss直接优化排序质量ICIR加权损失同时考虑IC均值和稳定性我个人的偏好是用可微分的IC损失因为它直接优化我们最终关心的指标def ic_loss(pred, target): # pred, target: shape (n_stocks,) pred_centered pred - pred.mean() target_centered target - target.mean() ic (pred_centered * target_centered).sum() / ( pred_centered.norm() * target_centered.norm() 1e-8 ) return -ic # 负号因为要最小化训练时的几个实操要点学习率1e-3起步配合余弦退火调度早停验证集IC连续5个epoch不提升就停截面标准化每个时间截面上对特征做z-score标准化这个非常重要滚动训练不要用全量数据训练一个模型要用滚动窗口比如用过去3年训练预测未来1个月4. 踩坑实录那些回测很美实盘很惨的教训4.1 未来信息泄露的几种隐蔽形式未来信息泄露是量化里最致命的错误而且往往非常隐蔽。我踩过的坑包括坑一用了复权价格但没注意复权基准日。前复权价格会随着新的除权除息事件而变化如果你在回测的每个时间点都用“当前最新”的前复权价格实际上就引入了未来信息。正确做法是用后复权或者在每个时间点重新计算前复权。坑二财务数据的“报告期”和“公告期”混淆。比如一季报的报告期是3月31日但公告期可能是4月25日。如果你在4月1日就用了这份数据就泄露了未来信息。坑三行业分类的动态调整。某只股票可能在被调入某个行业后表现很好但如果你用的是“当前”的行业分类去回测历史就相当于提前知道了它会被调入。坑四图的构建用了全时段数据。比如你用过去一年的相关系数建图但如果这个“过去一年”是相对于回测当前时点的那没问题如果你用了整个样本期的相关系数那就泄露了。4.2 过平滑问题的识别与缓解过平滑是GNN在金融场景下最常见的问题之一。表现是训练损失正常下降但验证集IC很低而且所有股票的预测值非常接近区分度很差。识别方法很简单计算所有节点表示的两两余弦相似度。如果平均相似度超过0.9基本可以确定过平滑了。缓解手段有这么几个减少层数2层不够就1层别硬上3层加残差连接让每层输出包含原始输入的信息用JKNet结构把每层的输出都连到最终输出让模型自己选择用哪层PairNorm一种专门对抗过平滑的归一化方法DropEdge训练时随机丢弃一些边减少信息过度聚合我实测下来残差连接DropEdge的组合最稳改动小效果好。4.3 图结构噪声的处理现实中的图从来不是完美的。行业分类可能过时供应链关系可能不完整相关系数可能被极端值污染。处理图噪声的几个实用技巧边剪枝把权重低于某个阈值的边直接去掉。阈值可以通过验证集IC来确定边采样训练时每次随机采样一部分边类似Dropout的思想多图融合不要只用一个图把行业图、相关性图、供应链图分别过GNN然后融合输出可学习的图结构让模型自己学习邻接矩阵而不是完全依赖预定义的图最后一点展开说一下。可学习图结构的思路是初始化一个邻接矩阵把它作为可训练参数训练过程中自动调整。但要注意加正则化约束否则学出来的图可能完全没有可解释性。5. 效果评估与迭代方向5.1 怎么判断GNN到底有没有用不要只看回测收益率。判断GNN是否真正带来了增量信息我通常看这几个指标指标含义合格线IC均值预测值与真实收益的截面相关性 0.03ICIRIC均值/IC标准差 0.3多空收益差预测最高组与最低组的收益差年化 10%换手率组合调仓频率与策略匹配与基准因子的相关性与已有因子的信息重叠度 0.5特别强调最后一项如果GNN的预测和传统动量因子相关性很高那它的增量价值就很有限。理想情况下GNN应该捕捉到传统因子捕捉不到的关系信息。5.2 后续可以往哪些方向迭代跑通基础版本之后有几个明显的迭代方向方向一时序图神经网络。目前的图是静态的但市场关系是动态变化的。可以用RNN或Transformer来建模图结构随时间的演化。方向二异构图。目前只有股票一种节点可以加入行业节点、概念节点、甚至新闻节点构建异构图。不同类型节点之间的消息传递方式可以不同。方向三多任务学习。同时预测收益率、波动率、成交量等多个目标共享底层GNN表示。多任务通常能提升泛化能力。方向四与强化学习结合。GNN输出股票表示RL agent基于这些表示做仓位决策。这个方向比较前沿但落地难度也大。方向五可解释性。用GNN的注意力权重来分析“哪些关系对预测最重要”这本身就能产生有价值的洞察。我个人在实际操作中的体会是不要一上来就追求复杂模型。把图建好把特征对齐做好把回测框架搭扎实用一个简单的2层GCN就能跑出不错的结果。很多人的问题不是模型不够复杂而是基础工作没做到位。先把简单版本跑通有了baseline之后再逐步叠加复杂度这样每一步改进都能清楚地知道是哪个因素带来的提升。最后分享一个小技巧在构建图的时候可以同时构建多个不同稀疏度的图然后用一个可学习的权重把它们融合起来。比如行业图比较稠密相关性图比较稀疏供应链图介于两者之间。让模型自己学习每个图的重要性通常比手动选一个图效果要好。这个做法实现起来不复杂但往往能带来意外的提升。