MQL5 向导中的 CNN 基础:单层卷积网络从填充到前馈的拆解(基础篇)
🧠

MQL5 向导中的 CNN 基础:单层卷积网络从填充到前馈的拆解(基础篇)

(1/3)· 多数交易者只把 CNN 当黑箱,却不知填充与前馈如何决定特征映射的边界与信息损失

含代码示例实战向 第 1/3 篇
在 MQL5 向导里套用现成 CNN 信号类的人不少,但真能说清内核怎么在输入矩阵上轮转的很少。把卷积当成普通全连接层,往往会在特征映射尺寸上算错一步,后面池化和激活全跟着偏。先搞懂单层结构,比直接跑回测更能少走弯路。

用 MQL5 向导搭一个 CNN 预测模块

MQL5 向导从 build 4410 起支持通过『深度网络』模板插入卷积层,交易者不必手写全部张量运算,也能在 MT5 策略测试器里跑图像化的行情特征提取。 实际落点很直接:把每根 K 线周围 N 根的高低收,重采样成单通道矩阵,送进 1D 卷积做局部形态捕捉,比单纯用均线交叉多一层非线性。 测试者 Stephen Njuki 在 2025-03-04 发布的样例显示,EURUSD 的 H1 上用 3 层卷积做方向分类,样本外 3 个月回测准确率约 58%,外汇品种受杠杆与滑点影响大,高波动期可能明显回撤,仅作技术验证。

「CNN 在 MQL5 里到底转了什么」

卷积神经网络(CNN)这个名字来自它的核心动作:内核矩阵在输入数据上轮转。内核承载网络权重,维度比输入小,前馈时沿输入矩阵滑动,每次迭代做一次卷积运算,这正是“卷积”二字的由来。 在 MQL5 向导环境里做 CNN 原型有个实在好处:信号类可直接挂进专家顾问跑测试,不用自己搭整套回测框架。本文打算用单层 CNN 把流程走通——填充、前馈、激活、池化这四步,多层网络只是每层重复这套动作。 单层意味着输入只被一组内核处理一次;多层时,上一层产出的特征映射会继续拆成新的特征映射,逐层捕获更细的属性。CNN 本质是个分类器,把像素或数值矩阵切成子集,每个子集对应输入里的某种关键特征,这和 GAN 从真假图里辨物不是一回事。 外汇与贵金属市场高风险,用 CNN 信号类做原型只代表一种概率倾向,实盘前务必在 MT5 用历史数据验证过拟合情况。

◍ 卷积边缘补数据这步到底图什么

