ARTICLE DETAIL

资讯详情

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

CNN-Attention-LSTM期货价格预测:从特征筛选到滚动回测全解析

CNN-Attention-LSTM期货价格预测:从特征筛选到滚动回测全解析 简介利用相关性分析的CNN-Attention-LSTM期货价格预测完整工程面向计算机相关专业学生毕业设计、课程设计或项目实战解决金融时间序列多因素预测难题。项目基于Python实现覆盖数据预处理、模型构建、训练评估与预测API等环节包含SQL/Excel原始数据、时间步处理脚本、CNN-Attention-LSTM核心代码及训练好的模型权重注释详尽并附PDF使用教程和热力图方便对照理解。压缩包共29个文件约30.28MB含8个py源码、6个npy数据、3个xlsx表格、2个模型权重、2个pdf文档等目录清晰可直接运行验证。已有81人学习浏览。通过该项目可掌握相关性分析方法筛选特征、理解注意力机制与LSTM在时序预测中的协同作用并学习完整深度学习项目组织方式为论文写作和答辩提供有力支撑。1. 用相关性分析给 CNN-Attention-LSTM 预测期货价格先划好数据边界期货价格预测的难点从来不是模型跑不出来而是喂给模型的列到底该怎么选。常见新手做法是把原油、美元指数、持仓量、资金流等几十个因子全部丢进 CNN-Attention-LSTM训练一轮就发现训练集拟合曲线漂亮得像教科书换到验证集却把方向全猜反。问题大多不在注意力度量失效而在特征里混进了太多跨期信息或者预处理时把未来统计量写进了历史样本。更隐蔽的是直接用价格本身做特征会带来非平稳序列的伪相关网络学到的是绝对值趋势而不是波动结构。所以先把数据边界用相关性分析划清楚再谈模型结构才有意义。本文将按照做期货日频或高频预测时最常见可靠的一条路线展开用 Spearman 相关性分析筛选与下一期收益率有单调关系的因子生成固定时间步长的滑窗样本再用带注释的 Python 代码搭建 CNN-Attention-LSTM最后给出滚动回测和方向准确率的验证方式。整个过程不依赖特定自建数据集只要把数据整理成统一 DataFrame 结构就可以跑注释会覆盖每段代码在做什么、为什么这样做、改哪个参数会影响什么。2. Python环境准备整理期货数据集并用 Spearman 相关性分析筛选特征2.1 Python安装与依赖选择在开始写模型之前先确认 Python 解释器和 TensorFlow 版本匹配。按目前社区成熟组合Python 3.10 搭配 TensorFlow 2.10 或 2.15 都可以不要直接用 3.13 去跑老版本 Keras。如果机器是空白环境先按 python 安装教程装好 3.10然后在 vscode 里做 python 环境配置装好 Python 扩展用CtrlShiftP打开 Python: Select Interpreter选中新建的 conda 环境。conda create -n future python3.10 -y conda activate future pip install pandas numpy matplotlib tensorflow scikit-learn openpyxl这里需要解释一下兼容性TensorFlow 2.10 以后在 Windows 上不再支持 GPU 的原生编译如果读者用的是 N 卡建议把 TensorFlow 换到 Linux 环境本机只有 CPU 时上面的命令也能跑通但慢一些。openpyxl是为了读 xlsx 格式的行情导出文件scikit-learn用来做归一化和时序交叉验证。所有代码尽量控制在 pandas、numpy、TensorFlow 三件套内便于改写。2.2 数据集字段结构和避免未来函数为了让本文不依赖某个具体仓库我在这里约定一个通用期货特征表结构。假设你已经把原始行情整理成如下 CSV列名如下表所示列名含义说明datetime时间戳日频数据用日期高频数据用时间close收盘价主力连续合约或加权合约均可volume成交量通常取原始手数open_interest持仓量用于观察资金进出brent_close外盘原油收盘价可选外部因子usd_index美元指数可选外部因子rsi14RSI 指标由 close 计算得到原始数据里极少有现成的“目标标签”需要我们自己计算下一期收益。这里最容易踩的一个坑是未来函数如果直接用df[close].pct_change()得到的是从上一期到本期已经实现的收益用同一根 K 线的 close 去预测已实现的收益等于把答案写在特征里。正确做法是让标签对齐到当前时间戳的“下一期收益”。import pandas as pd df pd.read_csv(future_daily.csv, parse_dates[datetime]) df df.sort_values(datetime).reset_index(dropTrue) # 先处理收益率特征原始价格列不直接进模型 df[close_ret] df[close].pct_change() df[volume_ret] df[volume].pct_change() df[oi_ret] df[open_interest].pct_change() # 外盘因子如果存在同样做收益率处理 for col in [brent_close, usd_index]: df[f{col}_ret] df[col].pct_change() # 目标下一期收益率用 shift(-1) 对齐到当前行 df[target] df[close].shift(-1) / df[close] - 1.0 # 去掉因为 pct_change 和 shift 产生的 NaN df df.replace([float(inf), float(-inf)], float(nan)).dropna()代码里有两个关键点pct_change()默认分母是上一期所以close_ret当前行表示本期的涨跌幅可以作为特征而target用了shift(-1)让当前行的标签成为未来一期相对当前期的收益。训练时模型确实是在用 t 时刻信息预测 t1 结果不存在信息穿越。但需要记得dropna()之后最后一行必然被删掉因为该行的 target 是 NaN真正的样本数是全表格减掉一个滑窗长度加一。2.3 用 Spearman 相关性矩阵挑选和 target 真正相关的因子为什么期货数据里常用 Spearman 而不是 Pearson因为收益率的分布往往是厚尾且很多技术指标与收益之间的关系并不是直线。Spearman 相关性分析计算的是等级相关不要求正态分布可以更稳健地找出“单调关系”。这一步是在为 CNN-Attention-LSTM 筛选输入特征如果因子和目标的关系接近随机噪声模型只能依靠记忆训练集来拟合泛化能力会很差。import seaborn as sns import matplotlib.pyplot as plt feature_cols [close_ret, volume_ret, oi_ret, rsi14, brent_close_ret, usd_index_ret] corr df[feature_cols [target]].corr(methodspearman) # 取出 target 与各因子之间的相关性绝对值 corr_with_target corr[target].drop(target).abs().sort_values(ascendingFalse) print(corr_with_target) # 画热力图便于观察因子之间的共线性 plt.figure(figsize(8, 6)) sns.heatmap(corr, annotTrue, cmapcoolwarm, fmt.2f) plt.title(Spearman Correlation Matrix) plt.show()这段代码里corr(methodspearman)直接输出 Spearman 系数。注意不要在相关系数大于 0.8 的两个因子里同时保留两个因为 CNN 的卷积核会放大这种共线性注意力机制也会把重复信息当作高优先级。我的经验做法是先按与 target 的相关性绝对值排序然后逐个查看因子间相关性如果两个因子相关系数超过 0.8保留与 target 相关性较高的一方。通常会保留 5 到 8 个特征就足够不需要把所有外盘数据都放进去。2.4 构造滑窗样本和按时间顺序划分数据集相关性分析完成后选出的特征列需要切成长度为time_steps的三维张量CNN 和 LSTM 才能处理。滑窗有两种常见做法一种是直接按时间步生成连续重叠窗口另一种是 stride 大于 1 的稀疏采样。高频数据里我一般用全重叠窗口因为一个品种的样本量就是几千根 K 线过度降采样会让训练样本不够。日频数据建议time_steps20相当于近一个月的交易日。import numpy as np from sklearn.preprocessing import MinMaxScaler selected_features [close_ret, volume_ret, oi_ret, rsi14, brent_close_ret, usd_index_ret] time_steps 20 X_list, y_list [], [] data df[selected_features [target]].to_numpy(dtypefloat) for i in range(time_steps, len(data)): X_list.append(data[i - time_steps:i, :len(selected_features)]) y_list.append(data[i, len(selected_features)]) X np.array(X_list) y np.array(y_list) # 保持时间顺序前 80% 训练后 10% 验证最后 10% 作为测试 split1 int(len(X) * 0.8) split2 int(len(X) * 0.9) X_train, y_train X[:split1], y[:split1] X_val, y_val X[split1:split2], y[split1:split2] X_test, y_test X[split2:], y[split2:] # 在训练集上拟合归一化器再用同一变换处理验证集和测试集 scaler MinMaxScaler() origin_shape X_train.shape X_train_flatten X_train.reshape(-1, origin_shape[-1]) scaler.fit(X_train_flatten) X_train scaler.transform(X_train_flatten).reshape(origin_shape) X_val scaler.transform(X_val.reshape(-1, origin_shape[-1])).reshape(X_val.shape) X_test scaler.transform(X_test.reshape(-1, origin_shape[-1])).reshape(X_test.shape)这段代码在切分窗口时没有打乱时间顺序因为金融时序一旦 shuffle未来信息就会混进训练集。归一化同样只在训练集上做fit验证集和测试集只是transform这是防止数据泄漏的基本要求。很多项目喜欢用scaler.fit_transform(X)处理全部数据看起来没毛病但测试集的均值和最大值已经参与了缩放预测阶段只要出现更大波动模型就会失真。滑窗生成的X形状是(样本数, time_steps, 特征数)这个顺序在 Keras 里正好是 Conv1D 和 LSTM 期望的输入格式。3. 搭建 CNN-Attention-LSTM 模型带着注释走一遍关键层3.1 按 CNN → Attention → LSTM 串接的理由期货行情序列里单看某一天的技术指标反应有限真正有预测能力的是“最近几个交易日的形态组合”比如放量滞涨、连续缩量回落、外盘和国内盘背离。CNN 的最大作用就是通过一维卷积核提取时间轴上的局部模式卷积核宽度设为 3 时每个局部窗口恰好覆盖三个交易日能够捕捉短期组合信号。Attention 的作用则是为每个时间步分配权重让模型在处理序列时自动放大与 target 相关程度更高的时刻。最后用 LSTM 建模时间依赖因为 Attention 权重只代表当前窗口内的静态重要性真正跨长周期的衰减记忆还是要靠门控循环单元。需要特别说明的是这里采用 CNN → Attention → LSTM 的顺序是严格遵循标题里的组合方向卷积输出先经过注意力加权再进入 LSTM。这样 LSTM 看到的不是原始特征而是在 CNN 和 Attention 双重过滤后对预测最有价值的时间步。如果某些实现把 Attention 放在 LSTM 之后做序列聚合那本质上等价于“先编码后加权”参数可以迁移但语义顺序不同调参时不要直接照抄。3.2 自定义 Attention 层与完整模型代码Keras 自带的Attention层默认会把时间维度的信息求和得到上下文向量而我们需要保留时间步序列以便继续输入 LSTM。所以这里实现一个简化的加性注意力层它对每个时间步计算一个标量权重然后用权重乘以原输入输出仍然是三维张量。import tensorflow as tf from tensorflow.keras import layers, Model, Input from tensorflow.keras import backend as K class TimeStepAttention(layers.Layer): def __init__(self, units32, **kwargs): super().__init__(**kwargs) self.units units def build(self, input_shape): # input_shape: (batch, steps, features) self.W self.add_weight( shape(input_shape[-1], self.units), initializerglorot_uniform, nameattention_w ) self.b self.add_weight( shape(self.units,), initializerzeros, nameattention_b ) self.u self.add_weight( shape(self.units,), initializerglorot_uniform, nameattention_u ) def call(self, x): # 将输入投影到 units 维空间 score K.tanh(K.dot(x, self.W) self.b) # (batch, steps, units) score K.sum(score * self.u, axis-1) # (batch, steps) alpha K.softmax(score, axis-1) # (batch, steps) alpha K.expand_dims(alpha, axis-1) # (batch, steps, 1) return x * alpha下面这段代码是完整的 CNN-Attention-LSTM 模型。为了让参数好理解我把time_steps和特征数都从数据准备阶段传进来避免硬编码。def build_cnn_attention_lstm(time_steps, n_features): inputs Input(shape(time_steps, n_features)) # CNN 模块局部特征提取 x layers.Conv1D(filters64, kernel_size3, paddingsame)(inputs) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.MaxPooling1D(pool_size2)(x) # Attention 模块对时间步加权不改变维度 x TimeStepAttention(units32)(x) # LSTM 模块捕捉序列依赖 x layers.LSTM(units64, dropout0.2, return_sequencesFalse)(x) # 输出层单目标回归到下一期收益率 x layers.Dense(16, activationrelu)(x) x layers.Dropout(0.2)(x) outputs layers.Dense(1)(x) model Model(inputs, outputs) return model model build_cnn_attention_lstm(time_steps20, n_featureslen(selected_features)) model.summary()这里几个设计点值得展开。卷积层的paddingsame保证滑窗维度不变否则 Attention 和后续 LSTM 需要重新计算时间步长度。BatchNormalization放在卷积和激活之间能够稳定训练过程尤其当 RSI 这类指标和其他因子数值范围差异较大时效果明显。MaxPooling1D 的pool_size2会将 20 个时间步压缩到 10 个减少 LSTM 的计算量但代价是损失部分短期连续信息如果数据集特别小建议去掉池化层让时间步保持为 20。3.3 必须说清楚的关键参数表期货价格预测的模型复杂性控制很关键参数不是越大越好。下表是我在日频主力合约数据上比较常用的初始参数适合几百到几千根 K 线的规模。参数初始值调整方向time_steps20观察窗口短适合捕捉短线反转长周期持仓可调到 40filters64卷积通道数特征少时降到 32特征多时提高到 128kernel_size31 到 2 容易高波动5 以上偏向周度趋势attention units32控制注意力投影空间过大会让注意力权重过拟合LSTM units64两层 LSTM 时第二层减半防止过拟合dropout0.2CNN 后不设LSTM 和最后 Dense 前各加 0.2batch_size32高频数据用 64日频样本少用 16 或 32learning_rate0.001Adam 优化器通常从 1e-3 开始max_epochs200配合早停使用不设固定 epoch参数调整的核心逻辑是如果验证集中方向准确率低于 52%先看是不是特征选择出了问题如果训练损失远低于验证损失再考虑减小 LSTM units 或增大 dropout。CNN 的卷积核数量对结果的影响没有 LSTM 隐层维度敏感所以不要一上来就盲目调大 filters。3.4 损失函数与评估指标符号期货收益率存在明显的极端值比如跳空高开、跌停、夜盘突发行情。直接用均方误差会让模型把注意力全放在少数极端样本上导致普通行情预测失真。这里建议用 Huber 损失它在误差较小时接近 MSE误差较大时接近 MAE能对离群值更稳健。model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losstf.keras.losses.Huber(delta0.5), metrics[tf.keras.metrics.MeanAbsoluteError()] )代码中的delta0.5是判断误差进入线性区的阈值。如果 label 已经是小数形式的收益率通常 0.1 到 1.0 之间比较合适如果直接预测价格需要把 delta 放大到几十甚至几百。这个值可以作为超参数记录在一起便于后期回测比较。4. 训练与回测早停回调、滚动预测和三个高频踩坑点4.1 训练代码和回调配置训练阶段必须同时配置早停和学习率衰减。期货数据样本量本身不大模型很容易在 20 个 epoch 内开始记忆训练集早停的前提是不要关掉 shuffle 吗注意Keras 的fit默认会把训练数据按 batch 随机打乱这在时序模型里本身没有问题因为样本来自滑窗窗口内部的时间顺序被保留了窗口之间的随机顺序不会破坏时间因果性。但验证集不能打乱。from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) checkpoint ModelCheckpoint( filepathbest_model.keras, monitorval_loss, save_best_onlyTrue ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size32, callbacks[early_stop, reduce_lr, checkpoint], verbose1 )EarlyStopping 的patience10表示连续 10 个 epoch 验证损失没下降就停止训练同时restore_best_weightsTrue会把模型恢复到验证集表现最好的权重而不是停留在最后一个 epoch。ReduceLROnPlateau 在验证损失停滞 5 个 epoch 后将学习率减半这个组合在日频期货上比固定学习率稳定很多。4.2 用滚动预测替换一次性预测拿到测试集后很多人直接把整个X_test扔进model.predict把输出和y_test对比。这种做法验证的是模型的拟合能力但和实盘操作方式并不一致。实盘交易中每一根新 K 线收盘后我们只能用最新的一段时间窗口去预测下一个 bar等新的真实数据产生后再把新窗口滚动起来。因此验证阶段也应该按时间顺序逐个预测。predictions [] last_window X_test[0].copy() # 第一个预测使用测试集第一个窗口 for i in range(len(X_test)): pred model.predict(last_window[None, :, :], verbose0)[0, 0] predictions.append(pred) # 将当前窗口的下一真实窗口滑进来 if i len(X_test) - 1: last_window X_test[i 1]注意这里没有把预测值塞回特征空间重新计算下一步因为 LSTM 不是自回归模型它的输入特征依赖的是真实观测值。滚动预测代码里X_test[i 1]直接用了下一个窗口的真实特征这等于假设每次预测都能在真实数据收盘后立刻执行符合实盘信号产生顺序。如果要是做多步预测可以用模型输出作为下一时间窗口的 target 更新特征但那样误差会累积需要单独讨论。4.3 三个容易做错的地方第一个坑是归一化泄漏。之前已经提到在全量数据上fit_transform很多教程这么写但期货数据在不同的市场状态下波动率差异极大。一旦测试集里包含了多年后的大行情极值训练集的 min-max 范围就会失守导致所有特征缩放后集中在 0 到 0.1 之间。保险做法是只对训练集每个特征做 min-max存储 scaler 并在模型保存时一起打包。第二个坑是相关性分析中混入了同步行情。比如用日频国内期货收盘价和当日 16:00 才更新的外盘原油收盘价对齐看起来有强相关但其实在当前 K 线未收盘前外盘数据还没产生真正可用的是前一日的 brent 收盘价而不是当日。因此在 prepare 数据阶段就要用shift(1)把外盘因子滞后一天保证所有特征在 t 时刻都是可观测的。第三个坑是时间序列交叉验证的使用方式。K 折随机打乱会把前年的样本和去年的样本混在一起网络会通过学习价格水平而非波动规律来预测。正确做法是使用TimeSeriesSplit每一折都按时间顺序切分并且需要把归一化器重新 fit 到每一折训练集上。如果只想验证最终模型用 80/10/10 的简单顺序划分已经足够。5. 结果验证方向准确率比 RMSE 更容易暴露过拟合5.1 计算方向准确率和收益相关性回归模型的评估指标通常看 MAE、RMSE但期货交易更关心预测方向和真实方向是否一致。一个模型可能在数值误差上表现很好却几乎把所有涨跌方向都预测成 0 附近的小幅度波动这种模型对交易没有帮助。方向准确率的定义是predictions * y_test 0的比例收益相关性则是预测值与真实收益的 Spearman 相关性。import numpy as np from scipy.stats import spearmanr y_pred np.array(predictions).flatten() y_true y_test.flatten() mae np.mean(np.abs(y_pred - y_true)) rmse np.sqrt(np.mean((y_pred - y_true) ** 2)) direction_acc np.mean((np.sign(y_pred) * np.sign(y_true)) 0) corr, p_value spearmanr(y_pred, y_true) print(fMAE: {mae:.6f}) print(fRMSE: {rmse:.6f}) print(fDirection Accuracy: {direction_acc:.2%}) print(fSpearman Correlation: {corr:.4f})输出结果里有一点需要警惕如果方向准确率超过 58%而样本量只有几百先不要高兴去检查测试集最后一段是否出现了极端趋势行情。因为单边上涨时模型只需要预测正方向就能获得高准确率。这种虚高通常伴随着corr提高但p_value很大说明预测值和真实收益之间的相关性并不显著。要补充一个更严格的分位检验把预测按大小分成五组看每组的真实收益率均值是否有单调递增趋势这比单一相关系数更能说明排序能力。5.2 用可视化对比预测和真实收益验证模型的最后一步是画图把预测曲线和真实收益率叠在一起看。不要直接画价格曲线价格有强自相关性预测值和真实值都会表现出高度同步看起来像拟合得很好实际是因为两者都继承了共同的趋势成分。画收益率对比图才看得出模型是否把握住了方向。import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(y_true, labelTrue Return, alpha0.8) plt.plot(y_pred, labelPred Return, alpha0.8) plt.axhline(y0, colorgray, linewidth0.8) plt.legend() plt.title(True vs Predicted Return on Test Set) plt.show()从这张图里重点找两个现象一是真实收益峰值处预测值是不是严重钝化如果是说明 Huber 的 delta 设置过大或者 LSTM 隐层容量不足二是预测值是否几乎为零只在零点附近抖动这是回归模型最常见的问题通常需要增大 batch size 或降低 dropout 来让梯度更稳定。还有一个实用技巧把方向准确率按周维度进行滚动计算比如每周 5 个交易日滚动求窗口内的方向一致率如果某些连续窗口准确率低于 30%说明模型在这段时间的预测系统性反向可以考虑在策略层加上预测置信度过滤而不是无条件跟随输出。本文还有配套的精品资源点击获取
返回列表