您应当知道的 MQL5 向导技术(第 35 部分):支持向量回归·进阶篇
「多层感知机结构体的参数布局」
在 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 就能换网络拓扑,不用动训练逻辑。 外汇与贵金属波动剧烈,这类模型仅作概率参考,实盘前请用历史数据回测验证稳定性。
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。 外汇与贵金属杠杆高、跳空频繁,这类信号在实盘可能因点差和滑点偏离回测,先用策略测试器跑样本外再谈信任。
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 控制。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> CTrailingSVR::GetOutput(<span class="keyword">vector</span> &Output) { m_learning.rate = m_learning_rate; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = m_epochs; i >= <span class="number">class="num">1</span>; i--) { MLP.LearningType(m_learning, i); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> ii = m_train_set; ii >= <span class="number">class="num">0</span>; ii--) { <span class="keyword">vector</span> _in, _in_new_hi, _in_new_lo, _in_old_hi, _in_old_lo; <span class="keyword">if</span> ( _in_new_hi.Init(__MLP_TRAIL_INPUTS) && _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) && _in_new_hi.Size() == __MLP_TRAIL_INPUTS && _in_old_hi.Init(__MLP_TRAIL_INPUTS) && _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) && _in_old_hi.Size() == __MLP_TRAIL_INPUTS && _in_new_lo.Init(__MLP_TRAIL_INPUTS) && _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) && _in_new_lo.Size() == __MLP_TRAIL_INPUTS && _in_old_lo.Init(__MLP_TRAIL_INPUTS) && _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) && _in_old_lo.Size() == __MLP_TRAIL_INPUTS ) { <span class="keyword">vector</span> _in_new = _in_new_hi - _in_new_lo; <span class="keyword">vector</span> _in_old = _in_old_hi - _in_old_lo; _in = _in_new - _in_old; MLP.Set(_in); MLP.Forward(); <span class="keyword">if</span>(ii > <span class="number">class="num">0</span>) { <span class="keyword">vector</span> _target, _target_new_hi, _target_old_hi, _target_new_lo, _target_old_lo; <span class="keyword">if</span> ( _target_new_hi.Init(__MLP_TRAIL_OUTPUTS) && _target_new_hi.<span class="functions">CopyRates</span>(m_symbol.Name(), m_period, <span class="number">class="num">8</span>, ii, __MLP_TRAIL_OUTPUTS) && _target_new_hi.Size() == __MLP_TRAIL_OUTPUTS &&
「用两段 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 看权重稳定性。
_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 目录编译即可。外汇与贵金属波动剧烈,这类实验策略仅作技术验证,实盘前务必用历史数据跑通再谈仓位。