以 MQL5 实现强化分类任务的融汇方法(基础篇)
◍ 用 MQL5 做强化分类的融汇思路
把强化学习的决策逻辑塞进分类任务,在 MT5 里并不是直接调个库就完事,而是要把行情状态、持仓动作和奖励函数写成可回测的闭环。 原文给出的基准发布于 2025 年 10 月 14 日,在 MetaTrader 5 环境下以 MQL5 实现,发布后统计阅读量为 410,评论数为 0,说明这类融汇方法仍属小众验证阶段。 实操上,建议先开 MT5 新建一个 EA 骨架,把分类标签(如突破 / 震荡 / 反转)和强化奖励(如净值回撤惩罚)分开定义,再观察样本外表现是否稳定。外汇与贵金属杠杆高,任何分类信号都只是概率倾向,需以小仓位做前向验证。
分类融汇的前提与排位输出的价值
把上篇的数值预测模型组合思路搬到分类任务上,不能直接套。数字组合技术依赖模型输出连续数值,但不少分类器只给离散类标签,且基于数字的分类器常有预测不稳定性,所以分类融汇需要专门方法。 分类融汇对组件模型有硬假设:在互斥且穷尽的类目标上训练,每个样本只归一类;若需“以上都不是”,要么单列为类,要么用数字组合配隶属阈值处理。给定输入向量,组件模型应输出 N 个值(N 为类数),可以是概率、置信度、二元独热(1.0/0.0),或 1 到 N 的整数排位。 排位输出在类数多时功用陡增。二元分类排位无额外信息,三类问题排位数值也用处有限;但类数众多且单笔预测带不确定性时,读模型的“次选”就很有价值。例如给 SVM 加上各类决策边界距离,就能从纯二元扩展出置信度洞察。 排位还能抹平不同分类模型的输出量纲差。两个看市场走势的模型,一个盯高流动性短期波动,一个看数周数月长期趋势,后者易在短周期里带噪声。把置信度转排位,短期见解就不会被宽趋势信号盖掉,融汇预测更平衡。外汇与贵金属市场波动剧烈、杠杆风险高,这类模型输出差异在实盘里可能被放大,建议先开 MT5 用历史数据验证排位转换效果。
「分类器组合不必死磕准确率」
做融汇分类器,多数人第一反应是提升分类准确率,但这未必是唯一目标。有些决策场景下,初判就可能错,这时候比准确率更有用的,是另一套成功量值:要么把真实类锁进最小高概率子集,要么把真实类顶到排位最前。 具体有两条互补路线。类集合缩减,只关心留下来的小子集里大概率含真实类,子集内部谁先谁后无所谓,紧凑和覆盖正确才是硬指标。类排序则不设固定阈值,专看真实类离顶部排位的平均距离,距离越短越好。 实际落地时,给某一条路线加权往往更稳。比如先跑向心化类集合缩减,筛出小型高概率类子集;再用次要方法给子集内类排位,取排位最高者为终判。这套两步走,在分类确定性波动大的复杂行情里,比单类预测更扛造。外汇与贵金属市场高风险,模型确定性本就时强时弱,这种双轨框架值得在 MT5 上自己搭一遍验证。
◍ 投票式融合里的平局怎么破
多数规则把多个分类模型当成评委,每个模型投一票给自己预测概率最高的那个类,得票最多的类就是集成输出。它只吃离散类选择,不碰连续概率,所以在模型能力有限、只能吐出顶层标签的系统里很实用,代码层面由 ensemble.mqh 里的 CMajority 类通过 classify() 管控。 classify() 接收预测因子向量和 IClassify 指针数组,返回 0 到(类总数-1)之间的整数。IClassify 接口规范了 train、classify、getNumInputs、getNumOutputs 几个方法,和前一篇的 IModel 操控方式接近,方便统一调度不同模型。 真正麻烦的是平票:两个或更多类票数一样时,现实里能重投,程序里不行。CMajority 在计票比较时给每个类的票数加了 0.999 * 均匀随机扰动(见代码 temp = m_output[i] + 0.999 * HQRndUniformR),让平局类被选中概率相等,避免确定性偏向。 但多数规则只采每个模型的顶层选择,丢掉了次优类里的信息;类数一多,简单投票也抓不住类间细微关系,而且所有模型被一视同仁,不管谁在历史行情里更靠谱。外汇与贵金属杠杆高、波动剧烈,直接拿这套融合信号下单风险很大,建议先在 MT5 策略测试器里跑分类准确率再谈。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compute the winner via simple majority | class=class="str">"cmt">//+------------------------------------------------------------------+ class CMajority { class="kw">private: class="type">ulong m_outputs; class="type">ulong m_inputs; vector m_output; matrix m_out; class="kw">public: CMajority(class="type">void); ~CMajority(class="type">void); class="type">ulong classify(vector &inputs, IClassify* &models[]); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| IClassify interface defining methods for manipulation of | class=class="str">"cmt">//|classification algorithms | class=class="str">"cmt">//+------------------------------------------------------------------+ interface IClassify { class=class="str">"cmt">//train a model class="type">bool train(matrix &predictors,matrix&targets); class=class="str">"cmt">//make a prediction with a trained model vector classify(vector &predictors); class=class="str">"cmt">//get number of inputs for a model class="type">ulong getNumInputs(class="type">void); class=class="str">"cmt">//get number of class outputs for a model class="type">ulong getNumOutputs(class="type">void); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| ensemble classification | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">ulong CMajority::classify(vector &inputs,IClassify *&models[]) { class="type">class="kw">double best, sum, temp; class="type">ulong ibest; best =class="num">0; ibest = class="num">0; CHighQualityRandStateShell state; CHighQualityRand::HQRndRandomize(state.GetInnerObj()); m_output = vector::Zeros(models[class="num">0].getNumOutputs()); for(class="type">uint i = class="num">0;i<models.Size(); i++) { vector classification = models[i].classify(inputs); m_output[classification.ArgMax()] += class="num">1.0; } sum = class="num">0.0; for(class="type">ulong i=class="num">0 ; i<m_output.Size() ; i++) { temp = m_output[i] + class="num">0.999 * CAlglib::HQRndUniformR(state); if((i == class="num">0) || (temp > best)) { best = temp ; ibest = i ;
用权重归一锁定最优输出索引
这段收尾逻辑把数组 m_output 里逐项累加,得到 sum 后再判断是否大于 0.0。只有和为正时才做 m_output /= sum 的归一化,避免除以零或负权导致的分布畸变。 归一化后直接 return ibest,把当前最优索引交还调用方。实盘里若 m_output 各元素代表不同信号强度,sum>0.0 的判定能保证只在存在正向共识时输出标准化权重。 复制这段代码到 MT5 的自定义指标 OnInit 尾声或 OnCalculate 末尾,把 m_output 换成你自己的信号数组,即可在日志或缓冲区观察 ibest 随 tick 的漂移。外汇与贵金属波动剧烈,归一结果仅反映历史窗口概率,不构成方向保证。
}
sum += m_output[i] ;
}
if(sum>class="num">0.0)
m_output/=sum;
class="kw">return ibest;
}「用 Borda 计数把多模型排位揉成一类」
Borda 计数干的事,是跨所有组件模型统计每个类「排位低于多少其他类」,把这个数当得分。假设系统有 m 个模型、k 个类,得分区间被钉死:所有模型里始终垫底的类计 0 分,始终排第一的类拿满分 m(k-1)。它比简单投票细,又没把计算量搞崩,属于效率和信息利用的折中方案。 二元分类时 Borda 和多数规则等价,优势只在三类及以上才显出来。跨模型的最终计数仍可能打平,得单独处理。CBorda 类在 ensemble.mqh 里跑,不需要训练前置,直接吃输入向量算输出。 流程是先建一个累积计数的输出向量,再挨个让组件模型对输入做分类,给类建索引数组,把每个模型的输出升序排好,最后按排位把分累加进 m_output。下一段直接看 MT5 里 CBorda 的骨架,复制进本地能改的参数主要是模型数组和输出维度。 外汇与贵金属模型集成属高风险,回测亮眼不代表实盘概率占优,参数请先在策略测试器跑样本外。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Compute the winner via Borda count |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CBorda { <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">ulong</span> m_outputs; <span class="keyword">class="type">ulong</span> m_inputs; <span class="keyword">vector</span> m_output; <span class="keyword">matrix</span> m_out; <span class="keyword">class="type">long</span> m_indices[]; <span class="keyword">class="kw">public</span>: CBorda(<span class="keyword">class="type">void</span>); ~CBorda(<span class="keyword">class="type">void</span>); <span class="keyword">class="type">ulong</span> classify(<span class="keyword">vector</span>& inputs, IClassify* &models[]); }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| ensemble classification |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">ulong</span> CBorda::classify(<span class="keyword">vector</span> &inputs,IClassify *&models[]) { <span class="keyword">class="type">class="kw">double</span> best=<span class="number">class="num">0</span>, sum, temp; <span class="keyword">class="type">ulong</span> ibest=<span class="number">class="num">0</span>; CHighQualityRandStateShell state; CHighQualityRand::HQRndRandomize(state.GetInnerObj()); <span class="keyword">if</span>(m_indices.Size()) <span class="functions">ArrayFree</span>(m_indices); m_output = <span class="keyword">vector</span>::Zeros(models[<span class="number">class="num">0</span>].getNumOutputs()); <span class="keyword">if</span>(<span class="functions">ArrayResize</span>(m_indices, <span class="keyword">class="type">int</span>(m_output.Size()))<<span class="number">class="num">0</span>) { <span class="functions">Print</span>(<span class="keyword">__FUNCTION__</span>, <span class="class="type">class="kw">string">" "</span>, <span class="keyword">__LINE__</span>, <span class="class="type">class="kw">string">" array resize error "</span>, <span class="functions">GetLastError</span>()); <span class="keyword">class="kw">return</span> <span class="macro">ULONG_MAX</span>; } <span class="keyword">for</span>(<span class="keyword">class="type">uint</span> i = <span class="number">class="num">0</span>; i<models.Size(); i++) { <span class="keyword">vector</span> classification = models[i].classify(inputs); <span class="keyword">for</span>(<span class="keyword">class="type">long</span> j = <span class="number">class="num">0</span>; j<<span class="keyword">class="type">long</span>(classification.Size()); j++) m_indices[j] = j; <span class="keyword">if</span>(!classification.Size()) { <span class="functions">Print</span>(<span class="keyword">__FUNCTION__</span>,<span class="class="type">class="kw">string">" "</span>, <span class="keyword">__LINE__</span>,<span class="class="type">class="kw">string">" empty vector "</span>); <span class="keyword">class="kw">return</span> <span class="macro">ULONG_MAX</span>; } qsortdsi(<span class="number">class="num">0</span>,classification.Size()-<span class="number">class="num">1</span>,classification,m_indices); <span class="keyword">for</span>(<span class="keyword">class="type">ulong</span> k =<span class="number">class="num">0</span>; k<classification.Size(); k++) m_output[m_indices[k]] += <span class="keyword">class="type">class="kw">double</span>(k); } sum = <span class="number">class="num">0.0</span>;