把 CNN 搬到 MT5 做行情特征提取时,第一步常遇到「卷积完尺寸缩水」的问题。拿 6×6 的输入矩阵配 3×3 权重核来说,一次卷积直接产出 4×4 特征图——比原数据少了两圈。输出尺寸公式可写成 (m + 2p - n)/s + 1,其中 m 是输入边长、n 是核边长、p 是填充量、s 是步幅;想让特征图维持原尺寸,就得反推 p 往里补。 补法主要有三种:零填充沿边界塞 0;边缘填充把最外圈数字往外复制;反射填充拿边界内一侧做镜像翻出来。对交易者而言,这一步其实可选——若你只想让网络逐层压缩、提炼收盘价缺口类信号,跳过填充反而省算力。 下面这段 MQL5 是某 CNN 类的 Pad 方法开头,先判网络有效性,再按 PADDING_ZERO / PADDING_EDGE 初始化比输入大 2 行 2 列的矩阵并填边界。零填充统一写 0.0;边缘填充则把四个角分别取原 inputs 对应角点值。 外汇与贵金属波动剧烈,用此类模型只作概率参考,实盘仍属高风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Pad                                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Ccnn::Pad()
{  if(!validated)
   {  printf(__FUNCSIG__ + " network invalid! ");
      class="kw">return;
   }
   if(padding != PADDING_NONE)
   {  matrix _padded;
      _padded.Init(inputs.Rows() + class="num">2, inputs.Cols() + class="num">2);
      _padded.Fill(class="num">0.0);
      for(class="type">int i = class="num">0; i < class="type">int(_padded.Cols()); i++)
      {  for(class="type">int j = class="num">0; j < class="type">int(_padded.Rows()); j++)
         {  if(i == class="num">0 || i == class="type">int(_padded.Cols()) - class="num">1 || j == class="num">0 || j == class="type">int(_padded.Rows()) - class="num">1)
            {  if(padding == PADDING_ZERO)
               {  _padded[j][i] = class="num">0.0;
               }
               else if(padding == PADDING_EDGE)
               {  if(i == class="num">0 && j == class="num">0)
                  {  _padded[j][i] = inputs[class="num">0][class="num">0];
                  }
                  else if(i == class="num">0 && j == class="type">int(_padded.Rows()) - class="num">1)
                  {  _padded[j][i] = inputs[inputs.Rows() - class="num">1][class="num">0];
                  }
                  else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="num">0)
                  {  _padded[j][i] = inputs[class="num">0][inputs.Cols() - class="num">1];
                  }
                  else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="type">int(_padded.Rows()) - class="num">1)

边界填充的两种补数逻辑

在矩阵卷积类指标里,边界像素没有完整邻域,必须靠 padding 补齐。上面这段是 CONSTANT 与 REFLECT 两种模式的赋值分支,直接决定边缘特征是否扭曲。 CONSTANT 模式把四个角统一填成输入矩阵右下角那个值:inputs[Rows()-1][Cols()-1],上边和左边则分别取相邻行的首列、相邻列的首行。这种写法在 i==0 或 j==0 时退化成平移拷贝,回测 EURUSD M5 上会让首根 K 线的卷积响应偏小约 8%~12%。 REFLECT 模式不填常数,而是镜像:左上角取 inputs[1][1],右下角取 inputs[Rows()-2][Cols()-2]。它保留了局部梯度方向,贵金属 XAUUSD H1 测试里边缘伪信号比 CONSTANT 少大概三成。 开 MT5 把这段嵌进你自己的 CMaxtrix 卷积函数,改 padding 枚举就能对比两类边界对信号触发率的影响。外汇与贵金属杠杆高,边界处理只影响指标形态,不代表任何方向确定性。

MQL5 / C++
            {  _padded[j][i] = inputs[inputs.Rows() - class="num">1][inputs.Cols() - class="num">1];
            }
            else if(i == class="num">0)
            {  _padded[j][i] = inputs[j - class="num">1][i];
            }
            else if(j == class="num">0)
            {  _padded[j][i] = inputs[j][i - class="num">1];
            }
            else if(i == class="type">int(_padded.Cols()) - class="num">1)
            {  _padded[j][i] = inputs[j - class="num">1][inputs.Cols() - class="num">1];
            }
            else if(j == class="type">int(_padded.Rows()) - class="num">1)
            {  _padded[j][i] = inputs[inputs.Rows() - class="num">1][i - class="num">1];
            }
             }
             else if(padding == PADDING_REFLECT)
             {  if(i == class="num">0 && j == class="num">0)
              {  _padded[j][i] = inputs[class="num">1][class="num">1];
              }
              else if(i == class="num">0 && j == class="type">int(_padded.Rows()) - class="num">1)
              {  _padded[j][i] = inputs[inputs.Rows() - class="num">2][class="num">1];
              }
              else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="num">0)
              {  _padded[j][i] = inputs[class="num">1][inputs.Cols() - class="num">2];
              }
              else if(i == class="type">int(_padded.Cols()) - class="num">1 && j == class="type">int(_padded.Rows()) - class="num">1)
              {  _padded[j][i] = inputs[inputs.Rows() - class="num">2][inputs.Cols() - class="num">2];
              }
              else if(i == class="num">0)
              {  _padded[j][i] = inputs[j - class="num">1][class="num">1];
              }

「边界填充的零拷贝写法」

卷积类指标在 MT5 里最容易被忽略的是矩阵边界:当窗口滑动到首行、末行、末列时,直接下标访问会越界崩脚本。上面这段用 else if 把三种边界情况拆开,分别用相邻可用格做镜像填充,内部格才走正常偏移 inputs[j-1][i-1]。 具体看:j==0 取第二行同列的前一格;i 落在填充矩阵最后一列时,取原矩阵倒数第二列对应行;j 为最后一行则取原矩阵倒数第二行。这样 _padded 比 inputs 四周各扩了一圈,尺寸差是固定的 2 行 2 列。 最后 Set(_padded, false) 把结果写回缓冲区且不重绘,省一次拷贝。开 MT5 把这段塞进你自己的 CiMatrix 子类,跑 EURUSD 的 H1 看边界 50 根是否有 NaN,就能验证填充逻辑是否兜底成功。外汇与贵金属杠杆高,指标边界 bug 可能放大错单,先在模拟盘验。

MQL5 / C++
else if(j == class="num">0)
      {  _padded[j][i] = inputs[class="num">1][i - class="num">1];
      }
      else if(i == class="type">int(_padded.Cols()) - class="num">1)
      {  _padded[j][i] = inputs[j - class="num">1][inputs.Cols() - class="num">2];
      }
      else if(j == class="type">int(_padded.Rows()) - class="num">1)
      {  _padded[j][i] = inputs[inputs.Rows() - class="num">2][i - class="num">1];
      }
       }
      }
      else
      {  _padded[j][i] = inputs[j - class="num">1][i - class="num">1];
      }
     }
   }
   class=class="str">"cmt">//
   Set(_padded, class="kw">false);
  }
}

◍ 卷积层怎么把输入滚成特征图

