从头开始采用 MQL 语言进行深度神经网络编程·进阶篇
🧠

从头开始采用 MQL 语言进行深度神经网络编程·进阶篇

(2/3)· 上一篇铺垫了基础概念,本篇直接落地可交易的深度神经网络结构与实盘 EA 雏形

实战向 第 2/3 篇
很多交易者以为深度学习必须依赖 Python 和外部 API,其实 MetaTrader 5 内就能用 MQL 搭出带隐藏层的网络。本篇带你从单个神经元一路写到能输出买入/卖出/持有三态的 DeepNeuralNetwork 类,跳过那些只讲理论的坑。

硬编码双隐藏层的 DNN 类骨架

在 MT5 里搭深度神经网络,先得有个能装权重和偏置的容器。下面这个类把结构锁死成 3-4-5-3 的全连接前馈网:输入 4 节点,隐藏层 A 5 节点,隐藏层 B 3 节点,输出层 3 节点,两个隐藏层是硬编码写死的。 权重矩阵按层间关系拆成三块:iaWeights 管输入到 A 层,abWeights 管 A 到 B 层,boWeights 管 B 到输出层。因为 MQL5 多维数组只有第一维能动态,其余维度必须静态,所以用 #define 把 SIZEI=4、SIZEA=5、SIZEB=3 顶在文件头,矩阵第二维直接吃常量。 偏置和局部输出也分层存:aBiases / bBiases / oBiases 对应两层隐藏加输出层;aOutputs 和 bOutputs 留在类作用域,专门接隐藏层算完的局部激活值,方便后面反向传播或调试时直接看中间结果。 开 MT5 把附件源码里的类头抄进 EA,先不动训练逻辑,只实例化一个 DeepNeuralNetwork(4,5,3,3),用随机权重跑一次 ComputeOutputs,确认 aOutputs 长度恰为 5、bOutputs 恰为 3,就能验证这套骨架没接错线。外汇与贵金属价格序列非线性强,用此类做信号建模属高风险尝试,实盘前务必离线回测。

MQL5 / C++
class="macro">#define SIZEI class="num">4
class="macro">#define SIZEA class="num">5
class="macro">#define SIZEB class="num">3
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class DeepNeuralNetwork
  {
class="kw">private:
   class="type">int               numInput;
   class="type">int               numHiddenA;
   class="type">int               numHiddenB;
   class="type">int               numOutput;
   class="type">class="kw">double            inputs[];
   class="type">class="kw">double            iaWeights[][SIZEI];
   class="type">class="kw">double            abWeights[][SIZEA];
   class="type">class="kw">double            boWeights[][SIZEB];
   class="type">class="kw">double            aBiases[];
   class="type">class="kw">double            bBiases[];
   class="type">class="kw">double            oBiases[];
   class="type">class="kw">double            aOutputs[];
   class="type">class="kw">double            bOutputs[];
   class="type">class="kw">double            outputs[];
class="kw">public:
                     DeepNeuralNetwork(class="type">int _numInput,class="type">int _numHiddenA,class="type">int _numHiddenB,class="type">int _numOutput)
     {...}

   class="type">void SetWeights(class="type">class="kw">double &weights[])
     {...}

   class="type">void ComputeOutputs(class="type">class="kw">double &xValues[],class="type">class="kw">double &yValues[])
     {...}

   class="type">class="kw">double HyperTanFunction(class="type">class="kw">double x)
     {...}

   class="type">void Softmax(class="type">class="kw">double &oSums[],class="type">class="kw">double &_softOut[])
     {...}

  };
class=class="str">"cmt">//+------------------------------------------------------------------+

「双隐藏层网络的逐层前向推导」

