
简介面向金融时序预测与深度学习实战的完整项目包聚焦CNN、Attention与LSTM融合模型在期货价格预测中的应用适合计算机相关专业学生用于毕业设计、课程设计或项目实战。压缩包共29个文件约30.28MB涵盖8个Python脚本含数据预处理、相关性分析、时间步处理、模型定义、训练与预测等模块、6个NumPy数据文件、3个Excel数据表以及训练得到的模型权重文件同时附带PDF使用教程、README说明与可视化热力图方便快速上手。项目包含详细注释清晰展示从相关性分析、特征提取到模型训练与预测的完整流程步骤衔接自然注释帮助理解时序数据建模和注意力机制的实际应用。已有81人学习对于希望深入掌握深度学习在金融领域应用的学生颇具参考价值既能作为毕设起点也可用于扩展复现和算法改进。1. 先搞清楚相关性分析 CNN-Attention-LSTM 能解决期货预测的什么问题一套能跑通的期货价格预测方案核心往往不在模型有多花哨而在数据怎么筛、时序怎么切、损失函数怎么定义。用相关性分析 CNN-Attention-LSTM 做期货价格预测思路是把几十个候选技术指标先用 Spearman 相关性分析压缩到少数有效特征再用 CNN 抓局部K线形态用 Attention 决定哪些时间步值得重点看最后由 LSTM 兜住长周期依赖。这套组合适合已经把 Python 环境装好、想从单一 LSTM 换成混合模型提升预测方向的从业者。哪怕你刚开始看 python 量化交易策略代码照第二章把数据准备和相关性分析跑通后面每一步也都能落地。2. 数据与特征用 Spearman 相关性分析把 30 个指标筛到 5 个2.1 数据集怎么准备主力连续合约 OHLCV 与目标变量做期货价格预测第一步不是建模型而是把数据集整理成模型能直接吃的样子。我一般用 tushare 拉国内期货主力连续合约的日线数据字段只需要六个date、open、high、low、close、volume做金融时序预测的还会额外加一个open_interest持仓量它对价格走势有真实的增量信息。import tushare as ts import pandas as pd # 设置你的 token去 tushare 官网注册即可获取 ts.set_token(你的token) pro ts.pro_api() # 拉螺纹钢主力连续合约日线2018 年到 2024 年 df pro.fut_daily( ts_codeRB.SHF, start_date20180101, end_date20241231, fieldstrade_date,open,high,low,close,vol,oi ) df.rename(columns{ trade_date: date, vol: volume, oi: open_interest }, inplaceTrue) df[date] pd.to_datetime(df[date]) df.sort_values(date, inplaceTrue) df.reset_index(dropTrue, inplaceTrue) print(df.head())取数逻辑说明fut_daily返回的是该品种主力连续合约的日频行情ts_codeRB.SHF指上期所螺纹钢换成其他品种改代码即可。日期排序后要重新设置索引否则后面构造滑动窗口时.iloc的定位容易出错。参数说明start_date和end_date决定了数据量。日线数据一般取 5 年以上太短覆盖不了完整的涨跌周期太长又会把早期流动性差、波动规律完全不同的行情也喂进模型。fields里我额外加上了持仓量期货不像股票只有量价持仓量的增减代表资金进出意愿相关性分析阶段它经常是排名靠前的特征。主力连续合约本身存在换月跳空问题。常见做法是直接用连续合约的原始报价模型学的是K线形态和量价关系不是绝对点位所以小幅跳空影响有限。如果你要做严格的回测可以按对数收益率把换月前后的价格拼接起来这一步不是必须的先把主流程跑通更重要。接下来定义目标变量。预测的是未来 N 天的收益率而不是未来某一天的绝对价格。用绝对价格做回归目标模型会倾向于学一个「昨天收盘价」的平庸解预测曲线比真实价格滞后一个周期。改成收益率目标后模型被迫去学涨跌方向。N 5 # 预测未来 5 个交易日 df[target] df[close].shift(-N) / df[close] - 1.0 df.dropna(inplaceTrue)这里shift(-N)把未来第 N 天的收盘价搬到今天这一行与今天收盘价做除法得到未来 N 日收益率。dropna会丢掉最后 N 行没有未来数据的样本这是正常现象。N 的选择直接影响任务难度N1 接近随机游走N5 到 N10 是量化策略里比较常见的预测周期既不至于太短噪声过大也不至于太长失去可操作性。2.2 Spearman 相关性分析为什么比 Pearson 更适合期货价格特征工程阶段我一般会手工构造 20 到 30 个候选因子比如动量、RSI、布林带位置、量比、持仓量变化等。但因子不是越多越好很多技术指标之间的相关性极高比如 5 日动量和 10 日动量几乎就是同一个信息。把所有因子直接塞进模型CNN 的第一层卷积会花大量参数去处理重复信号。筛选因子用 Spearman 相关性分析而不是 Pearson。原因很直接Pearson 衡量的是线性相关要求变量服从近似正态分布期货收益率序列有明显的尖峰厚尾极端行情下 Pearson 相关系数会被少数异常值牵着走。Spearman 计算的是两个变量排名的相关性只关心「一个变量增大时另一个是否同向增大」对异常值不敏感也能捕捉非线性的单调关系。价格和成交量之间的关系通常不是线性的Spearman 更贴近真实情况。如果你在 python 环境里还没有装 scipy先执行pip install scipy pandas numpy这是最基础的科学计算三件套。from scipy.stats import spearmanr # 假设 feature_df 是候选因子 DataFrametarget 是目标收益率 Series results [] for col in feature_df.columns: rho, p_value spearmanr(feature_df[col], target) results.append({ factor: col, spearman_rho: round(rho, 4), p_value: round(p_value, 6) }) result_df pd.DataFrame(results).sort_values(spearman_rho, keylambda x: x.abs(), ascendingFalse) print(result_df.head(10))逻辑说明循环对每个候选因子和目标变量做 Spearman 相关检验rho是相关系数取值 -1 到 1正数代表因子越大未来收益率倾向越高负数则相反。p_value是显著性检验的 p 值一般要求小于 0.05 才认为相关关系不是随机波动造成的。参数说明筛选时我会设两道门槛。第一道是abs(rho) 0.05期货价格预测里因子和未来收益率的相关系数普遍偏低这是正常的0.05 以上已经算有信号第二道是p_value 0.05避免小样本下的伪相关。如果你用的数据量不足 300 根K线p 值这道门槛基本筛不掉任何因子此时要先用数据量说话。2.3 特征之间的共线性去重相关性不只对目标算很多做预测的人在筛完因子和目标变量的相关性后就停了这是不够的。因子之间的共线性同样要处理否则 Attention 模块会在冗余特征上分配重复的权重模型训练时 loss 下降慢还容易过拟合。我习惯对通过第一轮筛选的因子再算一次 Spearman 相关矩阵两两相关性绝对值超过 0.7 的只保留和目标相关性更高的那个。# selected 是上一轮筛出的因子列表 selected result_df[result_df[p_value] 0.05].head(8)[factor].tolist() corr_matrix feature_df[selected].corr(methodspearman) # 找出高相关对手动去重 high_corr_pairs [] for i in range(len(selected)): for j in range(i 1, len(selected)): if abs(corr_matrix.iloc[i, j]) 0.7: high_corr_pairs.append((selected[i], selected[j], round(corr_matrix.iloc[i, j], 3))) print(高相关特征对, high_corr_pairs)逻辑说明DataFrame.corr(methodspearman)内部就是调用 scipy 的 Spearman 实现比手动循环两两算更快。高相关特征对打印出来后我的去重规则是先看这两个因子各自与目标变量的abs(rho)谁大留下大的删掉小的。这样既控制了特征数量又不会丢失信息贡献更强的那一方。参数说明相关性阈值 0.7 是经验值你可以根据最终特征数量微调。如果筛选完只剩 3 个特征说明阈值定高了降到 0.6 再筛一轮如果还有 10 个以上说明阈值太低提到 0.75 或 0.8。最终我一般保留 4 到 8 个因子这个数量对 CNN-Attention-LSTM 来说既不会让第一层卷积的输入维度过于稀疏也不至于信息冗余。3. CNN-Attention-LSTM 模型结构与 PyTorch 实现每个参数怎么定3.1 为什么把三个模块串起来各自解决什么问题单一 LSTM 做期货价格预测常见的问题是「预测曲线比真实曲线滞后一拍」。原因是 LSTM 倾向于记住最近时刻的数值当价格没有明显趋势时最简单的低误差策略就是输出接近上一个观测值。这本质上是模型没有能力从更早的历史行情里找到与当前走势相似的形态。CNN 的卷积核负责在时间维度上扫描局部K线形态。一个宽度为 5 的卷积核相当于把连续 5 根K线的组合模式提取成一个特征比如「放量长上影」「缩量十字星」这类形态。这种局部模式识别是 LSTM 的弱项LSTM 更擅长记住序列的依赖关系而不是识别形状。Attention 的作用是给每个时间步分配权重不是所有历史行情对当前预测同等重要比如某个时间点出现过放量突破后续几天走势大概率会延续这个时间步就应该拿到更高权重。三者串联的逻辑是CNN 先做局部特征提取Attention 在时间步上做重要性加权LSTM 把加权后的序列建模成长期依赖最后接全连接层输出预测值。3.2 完整模型代码与维度推导下面这个模型定义可以直接复制进你的 python 文件里使用依赖只有torch和torch.nn.functional。如果你还没装 PyTorch先在终端执行pip install torchCPU 版本对日线级别的数据量完全够用。import torch import torch.nn as nn import torch.nn.functional as F class CNNAttentionLSTM(nn.Module): def __init__(self, n_features, cnn_out64, kernel_size5, lstm_hidden64, num_layers2, dropout0.3): super().__init__() # 一维卷积输入 (batch, n_features, seq_len) self.cnn nn.Conv1d( in_channelsn_features, out_channelscnn_out, kernel_sizekernel_size, paddingkernel_size // 2 ) self.bn nn.BatchNorm1d(cnn_out) # 多头注意力batch_firstTrue 让输入格式更直观 self.attention nn.MultiheadAttention( embed_dimcnn_out, num_heads4, dropoutdropout, batch_firstTrue ) self.lstm nn.LSTM( input_sizecnn_out, hidden_sizelstm_hidden, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Sequential( nn.Linear(lstm_hidden, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) ) def forward(self, x): # x 形状: (batch, seq_len, n_features) x x.permute(0, 2, 1) # 转成 (batch, n_features, seq_len) 适配 Conv1d cnn_out self.cnn(x) cnn_out self.bn(cnn_out) cnn_out F.relu(cnn_out) cnn_out cnn_out.permute(0, 2, 1) # 还原成 (batch, seq_len, cnn_out) 给 Attention attn_out, _ self.attention(cnn_out, cnn_out, cnn_out) lstm_out, _ self.lstm(attn_out) last_hidden lstm_out[:, -1, :] # 取最后一个时间步的隐藏状态 return self.fc(last_hidden).squeeze(-1)维度推导是理解这段代码的关键。假设输入一个批次包含 64 个样本每个样本是 60 根K线、6 个特征那么x的原始形状是(64, 60, 6)。permute(0, 2, 1)后变成(64, 6, 60)因为Conv1d默认第二个维度是通道数。卷积层输出(64, 64, 60)其中 64 是cnn_out卷积核数量输入长度 60 因为padding2保持不变。再permute回(64, 60, 64)给MultiheadAttention它输出形状不变。LSTM 接收(64, 60, 64)输出(64, 60, 64)取[:, -1, :]得到每个样本最后一个时间步的隐藏状态形状(64, 64)最后全连接输出(64,)的预测值。参数说明kernel_size5的物理含义是每个卷积核一次扫描 5 根K线这个值我建议在 3 到 9 之间调。太小的卷积核只能看到两三根K线形态度量太短太大的卷积核会把不同阶段的行情混在一起特征反而模糊。num_heads4要求embed_dim能被 4 整除64 除以 4 等于 16每个头负责 16 维特征子空间。num_layers2让 LSTM 具备两层堆叠第二层学习第一层输出中的依赖关系超过 2 层在日线数据上容易过拟合因为样本量通常只有一两千条。dropout0.3是通用起点如果训练 loss 和验证 loss 差距大说明过拟合可以提到 0.4 或 0.5如果两个 loss 都高说明欠拟合降到 0.2。3.3 六个必调超参数范围与调参方向超参数建议范围调参方向滑动窗口 seq_len30 到 120窗口太短学不到中期趋势太长引入过多噪声卷积核大小 kernel_size3 到 9做几次实验观察卷积核提取的形态是否稳定cnn_out 卷积核数量32 到 128特征多但样本少时保持 64 以下lstm_hidden 隐层维度32 到 128和 cnn_out 保持一致量级避免维度突跳num_layers LSTM 层数1 到 3日线数据一般 2 层足够dropout0.2 到 0.5以训练/验证 loss 差距为判断依据这六个参数里最容易被忽略的是seq_len和kernel_size的配合关系。如果窗口是 60卷积核是 5模型能看到 60 根K线里的局部形态如果窗口是 120卷积核还是 5模型对更久远的历史行情也能提取局部模式但 Attention 要处理的序列更长训练时间几乎翻倍。我的起步配置是seq_len60, kernel_size5, cnn_out64, lstm_hidden64, num_layers2, dropout0.3先跑通再决定往哪个方向动。4. 训练与评估数据集切分、归一化、损失函数一套走通4.1 时序切分与归一化训练集、验证集、测试集不能随机打散模型结构定了之后训练流程里最常见的翻车点是数据切分方式。表格数据可以随机打散时序数据绝对不行。我采用的切分比例是 70% 训练、15% 验证、15% 测试严格按时间顺序切。验证集用来做早停和学习率衰减的参考测试集最后才碰。train_ratio, val_ratio 0.7, 0.15 train_size int(len(df) * train_ratio) val_size int(len(df) * val_ratio) train_df df.iloc[:train_size] val_df df.iloc[train_size:train_size val_size] test_df df.iloc[train_size val_size:]切分之后要做滑动窗口样本构造。每个样本是连续的seq_len根K线特征对应一个目标值。这里有一个容易忽略的细节相邻窗口高度重叠数据之间存在强相关性所以评估时不能像普通机器学习那样完全靠随机性假设只能靠时间顺序拉开训练集和验证集的距离。def create_sequences(data, feature_cols, seq_len60): xs, ys [], [] for i in range(len(data) - seq_len): x data[feature_cols].iloc[i:i seq_len].values y data[target].iloc[i seq_len - 1] xs.append(x) ys.append(y) return np.array(xs, dtypenp.float32), np.array(ys, dtypenp.float32) X_train, y_train create_sequences(train_df, feature_cols) X_val, y_val create_sequences(val_df, feature_cols)逻辑说明循环里i从 0 扫到len(data) - seq_len - 1每个x是一个(60, 6)的矩阵y取窗口结束后那天的target值。注意这里target已经是未来 N 日收益率所以不需要再额外偏移。归一化这一步要非常小心。常见错误是对全部数据一起fit标准化器这会造成数据泄露因为测试集的均值和方差已经提前参与了训练。正确做法是只用训练集拟合 scaler验证集和测试集复用同一组参数做transform。from sklearn.preprocessing import StandardScaler scaler StandardScaler() # 先把三维数组展平成二维来拟合 flat_train X_train.reshape(-1, X_train.shape[-1]) scaler.fit(flat_train) X_train scaler.transform(X_train.reshape(-1, X_train.shape[-1])).reshape(X_train.shape) X_val scaler.transform(X_val.reshape(-1, X_val.shape[-1])).reshape(X_val.shape)参数说明StandardScaler对每个特征独立计算均值和标准差然后减均值除标准差。reshape(-1, n_features)把(样本数, 60, 6)展平成(样本数*60, 6)这样才能让每个特征列的统计量覆盖到所有时间步。标准化后特征均值为 0 方差为 1对 CNN 的卷积核初始化和 LSTM 的梯度传播都更友好。如果价格序列不标准化数值在几千甚至几万一档模型很容易梯度爆炸。4.2 训练循环Huber 损失、学习率衰减与早停训练主循环我直接用 PyTorch 原生的写法不引入额外的训练库方便逐行调试。损失函数用HuberLoss而不是MSELoss因为期货收益率存在极端值MSE 会对这些极端值给出大得离谱的梯度导致训练震荡Huber 损失在误差小于delta时按 MSE 计算大于delta时按 MAE 计算兼顾了收敛速度和对异常值的鲁棒性。import torch.optim as optim from torch.utils.data import TensorDataset, DataLoader model CNNAttentionLSTM(n_featureslen(feature_cols)) optimizer optim.Adam(model.parameters(), lr1e-3) scheduler optim.lr_scheduler.ReduceLROnPlateau(optimizer, modemin, factor0.5, patience5) criterion nn.HuberLoss(delta1.0) train_dataset TensorDataset(torch.tensor(X_train), torch.tensor(y_train)) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) val_dataset TensorDataset(torch.tensor(X_val), torch.tensor(y_val)) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse) best_val_loss float(inf) early_stop_counter 0 epochs 100 for epoch in range(epochs): model.train() total_train_loss 0 for xb, yb in train_loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() total_train_loss loss.item() model.eval() total_val_loss 0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) loss criterion(pred, yb) total_val_loss loss.item() avg_train_loss total_train_loss / len(train_loader) avg_val_loss total_val_loss / len(val_loader) scheduler.step(avg_val_loss) if avg_val_loss best_val_loss: best_val_loss avg_val_loss torch.save(model.state_dict(), best_model.pt) early_stop_counter 0 else: early_stop_counter 1 if early_stop_counter 15: print(f早停于第 {epoch} 轮) break if epoch % 10 0: print(fEpoch {epoch}: train_loss{avg_train_loss:.4f}, val_loss{avg_val_loss:.4f})逻辑说明训练阶段model.train()开启 dropout 和 BatchNorm 的统计更新验证阶段切到model.eval()关闭这些随机性保证验证 loss 稳定可信。ReduceLROnPlateau监控验证 loss连续 5 轮没下降就把学习率乘以 0.5学习率衰减太慢可以调patience3。早停判断条件是 15 轮验证 loss 没有创新低就停止保存的永远是最低验证 loss 对应的权重。参数说明lr1e-3是 Adam 在中小型数据集上的通用起点如果训练 loss 震荡严重可以降到5e-4delta1.0是 Huber 损失的阈值它的含义是误差小于 1 时按平方损失放大细节大于 1 时按线性损失缓解极端值影响。batch_size64在日线数据量下没问题如果你的样本数不足 1000可以降到 32。shuffleTrue只用于训练集验证集保持顺序即可。4.3 评估指标MAE、RMSE 之外更该看方向准确率 DA回归模型的常规指标是 MAE 和 RMSE但对期货预测来说真正决定策略能不能赚钱的是方向准确率也就是预测的涨跌方向和真实涨跌方向一致的比例。一个模型即使 MAE 不大如果方向总是反的拿去交易只会稳定亏损。from sklearn.metrics import mean_absolute_error, mean_squared_error import numpy as np # 加载最优模型做预测 model.load_state_dict(torch.load(best_model.pt)) model.eval() X_test, y_test create_sequences(test_df, feature_cols) X_test scaler.transform(X_test.reshape(-1, X_test.shape[-1])).reshape(X_test.shape) with torch.no_grad(): preds model(torch.tensor(X_test)).numpy() mae mean_absolute_error(y_test, preds) rmse np.sqrt(mean_squared_error(y_test, preds)) direction_acc np.mean((preds 0) (y_test 0)) print(fMAE{mae:.4f}, RMSE{rmse:.4f}, 方向准确率{direction_acc:.2%})方向准确率的计算公式很简单(preds 0) (y_test 0)判断预测涨跌和真实涨跌是否一致取平均就是准确率。随机猜的方向准确率是 50%一套模型如果能在测试集稳定做到 53% 以上配合好的止盈止损规则已经有实盘验证的价值。这里还要补充一句评估误差的一个坑预测值和真实值的时间对齐。创建测试集序列时每个样本的y是data[target].iloc[i seq_len - 1]它代表的是窗口结束时刻的未来 5 日收益率预测结果也是对齐到这个时间点的。不要用shift再偏移一次否则 MAE 会莫名偏大而且方向准确率会退化到 50% 附近。4.4 预测与可视化的最小代码评估完之后把预测值和真实值画在同一张图上能直观看出模型是在跟随趋势还是只输出滞后值。绘制预测对比图时有个小技巧日期作为横坐标时数据点一多标签就会重叠我会把横坐标改成样本序号只保留少数几个刻度标签。import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(y_test, label真实值, alpha0.7) plt.plot(preds, label预测值, alpha0.7) plt.legend() plt.xticks(range(0, len(y_test), max(1, len(y_test) // 10)), rotation30) plt.xlabel(测试集样本序号) plt.ylabel(未来5日收益率) plt.tight_layout() plt.savefig(pred_vs_true.png, dpi150)5. 避坑期货时序预测里最容易翻车的 5 个细节5.1 验证集 loss 明显低于训练集 loss别高兴太早现象训练 20 轮后验证集 loss 比训练集低一大截模型在「验证集」表现异常好。原因大概率是标准化器提前在全量数据上做了fit验证集已经把均值和方差信息融入特征相当于偷看了答案。解决严格只用训练集拟合 scaler然后逐步检查验证集 loss 是否回归到略高于训练集的状态。如果重新修正后验证 loss 依然异常低检查是否在构造滑动窗口时把未来数据混进了特征列。5.2 训练时 shuffle 导致模型实盘预测全面失效现象训练集上 loss 收敛很快但在时间靠后的测试集上方向准确率只有 50% 左右。原因相邻滑动窗口的样本高度重叠shuffleTrue会让模型见到几乎相同的信息反复更新相当于变相过拟合更重要的是训练集中的未来信息被 shuffle 后和验证集样本混在了一起模型学到了数据集的整体分布而不是时序规律。解决训练集 shuffle 可以保留但验证集和测试集必须shuffleFalse并且样本构造时确保训练集最后一个样本的结束时间早于验证集第一个样本的起始时间。5.3 预测曲线是一条滞后一拍的复制品现象预测曲线与真实曲线形状几乎一样但整体向右平移了一格方向准确率勉强过 50%。原因模型在学习目标函数时发现输出「前一天的观测值」能让 MSE 或 MAE 很低这是一种偷懒解。解决改用收益率作为目标变量而不是绝对价格同时在损失函数里加方向惩罚项。常见做法是自定义损失把方向相反的样本损失放大 1.2 倍或者用二分类的涨跌头替换回归头先判断方向再回归幅度。def direction_aware_loss(pred, target, alpha0.2): base_loss F.huber_loss(pred, target, delta1.0) direction_wrong ((pred - target) * target 0).float() return base_loss alpha * (direction_wrong * base_loss).mean()这个损失函数在原始 Huber 损失基础上对方向预测错误的样本额外增加alpha * base_loss的惩罚。direction_wrong的计算逻辑是(pred - target) * target 0如果真实值为正而预测为负或真实值为负而预测为正左边乘积一定是负数。alpha取 0.2 是一个起步值调太大模型会过度关注方向而牺牲回归精度。5.4 Attention 模块训练不收敛或者 loss 剧烈震荡现象加入nn.MultiheadAttention后训练 loss 比单 LSTM 还不稳定甚至出现 NaN。原因多头注意力在训练初期对输入特征的尺度非常敏感如果 CNN 输出的 BatchNorm 在训练初期统计量不稳Attention 的 softmax 权重可能出现极端分布导致梯度放大。解决先确认特征已标准化然后检查embed_dim是否能被num_heads整除另外把学习率从 1e-3 降到 5e-4观察 loss 是否稳定。Attention 不是越早见效越好它需要 CNN 先输出相对稳定的局部特征。5.5 训练和推理的窗口生成方式不一致现象验证集指标不错但用最新数据做样本外预测时效果明显变差。原因训练时窗口按stride1滑动生成样本之间存在大量重叠推理时只取最新 60 根K线构造单一样本数据分布和训练样本的「密度」不一致。解决把推理也放到滑动窗口框架里用最近 60 根K线作为窗口输入但保持特征列和标准化参数完全一致。还有一个隐藏点预测未来 5 日收益率时当前这一天的最末一根K线还没有收盘特征里的close要往前挪一根避免用到当日未收盘数据。6. 验证模型值不值得上实盘滚动回测、基准对比与特征稳定性复检6.1 Walk-forward 滚动回测每 30 个交易日更新一次模型一次性划分训练集测试集只能说明模型在这段历史上有效不能说明它在未来持续有效。期货市场的波动特征会随时间漂移模型训练时学到的量价关系可能半年后就失效。我习惯做 walk-forward 滚动回测初始用前 800 根K线训练之后每 30 个交易日把模型在最新数据上微调一次同时用没有被训练过的未来数据预测。def walk_forward_train(df, feature_cols, retrain_interval30, initial_train800): all_preds, all_trues [], [] for start in range(initial_train, len(df) - 60, retrain_interval): train_part df.iloc[start - initial_train:start] test_part df.iloc[start:start 60] X_train, y_train create_sequences(train_part, feature_cols) X_test, y_test create_sequences(test_part, feature_cols) # 每次重新 fit scaler避免历史统计量漂移 scaler StandardScaler() scaler.fit(X_train.reshape(-1, X_train.shape[-1])) X_train scaler.transform(X_train.reshape(-1, X_train.shape[-1])).reshape(X_train.shape) X_test scaler.transform(X_test.reshape(-1, X_test.shape[-1])).reshape(X_test.shape) model CNNAttentionLSTM(n_featureslen(feature_cols)) # 这里复用第 4 章的训练循环略 preds predict(model, X_test) all_preds.append(preds) all_trues.append(y_test) return np.concatenate(all_preds), np.concatenate(all_trues)滚动回测的价值在于它逼近真实交易环境模型只能使用当前时点之前的数据未来信息零泄露。retrain_interval30意味着每个自然月左右更新一次权重对于日线级别的期货预测来说频率合理initial_train800保证第一次训练有足够的样本量如果数据不足 800 根可以降到 500但也要警惕欠拟合。6.2 特征稳定性复检每隔一段时间重算 Spearman 相关性滚到半年或者一年的时候当初筛选的特征很可能已经不再显著。我会在每次滚动回测时顺带重算一次特征和目标之间的 Spearman 相关系数和 p 值如果某个因子的abs(rho)跌到门槛以下就把它从特征列表里剔除换成新的候选因子。这个习惯帮我避免了很多「模型越跑越钝」的情况。def recheck_features(feature_df, target, threshold0.05): rho_list [] for col in feature_df.columns: rho, p spearmanr(feature_df[col], target) rho_list.append((col, abs(rho), p)) drop_cols [col for col, rho, p in rho_list if rho threshold or p 0.05] return drop_cols这套验证流程跑完模型能不能上实盘心里基本有数方向准确率稳定超过基准特征在不同时间段重复出现滚动回测的收益曲线不回撤过大。我最开始做这个模型时在标准化上翻过车验证集 loss 低得反常还以为模型强后来排查发现是 scaler 偷看了未来数据。这种血泪经验检验出来的流程比任何花哨的结构都可靠。希望帮到你。本文还有配套的精品资源点击获取