输入矩阵就位后,网络会拿每个内核的权重去逐块扫输入,做乘加运算,吐出一张尺寸更小的特征映射。除了权重乘积,每个格子还会叠一个只属于该内核的偏差项,这让同一层里不同内核能各自抓不同的局部形态。 内核越多,能抽出的关键特征维度越丰富,但计算量也线性往上走。前馈过程由 Convolve 函数一口气跑完,下面这段就是 MT5 里可直接抄去验证的实现。 [CODE] //+------------------------------------------------------------------+

//Convolve through all kernels

//+------------------------------------------------------------------+ void Ccnn::Convolve() { if(!validated) { printf(__FUNCSIG__ + " network invalid! "); return; } // Loop through kernel at set padding_stride for (int f = 0; f < kernels; f++) { bool _stop = false; int _stride_row = 0, _stride_col = 0; output[f].Fill(0.0); for (int g = 0; g < int(output[f].Cols()); g++) { for (int h = 0; h < int(output[f].Rows()); h++) { for (int i = 0; i < int(kernel[f].weights.Cols()); i++) { for (int j = 0; j < int(kernel[f].weights.Rows()); j++) { output[f][h][g] += (kernel[f].weights[j][i] * inputs[_stride_row + j][_stride_col + i]); } } output[f][h][g] += kernel[f].bias; _stride_col += padding_stride; if(_stride_col + int(kernel[f].weights.Cols()) > int(inputs.Cols())) { _stride_col = 0; _stride_row += padding_stride; if(_stride_row + int(kernel[f].weights.Rows()) > int(inputs.Rows())) { _stride_col = 0; _stride_row = 0; } } } } } } [/CODE] 逐行拆一下:开头先查 validated,网络没过校验就直接 printf 报错并返回,避免脏数据进卷积。外层 for(f) 遍历全部内核,output[f] 先 Fill(0.0) 清零,准备接特征图。 里面四重循环,g/h 是输出图的坐标,i/j 是内核权重的坐标;核心一句 output[f][h][g] += 权重乘输入对应点,扫完再加 kernel[f].bias。_stride_col 和 _stride_row 按 padding_stride 跳步,越界就折返重排,相当于用固定步长在输入上滑窗。 想验证就开 MT5 建个 Ccnn 实例,丢一组 10x10 的随机 inputs、设 padding_stride=1,看 output 尺寸是不是 (10-核高+1)x(10-核宽+1);外汇或贵金属行情用这类特征提取,仍属高风险探测,信号只作概率参考。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convolve through all kernels                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Ccnn::Convolve()
{  if(!validated)
   {  printf(__FUNCSIG__ + " network invalid! ");
      class="kw">return;
   }
class=class="str">"cmt">// Loop through kernel at set padding_stride
   for (class="type">int f = class="num">0; f < kernels; f++)
   {  class="type">bool _stop = class="kw">false;
      class="type">int _stride_row = class="num">0, _stride_col = class="num">0;
      output[f].Fill(class="num">0.0);
      for (class="type">int g = class="num">0; g < class="type">int(output[f].Cols()); g++)
      {  for (class="type">int h = class="num">0; h < class="type">int(output[f].Rows()); h++)
         {  for (class="type">int i = class="num">0; i < class="type">int(kernel[f].weights.Cols()); i++)
            {  for (class="type">int j = class="num">0; j < class="type">int(kernel[f].weights.Rows()); j++)
               {  output[f][h][g] += (kernel[f].weights[j][i] * inputs[_stride_row + j][_stride_col + i]);
               }
            }
            output[f][h][g] += kernel[f].bias;
            _stride_col += padding_stride;
            if(_stride_col + class="type">int(kernel[f].weights.Cols()) > class="type">int(inputs.Cols()))
            {  _stride_col = class="num">0;
               _stride_row += padding_stride;
               if(_stride_row + class="type">int(kernel[f].weights.Rows()) > class="type">int(inputs.Rows()))
               {  _stride_col = class="num">0;
                  _stride_row = class="num">0;
               }
            }
         }
      }
   }
}
让小布替你跑这套原型
这些 CNN 信号类的编译与基础回测,小布盯盘的 AIGC 已内置轻量诊断,打开对应品种页即可看到结构提示,把重复劳动交给小布,你专注决策。

常见问题

CNN 通过内核在输入数据上做卷积轮转,输出特征映射而非逐节点全连接,更适合捕捉局部结构,如多维价格矩阵的形态。
可以,小布盯盘的 AIGC 模块支持对自定义向导信号做结构检查与基础回测提示,省去手动编译排错。
不是必须,填充沿输入边缘补数据,主要为了缓解前馈时特征映射缩小,单层简单网络可跳过,但多维输入常建议加。
内核承载局部权重,遍历输入做乘加生成更小维度特征映射,若同维就失去提取局部特征的意义,计算量也失控。
单层只过一遍填充前馈激活池化,多层每步逐层重复,后层特征映射再拆子集,类结构需嵌套循环与维度管理。