神经网络实验基础篇:用 4-4-3 结构搭出 MT5 原生交易机器人骨架(基础篇)
🧠

神经网络实验基础篇:用 4-4-3 结构搭出 MT5 原生交易机器人骨架(基础篇)

(1/3)·从感知器到深度网络的过渡里,35 个权重偏差为何让优化器直接算崩

含代码示例 第 1/3 篇

不少交易者把现成神经网络库直接丢进策略测试器,发现遗传算法跑出来的权重毫无稳定性。问题往往不在模型本身,而在于你没控制优化空间维度——本文先从最朴素的 4-4-3 网络讲起,把这件事拆开。

「在 MT5 里给神经网络做智能调参」

MetaTrader 5 的测试器原生支持遗传算法优化,但面对神经网络这类高维参数空间,盲目跑全量优化会耗尽数小时甚至数天。2022 年 10 月的一篇实测显示,对一个含 12 个隐藏层权重的简单感知机模型做穷举,参数组合超过 2 197 组,单次完整 pass 在 4 核笔记本上耗时约 6 小时。 把优化目标从「最大化利润」改成「最小化预测误差 + 约束交易次数」,遗传算法的收敛代数从平均 140 代降到 60 代左右,候选解质量并未明显劣化。这说明神经网络 EA 的优化不应只看 equity 曲线,要先锁住泛化边界。 下面这段是当时测试用的基因组初始化片段,直接决定了种群是否覆盖有效权重区间: [CODE] // 初始化种群中单个神经元的权重 double InitWeight(int layer, int idx) { // 用层号做种子偏移,避免全 0 初始化陷入对称权重 int seed = layer * 1000 + idx; MathSrand(seed); // 权重限制在 [-1,1],符合 tanh 激活的输入尺度 return (MathRand() / 32767.0) * 2.0 - 1.0; } [/CODE] 逐行拆解:第 3 行用「层号×1000+索引」做随机种子,防止同一代神经元拿到相同初值导致对称权重无法打破;第 5 行 MathRand() 返回 0–32767 整数,除以 32767 归一化到 [0,1];乘 2 减 1 把区间映射到 [-1,1],匹配 tanh 输入习惯。外汇与贵金属市场高杠杆、高波动,这类未经过样本外验证的权重仅适合在策略测试器内做敏感性实验,实盘接入前必须做 walk-forward。

MQL5 / C++
class=class="str">"cmt">// 初始化种群中单个神经元的权重
class="type">class="kw">double InitWeight(class="type">int layer, class="type">int idx) {
   class=class="str">"cmt">// 用层号做种子偏移,避免全 class="num">0 初始化陷入对称权重
   class="type">int seed = layer * class="num">1000 + idx;
   MathSrand(seed);
   class=class="str">"cmt">// 权重限制在 [-class="num">1,class="num">1],符合 tanh 激活的输入尺度
   class="kw">return (MathRand() / class="num">32767.0) * class="num">2.0 - class="num">1.0;
}

从感知器迈向可用的交易机器人

上一阶段我用感知器跑通了一个简单示例,验证了把几何特征喂给神经网络能产出可盈利系统,但样本外衰减和过拟合两大坑还没填。本阶段目标很直接:换更深的网络结构,在 MT5 内闭环训练与推理,不依赖任何第三方软件。 为此我直接复用从零写的深度网络函数库,它把前向、反向、权重更新都封装好了,我只需接管数据管道和训练调度。也就是说,重点从‘网络怎么算’转移到‘喂什么、怎么喂’。 最终交付物是一个能直接在 MetaTrader 5 实盘/回测跑的 EA 雏形。外汇与贵金属杠杆高、滑点跳空频繁,任何神经网络信号都只是概率倾向,实盘前必须用历史数据交叉验证。

◍ 从 4-4-3 网络搭起自定义神经层

别一上来就追深度网络。先拿 4-4-3 结构练手:输入 4 节点、隐藏层 4 节点、输出 3 节点,权重与偏差总数 = (4*4)+4+(4*3)+3 = 35 个,手工调参也还看得过来。 代码里我改过的每一处都留了注释,你下载函数库后能直接对照着改自己的网络拓扑。W/X 是层间权重,B/S 是偏置项,含义和常规神经网络一致。 这一轮我们一次性跑六个 EA:两个旧形态(纯蝴蝶轨道、四条 MA 的 1 与 24 周期倾角形态)分别挂在原函数库策略、4-4-3、4-4-4-3 三套网络上做对照。 策略测试器里把权重和偏差优化区间设成 -1 到 1、步长 0.1,光这一组就冒出 3.68597592780611e+51 次验算量——MT5 optimizer 跑这种组合前先掂量下硬件。外汇与贵金属杠杆高,此类模型仅作概率参考,实盘前务必小资金验证。

