ARTICLE DETAIL

资讯详情

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

Python深度学习期货程序化交易系统:CNN-LSTM模型实战与避坑指南

Python深度学习期货程序化交易系统:CNN-LSTM模型实战与避坑指南 简介这是一套面向量化交易初学者与进阶开发者的期货程序化交易系统源码将Python与深度学习模型结合用于行情预测、信号生成与自动下单等场景适合具备一定Python基础、希望把神经网络落地到金融商贸实战的读者。压缩包共729个文件约19.56MB以401个py脚本为主体辅以36个cpp与52个h源码、38个csv行情数据、25个json与21个xml配置、14个pkl模型文件及若干dll、lib依赖库另有xlsx、md、yml等文档与配置覆盖数据、模型、回测与交易接口多个环节。目前已有238人学习下载。资源包含完整的工程目录与可运行代码读者可据此理解深度学习模型如何嵌入期货交易流程参考数据预处理、特征构造、模型训练与回测评估的实现思路并借助配置文件与依赖库快速搭建本地环境适合作为量化策略研究与二次开发的实践素材。1. 期货程序化交易系统遇上深度学习从手工规则到模型决策的跨越做过期货程序化交易的人大多经历过这个阶段用均线、MACD、布林带搭一套规则回测曲线漂亮得像艺术品一上实盘就开始钝刀子割肉。问题不在策略逻辑本身而在于手工规则很难刻画期货市场里那种非线性、多品种联动、日内节奏切换的复杂结构。python结合深度学习的期货程序化交易系统本质上就是用神经网络替代人工特征工程让模型从原始量价数据里自己学出可交易的信号。它适合两类人有 Python 基础、想从规则策略升级到模型策略的量化开发者以及做过深度学习项目、想把 CNN、LSTM 这类结构落到真实交易场景的算法工程师。这篇文章不讲空泛概念直接拆开一套可复现的架构数据怎么整、模型怎么搭、信号怎么转成下单指令、回测怎么避免自欺欺人。2. 数据管道与特征工程期货量价数据的清洗与张量构造2.1 为什么期货数据比股票数据更难伺候期货数据和股票数据最大的区别在于合约换月、夜盘、主力连续拼接这三件事。股票你拿到的是一支不变的代码期货你拿到的是 RB2401、RB2405 这种会到期消失的合约。如果直接拿单个合约做训练样本量根本不够深度学习模型塞牙缝。常见做法是构造主力连续合约但拼接时如果直接按价格差调整会在换月点引入虚假跳空模型会把这些跳空当成真实信号去学结果就是回测虚高、实盘打脸。我一般用后复权方式拼接以当前主力合约为基准换月时用新旧合约的价格比调整历史价格。这样价格序列连续但要注意成交量在换月点会有突变需要单独做归一化处理。另一个坑是夜盘数据不同品种夜盘收盘时间不一样有的 23:00有的凌晨 2:30对齐时间戳时如果用简单的前向填充会把不同交易时段的数据混在一起。正确做法是按品种分别构建时间索引再统一重采样到固定频率。2.2 从 tick 到张量构造模型可吃的输入深度学习模型不会直接吃 DataFrame你需要把数据转成三维张量样本数 × 时间步长 × 特征数。下面这段代码展示从清洗后的分钟线数据构造滑动窗口样本的完整流程。import numpy as np import pandas as pd from sklearn.preprocessing import RobustScaler def build_features(df): df: 单品种分钟线包含 open/high/low/close/volume 返回: 特征 DataFrame feat pd.DataFrame(indexdf.index) # 收益率类特征 feat[ret_1] df[close].pct_change(1) feat[ret_5] df[close].pct_change(5) feat[ret_15] df[close].pct_change(15) # 波动率类特征 feat[volatility] feat[ret_1].rolling(20).std() # 量价背离 feat[volume_ratio] df[volume] / df[volume].rolling(20).mean() # 高低价差 feat[hl_range] (df[high] - df[low]) / df[close] # 持仓量变化如果有 if open_interest in df.columns: feat[oi_change] df[open_interest].pct_change(5) return feat.dropna() def make_sequences(features, labels, seq_len60): features: (N, F) 特征矩阵 labels: (N,) 标签 seq_len: 回看窗口长度 返回: (样本数, seq_len, F) 和 (样本数,) X, y [], [] for i in range(seq_len, len(features)): X.append(features[i-seq_len:i]) y.append(labels[i]) return np.array(X), np.array(y) # 使用示例 # raw pd.read_csv(rb_main_1min.csv, parse_dates[datetime], index_coldatetime) # feat build_features(raw) # 标签未来 5 根 K 线收益率是否超过阈值 # label (raw[close].shift(-5) / raw[close] - 1 0.001).astype(int) # label label.loc[feat.index].dropna() # feat feat.loc[label.index] # scaler RobustScaler() # feat_scaled scaler.fit_transform(feat) # X, y make_sequences(feat_scaled, label.values, seq_len60)这段代码有几个关键参数需要根据品种调整。seq_len60表示模型每次看过去 60 根分钟线对于日内策略60 分钟大约是一个完整的小波段周期对于日线级别策略这个值要放大到 20 到 30。RobustScaler比StandardScaler更适合期货数据因为期货收益率分布尖峰厚尾用均值和标准差归一化会被极端值带偏RobustScaler 用中位数和四分位距抗异常值能力更强。标签构造里shift(-5)表示用未来第 5 根 K 线的价格算收益这个 5 要和你的持仓周期匹配如果你打算持仓 30 分钟标签就应该用shift(-30)。注意构造标签时一定要用shift做前向对齐确保第 t 个样本的标签只用到 t 之后的信息否则就是未来函数回测会好得离谱但实盘一塌糊涂。2.3 训练集/验证集/测试集的时间切分期货数据绝对不能随机切分。随机切分会让模型在训练集里见到未来的市场状态验证集指标虚高。正确做法是按时间顺序切前 70% 训练中间 15% 验证调参最后 15% 测试。更严格的做法是滚动窗口切分每训练一段就往前推一段模拟实盘中模型定期重训的过程。我一般会留出至少 6 个月的样本外数据做最终测试这 6 个月里最好包含一次趋势行情和一次震荡行情否则你无法判断模型在不同市场状态下的表现差异。3. 模型选型与训练CNN-LSTM 混合结构在期货信号提取中的落地3.1 为什么纯 LSTM 或纯 CNN 都不够用期货量价数据同时包含局部形态和时序依赖。局部形态比如“三根阴线后缩量十字星”这是 CNN 卷积核擅长抓的时序依赖比如“波动率放大后趋势延续概率上升”这是 LSTM 门控机制擅长记的。只用 LSTM模型容易在长序列里丢失局部形态的锐度只用 CNN模型看不到跨时间步的状态转移。我一般用 CNN 做特征提取前端LSTM 做时序聚合后端最后接全连接层输出信号概率。具体结构两层一维卷积每层后面接 BatchNorm 和 ReLU然后接两层 LSTM最后接 Dropout 和全连接。卷积核大小选 3 和 5 各一层分别抓短周期和稍长周期的局部模式。LSTM 隐藏单元数不要超过 128期货数据信噪比低隐藏单元太多会直接记住噪声。Dropout 设在 0.3 到 0.5 之间这是防止过拟合最有效的手段之一。3.2 训练脚本与关键参数import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class FuturesCNNLSTM(nn.Module): def __init__(self, n_features, seq_len, n_classes2): super().__init__() # 卷积前端提取局部量价形态 self.conv1 nn.Conv1d(n_features, 32, kernel_size3, padding1) self.bn1 nn.BatchNorm1d(32) self.conv2 nn.Conv1d(32, 64, kernel_size5, padding2) self.bn2 nn.BatchNorm1d(64) self.relu nn.ReLU() self.pool nn.MaxPool1d(2) # LSTM 后端聚合时序依赖 self.lstm nn.LSTM(64, 64, num_layers2, batch_firstTrue, dropout0.3) # 输出层 self.dropout nn.Dropout(0.4) self.fc nn.Linear(64, n_classes) def forward(self, x): # x: (batch, seq_len, n_features) - 转成 (batch, n_features, seq_len) x x.permute(0, 2, 1) x self.relu(self.bn1(self.conv1(x))) x self.pool(x) x self.relu(self.bn2(self.conv2(x))) x self.pool(x) # 转回 (batch, seq_len, 64) 给 LSTM x x.permute(0, 2, 1) x, _ self.lstm(x) # 取最后一个时间步的输出 x x[:, -1, :] x self.dropout(x) return self.fc(x) # 训练配置 def train_model(X_train, y_train, X_val, y_val, n_features, seq_len): device torch.device(cuda if torch.cuda.is_available() else cpu) model FuturesCNNLSTM(n_features, seq_len).to(device) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) criterion nn.CrossEntropyLoss() scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, patience5, factor0.5) train_ds TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train)) val_ds TensorDataset(torch.FloatTensor(X_val), torch.LongTensor(y_val)) train_loader DataLoader(train_ds, batch_size128, shuffleTrue) val_loader DataLoader(val_ds, batch_size256, shuffleFalse) best_val_loss float(inf) patience_counter 0 for epoch in range(100): model.train() for xb, yb in train_loader: xb, yb xb.to(device), yb.to(device) optimizer.zero_grad() loss criterion(model(xb), yb) loss.backward() # 梯度裁剪防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() # 验证 model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: xb, yb xb.to(device), yb.to(device) val_loss criterion(model(xb), yb).item() val_loss / len(val_loader) scheduler.step(val_loss) # 早停 if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter 15: print(fEarly stop at epoch {epoch}) break return model这段训练脚本里有几个参数直接决定模型能不能用。weight_decay1e-4是 L2 正则期货数据噪声大不加正则模型两周内就会过拟合到验证集准确率 90% 但实盘亏钱。clip_grad_norm_的max_norm1.0是 LSTM 训练的标配期货收益率序列偶尔出现极端值梯度爆炸会让模型参数直接飞掉。ReduceLROnPlateau的patience5表示验证损失连续 5 个 epoch 不降就砍学习率这个值不要设太小否则学习率频繁波动模型在局部最优附近震荡。提示训练时把随机种子固定住torch.manual_seed(42)和np.random.seed(42)都加上。期货数据本身随机性就大如果每次训练结果都不一样你根本分不清是模型结构问题还是随机初始化问题。3.3 类别不平衡与样本权重期货信号里真正值得交易的时刻可能只占 10% 到 20%剩下 80% 是震荡噪声。如果直接拿全样本训练模型会学会“全部预测不交易”就能拿到 80% 准确率但这个模型毫无用处。常见做法有两种一是对少数类样本过采样用 SMOTE 或者简单复制二是在损失函数里给少数类更高权重。我一般用后者因为过采样会改变原始分布期货数据的时间结构会被破坏。# 在 CrossEntropyLoss 里加权重 class_counts np.bincount(y_train) weights 1.0 / class_counts weights weights / weights.sum() class_weights torch.FloatTensor(weights).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)这段代码根据训练集里各类别的样本数自动算权重样本少的类别权重高。注意权重只在训练集上算验证集和测试集不要动否则评估指标会失真。4. 信号转下单从模型输出到期货交易指令的映射与风控4.1 模型输出概率怎么变成仓位模型输出的是 softmax 概率比如 [0.3, 0.7] 表示 70% 概率上涨。但概率不等于仓位你需要一个映射规则。最简单的是阈值法概率超过 0.6 做多低于 0.4 做空中间空仓。但固定阈值在不同波动率环境下表现差异很大趋势行情里 0.55 就该进场震荡行情里 0.65 进场都可能被反复打脸。我一般用波动率调整阈值阈值 基础阈值 k × 当前波动率。波动率高的时候提高阈值过滤掉噪声信号波动率低的时候降低阈值抓住小波段机会。基础阈值设 0.55k 设 0.5具体数值用验证集调。仓位大小用凯利公式的简化版仓位 (预测概率 - 0.5) × 2 × 最大仓位。这样概率越高仓位越重但最大不超过预设上限。4.2 止损止盈与仓位管理期货自带杠杆止损设置比股票严格得多。我一般用 ATR 止损入场价减去 2 倍 ATR 作为多头止损线入场价加上 2 倍 ATR 作为空头止损线。ATR 周期用 14这个参数在日线和分钟线上都适用。止盈用移动止盈浮盈超过 1 倍 ATR 后把止损线移到成本价浮盈超过 2 倍 ATR 后止损线跟着 ATR 上移。def calculate_position(signal_prob, atr, max_position1.0): signal_prob: 模型输出的上涨概率 atr: 当前 ATR 值 max_position: 最大仓位比例 # 波动率调整阈值 threshold 0.55 0.5 * (atr / atr.rolling(100).mean().iloc[-1] - 1) threshold np.clip(threshold, 0.5, 0.75) if signal_prob threshold: # 做多 raw_pos (signal_prob - 0.5) * 2 * max_position return min(raw_pos, max_position) elif signal_prob (1 - threshold): # 做空 raw_pos (0.5 - signal_prob) * 2 * max_position return -min(raw_pos, max_position) else: return 0.0 def check_stop_loss(entry_price, current_price, atr, direction, stop_mult2.0): 检查是否触发止损 direction: 1 多头, -1 空头 if direction 1: stop_price entry_price - stop_mult * atr return current_price stop_price else: stop_price entry_price stop_mult * atr return current_price stop_price这段代码里threshold的计算用当前 ATR 除以 100 周期 ATR 均值衡量当前波动率相对历史水平的高低。np.clip把阈值限制在 0.5 到 0.75 之间防止极端行情下阈值跑到不合理区间。止损函数里stop_mult2.0是经验值品种波动大就调到 2.5 或 3.0波动小就降到 1.5。4.3 回测框架的对接回测框架我一般用 backtrader 或者自己写轻量级事件驱动引擎。关键是把模型推理和回测循环解耦回测循环每根 K 线推进时把当前窗口数据喂给模型拿到概率后走仓位计算和风控逻辑再生成订单。注意模型推理要在 CPU 上做GPU 推理在回测循环里反而慢因为数据传输开销大于计算开销。# 回测循环伪代码 for i in range(seq_len, len(data)): window features[i-seq_len:i] window_tensor torch.FloatTensor(window).unsqueeze(0) with torch.no_grad(): prob torch.softmax(model(window_tensor), dim1)[0, 1].item() atr_val atr[i] target_pos calculate_position(prob, atr_val) # 检查止损 if current_pos ! 0: if check_stop_loss(entry_price, data[close][i], atr_val, current_pos): target_pos 0 # 生成订单 if target_pos ! current_pos: order_size target_pos - current_pos execute_order(order_size, data[close][i]) current_pos target_pos回测时一定要加手续费和滑点。期货手续费按手数收滑点按最小变动价位的 1 到 2 倍算。不加这两项的回测曲线没有任何参考价值。5. 避坑与排查期货深度学习策略从回测到实盘的五个血泪教训5.1 回测夏普 3.0实盘一周亏 15%现象回测曲线平滑向上夏普比率超过 3最大回撤不到 5%。实盘跑了一周亏损 15%回撤远超回测预期。原因最常见的是未来函数。检查标签构造时有没有用shift做前向对齐特征计算里有没有用到未来数据。另一个隐蔽原因是回测时用了收盘价成交但实盘收盘价你根本拿不到实际成交价是下一根 K 线的开盘价。这个差异在分钟线上尤其致命。解决把所有特征和标签的计算逻辑重新过一遍确保第 t 根 K 线的信号只用到 t 及之前的数据。回测成交价改成下一根 K 线开盘价加 1 到 2 个滑点。如果改完后回测夏普降到 1.5 以下说明原来的高夏普全是偷价偷出来的。5.2 模型在验证集上准确率 85%实盘信号全是噪声现象验证集准确率很高但实盘跑起来模型输出的概率一直在 0.5 附近晃偶尔跳到 0.6 又马上回来根本没法触发交易。原因验证集和实盘的数据分布不一致。最常见的是归一化参数不一致训练时用训练集的均值和标准差做标准化实盘时又用实时数据的均值和标准差导致模型看到的输入分布和训练时完全不同。解决把训练集的RobustScaler或StandardScaler参数保存下来实盘推理时直接用保存的参数做变换不要重新拟合。另外检查特征计算窗口是否一致训练时用 20 周期均线实盘也要用 20 周期不能改成 10 周期。5.3 换月当天模型突然发出满仓信号现象主力合约换月当天模型输出概率突然跳到 0.9 以上发出满仓做多信号但当天行情明明在跌。原因换月时价格序列拼接引入跳空模型把这个跳空当成强烈上涨信号。如果换月时新旧合约价差大跳空幅度可能超过正常日波动的好几倍。解决换月当天强制空仓或者把换月前后的样本从训练集里剔除。更稳妥的做法是在特征里加入“距换月天数”这个变量让模型自己学会在换月附近降低信号强度。5.4 训练 loss 降到 0.01验证 loss 从第 3 个 epoch 开始涨现象训练集损失一路降到接近零验证集损失前几个 epoch 下降后持续上升模型明显过拟合。原因期货数据信噪比极低模型参数量稍微大一点就会记住训练集里的噪声。另外如果训练集和验证集的时间跨度太近两个集合的市场状态高度相似验证集起不到独立评估的作用。解决先加 Dropout 和 weight_decay再把模型参数量砍一半。如果还过拟合就减少特征数量只保留最核心的 5 到 8 个特征。验证集一定要和训练集隔开至少 3 个月最好包含不同的市场状态。5.5 实盘滑点比回测设置大 3 倍现象回测里设置 1 个滑点实盘成交价经常偏离信号价 3 到 5 个最小变动价位。原因期货主力合约在非活跃时段盘口很薄市价单会直接吃穿好几档。另外如果策略在开盘或收盘附近交易流动性差的时候滑点更大。解决回测滑点设置至少 2 个最小变动价位开盘后 15 分钟和收盘前 15 分钟不开新仓。用限价单代替市价单但要注意限价单可能不成交需要设置超时撤单逻辑。6. 模型迭代与实盘验证滚动重训和信号衰减监控的具体做法模型上线不是终点期货市场的结构会变今天有效的信号三个月后可能就失效了。我一般每两周做一次滚动重训用最近两年的数据重新训练模型验证集用最近三个月测试集用最近一个月。如果新模型在测试集上的表现比旧模型差超过 20%就不替换继续用旧模型但缩短监控周期。信号衰减监控看两个指标一是模型输出概率的分布如果概率越来越集中在 0.5 附近说明模型对当前市场状态没有区分度二是策略的滚动夏普用 20 个交易日的滚动窗口算夏普如果连续 10 个交易日低于 0.5就触发人工检查。我自己的习惯是每周五收盘后跑一遍监控脚本把概率分布直方图和滚动夏普曲线画出来肉眼过一遍。这个习惯帮我躲过了两次模型失效一次是某品种交易所调整了手续费日内波动结构变了另一次是宏观事件导致波动率中枢整体上移原来的阈值全部偏低。实盘验证还有一个容易忽略的点用模拟盘跑新模型至少一个月但模拟盘的成交假设和实盘差异很大所以模拟盘只用来验证信号逻辑有没有 bug不用来评估收益。真正评估收益至少要看三个月实盘而且这三个月里最好包含一次趋势和一次震荡。如果三个月实盘夏普低于 1.0这个模型就不值得继续投入资金回去检查特征和标签构造而不是调参硬撑。希望帮到你。本文还有配套的精品资源点击获取
返回列表