在 MQL5 中提升数值预测的集成方法·进阶篇
(2/3)·单一模型挑最好的就够了吗?六种集成技术正在重写预测上限
很多交易者习惯从一堆训练好的模型里挑一个误差最小的直接上实盘,却把其余看似平庸的模型丢进回收站。其实在 MQL5里,把那些次优模型的输出重新组合起来,往往能拼出比冠军模型更稳的共识预测。本文作为系列第二篇,接着铺开六种纯MQL5实现的集成路径。
◍ 把多个模型塞进一个加权共识
做共识模型的核心,是先让每个子模型对训练样本吐出预测值,再拿这些预测当新特征去拟合目标。下面这段逻辑里,biased_x 存的就是第 i 个样本、第 j 个子模型的输出,biased_y 是真实标签。 拟合函数先按子模型数量初始化系数,默认给每个模型平分权重 1/nvars,偏置项置 0;随后调用 Optimize 做系数寻优。注意寻优目标函数里对负系数施加了 -1.e30 * p[j] 的惩罚,等于硬性禁止权重为负,避免多空抵消变成纯噪声。 优化完后有一行 m_coefs/=sum,把权重归一化。假设 3 个子模型优化出系数 [0.4, 0.35, 0.25],归一后直接可用于 predict:对新输入按各模型预测值乘系数求和。外汇与贵金属波动剧烈,这套加权方式只是降低单一模型过拟合概率,实盘仍属高风险。 预测阶段代码极简:遍历模型数组,output 累加 m_coefs[imodel] * forecast(inputs)。你在 MT5 里接好 IModel 接口后,复制这段就能跑通共识推断,下一步该调的就是 Optimize 的迭代次数与惩罚项量级。
for(class="type">int j=class="num">0 ; j<biased_nvars ; j++) class=class="str">"cmt">// For all model outputs pred += biased_x[i][j] * p[j] ; class=class="str">"cmt">// Weight them per call diff = pred - biased_y[i] ; class=class="str">"cmt">// Predicted minus true err += diff * diff ; class=class="str">"cmt">// Cumulate squared error } penalty = class="num">0.0 ; for(class="type">int j=class="num">0 ; j<biased_nvars ; j++) { if(p[j] < class="num">0.0) penalty -= class="num">1.e30 * p[j] ; } class="kw">return err + penalty ; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fit the consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool Cbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[]) { biased_ncases = class="type">int(train_vars.Rows()); biased_nvars = class="type">int(models.Size()); biased_x = matrix::Zeros(biased_ncases,biased_nvars); biased_y = train_targets; m_coefs = vector::Zeros(biased_nvars+class="num">1); for(class="type">int i = class="num">0; i<biased_ncases; i++) { vector ins = train_vars.Row(i); for(class="type">int j = class="num">0; j<biased_nvars; j++) biased_x[i][j] = models[j].forecast(ins); } m_coefs.Fill(class="num">1.0/class="type">class="kw">double(biased_nvars)); m_coefs[m_coefs.Size()-class="num">1] = class="num">0.0; class="type">int iters = Optimize(m_coefs,class="type">int(m_coefs.Size())); class="type">class="kw">double sum = m_coefs.Sum(); m_coefs/=sum; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Make prediction with consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double Cbiased::predict(vector &inputs,IModel* &models[]) { class="type">class="kw">double output=class="num">0.0; for(class="type">uint imodel=class="num">0 ; imodel<models.Size() ; imodel++) { output += m_coefs[imodel] * models[imodel].forecast(inputs); } class="kw">return output; }
无偏组件叠加时砍掉截距项
若一组组件模型的预测经检验没有显著偏置,共识模型里就不需要常数项。去掉它不仅能压住过拟合的苗头,还会顺带锁定权重非负——这一点在前面方法里已经铺垫过。 在此之上再补一条硬约束:所有权重加起来必须等于 1。它带来两个实在好处。一是保无偏,只要组件模型本身站得住,加权求和为一就能让共识输出也不偏;二是在预测间做插值,最终值被限定在各组件预测之间,不会因某个极端权重蹦出离谱结果。 代码层面和先前实现大体重合,CUnbiased 类真正改动的只有被最小化的目标函数。下面这段把非负与和为一都揉进了优化里,开 MT5 把类扔进自己的集成框架,重点看 func 里怎么归一化权重。 外汇与贵金属波动剧烈、杠杆风险高,这类无偏集成只能降低模型偏置,不预示任何方向性胜率。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//|Compute the optimal linear combination of the predictions |</span> <span class="comment">class=class="str">"cmt">//|subject to the constraints that the weights are all nonnegative |</span> <span class="comment">class=class="str">"cmt">//|and they sum to one. This is appropriate for unbiased predictors.|</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class</span> CUnbiased:<span class="keyword">class="kw">public</span> PowellsMethod { <span class="keyword">class="kw">public</span>: CUnbiased(<span class="keyword">class="type">void</span>) ; ~CUnbiased() ; <span class="keyword">class="type">bool</span> fit(<span class="keyword">matrix</span> & train_vars, <span class="keyword">vector</span> &train_targets,IModel* &models[]); <span class="keyword">class="type">class="kw">double</span> predict(<span class="keyword">vector</span> &inputs,IModel* &models[]) ; <span class="keyword">class="kw">private</span>: <span class="keyword">vector</span> m_coefs ; <span class="comment">class=class="str">"cmt">// Computed coefficients here</span> <span class="keyword">class="type">int</span> unbiased_ncases ; <span class="keyword">class="type">int</span> unbiased_nvars ; <span class="keyword">matrix</span> unbiased_x ; <span class="keyword">vector</span> unbiased_y ; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">class="kw">double</span> func(<span class="keyword">vector</span> &p,<span class="keyword">class="type">int</span> n=<span class="number">class="num">0</span>); } ; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Constructor |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> CUnbiased::CUnbiased(<span class="keyword">class="type">void</span>) { } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Destructor |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> CUnbiased::~CUnbiased(<span class="keyword">class="type">void</span>) { } <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Function to be optimized |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">double</span> CUnbiased::func(<span class="keyword">vector</span> &p,<span class="keyword">class="type">int</span> n = <span class="number">class="num">0</span>) { <span class="keyword">class="type">class="kw">double</span> sum, err, pred,diff, penalty ; <span class="comment">class=class="str">"cmt">// Normalize weights to sum to one</span> sum = p.Sum() ; <span class="keyword">if</span>(sum < <span class="number">class="num">1</span>.e-<span class="number">class="num">60</span>) <span class="comment">class=class="str">"cmt">// Should almost never happen</span> sum = <span class="number">class="num">1</span>.e-<span class="number">class="num">60</span> ; <span class="comment">class=class="str">"cmt">// But be prepared to avoid division by zero</span> <span class="keyword">vector</span> unbiased_work = p / sum ; <span class="comment">class=class="str">"cmt">// Compute criterion</span> err = <span class="number">class="num">0.0</span> ; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span> ; i<unbiased_ncases ; i++) { pred = <span class="number">class="num">0.0</span> ; <span class="comment">class=class="str">"cmt">// Will cumulate prediction</span> <span class="keyword">for</span>(<span class="keyword">class="type">int</span> j=<span class="number">class="num">0</span> ; j<unbiased_nvars ; j++) <span class="comment">class=class="str">"cmt">// For all model outputs</span>
「共识模型的拟合与预测实现」
这段 MQL5 代码把多个子模型的输出做加权平均,权重在拟合阶段用优化器求解,而非简单等权。拟合函数先以 1/N 初始化系数,再交给 Optimize 最小化带惩罚的平方误差。 误差计算里对负权重施加了 -1.e30 * p[j] 的巨额惩罚,等于在数值上禁止模型给出做空方向的权重,外汇与贵金属这类高杠杆品种上,这种硬约束可能让组合错过反向信号,需按策略手动放开。 预测函数只是把各子模型 forecast 结果乘系数累加,无额外计算开销。实盘前建议在 MT5 策略测试器里打印 m_coefs,确认求和归一后权重分布,避免某个弱模型被配到 90% 以上。
pred += unbiased_x[i][j] * unbiased_work[j] ; class=class="str">"cmt">// Weight them per call diff = pred - unbiased_y[i] ; class=class="str">"cmt">// Predicted minus true err += diff * diff ; class=class="str">"cmt">// Cumulate squared error } penalty = class="num">0.0 ; for(class="type">int j=class="num">0 ; j<unbiased_nvars ; j++) { if(p[j] < class="num">0.0) penalty -= class="num">1.e30 * p[j] ; } class="kw">return err + penalty ; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fit the consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CUnbiased::fit(matrix & train_vars, vector &train_targets,IModel* &models[]) { unbiased_ncases = class="type">int(train_vars.Rows()); unbiased_nvars = class="type">int(models.Size()); unbiased_x = matrix::Zeros(unbiased_ncases,unbiased_nvars); unbiased_y = train_targets; m_coefs = vector::Zeros(unbiased_nvars); for(class="type">int i = class="num">0;i<unbiased_ncases; i++) { vector ins = train_vars.Row(i); for(class="type">int j = class="num">0;j<unbiased_nvars; j++) unbiased_x[i][j] = models[j].forecast(ins); } m_coefs.Fill(class="num">1.0/class="type">class="kw">double(unbiased_nvars)); class="type">int iters = Optimize(m_coefs); class="type">class="kw">double sum = m_coefs.Sum(); m_coefs/=sum; class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Make prediction with consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CUnbiased::predict(vector &inputs,IModel* &models[]) { class="type">class="kw">double output=class="num">0.0; for(class="type">uint imodel=class="num">0 ; imodel<models.Size() ; imodel++) { output += m_coefs[imodel] * models[imodel].forecast(inputs); } class="kw">return output; }
◍ 按预测误差反比给模型配权重
把多个组件模型的输出揉成一个预测值,不一定非得等权平均。更讲道理的做法是:谁在历史样本上错得少,谁说话份量就重;错得多的模型,给个小权重即可。理论上的底子是,各模型无偏且不相关时,权重取误差倒数能让集成后的期望平方误差最小。 但别把这当成万能胶。当几个模型高度相关,比如都是同一套均线逻辑换了个周期,方差加权就可能还不如简单平均,甚至放大共同偏差。模型质量拉开差距时,这招才真正显出优势。 CWeighted 类的 fit() 就是按这套逻辑跑的:先给每个模型权重垫一个极小的 1e-60 防止除零,再遍历训练集每一行,累加各模型预测值与真实目标的差的平方。全部样本扫完后,用总误差倒数算权重,最后除以权重和归一化,保证加起来等于 1。 predict() 则拿归一化后的系数对各个模型预测做加权求和。你在 MT5 里把这段代码挂上,换几组相关性低的模型(例如趋势突破 + 波动率均值回复),回测 EURUSD 或 XAUUSD 这类高波动品种,可能看到集成误差比等权平均更收敛——外汇与贵金属杠杆高,信号仅作概率参考,实盘前务必自测。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compute the variance-weighted average of the predictions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CWeighted { class="kw">public: CWeighted(class="type">void) ; ~CWeighted() ; class="type">bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); class="type">class="kw">double predict(vector &inputs,IModel* &models[]) ; class="kw">private: vector m_coefs ; class=class="str">"cmt">// Computed coefficients here }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CWeighted::CWeighted(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Destructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CWeighted::~CWeighted(class="type">void) { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fit a consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CWeighted::fit(matrix &train_vars,vector &train_targets,IModel* &models[]) { m_coefs = vector::Zeros(models.Size()); m_coefs.Fill(class="num">1.e-60); class="type">class="kw">double diff = class="num">0.0; for(class="type">ulong i = class="num">0; i<train_vars.Rows(); i++) { vector ins = train_vars.Row(i); for(class="type">ulong j = class="num">0; j<m_coefs.Size(); j++) { diff = models[j].forecast(ins) - train_targets[i]; m_coefs[j] += (diff*diff); } } m_coefs=class="num">1.0/m_coefs; m_coefs/=m_coefs.Sum(); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Make a prediction with the consensus model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CWeighted::predict(vector &inputs,IModel* &models[]) { class="type">class="kw">double output = class="num">0.0; for(class="type">uint i = class="num">0; i<models.Size(); i++) output+=m_coefs[i]*models[i].forecast(inputs); class="kw">return output; }
用 GRNN 给多模型预测做加权插值
干净数据上跑集成共识模型通常没问题,但训练集一旦带噪声,传统回归容易泛化拉胯。广义回归神经网络(GRNN)在这时候更稳,它参数对模型影响小,不易过拟合,代价是牺牲一点精度,换来对复杂非线性关系的建模能力。 GRNN 的本质是对训练集目标值做插值:样本外案例和哪些样本内案例越像,那些案例拿到的权重就越高。它看着像平滑操作,但底子是统计学里的条件期望——联合密度未知时,靠训练数据估密度,再推出加权插值形式。 grnn.mqh 里的 CGrnn 类管这件事。构造函数默认 m_inner=100、m_outer=10、m_start_std=3.0,分别控制内外迭代次数和 sigma 权重初始标准差;fit 存下预测变量和目标,用模拟退火扰动 sigma 权重,按交叉验证误差加温度参数决定是否接受,温度逐步降以使搜索聚焦。 predict 算输入向量与每个训练点的距离,按距离赋权,输出是训练目标值的加权平均,sigma 决定单点影响程度。上层 CGenReg 类把各子模型预测拼成矩阵 preds 喂给 CGrnn,新样本收集进 m_work 后取返回向量首元素当最终共识预测。外汇与贵金属波动噪声大、高风险,这种插值共识只能降低过拟合概率,不保证方向判断正确,建议直接挂 MT5 用历史 tick 跑一遍交叉验证误差观察 sigma 收敛。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| General regression neural network | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGrnn { class="kw">public: CGrnn(class="type">void); CGrnn(class="type">int num_outer, class="type">int num_inner, class="type">class="kw">double start_std); ~CGrnn(class="type">void); class="type">bool fit(matrix &predictors,matrix &targets); vector predict(vector &predictors); class=class="str">"cmt">//class="type">class="kw">double get_mse(class="type">void); class="kw">private: class="type">bool train(class="type">void); class="type">class="kw">double execute(class="type">void); class="type">ulong m_inputs,m_outputs; class="type">int m_inner,m_outer; class="type">class="kw">double m_start_std; class="type">ulong m_rows,m_cols; class="type">bool m_trained; vector m_sigma; matrix m_targets,m_preds; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Default constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CGrnn::CGrnn(class="type">void) { m_inner = class="num">100; m_outer = class="num">10; m_start_std = class="num">3.0; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Parametric constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CGrnn::CGrnn(class="type">int num_outer,class="type">int num_inner,class="type">class="kw">double start_std) { m_inner = num_inner; m_outer = num_outer;
「把 GRNN 模型接到预测主流程」
上面这段把广义回归神经网络的训练与推断封装成了可直接调用的 C++ 类接口。fit() 先做维度校验:若目标矩阵行数与特征矩阵行数不一致,直接 Print 报错并返回 false,避免后续算距离时越界。训练集行数 m_rows 与列数 m_cols 在 fit 里从 predictors 提取,sigma 向量按列数初始化为零,意味着平滑系数初始为 0,实际训练时由 train() 内部重写。 predict() 是核心推断逻辑:对每条样本 i 遍历所有特征列 j,计算 (x_j - 样本_i_j)/sigma_j 的平方和,再取 exp(-dist) 作为核权重。若权重低于 EPS1 则钳位到 EPS1,防止分母为 0。所有样本的核权重累加进 psum,输出向量按权重加权平均后除以 psum 返回。 CGenReg 类在外层组合了 CGrnn 指针与多模型数组 IModel* &models[],fit 接收训练变量矩阵、目标向量与模型数组,predict 对输入向量跑集成回归。你在 MT5 里接自己的行情特征时,只要保证 predictors 每列对应一个已调好 sigma 的 GRNN,predict 返回的 vector 就是该时刻的概率化预测值。外汇与贵金属波动剧烈,这类模型输出仅代表历史模式下的条件概率,实盘须严控仓位。
m_start_std = start_std; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Fit data to a model | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CGrnn::fit(matrix &predictors,matrix &targets) { m_targets = targets; m_preds = predictors; m_trained = false; m_rows = m_preds.Rows(); m_cols = m_preds.Cols(); m_sigma = vector::Zeros(m_preds.Cols()); if(m_targets.Rows() != m_preds.Rows()) { Print(__FUNCTION__, " invalid inputs "); class="kw">return false; } m_trained = train(); class="kw">return m_trained; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Make a prediction with a trained model | class=class="str">"cmt">//+------------------------------------------------------------------+ vector CGrnn::predict(vector &predictors) { if(!m_trained) { Print(__FUNCTION__, " no trained model available for predictions "); class="kw">return vector::Zeros(class="num">1); } if(predictors.Size() != m_cols) { Print(__FUNCTION__, " invalid inputs "); class="kw">return vector::Zeros(class="num">1); } vector output = vector::Zeros(m_targets.Cols()); class="type">class="kw">double diff,dist,psum=class="num">0.0; for(class="type">ulong i = class="num">0; i<m_rows; i++) { dist = class="num">0.0; for(class="type">ulong j = class="num">0; j<m_cols; j++) { diff = predictors[j] - m_preds[i][j]; diff/= m_sigma[j]; dist += (diff*diff); } dist = exp(-dist); if(dist< EPS1) dist = EPS1; for(class="type">ulong k = class="num">0; k<m_targets.Cols(); k++) output[k] += dist * m_targets[i][k]; psum += dist; } output/=psum; class="kw">return output; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Compute the General Regression of the predictions | class=class="str">"cmt">//+------------------------------------------------------------------+ class CGenReg { class="kw">public: CGenReg(class="type">void) ; ~CGenReg(class="type">void) ; class="type">bool fit(matrix & train_vars, vector &train_targets,IModel* &models[]); class="type">class="kw">double predict(vector &inputs,IModel* &models[]) ; class="kw">private: CGrnn *grnn ; class=class="str">"cmt">// The GRNN object