ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

期货价格预测实战:相关性分析筛选特征后构建CNN-Attention-LSTM模型

期货价格预测实战:相关性分析筛选特征后构建CNN-Attention-LSTM模型 简介基于相关性分析的CNN-Attention-LSTM期货价格预测模型是一套面向深度学习、量化金融方向学生和开发者的完整项目包适用于毕业设计、课程设计及期货价格预测实践。项目将相关性分析用于特征筛选结合CNN、注意力机制与LSTM进行时序建模代码结构清晰、注释详尽从数据预处理、时间步处理到模型训练和预测均有可复现脚本可直接运行复现。压缩包共29个文件主要包含8个Python源码模型定义、训练、预测及API封装、6个numpy数值文件训练/测试数据及预测结果、3个Excel数据表原始周报、相关性分析用表及处理结果另附模型权重checkpoint、SQL源数据、PDF使用教程和Web前端配置说明整体30.29MB。目前已吸引809人学习浏览资源内既有完整数据集和训练好的模型也有分步说明文档适合需要快速上手、对比实验或进一步改进模型的读者。1. 基于相关性分析的 CNN-Attention-LSTM 期货价格预测为什么先做特征筛选比调模型更重要一个做期货量化的朋友跟我抱怨说他把 LSTM 的层数从 1 加到 4又试了各种 dropout回测结果还是上不去。我帮他把输入数据拉出来看了一眼发现他一次性塞进去 60 多个因子其中一半跟未来收益的相关系数绝对值不到 0.02纯噪声。这就是很多人在「基于相关性分析的 CNN-Attention-LSTM 期货价格预测模型」上翻车的起点模型结构再先进喂进去的是垃圾特征出来的自然只有玄学。这个方案的核心思路是先用相关性分析把特征压缩到有效范围再用 CNN 提取局部价格形态用 Attention 标注关键历史时刻最后交给 LSTM 处理长程依赖。适合已经能熟练操作 DataFrame、想从单模型预测走向结构化建模的量化入门者也适合手里有行情数据但不知道如何组织特征时序的 Python 工程师。2. 相关性分析这一步在过滤什么特征与未来收益之间的真实联系2.1 为什么必须先做相关性分析再做模型期货价格序列本身是非平稳的螺纹钢十年前的价格和今天没有直接可比性但它的收益率序列却有一定的统计规律可以挖掘。相关性分析在这里要解决的不是哪些因子对当前价格影响大而是当前能观测到的因子和未来一段时间的收益有没有稳定关联。这是两个完全不同的命题很多复现失败的团队就是栽在把两者混为一谈。价格受宏观、产业、资金情绪等多重因素驱动直接拿价格列去拟合模型学到的往往是紧跟最近一段趋势这种没有泛化能力的短期记忆。把价格转成收益率、振幅、成交量变化率这类派生特征后再做相关性筛选才能让模型输入里的每一列都有明确的预测语义。这一步做完后面 CNN-Attention-LSTM 才是在真正学习模式而不是在记忆价位。相关分析筛选因子的标准做法是计算每个特征与未来收益的皮尔逊相关系数和 p 值再结合滞后相关性判断因子的有效周期。注意这里分析的对象是收益率序列不是价格序列。价格序列的自相关极高做出来的相关系数几乎没有意义换成 pct_change 之后再去计算才是正确姿势。2.2 用 pandas 计算特征与未来收益的相关系数代码和判定阈值下面这段代码是相关性分析的最小实现输入是一份常见的主连日线数据包含 open、high、low、close、volume 五列。我以螺纹钢主力连续合约的日线为例但换成豆粕、铁矿、PTA 都一样。import pandas as pd import numpy as np # 读取日线数据date 列解析成时间类型后排序 df pd.read_csv(rb_daily.csv, parse_dates[date]) df df.sort_values(date).reset_index(dropTrue) # 构造未来 5 日收益率作为预测目标shift(-5) 是关键 df[ret_future_5] df[close].pct_change(5).shift(-5) # 候选特征多周期历史收益率、成交量变化、振幅等 for n in [3, 5, 10, 20]: df[fret_{n}] df[close].pct_change(n) df[vol_ratio] df[volume] / df[volume].rolling(5).mean() df[range_ratio] (df[high] - df[low]) / df[close] df[close_pos] (df[close] - df[low].rolling(20).min()) / ( df[high].rolling(20).max() - df[low].rolling(20).min() ) # 把所有特征和未来收益放在一起算相关矩阵 feats [ret_3, ret_5, ret_10, ret_20, vol_ratio, range_ratio, close_pos] df df.replace([np.inf, -np.inf], np.nan).dropna(subsetfeats [ret_future_5]) corr df[feats [ret_future_5]].corr()[ret_future_5].drop(ret_future_5) pvals df[feats].apply(lambda col: np.nan) # 占位下面用 scipy 计算上面只打印了相关系数p 值可以用 scipy 快速补上逻辑是逐个特征与目标列做相关检验。判定阈值是这类分析最容易引起争论的地方金融时间序列的相关系数天然比理工科实验数据小得多绝对值到 0.1 已经算是有预测力的特征。更靠谱的筛选标准是三个条件同时满足相关系数绝对值大于 0.05、p 值小于 0.05、相关系数符号在三个不同时间段子样本上保持一致。最后一个条件能过滤掉一大批靠运气出现的伪相关。2.3 滞后相关与滚动相关相关性分析的两个容易踩错的细节滞后相关性回答的是这个因子领先未来收益几天。比如成交量放大往往意味着情绪推升但它领先的周期可能是 2 天也可能是一周。计算方式是把特征列 shift(-k) 之后与未来收益求相关得到一个随 k 变化的相关性曲线。这个曲线有两个用途一是给滑窗长度定参考二是判断因子的衰减速度。滚动相关是用来检查稳定性的把样本按时间切成 5 到 8 段每段单独计算相关系数。如果某个因子在最近一段回撤期相关系数符号反转说明它的预测逻辑只在特定行情下成立引入模型会增加不确定性。我一般会做一张因子相关性热力图先删除两两相关性超过 0.7 的冗余因子再按与目标的相关性排序挑选 8 到 15 个进入模型。特征太多容易让 Attention 的权重分散太少则容易丢失有效信息这个范围是经验值具体品种可以微调。另一个容易翻车的细节是样本量。相关性分析至少需要 500 根以上的日线样本才有统计意义如果你用小时线或者分钟线样本量够但行情结构变化快最好按年份分段验证。注意不要只看全样本的相关性有一种情况是某个因子只在特定年份强相关把周期拉长后整体相关性被稀释到接近零这种因子进了模型就是黑匣子里的一团噪声。3. CNN-Attention-LSTM 结构拆解三种模块各自解决什么预测难题3.1 CNN 卷积部分用一维卷积提取价格形态的局部特征CNN 在这里用的是 Conv1d处理的是已经按时间排列好的特征序列。和图像分类里的二维卷积不同一维卷积核是沿时间轴滑动的每一小段窗口内的局部形态会映射成一个特征值。这种设计背后的直觉是三根 K 线组合成的形态往往比单根 K 线更有预测意义。比如连续三根缩量阳线和一根放量长阳在网络眼里应该是不同的抽象模式。卷积核大小就是看多长一段局部历史。核设为 3 表示一次只看 3 个时间步设为 5 就是 5 天。我之前做过对照组实验kernel_size 从 3 加到 7验证集精度先升后降说明窗口太大反而把局部模式抹平了。常见做法是叠两层 Conv1d第一层核小提取细粒度形态第二层核稍大在时间上做聚合。卷积之后接 BatchNorm 和 ReLU这部分务必加上否则网络很难收敛。这里有一个经常被忽略的细节Conv1d 的输入形状是 (batch, channels, length)channels 对应特征维度length 对应时间步。也就是说把特征当成通道数把序列长度当成卷积的宽。这和 NLP 里把词向量通道化的思路完全一致。处理前把输入从 (batch, seq_len, features) 转置成 (batch, features, seq_len)出来后记得转回去不然下一步 Attention 的时间维度和特征维度会错乱。3.2 Attention 注意力模块对历史时间步做加权取舍Attention 在序列模型里的含义很简单让网络自己学会重点关注哪几天。期货价格历史中不是每一个时间点对预测未来都同等重要某个关键突破日的信息密度可能顶得上之后十天的平淡行情。传统 LSTM 会把全部历史信息压成最后一个隐藏状态长距离信息在传递过程中不可避免地被稀释Attention 就是来纠正这个缺点的。具体做法是给每个时间步打分然后把分数过 softmax 归一化成权重用权重去缩放该时间步的特征向量。打分函数常见的是一个两层全连接加 tanh 激活输出一个标量。这里有一个参数需要注意打分函数的分值绝对值不宜过大否则 softmax 之后权重会退化成接近 one-hot把所有历史信息集中到单一天模型的鲁棒性反而下降。做法是在打分输出上乘一个缩放系数或者直接对分值做归一化。Attention 的输出有两种喂法。一种是把加权后的序列直接拼给 LSTM 的输入让 LSTM 在被强调过的历史基础上继续建模另一种是把加权后的向量作为上下文向量拼到 LSTM 最后一步的隐藏状态上再过全连接。前一种保留了时间结构后一种更省显存。我习惯用前一种因为期货价格的先后顺序对预测结果影响很大不应该在 Attention 这一步就把时间轴压缩掉。3.3 LSTM 承接长程依赖张量形状如何逐层流转LSTM 作为最后一个序列模块处理从 Attention 出来的加权序列。LSTM 的优势在于门控机制它能在几十步的时间跨度里保留关键信息同时主动遗忘无关内容。但很多刚接触 lstm 时间序列预测 python 实现的人容易把 LSTM 层数堆得很高反而导致过拟合和训练变慢。一个够用的配置是两层 LSTMhidden_size 在 32 到 64 之间dropout 设在 0.2 左右。数据集只有几千个交易日的话hidden_size 超过 128 就是明显的资源浪费参数量和样本量完全不匹配。张量形状的流转按这个顺序走原始输入是 (batch, seq_len, features)转置成 (batch, features, seq_len) 进 CNN出来后恢复成 (batch, seq_len, cnn_feature_dim)过 Attention 打分得到权重序列并加权送入 LSTM 得到 (batch, seq_len, lstm_hidden)取最后一个时间步的输出过一个全连接层得到预测值。有人会把 Attention 放在 LSTM 之后那是给 LSTM 输出做重要度加权不是本模型的顺序。标题里的顺序是 CNN-Attention-LSTM前面的特征提取得越干净LSTM 的长程依赖建模就越省力。整个模型的输入输出形状设计可以先用假的随机张量跑一遍 forward 确认维度再开始训练这能避免 80% 的张量不匹配报错。4. 复现完整流程数据清洗、滑窗构造与训练脚本4.1 数据集去重与换月处理拿到日线数据后的第一件事拿到一份 CSV 格式的日线数据第一件事不是直接算特征而是检查数据质量和合约连续性。常见问题包括同一时间戳出现多行重复数据、交易日期缺行、以及主力合约换月导致的跳空。最后一类问题最棘手因为期货主力合约到换月日会从旧合约跳到新合约收盘价可能瞬间跳变几个百分点如果不处理这个跳空会被模型当成一个真实的极端行情记录下来。换月处理的常见做法是合约拼接前做后复权把新合约的价格按换月日的比例调整到与旧合约连续。如果用的是交易所或数据供应商已经拼接好的主力连续合约也要确认价格序列在换月日没有异常波动。我在处理时通常先做一次去重再按时间排序检查相邻两根 K 线的涨跌幅如果出现超过该品种涨跌停板的幅度就重点看一下那一天是不是换月日。4.2 滑窗样本构造X 与 y 的时间对齐规则滑窗构造是整个流程里最容易引入未来函数的地方。假设我们要预测未来 5 日收益的方向输入窗口用过去 30 天的特征序列那么第 t 个样本的输入必须只包含 t-30 到 t 时刻的数据标签由 t1 到 t5 的收益构造。看起来很简单但代码里稍不留神就会让输入行和标签行错位。import numpy as np import torch def make_samples(df, feat_cols, seq_len30, horizon5): data df[feat_cols].values future_ret df[ret_future_5].values xs, ys [], [] for i in range(len(df) - seq_len - horizon 1): # 输入窗口[i, iseq_len)标签窗口结束后才算目标 x data[i: i seq_len] y future_ret[i seq_len - 1] # 与窗口最后一天对齐 xs.append(x) ys.append(y) return np.array(xs, dtypenp.float32), np.array(ys, dtypenp.float32) seq_len 30 horizon 5 x_arr, y_arr make_samples(df, feats, seq_len, horizon) # 输出形状(样本数, 30, 特征数) 和 (样本数,) print(x_arr.shape, y_arr.shape)上面的代码里future_ret 在数据预处理阶段已经用 shift(-5) 计算过所以第 i 行的 future_ret 表示的正是未来 5 天收益。取 iseq_len-1 行的值恰好是窗口最后一根 K 线时刻对应的未来收益时间对齐上没有错位。要注意的是样本尾部有 horizon 行数据会被浪费掉这是正常现象千万不要为了凑样本数把未来窗口的数据也拉进来。预测目标既可以是 5 日收益本身也可以是涨跌分类标签收益大于零为 1否则为 0后者在模型输出层只需一个带 sigmoid 的神经元。4.3 模型搭建与训练PyTorch 完整代码与超参表按 CNN-Attention-LSTM 的顺序搭模型下面是完整可运行的 PyTorch 实现。import torch import torch.nn as nn class CNNBlock(nn.Module): def __init__(self, in_channels, out_channels, kernel_size5): super().__init__() self.conv nn.Conv1d(in_channels, out_channels, kernel_size, paddingkernel_size // 2) self.bn nn.BatchNorm1d(out_channels) self.relu nn.ReLU() def forward(self, x): # x: (batch, features, seq_len) return self.relu(self.bn(self.conv(x))) class AttentionBlock(nn.Module): def __init__(self, embed_dim): super().__init__() self.score_layer nn.Sequential( nn.Linear(embed_dim, embed_dim), nn.Tanh(), nn.Linear(embed_dim, 1, biasFalse), ) def forward(self, x): # x: (batch, seq_len, embed_dim) scores self.score_layer(x) # (batch, seq_len, 1) weights torch.softmax(scores, dim1) # 沿时间步做归一化 return x * weights, weights # 加权序列 权重备份 class CNN_Att_LSTM(nn.Module): def __init__(self, n_features, conv_hidden64, lstm_hidden32, n_layers2, dropout0.2): super().__init__() self.conv1 CNNBlock(n_features, conv_hidden, kernel_size5) self.conv2 CNNBlock(conv_hidden, conv_hidden, kernel_size3) self.attention AttentionBlock(conv_hidden) self.lstm nn.LSTM(conv_hidden, lstm_hidden, n_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(lstm_hidden, 1) def forward(self, x): # 输入 (batch, seq_len, n_features) x x.transpose(1, 2) # (batch, n_features, seq_len) x self.conv1(x) x self.conv2(x) x x.transpose(1, 2) # (batch, seq_len, conv_hidden) x, attn_weights self.attention(x) out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步 return self.fc(out).squeeze(-1)模型里的超参数存在一套相对稳妥的默认值适合几千行日线数据量conv_hidden 设为 64lstm_hidden 设为 32attention 打分用两层全连接。LSTM 的 dropout 参数在 n_layers 大于 1 时才会生效单层 LSTM 内部不会做 dropout这一点经常让人困惑。模型输出的是一维标量训练时配合 MSE 损失用于收益回归如果做涨跌分类把输出改成一个神经元加 sigmoid配 BCEWithLogitsLoss 即可。4.4 训练循环与早停看两条曲线决定何时停止训练循环里最重要的是验证集的处理和早停机制。数据划分按时间顺序前 70% 训练、中间 15% 验证、最后 15% 测试不能用随机划分否则未来数据会混进训练窗口。早停的阈值我一般设 patience15即连续 15 轮验证 loss 没有创新低就停止训练并保存历史最优模型。best_loss float(inf) patience 15 wait 0 for epoch in range(100): model.train() train_loss 0.0 for xb, yb in train_loader: opt.zero_grad() pred model(xb) loss loss_fn(pred, yb) loss.backward() opt.step() train_loss loss.item() * xb.size(0) model.eval() val_loss 0.0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss loss_fn(pred, yb).item() * xb.size(0) val_loss / len(val_loader.dataset) print(fepoch {epoch1}: train_loss{train_loss/len(train_loader.dataset):.4f} fval_loss{val_loss:.4f}) if val_loss best_loss: best_loss val_loss wait 0 torch.save(model.state_dict(), best_model.pt) else: wait 1 if wait patience: print(early stop triggered) break训练循环里故意输出了每轮的 train_loss 和 val_loss这两条曲线是判断模型状态的核心依据。训练 loss 持续下降、验证 loss 也跟着降是健康状态训练 loss 降但验证 loss 开始回升说明开始过拟合早停应该在回升初期就触发。学习率建议从 1e-3 起步如果验证 loss 震荡明显调到 5e-4 或 3e-4。优化器用 Adambeta 默认值即合理不需要刻意调。整个训练在 CPU 上也能跑几千个样本的网络分钟级就能收敛不一定非得上 GPU。5. 期货价格预测避坑指南5 个让模型翻车的典型问题5.1 未来函数导致的标签泄露现象模型在训练集上 loss 低得惊人验证集上却完全失效甚至方向准确率不到 50%。原因标签构造时没有严格遵守时间对齐。最常见的错误是先用整个数据集计算了归一化参数然后再构造滑窗样本导致每个样本里混入了窗口之后的信息另一种错误是计算未来收益时用到了 shift(-1) 但 dropna 位置不对让最后几行样本的标签错误。解决严格按先划分时间段、再在训练段内计算特征和标准化参数的顺序执行。滑窗构造代码里确认输入窗口的最后一行时间戳和标签对应的时间点之间没有数据重叠。每构造一个样本打印一次输入最后一天和标签对应日期人工抽查 10 个样本这是最快排查未来函数的方法。5.2 归一化范围泄露现象训练和验证用同一个 StandardScaler 实例的 fit_transform验证指标看着不错实盘却一塌糊涂。原因标准化时用全量数据的均值和方差去缩放验证集这相当于把验证集的统计信息提前告诉了模型属于一种常见的数据泄露。验证集在现实中是未知的scaler 应该在训练集上 fit再用这个固定的均值和方差 transform 验证与测试集。解决把 sklearn 的 StandardScaler 拆成两个步骤train_scaler.fit(x_train)然后 train_scaler.transform(x_train) 和 train_scaler.transform(x_val)。仅在训练集上 fit 会让验证 loss 看起来略差那是真实水平不用焦虑。这个改动是区分回测可信度和纸面结果的分水岭。5.3 相关性分析里隐藏的幸存者偏差现象筛选出的因子在历史回测里很有效但进入实盘后衰减很快甚至出现反向波动。原因在用相关性分析筛选因子时如果只基于整个样本区间的一次性计算选出来的因子可能恰好和这段特殊行情吻合。比如某品种在一波大牛市中所有动量因子都表现抢眼后面的震荡行情里动量因子就会集体失效。解决按年度或按牛熊阶段做分段相关性检验只保留在各段方向上一致的因子。相关性的稳定性比绝对值更重要一个相关系数保持 0.06 十年的因子比一个有时 0.4 有时 -0.2 的因子更值得信任。这一步做完后面模型过拟合的压力会小很多。5.4 换月跳空与涨跌停对序列的冲击现象训练过程中某些批次的 loss 剧烈跳变或者模型在测试集上对某几天的预测误差特别大复盘发现都指向同一天。原因主力合约换月日的价格跳空被当成真实价格变动模型产生了不存在的巨额涨跌幻觉涨跌停那天行情被锁死价格波动幅度受限但次日如果连续涨停收益率会异常放大。解决换月日的数据要么剔除要么用后复权方式修正。涨跌停当日和次日各加一个是否涨跌停的 0/1 特征让模型显式感知这种特殊状态而不是把异常波动归因到正常行情里。这个处理对国内期货市场尤其重要黑色系和农产品里涨跌停并不罕见。5.5 训练 loss 下降但验证 loss 背离的过拟合现象train_loss 稳步降到 0.01 以下val_loss 在第三轮之后就持续反弹模型输出的预测值几乎全是极端值。原因网络参数量相对样本量太大模型直接记忆了训练样本的噪声。CNN-Attention-LSTM 三个模块加在一起参数量不小几千条样本想喂饱它本来就不现实。解决优先减小 lstm_hidden 和 conv_hidden而不是加 dropout。从 32 和 64 起步要是还过拟合就继续降到 16 和 32。同时把训练 epoch 限制在 50 以内配合早停。还有一种容易忽略的增强手段是给标签做平滑比如把硬 0/1 分类标签改成 0.9/0.1能缓解模型对样本边界过度自信的问题。6. 从模型输出到可交易信号滚动训练与仓位映射的进阶验证6.1 滚动训练代替固定划分一次性划分训练集和测试集只能验证模型在某一时段的泛化能力离真实交易场景还有距离。真实环境下模型应当每天或每周用最新数据重新训练或微调滚动窗口训练就是模拟这个过程从起点取固定长度窗口训练预测后面一段然后窗口向前滚动重复到序列末尾。这样得到的回测曲线包含了几十次独立的训练和预测更有说服力。每次滚动训练时早停参数可以保持一致数据量小就缩短窗口长度保证每次训练至少有两年的日线数据。6.2 预测概率到仓位的映射回归模型的输出值本身不能直接下单需要转换成信号。常见做法是把预测收益除以预测标准差得到一个 z-score再映射到仓位分类模型则用 sigmoid 输出概率方向正确率超过 52 到 55% 才值得开仓因为手续费和滑点会吃掉一部分优势。仓位比例可以用简单线性映射也可以按预测概率的置信区间分段设置。这条链路走通之后这套代码已经接近一份可实盘的 python 量化交易策略代码的骨架。6.3 参数搜索放在滚动窗口内进行网格搜索最优参数的时候如果把整个历史数据都用来搜索然后再滚动回测验证就已经引入了前视偏差。我自己的做法是把参数搜索限制在每个滚动窗口的训练段内部用该段的验证 loss 选参数然后在下一段行情上验证。每次窗口重新搜索一遍计算量不小但换来的是更真实的结果。这个环节体现了很多人对开源模型复现的不满来源他们复现出来的效果不如论文或说明文档展示的好不是模型不行而是验证方式里掺了过多的历史数据水分。参数波动本质上是行情状态的反映接受它而不是用全样本最优参数把它掩饰过去。这篇笔记牵涉的相关性筛选和模型调参仍然有大量依靠经验和试错的部分希望帮到你少走几段弯路。本文还有配套的精品资源点击获取
返回列表