交易中的神经网络:基于 ResNeXt 模型的多任务学习(终篇)(基础篇)
📘

交易中的神经网络:基于 ResNeXt 模型的多任务学习(终篇)(基础篇)

第 1/3 篇

◍ 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 的神经网络面板里验。

MQL5 / C++
<span class="keyword">class="type">bool</span> CreateDescriptions(CArrayObj *&amp;actor, CArrayObj *&amp;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。

MQL5 / C++
  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。外汇与贵金属行情下用这类深网做推理,过拟合与滑点风险偏高,参数仅作结构参考。

MQL5 / C++
  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,意味着这一层只做线性投射。 第十一层换成卷积类 defNeuronConvOCLcount = NActions/3window=3step=3window_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 跑离线回测看权重稳定性。

MQL5 / C++
  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()))

常见问题

先确定要同时预测的几个目标(如方向+波动率),再用共享底层提取特征,避免每个任务单独建模。落地时先跑通数据预处理再接网络。
ResNeXt 用分组卷积做多路径聚合,能在相近算力下抓更丰富的局部形态,适合多任务共享特征。对贵金属和外币对的高波动序列更稳。
小布可自动加载品种多任务信号并标注概率倾向,你直接看协同结论即可,不必自己写分组卷积代码。高风险品种会额外提示波动。
上一层输出通道要等于当前块的分组数乘每组的通道宽,残差相加前维度必须一致。建议先画张维度流转图再写装配代码。
要分开,共享编码后各自接全连接输出动作与概率,最后一层激活用 softmax 或 sigmoid 看任务类型。外汇贵金属务必带风控截断。