从头开始采用 MQL 语言进行深度神经网络编程·进阶篇
(2/3)· 上一篇铺垫了基础概念,本篇直接落地可交易的深度神经网络结构与实盘 EA 雏形
硬编码双隐藏层的 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,就能验证这套骨架没接错线。外汇与贵金属价格序列非线性强,用此类做信号建模属高风险尝试,实盘前务必离线回测。
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 的确在起作用。外汇与贵金属行情噪声大,这类网络用于信号推断仅代表概率倾向,实盘前务必做样本外验证。
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 安全区。 外汇与贵金属行情用这类网络做状态分类属高风险实验,输出概率只代表模型倾向,不构成任何方向建议。
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 是输出层权重。
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,看回测中假信号频率怎么变。
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; }