尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于时空图神经网络STGCN的股票走势预测与量化选股实战

基于时空图神经网络STGCN的股票走势预测与量化选股实战 简介时间序列预测是金融量化研究的基础但传统模型常将个股视为独立序列忽略了股票间资金联动、行业轮动等横截面信息。图神经网络GNN的出现为建模复杂关系提供了新思路。时空图神经网络STGCN通过图卷积聚合邻居信息同时利用时间卷积捕捉序列自相关在时间和空间两个维度上联合提取特征从而更真实地刻画市场结构。该技术可应用于多股联动预测、行业轮动分析和量化选股策略尤其适合沪深300等成分股构成的动态关联图场景。本文以PyTorch实现STGCN框架系统阐述了从相关性矩阵构建、特征工程、多任务标签设计到模型训练与回测落地的完整流程并总结了金融时序任务中数据泄漏、标签失衡、过拟合等关键避坑经验为量化策略开发者提供可复现的工程参考。 股票预测这块很多人一上来就堆LSTM、Transformer把单只股票当成独立时间序列去建模。我最初也是这么干的但做完几轮回测之后发现一个很扎心的问题A股个股之间的联动性太强了行业轮动、龙头带动、资金抱团单股模型根本吃不到这些横截面信息。这也是我后来转向时空图神经网络STGNN做沪深300成分股走势预测的直接原因。这套系统的核心链路很清晰先把沪深300成分股通过相关性矩阵构建成一张动态关联图再用图神经网络在时间维和空间维上同时提取特征最终输出个股未来走势的多维度预测结果并转换成量化选股信号。说白了就是把370多只股票当成一个相互影响的系统来建模而不是切成370个互不相干的小任务。这套方案适合正在研究金融时间序列、或者想把图神经网络落地到股票预测和量化策略开发里的同学参考。接下来我会把整个项目的设计思路、数据工程、模型实现、训练细节和踩坑记录完整拆开讲代码也都是基于PyTorch的可以直接照着复现。1. 项目整体设计为什么选STGNN而不是堆LSTM1.1 单股时序模型的盲区股票从来不是“独立”的传统的股票预测做法是把每只股票的历史价格、成交量喂给LSTM或者Transformer让模型从自身历史里找规律。这种思路在理论上是成立的但放到A股市场里有个天然缺陷它忽略了股票之间的相关性。举个例子某个行业龙头发布超预期财报第二天整个板块大概率跟着高开。如果你只盯着个股自己的历史序列这种“由其他股票传导过来的信息”是根本无法捕获的。又比如市场出现系统性回调几乎所有股票同步下跌单股模型很难区分这是个股自己的风险还是整个市场的贝塔风险。沪深300成分股恰好是一个非常典型的图结构场景这些股票覆盖了金融、消费、科技、制造等多个行业彼此之间存在资金联动、产业链上下游关系、同涨同跌的行业效应。把它们放在一张图里建模让信息沿着股票的关联关系流动是比“独立建模”更符合市场本质的做法。1.2 STGNN要解决的核心问题时间和空间一起学时空图神经网络英文全称Spatial-Temporal Graph Neural Network它的核心能力是同时处理两个维度的依赖关系。时间维度好理解就是股票价格序列本身的自相关性今天的收盘价和昨天的收盘价高度相关某只股票连续上涨后的惯性动量等等。这部分和LSTM、TCN要解决的问题是一样的。空间维度指的是股票之间的相互影响某个板块整体走强相关个股的走势会趋向一致某只权重股剧烈波动可能带动整个指数和关联股票共振。空间维度的关键是“谁影响谁、影响程度有多大”这需要通过图结构中的边来表达。STGNN的典型处理方式是用时间模块捕捉序列特征用图卷积模块聚合邻居信息然后把两者交替堆叠或并行融合。这样每个节点股票的最终表征里既包含自身过去一段时间的历史走势信息也包含与它关联的其他股票的综合状态。1.3 “多维度走势预测”到底预测什么标题里提到多维度很多人第一反应是“多因子”。但在这个系统里多维度的含义更接近“多输出”和“多视角”。从输出角度我最终设计了三类预测目标未来一定时间窗口的收益率方向上涨还是下跌、收益率的大小涨跌幅级别、以及波动风险未来区间的波动率估计。只预测方向的问题在于准确率到了50%附近就很难提升而且即使判断对了方向也区分不了大涨小涨和大跌小跌加上幅度和波动率后模型输出的信息量会大很多也更贴近实际交易需求。从输入角度特征也分了很多维度价格序列、成交量能、技术指标、横截面排名特征后面我会在数据工程部分详细展开。输入维度够丰富输出维度够立体这套系统才能支撑后续的量化策略开发。2. 数据工程与图结构构建预测系统的地基2.1 数据获取与预处理前复权、停牌、涨跌停一个都不能少做金融时间序列项目数据处理花的时间通常比模型还多。STGNN对数据质量尤其敏感因为图结构本身是从收益率序列里算出来的数据一旦有脏点整张图都会变形。数据源我建议优先考虑以下几个Tushare Pro、AkShare、Baostock都是国内常用的开源数据接口。如果只是做沪深300成分股的日线级别研究免费额度基本够用。我实际用的是日线频率从2010年至今的行情数据。预处理里有几个细节非常关键第一价格必须做前复权处理。如果不复权分红除权会让价格形成跳空缺口模型会把除权缺口当成真实的暴跌或暴涨信号预测结果直接失真。前复权是以当前价格为基准倒推历史价格这样历史的收益序列才是连续可比的。第二停牌数据的处理。沪深300成分股流动性好但也不排除个别股票因为重大事项停牌。一般做法是把停牌日的收益率填为0成交量填为0并在特征里加一个“是否停牌”的哑变量。千万不要直接删掉那天的数据否则会破坏时间序列的连续性。第三涨跌停的处理。A股有涨跌停限制涨停板时买不进、跌停板时卖不出这是策略落地时要考虑的实际约束。但在建模阶段涨跌停日的价格已经被抑制了如果预测模型不理解这一点很可能在涨停后继续预测大涨但实际上根本无法成交。我的做法是在特征里加入“当日是否触及涨跌停”让模型自己学习这种状态的影响。2.2 相关性矩阵与邻接矩阵怎么把370只股票变成一张图图神经网络的第一步是定义图结构。这里的节点就是股票边则代表股票之间的关联程度。如何定义“关联”直接决定了模型能学到什么。我采用的是滚动窗口的收益率相关性矩阵对每只股票取过去60个交易日的日收益率序列两两计算Pearson相关系数得到一个370×370的相关性矩阵。之所以用60日窗口是因为这个长度大概对应一个季度的交易日数量既能捕捉到中短期的联动关系又不会因为窗口太短而充满噪声。这里有一个藏在细节里的问题全连接图不能用。如果370只股票两两连边图卷积操作会把所有股票的噪声信息都聚合进来而且计算量巨大。需要把相关性矩阵稀疏化。我的做法是Top-K稀疏化和绝对值阈值相结合。具体来说对每只股票先只看相关系数绝对值排在前20的股票作为候选邻居再要求相关系数绝对值必须大于0.4才能连边。这样既保证了图不会太密也过滤掉了相关性过低造成的弱连接关系。需要特别说明的是我同时保留了正相关和负相关的边。很多人想当然地认为只有正相关的股票才有信息传递但实际上负相关蕴含着“跷跷板效应”比如商品涨价利空下游制造、利好上游资源这种负联动同样是重要的交易信号。只看正相关会丢掉这部分信息。邻接矩阵构建完后还需要做归一化。GCN的卷积本质上是对邻居特征做加权求和如果不对邻接矩阵归一化不同节点因为邻居数量不同聚合后的特征尺度会差别很大。我用的是对称归一化import numpy as np def build_adj(corr_matrix, k20, threshold0.4): n corr_matrix.shape[0] adj np.zeros((n, n)) for i in range(n): # 排除自身取相关性绝对值最大的 k 个候选 scores np.abs(corr_matrix[i]) scores[i] -1 candidates np.argsort(-scores)[:k] # 再套一个阈值过滤弱相关 valid candidates[corr_matrix[i, candidates].__abs__() threshold] adj[i, valid] corr_matrix[i, valid] adj[valid, i] corr_matrix[i, valid] # 对称归一化D^{-1/2} A D^{-1/2} deg adj.sum(axis1) deg_inv_sqrt np.power(deg, -0.5) deg_inv_sqrt[np.isinf(deg_inv_sqrt)] 0.0 norm_adj np.diag(deg_inv_sqrt) adj np.diag(deg_inv_sqrt) return norm_adj这里为什么用相关系数绝对值而不是原始值作为边的权重因为相关系数本身代表的是线性相关强度绝对值越大代表联动越强符号只代表方向不影响“是否要连边”的判断。当然负相关的权重最终在聚合时会被乘以负数这恰好实现了“反方向传导信息”的效果。还有一个重要细节图结构不是一成不变的。市场风格切换时比如从大盘价值切换到中小成长股票间的相关结构会发生明显变化。所以我在训练和预测阶段都会滚动重建邻接矩阵一般每20个交易日更新一次。静态图会让模型在风格切换后迅速失效。2.3 特征工程与标签设计多维度预测的输入与输出数据干净了、图结构也有了接下来需要设计模型的输入特征和预测标签。我的特征分为三大类第一类是基础行情特征包括前复权收盘价、开盘价、最高价、最低价、成交量、成交额。这些是最原始的信息但一般不直接喂给模型而是做变换。第二类是衍生技术和量能特征包括日收益率、5日和20日动量、RSI、MACD的DIF和DEA、布林带位置、ATR平均真实波幅、成交量与5日均量的比值、换手率、量比等。这些特征在传统量化里已经被证明了有价值作为深度学习模型的输入也能提供有效信息。第三类是横截面排名特征这是我觉得最值得强调的一类。比如个股当日收益率在全部沪深300成分股中的排名分位数、20日动量排名、成交额排名等。加入这些特征的原因很直观股票预测最终要用于选股本质上关心的是横截面排序——谁比谁更强而不是绝对涨幅是多少。横截面排名特征刚好把“相对强弱”这个信息直接编码进来而且对不同价格区间的股票也天然可比。至于标签我设计了三个预测头- 分类头未来5个交易日收益率 0 的概率 - 回归头未来5个交易日的累计对数收益率 - 风险头未来5个交易日的已实现波动率估计训练时三个任务共用底层的时空特征提取网络只是最后的输出头不同通过多任务损失联合优化。这种设计的好处是让模型同时学到方向、幅度和风险比单一任务学到的表征要丰富得多。3. 模型核心组件拆解时间和空间如何优雅地融合3.1 时间维度用时间卷积还是GRUSTGNN里的时间模块有很多种选择常见的有LSTM/GRU、一维卷积、TCN、Transformer编码器。我实际测试下来在日线级别的金融时间序列上时间卷积网络的性价比最高。原因有三点。第一金融日线序列长度通常不会太长我用的回看窗口是60个交易日卷积可以并行计算训练速度远快于RNN类模型。第二TCN通过空洞卷积可以灵活扩大感受野60步的历史信息用两三层的空洞卷积就能覆盖。第三LSTM在处理这种噪声极大的序列时容易过拟合而卷积自带正则化效果。我用的时间模块是这样设计的先用两层带空洞的一维卷积提取短期和中期模式再接一个跨通道的注意力池化把整个时间窗口压缩成一个固定维度的向量。import torch import torch.nn as nn import torch.nn.functional as F class TimeConvBlock(nn.Module): def __init__(self, in_dim, hidden_dim, kernel_size3): super().__init__() self.conv1 nn.Conv1d(in_dim, hidden_dim, kernel_size, paddingkernel_size // 2, dilation1) self.conv2 nn.Conv1d(hidden_dim, hidden_dim, kernel_size, paddingkernel_size, dilation2) self.bn nn.BatchNorm1d(hidden_dim) def forward(self, x): # x: [B, N, F, T] 或者 reshape 成 [B*N, F, T] B, N, F, T x.shape x x.reshape(B * N, F, T) h F.relu(self.bn(self.conv1(x))) h F.relu(self.bn(self.conv2(h))) # 时间维池化保留最重要的信息 h h.mean(dim-1) # [B*N, hidden] return h.reshape(B, N, -1)这里用了BatchNorm不要小看这一个操作。金融序列的尺度经常漂移比如某段时间价格整体从100涨到200BatchNorm能帮模型快速适应这种分布变化让训练稳定很多。3.2 空间维度GCN和GAT怎么选空间模块的作用是让每个节点的表征融合邻居节点的信息。最常用的是GCN和GAT。GCN的权重对所有邻居是一样的只是用邻接矩阵的权重做了加权GAT则通过注意力机制动态学习每对邻居之间的重要程度。在我这套系统里最开始用的是GCN后来对比过GAT最终保留了GCN作为主力模块。原因倒不是说GAT不好而是股票图本身已经有相关性矩阵作为边的权重了GCN可以直接利用这个先验信息而GAT反而会从头学注意力权重训练难度更大在样本量有限的金融数据上更容易过拟合。GCN的前向传播本质上就是两步线性变换 邻居聚合。class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear nn.Linear(in_dim, out_dim) self.dropout nn.Dropout(0.2) def forward(self, x, adj): # x: [B, N, F], adj: [N, N] h self.linear(x) # bnh,nm-bmh对邻居节点 n 的特征按邻接矩阵加权求和 h torch.einsum(bnh,nm-bmh, h, adj) return F.relu(self.dropout(h))用通俗的话解释图卷积每只股票的新特征等于“自己特征的线性变换”加上“所有邻居股票特征的加权平均”。权重就是相关系数。这样经过两层图卷积后股票的表征就已经包含了二阶邻居的信息也就是朋友的朋友的状态这在刻画板块联动时非常有用。3.3 时空融合结构T2G、G2T还是并行堆叠时间模块和空间模块是两头独立的怎么把它们组合成一个完整的STGNN是一个值得仔细想的设计问题。主流的组合方式有三种。第一种是T2G先做时间卷积提取每只股票自己的时序特征再做图卷积融合邻居信息第二种是G2T先融合空间信息再做时序建模第三种是交替堆叠类似STGCN里的时空卷积块时间卷积和图卷积交替进行每层都能在更抽象的层次上重新融合时空信息。我的最终结构是交替堆叠实际代码里设计了两个ST-BlockST-Block 时间卷积 - 图卷积 - 残差连接两个ST-Block堆叠后经过一个全连接输出头。残差连接在金融时序任务里非常重要因为原始特征本身就有很强的预测能力如果深层网络完全覆盖掉原始信息模型会丢掉低阶信号。残差让模型“在原有信息基础上做增量修正”训练更稳效果也更好。class STGCN(nn.Module): def __init__(self, num_nodes, in_dim, hidden_dim64, out_dim3, seq_len60): super().__init__() self.input_proj nn.Linear(in_dim, hidden_dim) self.time_conv TimeConvBlock(hidden_dim, hidden_dim) self.gcn1 GCNLayer(hidden_dim, hidden_dim) self.gcn2 GCNLayer(hidden_dim, hidden_dim) self.head nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden_dim, out_dim) ) def forward(self, x, adj): # x: [B, T, N, F] B, T, N, F x.shape # 先做输入维对齐 x x.permute(0, 2, 3, 1) # [B, N, F, T] h self.time_conv(x) # [B, N, hidden] h self.gcn1(h, adj) # [B, N, hidden] h self.gcn2(h, adj) # [B, N, hidden] out self.head(h) # [B, N, out_dim] return out这里输入的Tensor维度是[B, T, N, F]batch、时间步、节点数、特征维。很多第一次写图时序模型的人容易把维度搞混我建议在代码里每一步都写清楚当前的shape跑起来会少很多bug。4. PyTorch实现从数据管道到完整训练管线4.1 数据集与邻接矩阵的加载金融时序数据集的写法和图像分类不太一样它不能随机打乱样本因为时间顺序本身是有意义的。我的做法是先把所有股票的历史特征整理成一个大矩阵然后按时间窗口切样本训练集、验证集、测试集严格按时间顺序切分。具体来说用过去60个交易日的特征预测未来5个交易日的标签。训练集用2013到2021年的数据验证集用2022年测试集用2023到2024年。这种切分方式保证了验证和测试阶段完全模拟“预测未来”的场景不会出现时间穿越。import torch from torch.utils.data import Dataset class StockDataset(Dataset): def __init__(self, X, Y): self.X X # [样本数, T, N, F] self.Y Y # [样本数, N, 3] 三个预测目标 def __len__(self): return len(self.X) def __getitem__(self, idx): return ( torch.tensor(self.X[idx], dtypetorch.float32), torch.tensor(self.Y[idx], dtypetorch.float32) )注意邻接矩阵不需要每个样本都返回一份因为一个交易日所有的股票共享同一张图所以在训练循环里单独传入即可能省不少内存。4.2 图卷积层与STGCN模型实现这一节把第3节的代码整合成一个可运行的完整模型文件。我在实际项目中还会加一个可选的GAT分支但这里先给出最简版本方便理解核心结构。class TimeConvBlock(nn.Module): def __init__(self, in_dim, hidden_dim): super().__init__() self.conv1 nn.Conv1d(in_dim, hidden_dim, kernel_size3, padding1) self.conv2 nn.Conv1d(hidden_dim, hidden_dim, kernel_size3, padding2, dilation2) self.bn nn.BatchNorm1d(hidden_dim) def forward(self, x): B, N, F, T x.shape x x.reshape(B * N, F, T) h F.relu(self.bn(self.conv1(x))) h F.relu(self.bn(self.conv2(h))) h h.mean(dim-1) return h.reshape(B, N, -1) class GCNLayer(nn.Module): def __init__(self, in_dim, out_dim): super().__init__() self.linear nn.Linear(in_dim, out_dim) self.dropout nn.Dropout(0.2) def forward(self, x, adj): h self.linear(x) h torch.einsum(bnh,nm-bmh, h, adj) return F.relu(self.dropout(h)) class STGCN(nn.Module): def __init__(self, num_nodes, in_dim, hidden_dim64, out_dim3): super().__init__() self.proj nn.Linear(in_dim, hidden_dim) self.time_conv TimeConvBlock(hidden_dim, hidden_dim) self.gcn1 GCNLayer(hidden_dim, hidden_dim) self.gcn2 GCNLayer(hidden_dim, hidden_dim) self.head nn.Sequential( nn.Linear(hidden_dim, hidden_dim), nn.ReLU(), nn.Dropout(0.2), nn.Linear(hidden_dim, out_dim) ) def forward(self, x, adj): # x: [B, T, N, F] B, T, N, F x.shape x x.permute(0, 2, 3, 1) # [B, N, F, T] h self.time_conv(x) # [B, N, hidden] h self.gcn1(h, adj) # [B, N, hidden] h self.gcn2(h, adj) out self.head(h) # [B, N, out_dim] return out这个模型参数量不大在小规模金融数据上不容易过拟合。如果后续想提升表达能力可以把单层时间卷积换成更深的TCN或者在GCN后面加一个Transformer编码器但随之而来的是训练难度和数据需求量的上升需要根据实际效果取舍。4.3 训练循环与超参数配置训练环节我踩过不少坑最终沉淀下来一套比较稳定的配置优化器用AdamW初始学习率1e-3加上warmup和余弦退火梯度裁剪设为1.0Early Stopping看验证集RankIC连续15个epoch不提升就停。import torch.optim as optim def train_model(model, train_loader, val_loader, adj, epochs60): optimizer optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-5) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_maxepochs) for epoch in range(epochs): model.train() total_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() pred model(x_batch, adj) loss_cls F.binary_cross_entropy_with_logits(pred[:, :, 0], y_batch[:, :, 0]) loss_reg F.mse_loss(pred[:, :, 1], y_batch[:, :, 1]) loss_var F.mse_loss(torch.relu(pred[:, :, 2]), y_batch[:, :, 2]) loss loss_cls 0.5 * loss_reg 0.2 * loss_var loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() scheduler.step() if epoch % 5 0: val_rankic evaluate_rankic(model, val_loader, adj) print(fepoch {epoch}, loss {total_loss / len(train_loader):.4f}, val RankIC {val_rankic:.4f})三个loss的权重需要解释一下。方向分类是主任务权重最高回归的收益率次之波动率预测最难做准权重最低。这样设计多任务损失是希望模型优先保证方向判断的准确同时从幅度和风险任务中学习到额外的信息来辅助主任务。5. 金融时序任务最容易翻车的五个坑5.1 数据泄漏回测漂亮实盘崩的元凶金融时序任务里最致命的问题就是数据泄漏。回测结果非常惊艳一到实盘就原形毕露绝大多数情况都是特征里隐含了未来信息。常见的泄漏有几种一是特征标准化时用了全样本的均值和方差正确做法是只在训练集上fit然后应用到验证集和测试集二是滚动特征计算时窗口跨越了标签区间比如用未来第5天的数据去算当前的特征三是行业相关性矩阵用了包含预测窗口未来数据的整个区间来计算。我在项目里特意加了一个防泄漏的检查函数对每条特征单独验证它的计算时间点是否在标签时间点之前。每次做特征更新后都要跑一遍这个检查确保万无一失。排查方法也分享一下把训练和测试的RankIC曲线放在一起看如果测试集提升异常明显或者训练集上模型预测值偏大基本就是泄漏了。还有一个土办法把标签整体往后随机错位5天如果模型还能拿到很高的准确率说明特征里肯定有问题。5.2 标签设计失衡方向预测总在“复读”昨天另一个常见问题是模型输出的方向预测集中在某个类别上比如80%的预测都是“上涨”。原因不一定是模型坏了而可能是标签设计有偏。沪深300成分股整体走势跟指数高度相关如果大盘处于上行趋势样本里“未来5日上涨”的比例本来就偏高模型学到“猜上涨、赔率高”是很自然的。这时候单纯看分类准确率没有意义因为就算全部预测上涨准确率可能也有55%。我的解决思路是改用“相对标签”不预测个股未来5日绝对收益方向而是预测它相对沪深300指数未来5日的超额收益方向。也就是说标签从“这只股票涨不涨”变成“这只股票跑不跑得赢指数”。这样一来市场系统性涨跌的影响被剥离掉了模型被迫去学习真正的横截面选股信息。这个改动对后续策略开发的效果提升非常明显。5.3 预测滞后模型为什么会慢半拍时序模型普遍存在预测滞后的问题模型的预测曲线明显落后于真实价格曲线涨的时候慢半拍跌的时候也慢半拍。这在本质上是因为模型用了大量历史信息做平滑天然倾向于预测“惯性延续”。缓解滞后有几个实操手段。第一在特征里加入近期收益率的差分项相当于给模型一个“加速度”信号让模型能更快感知趋势变化第二缩短回看窗口比如从60天缩短到40天让模型更关注近期信息第三在损失函数里加大近期样本的权重但这需要谨慎使用否则会牺牲长周期稳定性。滞后问题不可能完全消除目标是把滞后期压缩到1到2个交易日以内。如果滞后期太长趋势跟踪策略就很难做出正收益。5.4 过拟合与图结构失效模型刚上线就变菜金融数据的信噪比极低深度学习模型极易过拟合。一个典型的信号是训练集RankIC高达0.1以上验证集却只有0.02甚至更低。我控制过拟合的手段主要有几个模型参数量刻意控制在较小的规模hidden_dim只用64Dropout统一设为0.2并且在图卷积和时间卷积之后都加使用早停法验证集性能连续不提升就立即停止多任务联合训练本身也是一种正则化手段让模型学到的表征更通用。图结构失效是另一个隐蔽的问题。市场风格切换后之前算出来的相关性矩阵可能不再适用模型在旧图上训练出来的空间聚合模式就失去了意义。我后来在系统里加入了一个图结构监控模块定期跟踪训练集和验证集的相关性矩阵分布差异当差异超过阈值时触发图结构和模型的定期重训练。5.5 训练稳定性与显存优化训练STGNN模型还会遇到一些工程层面的问题。最常见的是梯度爆炸金融序列的极端值比如某天暴涨暴跌很容易导致loss瞬间变大。解决办法就是梯度裁剪我一般设max_norm1.0几乎不会出错。显存方面沪深300成分股有370只如果再加上时间维度和特征维度一个batch的数据量其实不小。邻接矩阵要用稀疏矩阵存储或者直接用CPU上的稠密矩阵参与einsum运算避免把[370, 370]的邻接矩阵复制到GPU上重复计算。另外混合精度训练AMP也能明显减少显存占用。6. 从预测结果到量化选股信号生成、回测与策略落地6.1 信号生成与选股组合RankIC比准确率更值得关注模型训练好之后预测结果不会直接变成买卖信号中间还需要一层转换。我的做法是在每一个调仓日用模型对全部沪深300成分股输出未来5日的预测值然后按预测的预期超额收益从高到低排序取前20只股票等权买入。这个逻辑本质上是一种横截面选股核心评估指标不是准确率而是RankIC——也就是预测排名与未来真实收益排名的秩相关系数。为什么RankIC比准确率重要因为选股策略只关心排序前20名是否真的跑赢了后20名。如果模型对每只股票的预测值都偏高但相对排名大致正确RankIC就会比较高策略依然能赚到超额收益。反过来如果准确率很高但排序混乱策略照样赚不到钱。RankIC的绝对值在0.03到0.05之间在A股日线级别已经算不错的效果了。金融时序的信噪比决定了你不能指望像图像分类那样动辄90%以上的准确率。6.2 回测框架与评估指标不要只看总收益率回测框架我建议直接用开源的backtrader或者vectorbt也可以自己写一个简单的向量化回测。核心要关注四类指标第一类是收益指标包括累计收益率、年化收益率、超额收益相对沪深300指数。第二类是风险指标包括最大回撤、年化波动率。第三类是风险调整收益指标最常用的是夏普比率和卡玛比率年化收益除以最大回撤。第四类是交易统计包括交易次数、胜率、盈亏比、平均持仓周期。我只想说一个常被忽略的点不要只看总收益率。很多策略总收益很好看但最大回撤高达30%普通人根本拿不住。我评价策略的第一个指标永远是最大回撤和卡玛比率这两个指标过不了关的策略收益再高也不会上实盘。6.3 策略落地中的成本与风控细节从回测到实盘最难过的一关是交易成本。A股的实际交易成本包括佣金万1.5到万3、印花税卖出时千分之一、滑点和冲击成本。对于日频调仓策略这些成本加起来可能吃掉相当一部分超额收益。我的建议是回测时至少按单边千分之二到千分之三来模拟成本如果策略在这个成本假设下依然能跑出超额收益才值得进一步考虑。另外还要注意A股的交易制度约束T1制度意味着当天买入的股票不能当天卖出涨跌停时可能无法按模型信号成交停牌股票无法交易。这些约束如果不在回测中处理实盘结果会和回测有巨大出入。风控层面我一般会加一个行业中性化的约束选出的20只股票单一行业占比不超过30%避免整个组合变成行业赌注。同时设置个股最大权重上限防止某只股票的极端行情主导整个组合。这些做下来策略才算真正具备了落地的可能。最后再说一点我的个人体会。STGNN这套系统跑下来我发现模型架构本身不是最大的难点真正决定效果上限的是图结构怎么构建、特征怎么设计、标签怎么定义。同样的模型换一套更合理的相关性矩阵构造方式效果可能差一个档次换一种标签定义方式策略逻辑可能就完全不同。图神经网络给金融预测带来的真正价值不是“深度学习很厉害”而是它逼着你去思考和建模股票之间的关联结构这个思考过程本身就是最大的收获。回测结果只能代表历史规律市场永远在变化没有哪个模型能一劳永逸。关键是建立一套可以持续迭代的流程数据更新、模型重训、图结构重建、策略回测形成闭环系统才能跟上市场的变化。这套系统后续还可以往分钟级高频数据、另类数据舆情、资金流等方向扩展每一步扩展都是新的研究空间。本文还有配套的精品资源点击获取
返回列表