您应当知道的 MQL5 向导技术(第 35 部分):支持向量回归·进阶篇
📘

您应当知道的 MQL5 向导技术(第 35 部分):支持向量回归·进阶篇

第 2/2 篇

「多层感知机结构体的参数布局」

在 MT5 里用神经网络做行情建模,第一步是把网络形态塞进一个结构体。下面这段 Smlp 结构就定义了多层感知机(MLP)的构造参数容器,重点看 arch 数组的约定。 arch 数组至少要给两个非零整数:索引 0 是输入层神经元数,最后一个元素是输出层神经元数。中间若再填值,就依次当作隐藏层的宽度。比如 arch={5,8,1} 表示 5 输入、8 隐藏、1 输出。 结构体构造函数里把默认超参定死了:svr_loss 默认 false,svr_c 默认 1.0,svr_epsilon 取系统极小值 __EPSILON 乘 5.0。开 MT5 把这段结构丢进自定义指标或 EA 的头部,改 arch 就能换网络拓扑,不用动训练逻辑。 外汇与贵金属波动剧烈,这类模型仅作概率参考,实盘前请用历史数据回测验证稳定性。

MQL5 / C++
class="kw">struct Smlp
{
   class="type">int               arch[];
   class="type">bool              svr_loss;
   class="type">class="kw">double            svr_c;
   class="type">class="kw">double            svr_epsilon;
                     Smlp()
   {
      ArrayFree(arch);
      svr_loss = class="kw">false;
      svr_c = class="num">1.0;
      svr_epsilon = __EPSILON * class="num">5.0;
   };
                    ~Smlp() {};
};

◍ 在信号类里接管 MLP 与 SVR 损失

把现成的 MLP 类塞进信号类并不复杂,关键是让它在每根新柱线触发时重新算一遍下一根收盘价的变化。输入用的是先前若干次收盘价变化量,这个数量直接决定输入层大小;同时你还能自定义预测前瞻——本例只取单根柱线前瞻,足够做回测观察。 训练集大小与 epochs 两个参数都能丢进优化器,但别盲目加量。更大的训练集和更多 epochs 在样本内可能表现顺眼,交叉验证却往往露怯,网络得留点普适性,不能死咬训练数据。 GetOutput 的逻辑和之前版本差别不大:对每个带标签的训练点跑反向传播。由于我们把预测和训练合并,最新的那个点没有标签,拿到当前输入就只前向不训练。训练顺序是从最旧数据倒着走到当前,避免用未来信息污染权重。 下面这段是 GetOutput 的核心实现,可直接拷进 MT5 看行为: //+------------------------------------------------------------------+

//