MQL5 / C++
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 x0=class="num">1.0;
class="kw">input class="type">class="kw">double x1=class="num">1.0;
class="kw">input class="type">class="kw">double x2=class="num">1.0;
class="kw">input class="type">class="kw">double x3=class="num">1.0;
class="kw">input class="type">class="kw">double x4=class="num">1.0;
class="kw">input class="type">class="kw">double x5=class="num">1.0;
class="kw">input class="type">class="kw">double x6=class="num">1.0;
class="kw">input class="type">class="kw">double x7=class="num">1.0;
class="kw">input class="type">class="kw">double x8=class="num">1.0;
class="kw">input class="type">class="kw">double x9=class="num">1.0;
class="kw">input class="type">class="kw">double x10=class="num">1.0;
class="kw">input class="type">class="kw">double x11=class="num">1.0;
class="kw">input class="type">class="kw">double x12=class="num">1.0;
class="kw">input class="type">class="kw">double x13=class="num">1.0;
class="kw">input class="type">class="kw">double x14=class="num">1.0;
class="kw">input class="type">class="kw">double x15=class="num">1.0;
class="kw">input class="type">class="kw">double s0=class="num">1.0;
class="kw">input class="type">class="kw">double s1=class="num">1.0;
class="kw">input class="type">class="kw">double s2=class="num">1.0;
class="macro">#include <DeepNeuralNetwork2.mqh>
class="type">int numInput=class="num">4;
class="type">int numHiddenA = class="num">4;
class="type">int numOutput=class="num">3;
DeepNeuralNetwork dnn(numInput,numHiddenA,numOutput);
class="type">int error=CandlePatterns(ind_In1[class="num">1], ind_In1[class="num">10], ind_In2[class="num">1], ind_In2[class="num">10], _xValues);class=class="str">"cmt">// Call the function
class="type">int CandlePatterns(class="type">class="kw">double v1,class="type">class="kw">double v2,class="type">class="kw">double v3,class="type">class="kw">double v4,class="type">class="kw">double &xInputs[])class=class="str">"cmt">// Function
  {
   xInputs[class="num">0] = (v1-v2)/Point();
   xInputs[class="num">1] = (v3-v4)/Point();
   xInputs[class="num">2] = (v1-v4)/Point();
   xInputs[class="num">3] = (v3-v2)/Point();

   class="kw">return(class="num">1);
  }

「把K线差值压成可喂模型的数组」

做价格行为量化时,常需要把不同周期长度的K线波动归一化,再丢给后续分类或AIGC特征层。下面这段逻辑用两个指标序列在偏移 1、5、10 根的位置取值,算出四组相对位移并写入引用数组。 调用处先取 ind_In1 与 ind_In2 在 [1]、[5]、[10] 三个偏移量上的数值,传给 CandlePatterns,错误码由返回值接管,函数本身固定 return(1) 仅作占位。 函数体内核心在四行赋值:xInputs[0] 是 (v1-v2) 除以 Point() 再除 5,即近 1 与 5 根的差以点为单位的五分之一;xInputs[1] 用 1 与 10 根差除 10;xInputs[2]、[3] 同理套用第二组序列,只是 [3] 分母用了 50,把长跨度的差压缩得更小。外汇与贵金属点值随品种变动,跑之前先确认 Point() 在当前品种下是 0.00001 还是 0.0001,否则归一尺度会偏。 直接把代码贴进 MT5 自定义指标,打印 _xValues 四个元素,能立刻看到不同品种下数值量级的差异,方便你调后面那几个除数。

MQL5 / C++
class="type">int error=CandlePatterns(ind_In1[class="num">1], ind_In1[class="num">5], ind_In1[class="num">10], ind_In2[class="num">1], ind_In2[class="num">5], ind_In2[class="num">10], _xValues);class=class="str">"cmt">// Call the function
class="type">int CandlePatterns(class="type">class="kw">double v1,class="type">class="kw">double v2,class="type">class="kw">double v3,class="type">class="kw">double v4,class="type">class="kw">double v5,class="type">class="kw">double v6,class="type">class="kw">double &xInputs[])class=class="str">"cmt">// Function
  {
   xInputs[class="num">0] = (((v1-v2)/Point())/class="num">5);
   xInputs[class="num">1] = (((v1-v3)/Point())/class="num">10);
   xInputs[class="num">2] = (((v4-v5)/Point())/class="num">5);
   xInputs[class="num">3] = (((v4-v6)/Point())/class="num">50);
  
   class="kw">return(class="num">1);
  }

用单变量和 CSV 绕开神经网络优化瓶颈

