深度学习期货程序化交易:数据、特征、模型与回测全流程
简介面向量化交易开发者与金融科技研究者这是一套完整的python结合深度学习的期货程序化交易系统源码包涵盖行情接口、信号生成、策略回测与交易执行等环节适合有Python基础、希望将深度学习模型落地到期货场景的中高级读者。压缩包共729个文件容量19.56MB核心代码以401个Python脚本、36个C源文件及52个头文件为主另有pkl模型文件、csv训练数据、json/xml配置和dll动态库分别对应模型权重、历史行情、参数设置与底层交易接口封装。从内容预览可见包内包含vnctptd.cpp等CTP对接代码以及多个配置文件能够帮助理解交易终端与柜台通信的完整细节。目前已有240人学习对于希望将深度学习技术应用于期货策略的开发者这套源码提供了可直接运行的工程骨架便于二次开发与学术实验。1. python深度学习做期货程序化交易先把“预测任务”拆成“可执行信号”期货和股票最大的区别就是T0、多空双向、带杠杆行情信号密度很高人工盯盘很容易被分钟级别的噪声牵着走。python在这个方向上可以干完整条链路数据清洗、特征计算、模型训练、回测、下单执行而深度学习的真正价值不在“预测精确涨跌”而在把历史量价关系和盘口特征映射成一个概率信号。我习惯把这套系统拆成四层数据层处理连续合约和分钟bar特征层负责归一化和序列窗口模型层输出多/空/观望三档信号执行层用规则把信号转成仓位并做风控。适合读这篇文章的人是已经会写Python、愿意在数据上花时间而不是急着调模型的从业者。2. 期货行情数据与特征工程分钟bar怎么建、连续合约怎么对特征才不偷看未来2.1 先解决“数据对不上”主力合约怎么选、换月跳空怎么处理期货不能像股票那样直接拿一个代码拉十年日线就完事。主力合约会定期换月换月那天价格经常跳空这个跳空不是市场真的涨跌只是换了合约标的。如果你直接把不同月份的K线拼在一起深度学习模型会把“换月跳空”当成一个高收益信号去学习实盘一跑就翻车。我一般会先拿到交易所官方品种指数比如螺纹指数、铁矿指数或者自己做主力映射每天按持仓量和成交量排序取最大那个合约作为当日主力得到一个“主力合约代码Bar数据”的序列。注意换月当日前后两根bar之间的价格断裂要单独处理常见做法是记录换月日期在特征计算时跳过跨越换月点的收益率或者用复权因子把两根价格接上。关键原则是训练、验证、回测必须用同一套合约拼接逻辑不能训练用指数、回测用主力连续否则模型学到的分布和回测环境对不上。这里的坑在于很多人直接用第三方库里的“主力连续”数据以为它是干净的。实际上不同数据商的主力连续算法不同有的是按成交量加权有的是按持仓量切换复权方式也不一样。你在模型里看到的“涨跌”很可能有一半是换月造成的假信号。所以做特征工程之前先花一天时间把合约序列和换月表对齐比多调十个超参都值。2.2 从tick到分钟bar一段可直接跑的聚合代码模型吃的最小时钟单位我一般用1分钟。tick数据量太大特征噪声也太高5分钟或15分钟bar又太粗信号更新频率低交易执行跟不上。下面这段代码把tick聚合成1分钟bar并额外计算主动买卖量比作为盘口维度的补充特征。import pandas as pd import numpy as np def ticks_to_1m_bars(ticks: pd.DataFrame) - pd.DataFrame: ticks 至少包含列: ts(秒), price, volume, side(主动方向: B买/S卖) ticks ticks.sort_values(ts).copy() ticks[bar_ts] ticks[ts] // 60 * 60 # 向下取整到分钟 # ---- 基础OHLCV ---- agg ticks.groupby(bar_ts).agg( open(price, first), high(price, max), low(price, min), close(price, last), volume(volume, sum), ) # ---- 主动买卖量比买盘主动性越强short-term动量往往越强 ---- side_volume ticks.groupby([bar_ts, side])[volume].sum().unstack(fill_value0) agg[buy_volume] side_volume[B] if B in side_volume else 0 agg[sell_volume] side_volume[S] if S in side_volume else 0 agg[buy_sell_ratio] (agg[buy_volume] 1) / (agg[sell_volume] 1) agg agg.reset_index() return agg这段代码的关键逻辑先按秒级时间戳把tick切到分钟桶然后做组内聚合。first取分钟第一笔价格作为openlast取最后一笔作为closesum累加成交量。主动买卖量比是很多人在分钟级别上容易忽略的维度它能反映这1分钟里是买方主动吃单还是卖方主动砸单对短周期方向判断比单纯收盘价更敏感。参数上值得调整的只有两处bar_ts ts // 60 * 60决定分钟周期改成// 300 * 300就是5分钟barbuy_sell_ratio里加1是为了避免除零。如果你拿到的是盘口五档数据还可以把“买一量/卖一量”加进来但要注意五档快照和成交tick的时间戳对齐问题很多数据商这两个字段的时间基准不一样强行对齐反而引入噪声。2.3 特征矩阵哪些量价因子能进模型哪些只是玄学聚合成bar之后下一步是构造特征矩阵。我通常给每个bar生成三个维度的特征基本量价、时序统计、微观结构。基本量价包括对数收益率、bar内振幅、成交量相对前N根的比率时序统计是过去L根bar的滚动均值、滚动标准差、RSV等微观结构就是上面算的主动买卖量比。下面这个特征列表是我在螺纹、铁矿、沪铜上常用的起点不一定全用但没这几个特征模型几乎学不到东西。对数收益率比原始价格更适合做输入因为价格在不同月份合约上绝对值差异很大模型不该把“价格是4000还是4500”当作有效信息。特征名计算方式说明log_retln(close / prev_close)对数收益率跨合约相对稳定range_pct(high - low) / prev_close单根bar振幅代表波动强度vol_ratiovolume / mean(volume, 20)放量/缩量程度放量往往是行情启动信号buy_sell_ratio主动买量 / 主动卖量买卖失衡度短周期方向参考max_single_volbar内最大单笔成交量 / avg单笔大单异动标记rsv(close - min_low) / (max_high - min_low)随机指标原始值注意窗口不要跨换月点真正要警惕的是“特征工程玄学化”。我看到有人在分钟特征里加MACD、KDJ这些日线指标的重采样版本模型没变强训练时间变长回测还更好看了——因为指标密集区的边界点和换月点重合模型学到了换月跳空。判断特征是否有效的方法很简单在验证集上只替换这一个特征其他全不动看绩效指标是否真的变化超过噪声区间。我一般会做两次这样的对照超过一半的所谓“新特征”其实只是让模型更过拟合。2.4 按时间切分训练集唯一不会偷看未来的切法这是整条流水线上最重要的一条纪律训练集、验证集、测试集必须按时间顺序切分不能随机切分。随机切分会把某天的数据同时放进训练和验证里相邻bar的强自相关性会直接泄漏未来信息模型在回测里跑出虚高绩效是完全正常的。我的切法是这样假设你有三年1分钟bar按时间顺序取前70%训练中间15%验证最后15%做一次性的样本外测试。调超参只看验证集测试集只跑一次。如果测试集结果明显比验证集差不是运气问题是策略过拟合了验证集应该回头简化模型而不是继续调参。特征标准化也是一个泄漏高发点。无论你用z-score还是min-maxscaler对象只能用训练段的数据去fit再用这个训练好的scaler去变换验证段和测试段。很多人的做法是拿到全量数据先标准化再切分这等于让模型在训练阶段见过验证集的均值和方差验证集就丧失了检验能力。这一点放在第5章的避坑清单里再展开。3. 深度学习模型选型与训练三分类信号为什么比价格回归稳param量和超参怎么权衡3.1 别做价格回归三分类信号设计更符合交易本质把深度学习接到期货上第一个要拿定的主意是模型输出什么。不少从股票量化转过来的人会先试LSTM预测下一根bar的收盘价然后根据预测价和当前价的差决定多空。我试过这个方案结论是不行期货分钟级别的价格噪声非常大回归模型会把大量权重花在拟合噪声上而且回归损失MSE优化的方向和交易目标不一致——预测价差0.5个tick和1.5个tick在交易上可能完全没有区别但在MSE里会被当成两倍错误惩罚。更稳的建模方式是做三分类给定过去L根bar的特征序列预测未来K根bar的累计收益率属于“上涨/下跌/震荡”哪一类。阈值不设死可以用未来K根bar的收益率分布分位数切比如最高的30%标记为上涨最低的30%标记为下跌中间40%为震荡。这样模型的优化目标变成了“方向是否判断正确”和交易决策的耦合度更高。3.2 网络结构选型一维CNNLSTM在期货量价上的理由我的默认结构是一维CNN接LSTM再接全连接。CNN负责在时间维度上提取局部模式比如连续3-5根bar的放量突破、买卖比持续失衡LSTM负责捕捉中短期的序列依赖比如过去40分钟的趋势持续性。这个结构在分钟级期货数据上比纯LSTM更稳因为它用卷积先做了一层降噪LSTM需要记住的原始信息更少。为什么不上Transformer我在这个方向上的经验是分钟级金融数据的有效样本量撑不起Transformer的注意力参数规模而且它训练不稳定学习率稍高就发散稍低就学不动。对于期货程序化交易这个场景CNNLSTM的参数量、训练稳定性和推理速度都更实用。如果你确实想试Transformer先把序列长度控制在64以内预训练从头开始不要直接上BERT那套初始化。3.3 模型代码PyTorch里的一维CNNLSTM实现下面这段代码是一个可以直接跑通的最小实现。输入张量形状是[batch_size, seq_len, feat_dim]输出是三个类别的logits。import torch import torch.nn as nn class CNNLSTMModel(nn.Module): def __init__(self, feat_dim: int, seq_len: int 64, num_classes: int 3): super().__init__() # ---- 一维CNN局部特征提取 ---- self.conv1 nn.Conv1d(feat_dim, 32, kernel_size5, padding2) self.bn1 nn.BatchNorm1d(32) self.conv2 nn.Conv1d(32, 32, kernel_size3, padding1) self.bn2 nn.BatchNorm1d(32) self.relu nn.ReLU() # ---- LSTM序列建模 ---- self.lstm nn.LSTM(32, 64, num_layers2, batch_firstTrue, dropout0.2) self.head nn.Linear(64, num_classes) def forward(self, x): # x: [B, L, F] - 转为 [B, F, L] 给Conv1d x x.permute(0, 2, 1) x self.relu(self.bn1(self.conv1(x))) x self.relu(self.bn2(self.conv2(x))) # 转回 [B, L, 32] 给LSTM x x.permute(0, 2, 1) lstm_out, _ self.lstm(x) last lstm_out[:, -1, :] # 取最后一个时间步的隐状态 logits self.head(last) return logits这里三个关键参数需要解释。kernel_size5意味着卷积一次看5根bar如果特征里含波动率这类慢变量可以把一个卷积核改成kernel_size9做多尺度感受野。num_layers2已经够用堆到4层在分钟数据上几乎必然过拟合。dropout0.2只加在LSTM层之间输出层不要加dropout否则推理和训练行为不一致。训练代码的要点优化器用AdamWlr1e-3是起点weight_decay1e-4到1e-5之间按验证集表现选加梯度裁剪clip_grad_norm_(model.parameters(), 1.0)这能显著避免LSTM梯度爆炸。batch size从64开始调如果训练集不足10万根barbatch size降到32。损失函数用带类别权重的CrossEntropyLoss因为三分类里“震荡”类样本通常占比更高不让多数类主导梯度方向。def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0.0 for x_batch, y_batch in loader: x_batch x_batch.to(device) y_batch y_batch.to(device) logits model(x_batch) loss criterion(logits, y_batch) optimizer.zero_grad() loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss loss.item() * x_batch.size(0) return total_loss / len(loader.dataset)这段训练循环里的clip_grad_norm_是我特别强调的。序列模型在长序列上反向传播很容易出现梯度范数突然涨到几十甚至几百的情况不裁剪的话一两个batch就能把训练搞崩。裁剪阈值1.0是保守值如果你发现loss曲线下降变慢可以放到2.0但超过5.0基本说明学习率或数据本身有问题。3.4 模型参数量权衡几十万参数已经很多了这里想澄清一个容易懵的点深度学习模型里说的“parameter”单位是M百万个参数不是MB。一个CNNLSTM模型参数量通常在几十万到两三百万之间也就是0.3M到2.3M它对应的是结构定义里的权重矩阵规模跟显存占用是两码事。你在训练日志里看到“参数量2.1M”指的是210万个浮点数权重不是210MB显存。在期货分钟数据上我的经验是参数量超过3M就要警惕了。以三年1分钟bar计算一个品种大约50万根训练样本模型如果做到百万级参数每个参数平均只能分到不到1个样本的信息模型几乎必然把训练集背下来。判断是否过拟合的标准很简单训练loss和验证loss的gap持续拉大且超过0.15左右优先减小LSTM隐层维度64降到32或把序列长度从64降到48而不是加正则硬压。3.5 从softmax概率到交易信号阈值和margin的粗调方法模型输出的是三分类logits经过softmax变成[p_up, p_down, p_flat]三个概率。直接取argmax作为信号是最差的做法因为概率0.34和0.33之间的差距毫无意义扣掉手续费滑点就是亏损源。我一般给每个方向设一个置信度门槛做多要求p_up 0.5且p_up - p_down margin这里的margin是验证集上调出来的初值0.1步长0.05。做空同理p_down 0.5且p_down - p_up margin。其余情况一律输出观望。margin调大交易次数减少、单笔胜率上升调小则交易频繁、手续费吃掉利润。这个参数直接决定了策略的“手数密度”值得用验证集做一次网格搜索别拍脑袋定。4. 信号到下单之间回测引擎怎么写、滑点和手续费怎么扣、哪些指标才算数4.1 信号到仓位三档信号为什么不能直接满仓模型输出“多/空/观望”之后这只是原始信号不是仓位指令。直接全仓多或全仓空在期货的杠杆下连续三次错误信号就能让账户接近爆仓。我常用的映射方式是多 0.5空 -0.5观望 0这里的0.5是账户净值的仓位比例再叠加一个整体杠杆系数。杠杆系数按品种波动率定波动率大的品种比如原油用小杠杆波动率小的比如国债期货可以放大一些。还要加一条铁律单笔交易的风险预算固定。比如单笔最大亏损不超过账户净值的0.5%根据当前波动率和止损距离倒推开仓手数。这一步必须在执行层做不能指望模型来控风险。模型的职责是给出方向判断执行层的职责是确保任何单次判断错误都不会伤到账户根基这两个角色分开系统才扛得住连续亏损的行情。4.2 回测引擎的最小实现信号在下一根K线开盘成交回测引擎是检验整个系统能不能用的关键。我写过一个最小版本核心原则只有一条第i根bar收盘后产生信号最早只能在第i1根bar开盘价成交。这样避免“用收盘价和信号在同一个bar内成交”的典型未来函数。import pandas as pd def run_backtest(bars: pd.DataFrame, signals: pd.Series, fee_rate: float 0.0001, slippage_ticks: int 1, min_tick: float 1.0, lots: int 1, init_cash: float 1_000_000.0): bars: 含 open/high/low/close 的bar数据索引为时间 signals: 与bars对齐的信号序列取值 -1/0/1表示第i根bar收盘后产生的目标方向 cash init_cash position 0 # 当前持仓方向-1空/0空仓/1多 entry_price 0.0 equity_curve [] total_fee 0.0 for i in range(1, len(bars)): # 第 i 根bar开盘执行第 i-1 根bar收盘产生的信号 target_signal signals.iloc[i - 1] open_price bars[open].iloc[i] if target_signal ! position: # 先平旧仓再开新仓 if position ! 0: cash position * (open_price - entry_price) * min_tick * lots if target_signal ! 0: entry_price open_price fee (entry_price * min_tick * lots) * fee_rate cash - fee total_fee fee position target_signal # 当日盯市用收盘价计算浮动权益 close_price bars[close].iloc[i] floating position * (close_price - entry_price) * min_tick * lots equity_curve.append(cash floating) return pd.Series(equity_curve, indexbars.index[1:])这个回测引擎的逻辑重点有三处。第一target_signal signals.iloc[i - 1]表示信号一定比成交晚一个bar周期杜绝盘中瞬移成交。第二平仓时position * (open_price - entry_price)用正负号区分多空方向做空时价格上涨是负收益方向判断相反则亏钱。第三交易手续费只在开仓时扣一次平仓手续费可以按同样方式在平仓分支再扣一次这里简化成开仓扣费实盘记得两边都算。滑点的处理方式是min_tick * 1也就是假设每次开仓比理想价多滑1个最小变动价位。如果你的品种流动性一般我会把slippage_ticks调到2-3。回测结果对滑点非常敏感一个看起来年化30%的策略滑点从1个tick变成3个tick可能直接掉到10%以下。4.3 手续费、滑点、保证金回测参数按两种口径设手续费和滑点这两个参数是回测和实盘差距的最大来源我一般用保守口径和乐观口径同时跑。乐观口径手续费按交易所标准费率上浮10%滑点1个tick保守口径手续费按期货公司加收后的真实标准一般是交易所标准的1.5-2倍滑点按2-3个tick同时把跳空开盘的情况也计入滑点。保证金和强平在这个最小回测引擎里没有建模这是它的已知边界。如果你做的是日内短线、仓位比例不超过0.5保证金占用对净值曲线的扰动很小可以忽略。但如果你做隔夜或者仓位放大必须把逐日盯市和强平规则加进去否则回测里一个超过100%的回撤在实盘里早就爆仓了。4.4 绩效指标要看这些年化、回撤之外盈亏分布和换手率很多人看回测结果只盯年化收益和最大回撤这两个指标远远不够。我更看重四个数盈亏比平均盈利/平均亏损、胜率、单月最大回撤、换手率。盈亏比低于1.5的策略在实盘里几乎没有生存空间因为滑点和手续费会进一步压缩每一笔交易的利润。换手率这个指标特别容易忽视。同样年化20%的策略一个每天交易20次一个每天交易2次实盘表现会完全不同——前者对滑点和手续费的敏感度极高后者的收益质量更高。我在回测报告里会打印一段交易明细统计总交易次数、平均持仓bar数、单日最大亏损、连续亏损次数。连续亏损次数超过5次就要检查是不是市场状态变了而不是模型坏了。回测跑完先看这批统计量再去看净值曲线这个顺序能避免被一条漂亮的曲线骗进去。5. 期货策略上线前必须避开的六个坑每条都是“现象→原因→解决”5.1 标准化泄漏验证集表现虚高上线立刻失灵现象训练集上loss正常下降验证集上准确率明显高于训练集当时我还觉得是模型泛化能力强。结果放到样本外测试段绩效直接腰斩。原因我在切分数据集之前先对全量数据做了z-score标准化scaler对象用了未来数据计算均值和方差。标准化本身没有信息泄露但它把未来统计量带进了训练过程模型实际上“见过”验证集的数据分布。解决所有标准化、归一化操作都必须在时间切分之后做只对训练段调用scaler.fit()然后对验证段和测试段只调用transform()。更稳妥的做法是把标准化器放进sklearn的Pipeline里和模型一起做时间序列切分的交叉验证这样不会漏。5.2 换月跳空被当成交易信号策略在换月日集中亏损现象回测净值曲线整体向上但每隔一两个月就出现一根突兀的大阴线而且这些阴线的时间点高度集中在主力合约换月日附近。原因直接用主力连续合约做训练没有处理换月跳空。模型把换月造成的价格断层学成了“突破信号”在换月前后频繁开仓实盘里换月跳空的方向和幅度根本无法预知止损被打穿。解决在特征计算阶段把换月日标记出来计算收益率和波动率特征时跳过跨越换月点的bar或者在训练标签里直接删除换月前后各5根bar的样本。回测时同样要剔除这些样本否则回测和训练的数据分布不一致结果没有参考价值。5.3 用收盘价成交看似完美的净值曲线其实是未来函数现象策略在回测里胜率高达65%但一上模拟盘就完全不是那么回事模拟盘成交价比回测价差一截利润被吃掉一大块。原因回测引擎里用第i根bar的收盘价生成信号又用同一根bar的收盘价成交。这在逻辑上是做不到的——你看到收盘价的那一刻这一根bar已经结束了最早只能按下一根bar的开盘价成交。解决回测成交价统一改成信号产生bar的下一根bar开盘价且开盘价要加滑点。如果开盘跳空比较大还可以设一个“跳空超过阈值则不成交”的规则更贴近实盘执行逻辑。这个改动会让回测收益明显下降但下降后的数字才是你实盘能拿到的近似值。5.4 置信度不足还强行开仓手续费和滑点把微弱优势吃光现象模型胜率有53%手续费扣完以后账户净值曲线几乎是水平的偶尔还缓慢向下。原因53%的胜率来自大量“边缘样本”——模型对每根bar都输出略微偏向某一方向的概率比如0.52对0.48。这些样本的期望收益扣掉手续费后是负的拉低了整体绩效。解决用验证集做置信度阈值搜索只保留模型把握度高的信号。比如要求做多时p_up0.55且p_up-p_down0.15把交易频率从每天50次降到每天15次胜率可能从53%升到58%扣掉手续费后净值曲线才开始真正向上。信号宁缺毋滥这是程序化交易系统最实在的一句经验。5.5 深度强化学习奖励函数没约束回撤策略赚得猛亏得更猛现象用深度强化学习算法训练出来的策略回测前半段收益曲线陡峭上涨后来一次大回撤把半年利润全吐回去。调了几轮奖励权重也没有本质改善。原因奖励函数只写成了“最大化累计收益”模型发现加大仓位、提高交易频率能短期获得更多奖励完全没有把回撤和单笔亏损写进约束。深度强化学习在这个任务里的收敛稳定性本来就比监督学习差奖励设置不当很容易学到激进赌博式策略。解决我的建议是优先做监督学习三分类任务用第3章的方案稳定性和可解释性都好得多。如果一定要上深度强化学习算法至少在奖励函数里加入回撤惩罚项比如reward - 3 * max_drawdown_penalty并且把单日最大亏损熔断逻辑放在执行层不让模型自己决定“要不要扛回来”。5.6 过度堆参数深度学习里的param是“M”不是“MB”堆模型不等于是深度学习现象训练集准确率一路飙升到99%验证集准确率却滞留在61%左右gap越来越大。我当时以为是训练轮数不够又加了两个LSTM层结果验证集反而掉到58%。原因模型参数量太大训练集只有几十万根bar模型开始背诵训练样本而不是学习可迁移的模式。另外我把“参数量”和“显存占用”混在一起理解以为3M参数不算多实际上它在分钟级数据规模下已经非常臃肿。解决先打印模型的参数量控制在1M以下再对比训练和验证的loss曲线gap超过0.15就立刻缩小模型不要靠正则硬压。深度学习在这里是特征提取工具不是拿来堆容量的。模型够用就好多出来的参数全部是过拟合的温床。6. 上模拟盘前先做两件事样本外滚动验证和信号冷却模拟盘和回测最大的差异在于回测是你看着完整历史算出来的结果模拟盘是你不知道未来、每天只能看到当下数据、还要处理延迟和滑点的真实环境。上模拟盘之前我会先做两件事。第一件是样本外滚动验证。把三年数据切成6段每次用前5段训练、最后1段验证然后整体往前挪一段重复最终得到6次独立的样本外验证结果。这比单一的一次性测试集更接近实盘的真实情况——模型不会只适应某一年的行情状态。如果6段结果有某一段明显差于其他段我不会跳过这段直接上模拟盘而是先确认差的那段是不是极端行情比如2020年负油价那种如果不是模型可能并没有理解市场只是在某一两年里运气好。第二件是给信号加冷却和确认机制。模型输出的信号不要直接转成下单指令先过一个过滤函数。def filter_signals(signals: list, min_gap: int 5, confirm: bool True) - list: 信号冷却与二次确认 signals: 模型原始输出-1/0/1 min_gap: 两次同向或反向交易之间的最小间隔根bar confirm: 是否要求连续两根同向信号才执行 final_signals [0] * len(signals) last_trade_idx -len(signals) # 保证策略启动前不受冷却限制 for idx, sig in enumerate(signals): if sig 0: continue # 冷却期距上次操作太近则放弃本次信号 if idx - last_trade_idx min_gap: continue # 二次确认要求前一根bar信号也同向 if confirm and idx 0 and signals[idx - 1] ! sig: continue final_signals[idx] sig last_trade_idx idx return final_signals我一般把min_gap设为5根1分钟bar即5分钟冷却confirmTrue要求连续两根bar信号同向才出手。这两个参数的实际价值是过滤掉模型在前一根bar看多、后一根bar看空这种高频翻转信号减少无效交易。成本是每笔交易少赚前面一两个bar的启动行情但换来的交易频率下降和噪声过滤通常远大于这个成本。这个函数还有一个隐藏作用它强制你每笔交易都有一个可解释的触发条件。实盘出现亏损时你能明确说清“这笔是冷却前开的”还是“确认信号后开的”而不是把所有亏损都归咎于“模型不准”。我的习惯是凡是模型输出和最终下单指令不一致的情况都记日志每周统计一次过滤率。过滤率超过50%就说明模型训练阶段置信度阈值设得太松了返回第3章调margin参数。一个过滤率长期低于20%的系统本质上是在跟模型的每个轻微波动较劲这种系统通常活不过一个完整的多空周期。希望这个从数据处理到信号过滤的完整链路能帮你少走我踩过的弯路。本文还有配套的精品资源点击获取