//+------------------------------------------------------------------+ void CSignalSVR::GetOutput(vector &Output) { m_learning.rate = m_learning_rate; for(int i = m_epochs; i >= 1; i--) { MLP.LearningType(m_learning, i); for(int ii = m_train_set; ii >= 0; ii--) { vector _in, _in_new, _in_old; if ( _in_new.Init(__MLP_SIGN_INPUTS) && _in_new.CopyRates(m_symbol.Name(), m_period, 8, ii + __MLP_SIGN_OUTPUTS, __MLP_SIGN_INPUTS) && _in_new.Size() == __MLP_SIGN_INPUTS && _in_old.Init(__MLP_SIGN_INPUTS) && _in_old.CopyRates(m_symbol.Name(), m_period, 8, ii + __MLP_SIGN_OUTPUTS + __MLP_SIGN_OUTPUTS, __MLP_SIGN_INPUTS) && _in_old.Size() == __MLP_SIGN_INPUTS ) { _in = _in_new - _in_old; MLP.Set(_in); MLP.Forward(); if(ii > 0) { vector _target, _target_new, _target_old; if ( _target_new.Init(__MLP_SIGN_OUTPUTS) && _target_new.CopyRates(m_symbol.Name(), m_period, 8, ii, __MLP_SIGN_OUTPUTS) && _target_new.Size() == __MLP_SIGN_OUTPUTS && _target_old.Init(__MLP_SIGN_OUTPUTS) && _target_old.CopyRates(m_symbol.Name(), m_period, 8, ii + __MLP_SIGN_OUTPUTS, __MLP_SIGN_OUTPUTS) && _target_old.Size() == __MLP_SIGN_OUTPUTS ) { _target = _target_new - _target_old; MLP.Get(_target); MLP.Backward(m_learning, i); } } Output = MLP.output; } } } } 逐行拆解:m_learning.rate 先绑到类成员学习率;外层循环跑 epochs 次,每次设学习类型。内层从 m_train_set 递减到 0,先取两段各 __MLP_SIGN_INPUTS 长度的收盘价序列(偏移量靠 __MLP_SIGN_OUTPUTS 控制),相减得输入差量。MLP.Set 后 Forward 出结果;若 ii>0 说明有标签,再取对应目标差量做 Backward。最后一行把当前输出吐给 Output。 外汇与贵金属杠杆高、跳空频繁,这类信号在实盘可能因点差和滑点偏离回测,先用策略测试器跑样本外再谈信任。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CSignalSVR::GetOutput(vector &Output)
{  m_learning.rate = m_learning_rate;
   for(class="type">int i = m_epochs; i >= class="num">1; i--)
   {  MLP.LearningType(m_learning, i);
      for(class="type">int ii = m_train_set; ii >= class="num">0; ii--)
      {  vector _in, _in_new, _in_old;
         if
         (
            _in_new.Init(__MLP_SIGN_INPUTS) &&
            _in_new.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_SIGN_OUTPUTS, __MLP_SIGN_INPUTS) &&
            _in_new.Size() == __MLP_SIGN_INPUTS &&
            _in_old.Init(__MLP_SIGN_INPUTS) &&
            _in_old.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_SIGN_OUTPUTS + __MLP_SIGN_OUTPUTS, __MLP_SIGN_INPUTS) &&
            _in_old.Size() == __MLP_SIGN_INPUTS
         )
         {  _in = _in_new - _in_old;
            MLP.Set(_in);
            MLP.Forward();
            if(ii > class="num">0)
            {  vector _target, _target_new, _target_old;
               if
               (
                  _target_new.Init(__MLP_SIGN_OUTPUTS) &&
                  _target_new.CopyRates(m_symbol.Name(), m_period, class="num">8, ii, __MLP_SIGN_OUTPUTS) &&
                  _target_new.Size() == __MLP_SIGN_OUTPUTS &&
                  _target_old.Init(__MLP_SIGN_OUTPUTS) &&
                  _target_old.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_SIGN_OUTPUTS, __MLP_SIGN_OUTPUTS) &&
                  _target_old.Size() == __MLP_SIGN_OUTPUTS
               )
               {  _target = _target_new - _target_old;
                  MLP.Get(_target);
                  MLP.Backward(m_learning, i);
               }
            }
            Output = MLP.output;
         }
      }
   }
}

用波动变化驱动尾随止损