在 MT5 策略测试器里跑这套 EA,慢速完整算法模式会做略多于 10,000 次验算,量级太小,喂不饱一个要调权重和偏差的神经网络;快速遗传算法在这种结构下也没什么用。思路改成只对外暴露一个变量当排序计数器,其余权重偏差全在 EA 内部用随机数生成器填,顺带记录哪些组合已经试过。 数组在每次初始化会被清零,全局变量扛不住数据量,所以落盘选了 CSV 文件:优化时写、交易模式读,还能在优化进程之外翻结果。优化变量 Passages 从 1 到上限、步长 1,试跑下来单变量模式能撑到 100,000,000 次验算,基本够用。 最初想拆成优化 EA 和交易 EA 两个程序,后来觉得单一 EA 加个模式开关更顺手。优化主逻辑塞在 OnInit() 里,难点是多核并行优化时抢同一个 CSV:第一个 while 循环卡住不让 OnInit 退出,直到文件能安全打开写入,实测 EA 在优化里是逐个核拉起的。 RNDfromCI 负责在 Min~Max 之间吐随机值并归一化到 1 位小数;下面这段把 16 个权重 sw0~sw15、4 个偏差 sb0~sb3、2 个额外参数 sx0~sx1 全拼成字符串,再交给 VerifyPassagesInFile 查重,文件里没有才写尾端并填进 weight[]。外汇和贵金属杠杆高、滑点跳空频繁,这种随机搜索只是扩大候选面,实盘前务必用历史数据复验。 EA 参数里 Optimization 切模式,Min/Max 框住随机范围,OptimizationFileName 指向 CSV,默认落点在 C:\Users\用户名\AppData\Roaming\MetaQuotes\Terminal\Common\Files。LL 是 Softmax 阈值,0.6 代表类别概率高于 60% 才触发。开 MT5 把代码丢进 OnInit 跑一遍优化,看 Common\Files 下 CSV 行数增长是否符合 Passages 设定,就能验证这套去重机制有没有漏。

MQL5 / C++
<span class="keyword">class="type">class="kw">double</span> RNDfromCI(<span class="keyword">class="type">class="kw">double</span> minp, <span class="keyword">class="type">class="kw">double</span> maxp)
{
&nbsp;&nbsp;<span class="keyword">if</span>(minp == maxp)
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span> (minp);
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> Minp, Maxp;
&nbsp;&nbsp;<span class="keyword">if</span>(minp &gt; maxp)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;Minp = maxp;
&nbsp;&nbsp;&nbsp;&nbsp;Maxp = minp;
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;Minp = minp;
&nbsp;&nbsp;&nbsp;&nbsp;Maxp = maxp;
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="kw">return</span> (<span class="functions">NormalizeDouble</span>(<span class="keyword">class="type">class="kw">double</span>(Minp + ((Maxp - Minp) * (<span class="keyword">class="type">class="kw">double</span>)<span class="functions">MathRand</span>() / <span class="number">class="num">32767.0</span>)),<span class="number">class="num">1</span>));
}
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> Passages = <span class="number">class="num">10000</span>;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">bool</span> Optimization = <span class="macro">true</span>;
<span class="keyword">if</span> (Optimization==<span class="macro">true</span>){
<span class="keyword">class="type">int</span> r=<span class="number">class="num">0</span>;
<span class="keyword">class="type">int</span> q=<span class="number">class="num">0</span>;
<span class="keyword">class="kw">while</span>(q!=<span class="number">class="num">1</span> &amp;&amp; !<span class="functions">IsStopped</span>())
{
<span class="keyword">class="type">class="kw">string</span> str;
<span class="keyword">class="kw">while</span>(r!=<span class="number">class="num">1</span> &amp;&amp; !<span class="functions">IsStopped</span>())
{
&nbsp;&nbsp; sw0=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw1=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw2=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw3=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw4=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw5=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw6=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw7=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw8=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw9=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw10=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw11=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw12=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw13=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw14=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sw15=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp;
&nbsp;&nbsp; sb0=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sb1=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sb2=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sb3=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp;
&nbsp;&nbsp; sx0=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
&nbsp;&nbsp; sx1=<span class="functions">DoubleToString</span>(RNDfromCI(Min, Max),<span class="number">class="num">1</span>);
把参数扫描交给小布盯盘
这类多权重枚举的优化实验,小布盯盘的 AIGC 已内置部分诊断视图,打开对应品种页即可看到神经网络信号的离散度,你只需专注结构取舍。

常见问题

按(4*4)+4+(4*3)+3 展开:输入到隐层权重 16、隐层偏置 4、隐层到输出权重 12、输出偏置 3,合计 35 个可训练参数。
当权重偏差范围 -1 到 1、步长 0.1 时,组合空间达到约 3.68e51 次,一万次采样覆盖率极低,无法收敛到可用解。
可以,小布盯盘的品种页提供信号离散与样本外表现对照,能帮助判断某组权重是否只在历史窗口内巧合盈利。
数组每次初始化清零,全局变量容量有限且难在优化外读取,本文改用 CSV 文件持久化已验算参数组合。
本篇基础篇后,实战篇会对比 4-4-4-3 等更深结构在 MT5 下的训练与落地差异。