您应当知道的 MQL5 向导技术(第 37 部分):配以线性和 Matérn 内核的高斯过程回归·进阶篇
◍ 用 ν 调平滑度:Matérn 内核怎么选
Matérn 内核靠一个平滑度参数 ν 控制协方差形态,这是它比固定形态核灵活的地方。ν 取 ½ 时退化为锯齿状指数核,ν 趋向无穷则变成径向基函数核;实战里常落在 ½、3/2、5/2 这三档,数值越大曲线越平滑。 从盘面视角看,ν=½ 对应的是假定价格过程不连续、跳空频出的环境,对外汇或贵金属这种高波动品种更敏锐,但输出结果偏毛刺。ν=3/2 是中等平滑,能兼顾轻度波动与轻度趋势,常被用来捕捉时间序列里的转折或波动节点;ν=5/2 及以上更贴趋势市,尤其是汇率走出连贯方向时。 要注意,ν 越大内核可微次数越多,伴随的是 MT5 回测或实时计算里更吃资源。小 ν 适配含噪、跳空数据,大 ν 适配平缓变化数据,这个取舍直接决定你高斯过程模型是对噪声过度反应还是滞后。 下面这段 MQL5 实现把 ν 锁在 3/2,公式已简化为 (1+√3·d/l)·exp(-√3·d/l),其中 d 是两点距离、l 是长度参数 m_next: //+------------------------------------------------------------------+ // Matern Kernel Function //+------------------------------------------------------------------+ matrix CSignalGauss::Matern_Kernel(vector &Rows,vector &Cols) { matrix _matern; _matern.Init(Rows.Size(), Cols.Size()); for(int i = 0; i < int(Rows.Size()); i++) { for(int ii = 0; ii < int(Cols.Size()); ii++) { _matern[i][ii] = (1.0 + (sqrt(3.0) * fabs(Rows[i] - Cols[ii]) / m_next)) * exp(-1.0 * sqrt(3.0) * fabs(Rows[i] - Cols[ii]) / m_next); } } return(_matern); } 逐行拆解:函数接收两个 vector(行、列样本),初始化等尺寸矩阵;双层循环遍历每对样本,算绝对距离 fabs(Rows[i]-Cols[ii]) 再除以长度参数 m_next 得到缩放距离;按 3/2 简化式填入矩阵元;返回协方差矩阵供后续高斯过程调用。 开 MT5 把 m_next 从默认改到品种典型振幅量级(如 EURUSD 用 0.001~0.005 试),能直观看到协方差衰减快慢变化。外汇贵金属属高风险,核函数仅描述统计依赖,不预示方向。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">// Matern Kernel Function class=class="str">"cmt">//+------------------------------------------------------------------+ matrix CSignalGauss::Matern_Kernel(vector &Rows,vector &Cols) { matrix _matern; _matern.Init(Rows.Size(), Cols.Size()); for(class="type">int i = class="num">0; i < class="type">int(Rows.Size()); i++) { for(class="type">int ii = class="num">0; ii < class="type">int(Cols.Size()); ii++) { _matern[i][ii] = (class="num">1.0 + (sqrt(class="num">3.0) * fabs(Rows[i] - Cols[ii]) / m_next)) * exp(-class="num">1.0 * sqrt(class="num">3.0) * fabs(Rows[i] - Cols[ii]) / m_next); } } class="kw">return(_matern); }
把两个核函数塞进同一个信号类
自定义信号类 CSignalGauss 里,我把线性核与 Matern 核做成可切换的两种实现,靠成员变量 m_kernel 在运行时挑一个用。GetOutput 被重写,按选中的核去算 _k_s 与 _k_ss 两个矩阵,供后续预测下一档价格变化使用。 切换逻辑很直接:m_kernel == KERNEL_LINEAR 就调 Linear_Kernel 填矩阵,否则走 Matern_Kernel。核矩阵维度由 _next_time 和 _past_time 的长度决定,_k_s 是 NxM、_k_ss 是 NxN,这一步和早先单核版本的步骤一致。 多空条件沿用「预测变化正负」的旧思路,再把差值归一化到 0–100 的整数区间——这是所有自定义信号类实例给智能系统喂分的统一格式。LongCondition 里若末尾输出大于首项,就按 (末项-首项)/(最大值-最小值)*100 四舍五入出分;ShortCondition 反过来判首项大于末项。 下面这段是类里最该在 MT5 里跑一遍的部分,注意 _o.Size()-1 取的是预测序列最后一个元素,归一化除零风险在 _o.Max()-_o.Min() 接近 0 时会出现,实盘前先打印 _o 分布。外汇与贵金属波动剧烈,该信号仅作概率参考,不代表方向必现。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CSignalGauss::GetOutput(class="type">class="kw">double BasisMean, vector &Output) { ... matrix _k_s; matrix _k_ss; _k_s.Init(_next_time.Size(), _past_time.Size()); _k_ss.Init(_next_time.Size(), _next_time.Size()); if(m_kernel == KERNEL_LINEAR) { _k_s = Linear_Kernel(_next_time, _past_time); _k_ss = Linear_Kernel(_next_time, _next_time); } else if(m_kernel == KERNEL_MATERN) { _k_s = Matern_Kernel(_next_time, _past_time); _k_ss = Matern_Kernel(_next_time, _next_time); } ... } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Voting" that price will grow. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSignalGauss::LongCondition(class="type">void) { class="type">int result = class="num">0; vector _o; GetOutput(class="num">0.0, _o); if(_o[_o.Size()-class="num">1] > _o[class="num">0]) { result = class="type">int(round(class="num">100.0 * ((_o[_o.Size()-class="num">1] - _o[class="num">0])/(_o.Max() - _o.Min())))); } class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, change is: %.5f, and result is: %i", _mlp_output, m_symbol.Bid()-_mlp_output, result);class="kw">return(class="num">0); class="kw">return(result); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Voting" that price will fall. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSignalGauss::ShortCondition(class="type">void) { class="type">int result = class="num">0; vector _o; GetOutput(class="num">0.0, _o); if(_o[_o.Size()-class="num">1] < _o[class="num">0]) { result = class="type">int(round(class="num">100.0 * ((_o[class="num">0] - _o[_o.Size()-class="num">1])/(_o.Max() - _o.Min())))); } class=class="str">"cmt">//printf(__FUNCSIG__ + " output is: %.5f, change is: %.5f, and result is: %i", _mlp_output, m_symbol.Bid()-_mlp_output, result);class="kw">return(class="num">0); class="kw">return(result); }
「回测报告里的内核表现与资金管理改造」
我们在 GBPJPY 的 2023 年日线周期上,分别跑通了线性内核与 Matérn 内核的优化。结果只说明智能系统在样本内可用,不指向未来表现;Matérn 内核在日线尺度上给出了更平滑的回归轨迹,但两者都只是验证「能跑」而非「能赚」。 高斯过程回归若想同时喂给信号类和资金管理类,最好抽一个通用锚点类让两边引用,否则两套自定义类里重复写相似函数,维护成本直接翻倍。信号类吃的是收盘价变化,而资金管理类这边换成 ATR 变化做内核输入,训练输出变成「下一根 ATR 变化量」。 原版仓位优化类会在连亏时按亏损串长度等比砍手数。我们改的逻辑是:只有「账户刚亏」且「预测的 ATR 上升」才降手数,预测步长由参数 m_next 控制。外汇与贵金属杠杆高,这类动态缩仓只能降低连续回撤杀伤,不保证扭亏。 下面这段是改造后的手数优化核心,注意它先拉历史成交、统计连亏,再调 GetOutput 拿 ATR 预测,决定是否缩仓。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Optimizing lot size for open. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">double</span> CMoneyGAUSS::Optimize(<span class="keyword">class="type">int</span> Type, <span class="keyword">class="type">class="kw">double</span> lots) { <span class="keyword">class="type">class="kw">double</span> lot = lots; <span class="comment">class=class="str">"cmt">//--- calculate number of losses orders without a break</span> <span class="keyword">if</span>(m_decrease_factor > <span class="number">class="num">0</span>) { <span class="comment">class=class="str">"cmt">//--- select history for access</span> <span class="functions">HistorySelect</span>(<span class="number">class="num">0</span>, <span class="functions">TimeCurrent</span>()); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">int</span> orders = <span class="functions">HistoryDealsTotal</span>(); <span class="comment">class=class="str">"cmt">// total history deals</span> <span class="keyword">class="type">int</span> losses = <span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// number of consequent losing orders</span> <span class="comment">class=class="str">"cmt">//--</span> <span class="keyword">class="type">int</span> size = <span class="number">class="num">0</span>; <span class="keyword">matrix</span> series; series.Init(<span class="functions">fmin</span>(m_series_size, orders), <span class="number">class="num">2</span>); series.Fill(<span class="number">class="num">0.0</span>); <span class="comment">class=class="str">"cmt">//--</span> CDealInfo deal; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = orders - <span class="number">class="num">1</span>; i >= <span class="number">class="num">0</span>; i--) { deal.Ticket(<span class="functions">HistoryDealGetTicket</span>(i)); <span class="keyword">if</span>(deal.Ticket() == <span class="number">class="num">0</span>) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"CMoneySizeOptimized::Optimize: HistoryDealGetTicket failed, no trade history"</span>); <span class="keyword">break</span>; } <span class="comment">class=class="str">"cmt">//--- check symbol</span> <span class="keyword">if</span>(deal.<span class="functions">Symbol</span>() != m_symbol.Name()) <span class="keyword">class="kw">continue</span>; <span class="comment">class=class="str">"cmt">//--- check profit</span> <span class="keyword">class="type">class="kw">double</span> profit = deal.Profit(); <span class="comment">class=class="str">"cmt">//--</span> series[size][<span class="number">class="num">0</span>] = profit; size++; <span class="comment">class=class="str">"cmt">//--</span> <span class="keyword">if</span>(size >= m_series_size) <span class="keyword">break</span>; <span class="keyword">if</span>(profit < <span class="number">class="num">0.0</span>) losses++; } <span class="comment">class=class="str">"cmt">//--</span> <span class="keyword">class="type">class="kw">double</span> _cond = <span class="number">class="num">0.0</span>; <span class="comment">class=class="str">"cmt">//--</span> <span style="background-class="type">color:rgb(class="num">216, class="num">232, class="num">194);"> <span class="keyword">vector</span> _o; GetOutput(<span class="number">class="num">0.0</span>, _o); <span class="comment">class=class="str">"cmt">//---</span> <span class="comment">class=class="str">"cmt">//decrease lots on rising ATR</span>
◍ 手数衰减与平台限制的对齐
当上一根 K 线的开盘价高于当前 K 线开盘价(即近期处于空头排列倾向)且账户出现亏损时,代码会把基础手数按亏损比例缩减:lot = NormalizeDouble(lot - lot * losses / m_decrease_factor, 2),保留两位小数,这是一种随回撤自动降仓的硬逻辑。 缩减完还不是终点,必须过一遍交易品种的真实合约限制。先取 LotsStep() 得到最小步进,把 lot 对齐到合法步长整数倍;再分别用 LotsMin() 和 LotsMax() 夹住下限与上限,低于最小手数直接拉回 minvol,高于最大手数压回 maxvol。 这段逻辑没有预测功能,只在执行层保证「降仓后报单不会被 MT5 拒掉」。外汇与贵金属杠杆高,自动降仓只能控制单笔风险暴露,不表示回撤会停止,实盘前请在策略测试器用历史数据跑一遍确认手数曲线。
if(_o[_o.Size()-class="num">1] > _o[class="num">0]) lot = NormalizeDouble(lot - lot * losses / m_decrease_factor, class="num">2); } class=class="str">"cmt">//--- normalize and check limits class="type">class="kw">double stepvol = m_symbol.LotsStep(); lot = stepvol * NormalizeDouble(lot / stepvol, class="num">0); class=class="str">"cmt">//--- class="type">class="kw">double minvol = m_symbol.LotsMin(); if(lot < minvol) lot = minvol; class=class="str">"cmt">//--- class="type">class="kw">double maxvol = m_symbol.LotsMax(); if(lot > maxvol) lot = maxvol; class=class="str">"cmt">//--- class="kw">return(lot); }
别急着下结论
线性核与 Matérn 核在金融时间序列建模里几乎是两条相反的路线。前者只吃得下规整、样本少的起始数据,后者靠参数 v 的可调性,能在噪声、断点甚至极平滑序列间切换角色,样本量从几十根 K 线涨到上千根时优势更明显。 附带工程文件里 MoneyWZ_37.mqh 曾在 197 行触发索引越界(series[size][0] = profit),作者已于 2024-09-18 发补丁,但 MT5 实盘前仍建议自己编译跑一遍策略测试器确认无报错。 高斯过程回归不是银弹,外汇与贵金属杠杆高、跳空频繁,核函数换得再花哨也只提高拟合概率,不消除爆仓风险。拿历史样本调通了,也先丢进tick级回测看滑点吃掉多少边缘收益,再谈上信号。