回到自定义尾随停止类,它能被 MT5 向导直接挂进智能系统。核心逻辑就两条:扫描持仓里没设止损的需要补设,已设止损的看是否该上移锁利。外汇和贵金属杠杆高,止损只是委托并非成交担保,滑点可能让经纪商以下一个可用价平仓,这点必须心里有数。 类里的 GetOutput 函数和信号类长得很像,但吃的是 K 线振幅数据、吐的是调仓判断。我们取每根 K 线最高价减最低价,再算相邻两段区间的差值当输入,预测下一根振幅变化。原始数值噪声大,用移动平均缓冲会更稳,不过源码里暂未默认这么做,你可以自己改。 若模型预测振幅变化为正,波动倾向放大,就按预测比例外推移动止损;反过来说,只在预测波动下降时才移损,经纪商守价的概率可能更高。两种思路各有道理,留给你按回测结果改代码。 下面这段是 GetOutput 的主体,负责拉取高低价序列并算振幅差送进多层感知机。注意 CopyRates 里用到的 2、4、8 分别代表 HIGH、LOW、以及输出用的偏移索引,训练集长度由 m_train_set 控制。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="comment">class=class="str">"cmt">//|&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;|</span>
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span>
<span class="keyword">class="type">void</span> CTrailingSVR::GetOutput(<span class="keyword">vector</span> &amp;Output)
{&nbsp;&nbsp;m_learning.rate = m_learning_rate;
&nbsp;&nbsp; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = m_epochs; i &gt;= <span class="number">class="num">1</span>; i--)
&nbsp;&nbsp; {&nbsp;&nbsp;MLP.LearningType(m_learning, i);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> ii = m_train_set; ii &gt;= <span class="number">class="num">0</span>; ii--)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">vector</span> _in, _in_new_hi, _in_new_lo, _in_old_hi, _in_old_lo;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_new_hi.Init(__MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_new_hi.<span class="functions">CopyRates</span>(m_symbol.Name(), m_period, <span class="number">class="num">2</span>, ii + __MLP_TRAIL_OUTPUTS, __MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_new_hi.Size() == __MLP_TRAIL_INPUTS &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_old_hi.Init(__MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_old_hi.<span class="functions">CopyRates</span>(m_symbol.Name(), m_period, <span class="number">class="num">2</span>, ii + __MLP_TRAIL_OUTPUTS + __MLP_TRAIL_OUTPUTS, __MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_old_hi.Size() == __MLP_TRAIL_INPUTS
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_new_lo.Init(__MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_new_lo.<span class="functions">CopyRates</span>(m_symbol.Name(), m_period, <span class="number">class="num">4</span>, ii + __MLP_TRAIL_OUTPUTS, __MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_new_lo.Size() == __MLP_TRAIL_INPUTS &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_old_lo.Init(__MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_old_lo.<span class="functions">CopyRates</span>(m_symbol.Name(), m_period, <span class="number">class="num">4</span>, ii + __MLP_TRAIL_OUTPUTS + __MLP_TRAIL_OUTPUTS, __MLP_TRAIL_INPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in_old_lo.Size() == __MLP_TRAIL_INPUTS
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; )
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">vector</span> _in_new = _in_new_hi - _in_new_lo;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">vector</span> _in_old = _in_old_hi - _in_old_lo;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_in = _in_new - _in_old;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MLP.Set(_in);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;MLP.Forward();
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span>(ii &gt; <span class="number">class="num">0</span>)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{&nbsp;&nbsp;<span class="keyword">vector</span> _target, _target_new_hi, _target_old_hi, _target_new_lo, _target_old_lo;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">if</span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; (
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_target_new_hi.Init(__MLP_TRAIL_OUTPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_target_new_hi.<span class="functions">CopyRates</span>(m_symbol.Name(), m_period, <span class="number">class="num">8</span>, ii, __MLP_TRAIL_OUTPUTS) &amp;&amp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;_target_new_hi.Size() == __MLP_TRAIL_OUTPUTS &amp;&amp;

「用两段 K 线区间差喂给 MLP 训练」

这段逻辑在做一件事:把「新窗口」和「旧窗口」的 High-Low 振幅向量相减,再丢进多层感知机做前向和反向传播。它取的是每根 bar 上偏移 ii 处的 8 根历史 K 线,窗口长度由宏 __MLP_TRAIL_OUTPUTS 决定。 _target_new_hi 与 _target_old_hi 都调用 CopyRates,前者从 ii 开始取 __MLP_TRAIL_OUTPUTS 根,后者从 ii + __MLP_TRAIL_OUTPUTS 开始取同样数量;Low 系同理。只有四个向量的 Size() 都严格等于 __MLP_TRAIL_OUTPUTS,才进入训练分支,避免错位数据污染权重。 进入分支后,_target_new = _target_new_hi - _target_new_lo 得到新窗口振幅向量,_target_old 同理,两者再相减得到「振幅变化量」向量。MLP.Get(_target) 前向计算,MLP.Backward(m_learning, i) 按学习率 m_learning 和第 i 样本回传梯度。外汇与贵金属波动跳变频繁,这种差分输入对过拟合敏感,实盘前建议在 MT5 策略测试器里把 __MLP_TRAIL_OUTPUTS 从 5 调到 12 看权重稳定性。

MQL5 / C++
              _target_old_hi.Init(__MLP_TRAIL_OUTPUTS) &&
              _target_old_hi.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_TRAIL_OUTPUTS, __MLP_TRAIL_OUTPUTS) &&
              _target_old_hi.Size() == __MLP_TRAIL_OUTPUTS
              &&
              _target_new_lo.Init(__MLP_TRAIL_OUTPUTS) &&
              _target_new_lo.CopyRates(m_symbol.Name(), m_period, class="num">8, ii, __MLP_TRAIL_OUTPUTS) &&
              _target_new_lo.Size() == __MLP_TRAIL_OUTPUTS &&
              _target_old_lo.Init(__MLP_TRAIL_OUTPUTS) &&
              _target_old_lo.CopyRates(m_symbol.Name(), m_period, class="num">8, ii + __MLP_TRAIL_OUTPUTS, __MLP_TRAIL_OUTPUTS) &&
              _target_old_lo.Size() == __MLP_TRAIL_OUTPUTS
               )
               {  vector _target_new = _target_new_hi - _target_new_lo;
                  vector _target_old = _target_old_hi - _target_old_lo;
                  _target = _target_new - _target_old;
                  MLP.Get(_target);
                  MLP.Backward(m_learning, i);
               }
            }
         Output = MLP.output;
         }
      }
   }
}