前向计算的本质是把输入向量沿权重矩阵推过两个隐藏层,再压到输出层。代码里先用 aSums、bSums、oSums 三块临时数组承接每一层激活前的加权和,避免反复分配内存。 两个隐藏层都用双曲正切(Tan-h)做激活:输入强负时输出落进 (-1,1) 的负区间,只有接近 0 的输入才映射到接近 0 的输出,这和 Sigmoid 的纯正区间不同,能让中心附近的梯度更对称。输出层则换用 Softmax,把 oSums 转成若干类上的小数概率,且这些概率之和被约束为 1.0,利于多类判别时训练更快收敛。 看这段 MT5 里的 ComputeOutputs,能直接照着改层数或节点数来验证: void ComputeOutputs(double &xValues[],double &yValues[]) { double aSums[]; // hidden A nodes sums scratch array double bSums[]; // hidden B nodes sums scratch array double oSums[]; // output nodes sums ArrayResize(aSums,numHiddenA); ArrayFill(aSums,0,numHiddenA,0); ArrayResize(bSums,numHiddenB); ArrayFill(bSums,0,numHiddenB,0); ArrayResize(oSums,numOutput); ArrayFill(oSums,0,numOutput,0); int size=ArraySize(xValues); for(int i=0; i<size;++i) // copy x-values to inputs this.inputs[i]=xValues[i]; for(int j=0; j<numHiddenA;++j) // compute sum of (ia) weights * inputs for(int i=0; i<numInput;++i) aSums[j]+=this.inputs[i]*this.iaWeights[i][j]; // note += for(int i=0; i<numHiddenA;++i) // add biases to a sums aSums[i]+=this.aBiases[i]; for(int i=0; i<numHiddenA;++i) // apply activation this.aOutputs[i]=HyperTanFunction(aSums[i]); // hard-coded for(int j=0; j<numHiddenB;++j) // compute sum of (ab) weights * a outputs = local inputs for(int i=0; i<numHiddenA;++i) bSums[j]+=aOutputs[i]*this.abWeights[i][j]; // note += for(int i=0; i<numHiddenB;++i) // add biases to b sums bSums[i]+=this.bBiases[i]; for(int i=0; i<numHiddenB;++i) // apply activation this.bOutputs[i]=HyperTanFunction(bSums[i]); // hard-coded for(int j=0; j<numOutput;++j) // compute sum of (bo) weights * b outputs = local inputs for(int i=0; i<numHiddenB;++i) oSums[j]+=bOutputs[i]*boWeights[i][j]; for(int i=0; i<numOutput;++i) // add biases to input-to-hidden sums oSums[i]+=oBiases[i]; double softOut[]; Softmax(oSums,softOut); // softmax activation does all outputs at once for efficiency ArrayCopy(outputs,softOut); 逐行拆一下关键处:ArrayResize 按 numHiddenA / numHiddenB / numOutput 定长,ArrayFill 清 0 是防止残值污染加权和;iaWeights 是输入层到 A 层的二维权重,aSums[j]+=… 走的是典型点积累加;HyperTanFunction 写死在 A、B 两层,说明网络结构里激活函数不是运行时可配的;最后 Softmax 一次性算完所有输出类概率,再 ArrayCopy 进 outputs 供外部读。 在 MT5 里把 numHiddenA 设成 8、numHiddenB 设成 6 跑一遍,观察 aOutputs 是否出现明显的负区间值,就能确认 Tan-h 的确在起作用。外汇与贵金属行情噪声大,这类网络用于信号推断仅代表概率倾向,实盘前务必做样本外验证。

MQL5 / C++
class="type">void ComputeOutputs(class="type">class="kw">double &xValues[],class="type">class="kw">double &yValues[])
  {
    class="type">class="kw">double aSums[]; class=class="str">"cmt">// hidden A nodes sums scratch array
    class="type">class="kw">double bSums[]; class=class="str">"cmt">// hidden B nodes sums scratch array
    class="type">class="kw">double oSums[]; class=class="str">"cmt">// output nodes sums
    ArrayResize(aSums,numHiddenA);
    ArrayFill(aSums,class="num">0,numHiddenA,class="num">0);
    ArrayResize(bSums,numHiddenB);
    ArrayFill(bSums,class="num">0,numHiddenB,class="num">0);
    ArrayResize(oSums,numOutput);
    ArrayFill(oSums,class="num">0,numOutput,class="num">0);
    class="type">int size=ArraySize(xValues);
    for(class="type">int i=class="num">0; i<size;++i) class=class="str">"cmt">// copy x-values to inputs
       this.inputs[i]=xValues[i];
    for(class="type">int j=class="num">0; j<numHiddenA;++j) class=class="str">"cmt">// compute sum of(ia) weights * inputs
      for(class="type">int i=class="num">0; i<numInput;++i)
         aSums[j]+=this.inputs[i]*this.iaWeights[i][j]; class=class="str">"cmt">// note +=
    for(class="type">int i=class="num">0; i<numHiddenA;++i) class=class="str">"cmt">// add biases to a sums
       aSums[i]+=this.aBiases[i];
    for(class="type">int i=class="num">0; i<numHiddenA;++i) class=class="str">"cmt">// apply activation
       this.aOutputs[i]=HyperTanFunction(aSums[i]); class=class="str">"cmt">// hard-coded
    for(class="type">int j=class="num">0; j<numHiddenB;++j) class=class="str">"cmt">// compute sum of(ab) weights * a outputs = local inputs
      for(class="type">int i=class="num">0; i<numHiddenA;++i)
         bSums[j]+=aOutputs[i]*this.abWeights[i][j]; class=class="str">"cmt">// note +=
    for(class="type">int i=class="num">0; i<numHiddenB;++i) class=class="str">"cmt">// add biases to b sums
       bSums[i]+=this.bBiases[i];
    for(class="type">int i=class="num">0; i<numHiddenB;++i) class=class="str">"cmt">// apply activation
       this.bOutputs[i]=HyperTanFunction(bSums[i]); class=class="str">"cmt">// hard-coded
    for(class="type">int j=class="num">0; j<numOutput;++j) class=class="str">"cmt">// compute sum of(bo) weights * b outputs = local inputs
      for(class="type">int i=class="num">0; i<numHiddenB;++i)
         oSums[j]+=bOutputs[i]*boWeights[i][j];
    for(class="type">int i=class="num">0; i<numOutput;++i) class=class="str">"cmt">// add biases to class="kw">input-to-hidden sums
       oSums[i]+=oBiases[i];
    class="type">class="kw">double softOut[];
    Softmax(oSums,softOut); class=class="str">"cmt">// softmax activation does all outputs at once for efficiency
    ArrayCopy(outputs,softOut);

◍ 双曲正切与Softmax的截断实现

在 MT5 里跑神经网络推理,激活函数若不做数值保护,x 绝对值过大时 MathTanh 会吃浮点下溢。下面这段把 x<-20.0 直接截断返回 -1.0、x>20.0 返回 1.0,注释写明该近似在 30 位小数内正确,中间段才走 MathTanh(x)。 Softmax 函数一次性处理所有输出节点,先扫一遍 oSums 取最大值 max,再算 scale = Σ exp(oSums[i]-max),最后逐节点写 _softOut[i]=exp(oSums[i]-max)/scale。减 max 这步不是优化炫技,是防 exp 爆 INF 的硬手段,你开 MT5 把 oSums 塞入 [30, 31, 32] 试算就能看到不减去 max 时 MathExp(32) 已超 double 安全区。 外汇与贵金属行情用这类网络做状态分类属高风险实验,输出概率只代表模型倾向,不构成任何方向建议。

MQL5 / C++
   ArrayCopy(yValues,this.outputs);
   }
   class="type">class="kw">double HyperTanFunction(class="type">class="kw">double x)
   {
      if(x<-class="num">20.0) class="kw">return -class="num">1.0; class=class="str">"cmt">// approximation is correct to class="num">30 decimals
      else if(x > class="num">20.0) class="kw">return class="num">1.0;
      else class="kw">return MathTanh(x); class=class="str">"cmt">//Use explicit formula for MQL4(class="num">1-exp(-class="num">2*x))/(class="num">1+exp(-class="num">2*x))
   }
   class="type">void Softmax(class="type">class="kw">double &oSums[],class="type">class="kw">double &_softOut[])
   {
      class=class="str">"cmt">// determine max output sum
      class=class="str">"cmt">// does all output nodes at once so scale doesn&class="macro">#x27;t have to be re-computed each time
      class="type">int size=ArraySize(oSums);
      class="type">class="kw">double max= oSums[class="num">0];
      for(class="type">int i = class="num">0; i<size;++i)
         if(oSums[i]>max) max=oSums[i];
      class=class="str">"cmt">// determine scaling factor -- sum of exp(each val - max)
      class="type">class="kw">double scale=class="num">0.0;
      for(class="type">int i= class="num">0; i<size;++i)
         scale+= MathExp(oSums[i]-max);
      ArrayResize(_softOut,size);
      for(class="type">int i=class="num">0; i<size;++i)
         _softOut[i]=MathExp(oSums[i]-max)/scale;
   }

用蜡烛相对值喂给 4-4-5-3 深度网络

做 EA 前先定输入:深度网络擅长形态分类,这里直接喂日本蜡烛的相对值——上影线、实体、下影线占整根蜡烛的比例,再加一个方向(看涨或看跌)。样本数不用贪多,演示程序够跑通即可。 网络结构 4-4-5-3,权重加偏置总数按 (4*4)+4+(4*5)+5+(5*3)+3 算出来正好是 63 个。输入用各部分尺寸除以蜡烛总长的百分比,归一后穿过网络。 Softmax 吐出 3 个输出,合计 100%,存进 yValues 数组。只有某个输出大于 60% 才认定为有效信号去触发交易——外汇和贵金属波动剧烈,这种阈值只是过滤噪声,不表示胜率保证。 下面这段是 EA 头部对网络的声明和 63 个参数初值(全设 1.0 仅为占位,实盘要训练后替换)。逐行看:前 4 行定各层节点数;w0~w15 加 b0~b3 覆盖第一隐层;w40~w59 加 b4~b8 覆盖第二隐层;w60~w62 是输出层权重。

MQL5 / C++
class="macro">#include <DeepNeuralNetwork.mqh>
class="type">int numInput=class="num">4;
class="type">int numHiddenA = class="num">4;
class="type">int numHiddenB = class="num">5;
class="type">int numOutput=class="num">3;
DeepNeuralNetwork dnn(numInput,numHiddenA,numHiddenB,numOutput);
class=class="str">"cmt">//--- weight & bias values
class="kw">input class="type">class="kw">double w0=class="num">1.0;
class="kw">input class="type">class="kw">double w1=class="num">1.0;
class="kw">input class="type">class="kw">double w2=class="num">1.0;
class="kw">input class="type">class="kw">double w3=class="num">1.0;
class="kw">input class="type">class="kw">double w4=class="num">1.0;
class="kw">input class="type">class="kw">double w5=class="num">1.0;
class="kw">input class="type">class="kw">double w6=class="num">1.0;
class="kw">input class="type">class="kw">double w7=class="num">1.0;
class="kw">input class="type">class="kw">double w8=class="num">1.0;
class="kw">input class="type">class="kw">double w9=class="num">1.0;
class="kw">input class="type">class="kw">double w10=class="num">1.0;
class="kw">input class="type">class="kw">double w11=class="num">1.0;
class="kw">input class="type">class="kw">double w12=class="num">1.0;
class="kw">input class="type">class="kw">double w13=class="num">1.0;
class="kw">input class="type">class="kw">double w14=class="num">1.0;
class="kw">input class="type">class="kw">double w15=class="num">1.0;
class="kw">input class="type">class="kw">double b0=class="num">1.0;
class="kw">input class="type">class="kw">double b1=class="num">1.0;
class="kw">input class="type">class="kw">double b2=class="num">1.0;
class="kw">input class="type">class="kw">double b3=class="num">1.0;
class="kw">input class="type">class="kw">double w40=class="num">1.0;
class="kw">input class="type">class="kw">double w41=class="num">1.0;
class="kw">input class="type">class="kw">double w42=class="num">1.0;
class="kw">input class="type">class="kw">double w43=class="num">1.0;
class="kw">input class="type">class="kw">double w44=class="num">1.0;
class="kw">input class="type">class="kw">double w45=class="num">1.0;
class="kw">input class="type">class="kw">double w46=class="num">1.0;
class="kw">input class="type">class="kw">double w47=class="num">1.0;
class="kw">input class="type">class="kw">double w48=class="num">1.0;
class="kw">input class="type">class="kw">double w49=class="num">1.0;
class="kw">input class="type">class="kw">double w50=class="num">1.0;
class="kw">input class="type">class="kw">double w51=class="num">1.0;
class="kw">input class="type">class="kw">double w52=class="num">1.0;
class="kw">input class="type">class="kw">double w53=class="num">1.0;
class="kw">input class="type">class="kw">double w54=class="num">1.0;
class="kw">input class="type">class="kw">double w55=class="num">1.0;
class="kw">input class="type">class="kw">double w56=class="num">1.0;
class="kw">input class="type">class="kw">double w57=class="num">1.0;
class="kw">input class="type">class="kw">double w58=class="num">1.0;
class="kw">input class="type">class="kw">double w59=class="num">1.0;
class="kw">input class="type">class="kw">double b4=class="num">1.0;
class="kw">input class="type">class="kw">double b5=class="num">1.0;
class="kw">input class="type">class="kw">double b6=class="num">1.0;
class="kw">input class="type">class="kw">double b7=class="num">1.0;
class="kw">input class="type">class="kw">double b8=class="num">1.0;
class="kw">input class="type">class="kw">double w60=class="num">1.0;
class="kw">input class="type">class="kw">double w61=class="num">1.0;
class="kw">input class="type">class="kw">double w62=class="num">1.0;

「把 K 线拆成百分比再喂给神经网络」

这段逻辑干的事很直接:把单根蜡烛的上影、下影、实体占整根长度的比例算出来,再带上趋势方向,塞进一个 DNN 做推理。外汇和贵金属波动快,这种比例化特征对跨品种迁移更稳,但杠杆风险高,实盘前务必在 MT5 策略测试器里跑过。 CandlePatterns 函数先按 uod(收盘价减开盘价)正负区分收阳还是收阴:阳线时上影=high-close、下影=open-low、实体=close-open,trend 记 1;阴线反过来,trend 记 0。p100 为 0 直接返回 -1,避免除零崩脚本。 算完四个值写进 xInputs 数组:xInputs[0] 上影占比、xInputs[1] 下影占比、xInputs[2] 实体占比、xInputs[3] 趋势标记。随后 CopyRates 取最近 5 根、用最新一根调用,若 error<0 直接 return 不交易。 DNN 权重由 dnn.SetWeights(weight) 载入,ComputeOutputs 出 yValues。只有当 yValues[0] > 0.6 时才考虑动手:有空单先平掉、有多单就不动,相当于把模型输出阈值卡在 60% 才信信号。你可以把 0.6 调到 0.5 或 0.7,看回测中假信号频率怎么变。

MQL5 / C++
class="kw">input class="type">class="kw">double w63=class="num">1.0;
class="kw">input class="type">class="kw">double w64=class="num">1.0;
class="kw">input class="type">class="kw">double w65=class="num">1.0;
class="kw">input class="type">class="kw">double w66=class="num">1.0;
class="kw">input class="type">class="kw">double w67=class="num">1.0;
class="kw">input class="type">class="kw">double w68=class="num">1.0;
class="kw">input class="type">class="kw">double w69=class="num">1.0;
class="kw">input class="type">class="kw">double w70=class="num">1.0;
class="kw">input class="type">class="kw">double w71=class="num">1.0;
class="kw">input class="type">class="kw">double w72=class="num">1.0;
class="kw">input class="type">class="kw">double w73=class="num">1.0;
class="kw">input class="type">class="kw">double w74=class="num">1.0;
class="kw">input class="type">class="kw">double b9=class="num">1.0;
class="kw">input class="type">class="kw">double b10=class="num">1.0;
class="kw">input class="type">class="kw">double b11=class="num">1.0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|percentage of each part of the candle respecting total size      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CandlePatterns(class="type">class="kw">double high,class="type">class="kw">double low,class="type">class="kw">double open,class="type">class="kw">double close,class="type">class="kw">double uod,class="type">class="kw">double &xInputs[])
  {
   class="type">class="kw">double p100=high-low;class=class="str">"cmt">//Total candle size
   class="type">class="kw">double highPer=class="num">0;
   class="type">class="kw">double lowPer=class="num">0;
   class="type">class="kw">double bodyPer=class="num">0;
   class="type">class="kw">double trend=class="num">0;
   if(uod>class="num">0)
     {
      highPer=high-close;
      lowPer=open-low;
      bodyPer=close-open;
      trend=class="num">1;
     }
   else
     {
      highPer=high-open;
      lowPer=close-low;
      bodyPer=open-close;
      trend=class="num">0;
     }
   if(p100==class="num">0)class="kw">return(-class="num">1);
   xInputs[class="num">0]=highPer/p100;
   xInputs[class="num">1]=lowPer/p100;
   xInputs[class="num">2]=bodyPer/p100;
   xInputs[class="num">3]=trend;
   class="kw">return(class="num">1);
   }
   class="type">MqlRates rates[];
   ArraySetAsSeries(rates,true);
   class="type">int copied=CopyRates(_Symbol,class="num">0,class="num">1,class="num">5,rates);
   class=class="str">"cmt">//Compute the percent of the upper shadow, lower shadow and body in base of sum class="num">100%
   class="type">int error=CandlePatterns(rates[class="num">0].high,rates[class="num">0].low,rates[class="num">0].open,rates[class="num">0].close,rates[class="num">0].close-rates[class="num">0].open,_xValues);
   if(error<class="num">0)class="kw">return;

   dnn.SetWeights(weight);
   class="type">class="kw">double yValues[];
   dnn.ComputeOutputs(_xValues,yValues);
class=class="str">"cmt">//--- if the output value of the neuron is mare than class="num">60%
   if(yValues[class="num">0]>class="num">0.6)
     {
      if(m_Position.Select(my_symbol))class=class="str">"cmt">//check if there is an open position
        {
         if(m_Position.PositionType()==POSITION_TYPE_SELL) m_Trade.PositionClose(my_symbol);class=class="str">"cmt">//Close the opposite position if exists
         if(m_Position.PositionType()==POSITION_TYPE_BUY) class="kw">return;
        }
把网络诊断交给小布盯盘
这些网络推理与信号输出的逻辑,小布盯盘的 AIGC 模块已内置了可视化诊断,打开对应品种页即可看到实时神经元激活分布,你只需专注阈值调参。

常见问题

隐藏层通常用双曲正切 Tan-h 做非线性变换,输出层用 Softmax 把结果归一化为买入、卖出、持有三类概率,概率最高者作为信号倾向。
严格说单层隐藏网络常被称为浅层网络;本篇实现的 DeepNeuralNetwork 类支持堆叠多层隐藏层,层数增加后才更接近深度结构,具体深度按数据复杂度定。
小布盯盘侧重行情侧的信号解释与盘口诊断,MQL 网络代码需在 MT5 策略测试器内编译运行;小布可承接其输出做二次可读化呈现。
外汇与贵金属杠杆高、噪声大,网络在历史数据上分类准确不代表未来概率稳定,实盘前务必用跨周期样本压力测试。
类返回最大概率标签后,EA 在 Tick 或柱闭合事件里判别标签,再走 CTrade 下单接口;仓位与止损规则建议外置参数,便于概率化调优。