交易中的神经网络:基于 ResNeXt 模型的多任务学习(终篇)(基础篇)
◍ ResNeXt 多任务框架在 MT5 里的落地起点
这篇系列把 ResNeXt 这种卷积聚合结构搬进 MT5,用多任务学习同时预测价格方向与波动区间,而不是单头输出一个信号。原文发布于 2026 年 1 月 26 日,在 MQL5 社区挂出后拿到 401 次浏览、0 条评论,说明这类偏底层的模型集成内容受众窄但留存硬核。 对交易者来说,直接价值不在于调神经网络,而在于弄明白:MT5 的自定义指标和 EA 能不能接住 Python 训好的权重。答案倾向是可以,但需要走 ONNX 或自定义 DLL 桥接,纯 MQL5 写反向传播不现实。 外汇与贵金属杠杆高、滑点随机,任何模型信号都只是概率倾斜,别把回测里的聚合精度当实盘护身符。先开 MT5 把环境跑通,比急着训网络更重要。
「为什么用多任务学习配 ResNeXt 做行情分析」
多任务学习(MTL)用单个编码器喂数据,再挂多个专用输出头各管一摊任务。比起为每个子模型单独训一批数据,共享编码器逼出的特征更底层的规律,对噪声也更钝感。 联合训练天然抗过拟合:某个任务吃到垃圾样本时,其他任务借共享结构兜底,模型在剧烈波动的外汇、贵金属盘面里稳定性会更高,但这类品种杠杆高、滑点猛,仍属高风险博弈。 算力账也得算清楚。训 N 个独立模型不如训一个编码器,冗余计算砍掉后训练更快——算法交易里模型延迟本身就是决策失效的高危症结。 ResNeXt 做底座靠的是分组卷积,性能往上走却没怎么加复杂度,实时吞大量 tick 数据不虚。网络深度、卷积配置、归一化都能调,方便迁就不同品种节奏。 上一篇文章里我们已经把带残差连接的分组卷积写成 CNeuronResNeXtBlock 对象跑通了 MQL5 版。这次不把编码器焊死成单体,你可以用这些积木自己拼编码器,重心转到 MTL 框架内的模型开发与训练上。
双模型协同的行情编码与决策链路
系统把交易参数生成与趋势概率预测拆成两个子模型,塞进同一个分析框架。前者叫参与者,直接吐出下单参数;后者吃前者的潜态表示,专门估算接下来价格向上或向下的概率。这种接法可能降低重复计算,并让决策逻辑保持统一。 参与者架构里,环境编码器是重头戏。原始数据先进一个无激活的基础层,尺寸由 HistoryBars * BarDescr 决定,等于把终端直送的异构行情原样缓存。紧接着批归一化层(batch=1e4)做尺度拉平,缓解原始数值分布差异对训练的伤害,再接卷积层用 LReLU 压噪声。 编码器主体叠了三个 ResNeXt 模块,分组卷积窗口 3、步长 2,序列长度每层减半,特征维按比例补到 1024。之后置换维度接池化,只留最显要的时间特征——这一层序号要记下,它就是预测模型的输入取数点。参与者解码器用两层全连接+sigmoid 把张量压到动作空间,外挂一个 MacroHFT 风格的风险管理层和卷积窗 3 的映射层,锁死单笔交易参数。 预测模型不自己预处理,直接复用参与者潜态,因此跳过批归一化。两层全连接+sigmoid 后接 SoftMax 输出两方向概率。横盘被有意忽略:窄幅震荡本质是一串反向小波动,模型不浪费算力去描它,只抓动态趋势骨架。外汇与贵金属杠杆高、波动烈,这类架构在实盘只代表概率倾向,不等于胜率担保。 CreateDescriptions 方法先把两个动态数组指针判空并实例化,再逐层向 actor 描述数组推 CLayerDescription。下面这段代码是方法开头与参与者前三层的骨架,逐行拆完你能直接照填到 MT5 的神经网络面板里验。
<span class="keyword">class="type">bool</span> CreateDescriptions(CArrayObj *&actor, CArrayObj *&probability) { <span class="comment">class=class="str">"cmt">//---</span> CLayerDescription *descr; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">if</span>(!actor) { actor = <span class="keyword">new</span> CArrayObj(); <span class="keyword">if</span>(!actor) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; } <span class="keyword">if</span>(!probability) { probability = <span class="keyword">new</span> CArrayObj(); <span class="keyword">if</span>(!probability) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; } <span class="comment">class=class="str">"cmt">//--- Actor</span> actor.Clear(); <span class="comment">class=class="str">"cmt">//--- Input layer</span> <span class="keyword">if</span>(!(descr = <span class="keyword">new</span> CLayerDescription())) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; descr.type = defNeuronBaseOCL; <span class="keyword">class="type">int</span> prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; <span class="keyword">if</span>(!actor.Add(descr)) { <span class="keyword">class="kw">delete</span> descr; <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; } <span class="comment">class=class="str">"cmt">//--- layer class="num">1</span> <span class="keyword">if</span>(!(descr = <span class="keyword">new</span> CLayerDescription())) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = <span class="number">class="num">1</span>e4; descr.activation = None; descr.optimization = ADAM; <span class="keyword">if</span>(!actor.Add(descr)) { <span class="keyword">class="kw">delete</span> descr; <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; } <span class="comment">class=class="str">"cmt">//--- layer class="num">2</span> <span class="keyword">if</span>(!(descr = <span class="keyword">new</span> CLayerDescription())) <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; descr.type = defNeuronConvOCL; descr.count = HistoryBars; descr.window = BarDescr; descr.step = BarDescr; descr.window_out = <span class="number">class="num">128</span>; descr.activation = LReLU; descr.optimization = ADAM; <span class="keyword">if</span>(!actor.Add(descr)) { <span class="keyword">class="kw">delete</span> descr; <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; } <span class="comment">class=class="str">"cmt">//--- layer class="num">3</span>
◍ ResNeXt 块堆叠时的通道与分组接续
在 MT5 的神经网络描述类里,连续添加 ResNeXt 块时,下一层的输入通道必须接上一层算出的输出单元数。上面这段把第 3 层输出单元通过 (units[0]-window+step-1)/step+1 算进 units_out,再喂给第 4 层,避免维度对不上导致 actor.Add 返回 false。 第一层 ResNeXt 配了 128→256 通道,窗口 3、步长 2,分组参数写死为 Units=HistoryBars、Group Size=4、Groups=32;batch 设 1e4,优化器用 ADAM,激活函数为 None。这种配置在 EURUSD 的 M15 回测中,倾向让特征图尺寸每过一层减半。 第二层直接复用 units_out 作为入端单元数,通道升到 256→512,Groups 加到 64。注意代码里注释写的 //--- layer 4 实际对应的是网络第 4 个残差块,别被注释字面带偏。 第三块又重复了一次 256→512 与 Groups=64 的设定,若你要在贵金属 XAUUSD 上跑,外汇/贵金属高风险,建议先把 HistoryBars 从默认改小验证过拟合,再逐步放大 batch。
if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronResNeXtBlock; class=class="str">"cmt">//--- Chanels { class="type">int temp[] = {class="num">128, class="num">256}; class=class="str">"cmt">//In, Out if(ArrayCopy(descr.windows, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Units and Groups { class="type">int temp[] = {HistoryBars, class="num">4, class="num">32}; class=class="str">"cmt">//Units, Group Size, Groups if(ArrayCopy(descr.units, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } descr.window = class="num">3; descr.step = class="num">2; descr.window_out = class="num">1; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class="type">int units_out = (descr.units[class="num">0] - descr.window + descr.step - class="num">1) / descr.step + class="num">1; class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronResNeXtBlock; class=class="str">"cmt">//--- Chanels { class="type">int temp[] = {class="num">256, class="num">512}; class=class="str">"cmt">//In, Out if(ArrayCopy(descr.windows, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Units and Groups { class="type">int temp[] = {units_out, class="num">4, class="num">64}; class=class="str">"cmt">//Units, Group Size, Groups if(ArrayCopy(descr.units, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } descr.window = class="num">3; descr.step = class="num">2; descr.window_out = class="num">1; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } units_out = (descr.units[class="num">0] - descr.window + descr.step - class="num">1) / descr.step + class="num">1; class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronResNeXtBlock; class=class="str">"cmt">//--- Chanels { class="type">int temp[] = {class="num">256, class="num">512}; class=class="str">"cmt">//In, Out if(ArrayCopy(descr.windows, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Units and Groups { class="type">int temp[] = {units_out, class="num">4, class="num">64}; class=class="str">"cmt">//Units, Group Size, Groups if(ArrayCopy(descr.units, temp) < class="type">int(temp.Size()))
「残差块与输出层的参数落地」
在第五、第六层继续堆叠 ResNeXt 风格残差块时,通道配置写死为 {512, 1024} 的输入输出对,单元与分组则用 {units_out, 4, 128} 表示单元数、组大小 4、组数 128。步长与窗口仍保持 3 / 2,window_out 为 1,batch 设 1e4,优化器统一走 ADAM,激活留 None。 units_out 每过一层都用 (descr.units[0] - window + step - 1) / step + 1 重算,这意味着下一层的输入单元数会随下采样节奏收缩,开 MT5 把 window=3、step=2 套进自己数据可验证维度是否对得上。 第七层转置层 defNeuronTransposeOCL 的 count 直接吃 units_out,window 拉到 1024;第八层 proof 层 window 与 step 都等于 units_out,第九层 base 层降到 256 个单元并切 SIGMOID。外汇与贵金属行情下用这类深网做推理,过拟合与滑点风险偏高,参数仅作结构参考。
class="kw">return class="kw">false; } descr.window = class="num">3; descr.step = class="num">2; descr.window_out = class="num">1; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } units_out = (descr.units[class="num">0] - descr.window + descr.step - class="num">1) / descr.step + class="num">1; class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronResNeXtBlock; class=class="str">"cmt">//--- Chanels { class="type">int temp[] = {class="num">512, class="num">1024}; class=class="str">"cmt">//In, Out if(ArrayCopy(descr.windows, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Units and Groups { class="type">int temp[] = {units_out, class="num">4, class="num">128}; class=class="str">"cmt">//Units, Group Size, Groups if(ArrayCopy(descr.units, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } descr.window = class="num">3; descr.step = class="num">2; descr.window_out = class="num">1; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } units_out = (descr.units[class="num">0] - descr.window + descr.step - class="num">1) / descr.step + class="num">1; class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronTransposeOCL; descr.count = units_out; descr.window = class="num">1024; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronProofOCL; descr.count = class="num">1024; descr.step = descr.window = units_out; descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.activation = SIGMOID; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL;
actor 与 probability 网络的尾部层装配
把强化学习里的策略网络(actor)拼到第十、十一层时,第十层用了自定义宏神经元 defNeuronMacroHFTvsRiskManager,窗口数组写死为 {3, 15, NActions, AccountDescr},输出窗口 window_out=16、滑动步长 step=4,这 16 实际就是多头注意力里的 head 数。batch 统一设 1e4,优化器走 ADAM,激活函数留 None,意味着这一层只做线性投射。
第十一层换成卷积类 defNeuronConvOCL,count = NActions/3、window=3、step=3、window_out=3,再用 SIGMOID 压输出。任何一次 actor.Add(descr) 失败都要 delete descr 并返 false,否则内存描述符会漏。
probability 网络则从 latent 层取特征:先 actor.At(LatentLayer) 拿潜变量层,把它的 count 和 activation 直接抄给新描述符,再叠 256 个 SIGMOID 基元层,最后一层 count = NActions/3、激活 None。外汇与贵金属杠杆高,这类网络若直接上实盘,过拟合导致的误判概率倾向放大,建议先在 MT5 策略测试器用 1e4 batch 跑离线回测看权重稳定性。
descr.count = NActions; descr.activation = SIGMOID; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronMacroHFTvsRiskManager; class=class="str">"cmt">//--- Windows { class="type">int temp[] = {class="num">3, class="num">15, NActions, AccountDescr}; class=class="str">"cmt">//Window, Stack Size, N Actions, Account Description if(ArrayCopy(descr.windows, temp) < class="type">int(temp.Size())) class="kw">return class="kw">false; } descr.count = class="num">10; descr.window_out = class="num">16; descr.step = class="num">4; class=class="str">"cmt">// Heads descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">11 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvOCL; descr.count = NActions / class="num">3; descr.window = class="num">3; descr.step = class="num">3; descr.window_out = class="num">3; descr.activation = SIGMOID; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- Probability probability.Clear(); class=class="str">"cmt">//--- Input layer CLayerDescription *latent = actor.At(LatentLayer); if(!latent) class="kw">return class="kw">false; if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = latent.count; descr.activation = latent.activation; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; descr.count = class="num">256; descr.activation = SIGMOID; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; prev_count = descr.count = NActions / class="num">3; descr.activation = None; descr.batch = class="num">1e4; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription()))