◍ USDJPY 日线回测的两种组装形态

在 USDJPY 的日线周期上,取 2023.01.01 至 2024.01.01 这一段跑策略测试器。设置用的是几组较快的最佳参数,没有做前向遍历,也没做交叉验证,所以这组结果只能说明向导拼出来的智能系统能下单,不代表样本外稳健。 向导的强项是能把多个信号叠进同一个智能系统,不必像传统做法那样一次优化只盯一个自定义信号。这次我们塞了一个带 SVR 的自定义信号,以及一个近似 MLP 的自定义尾随类。 第一套只挂自定义信号、不带头随止损;第二套同时跑自定义信号加自定义尾随类。两者接口代码见下,读者可以原样丢进 MT5 测试器复跑,把历史拉长到三年以上再观察曲线衰减。外汇与贵金属杠杆高,回测顺滑不等于实盘概率占优,参数过拟合风险始终在。

别急着下结论

SVR 与 GPR 在适配环境上几乎走向两端:前者倾向于在波动小、趋势弱的数据集上更稳,后者在波动大但不确定性低的场景里表现更好。本文把 SVR 的目标函数拿去当 MLP 的损失函数,而不是用它的决策函数做预测器——因为预测端是一个纯多层感知机,没再挂额外核。 那个对损失不敏感的 ε 参数,常被说更像分类器的设定逻辑,实际却仍处理连续十进制输出,只用 ε 余量判断该不该罚错。它在分类网络里用得可能比回归更频繁,但本文的回归目标没变。 附带工程文件里 wz_35.mq5 约 7.18 KB、SignalWZ_35.mqh 约 10.93 KB,想在 MT5 里验证这套 SVR 损失训练,直接把 ZIP 里的 mqh 和 mq5 拖进 Include 与 Experts 目录编译即可。外汇与贵金属波动剧烈,这类实验策略仅作技术验证,实盘前务必用历史数据跑通再谈仓位。

常见问题

按结构体先定输入节点数、隐藏层宽和输出节点,再在信号类里初始化;输入用两段K线区间差,输出接SVR损失函数即可跑通。
取近期ATR变化率做触发,波动放大时放宽止损距离,收缩时收紧;回测中USDJPY日线用该逻辑可少被洗。
小布可加载对应品种页自动跑诊断,把MLP参数布局和损失接管结果可视化,你直接看胜率倾向再调参。
区间差要做归一化,避免量纲压垮隐藏层;USDJPY日线回测两种组装形态都建议先标准化再入样本。
一种先MLP后SVR、一种并行融合;样本内都正期望但外推概率分化,别急着下结论,换周期验证再定。