一种采用纯MQL5语言实现的基于能量学习的特征选择算法·进阶篇

一种采用纯MQL5语言实现的基于能量学习的特征选择算法·进阶篇

(2/3)·从加权近邻到能量函数,手把手用MQL5脚本跑通特征加权训练闭环

实战向 第 2/3 篇
很多交易者把特征选择当成离线python的事,殊不知纯MQL5脚本就能在终端内直接完成能量学习加权。本篇接上篇概念,继续深挖FREL在MetaTrader 5里的具体落地路径,少绕一道数据导出弯子。

◍ FREL类的构造与权重求解入口

FREL 在 MT5 里被封装成 FREL 类,它继承自 PowellsMethod( Powell 优化在 Powells.mqh 里实现)。你真正要调用的只有 WeighVars() 一个方法,其余分箱、自举、优化都在内部跑完。 训练矩阵有固定结构:每行是一个样本,每列是一个候选变量,最后一列必须是目标值。构造函数最少吃一个 matrix 参数,另有两个可选整型参数 numboot(默认1,自举次数)和 bootsize(默认0,单样本量)。若 bootsize 大于样本数,numboot 会自动回退成1、bootsize 改成样本总量,这块用的时候要小心别踩坑。 WeighVars() 的参数里,num_bins_target 把目标值切成2到样本数之间的箱;reg_factor 是正双精度,0 即关正则化;index[] 回写按相关度降序的原始列号,weights[] 回写同序最优权重,总和会被规整到100方便读。方法返回 false 代表过程没跑完。 权重估计先抽目标列做分箱,再进 calc_wt() 做 bootstrap 打乱并调用父类 Optimize(),每次自举的最优权重累加到 w[] 后取平均。被最小化的损失函数在重写的 func() 里,最终平均权重写回用户数组前会排序并转成百分制。 下面这段是构造函数与 WeighVars() 开头的原文,逐行拆一下内存分配与参数守卫逻辑。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
  class=class="str">"cmt">//| constructor                                                            |
  class=class="str">"cmt">//+------------------------------------------------------------------+
                     FREL(matrix &in_data,class="type">int numboot=class="num">1, class="type">int bootsize=class="num">0)
   {
   m_data = in_data;
   m_num_boot=(numboot>class="num">0)?numboot:class="num">1;
   m_bootsize=(bootsize>class="num">2 && bootsize<=class="type">int(m_data.Rows()) && m_num_boot>class="num">1)?bootsize:class="type">int(m_data.Rows());


   if(ArrayResize(m_indices, class="type">int(m_data.Rows()))!=class="type">int(m_data.Rows()) ||
     ArrayResize(m_target_bin, class="type">int(m_data.Rows()))!=class="type">int(m_data.Rows()) ||
     ArrayResize(m_trial_weights, class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1) ||
     ArrayResize(m_work_weights, class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1)
     )
     {
     Print(__FUNCTION__, " error ", GetLastError());
     m_memory_allocated = false;
     }
   else
     m_memory_allocated = true;
   }
class=class="str">"cmt">//+-----------------------------------------------------------------------+
  class=class="str">"cmt">//| Find the most relevant variables from a dataset of candidate variables|
  class=class="str">"cmt">//+-----------------------------------------------------------------------+
  class="type">bool                WeighVars(class="type">int num_bins_target, class="type">class="kw">double reg_factor,class="type">int &index[],class="type">class="kw">double &weights[])
   {

   if(!m_memory_allocated)
     {
     Print(" INTERNAL ERROR ");
     class="kw">return false;
     }


   if(num_bins_target<=class="num">1 || num_bins_target>class="type">int(m_data.Rows()))
   {

权重求解里的分箱与归一化细节

这段逻辑出现在某特征加权函数的后半段,先对目标向量做分箱,再反算各候选列的权重并归一。num_bins_target 若小于 2 会直接打印 invalid function parameter 并返回 false,这是调用方最容易踩的坑。 目标列通过 np::vecAsArray 从矩阵最后一列取出,bin_array 将连续收益/价格变化切成 k 段;若实际分出箱数 k 小于请求值,说明存在重复极值导致合并,函数同样返回 false。 权重计算走 calc_wt,随后用每列标准差乘指数权重:weights[var] = m_data.Col(var).Std() * exp(weights[var]),再累加进 sum。最终每层权重乘以 100.0/sum 转成百分比,并用 MathQuickSortDescending 按权重降序排好 index,方便后续截断-topN 特征。 在 MT5 里把 num_bins_target 设成 1 跑同一段,会立即在专家日志看到参数报错而非静默出 0 权重;外汇与贵金属数据波动大,分箱数过低可能让少数极端 K 线吞掉整段区间,实盘前建议先用历史样本测 5~10 箱的稳定性。

MQL5 / C++
Print(__FUNCTION__, " invalid function parameter: num_bins_target. Parameter should be >=class="num">2 ");
      class="kw">return false;
   }  
   class="type">int ret=class="num">0;
   class="type">class="kw">double target[], target_thresholds[] ;
   class="type">class="kw">double sum ;
   class="type">int n_cases = class="type">int(m_data.Rows());
   m_reg_factor = MathAbs(reg_factor);
   m_loss = class="num">0.0;
   if(ArrayResize(index,class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1) ||
      !np::vecAsArray(m_data.Col(m_data.Cols()-class="num">1),target)
      )
      {
       Print(__FUNCTION__, " error ", GetLastError());
       class="kw">return false;
      }
   class="type">int k = num_bins_target ;
   if(!bin_array(target, k, target_thresholds, m_target_bin))
      class="kw">return false;
   if(k<num_bins_target)
      {
       Print("error bins of target vector ", num_bins_target," : ", k);
       class="kw">return false;
      }
   for(class="type">int i=class="num">0 ; i<n_cases ; i++)
      {
       if(m_target_bin[i] >= num_bins_target)
         {
          Print("error m_target_bin array at index ", i, " is ",m_target_bin[i], " should be less than ", num_bins_target);
          class="kw">return false;
         }
      }
   ret = calc_wt(num_bins_target,m_loss,weights);
   if(ret<class="num">0)
      class="kw">return false;
   sum = class="num">0.0 ;
   for(class="type">ulong var=class="num">0 ; var<m_data.Cols()-class="num">1 ; var++)
      {
       weights[var] = m_data.Col(var).Std() * exp(weights[var]);
       sum += weights[var] ;
      }
   for(class="type">ulong var=class="num">0 ; var<m_data.Cols()-class="num">1 ; var++)
      {
       weights[var] *= class="num">100.0 / sum ;
       index[var] = class="type">int(var) ;
      }
   MathQuickSortDescending(weights,index,class="num">0,class="type">int(weights.Size()-class="num">1)) ;
   class="kw">return true;
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| calculates the optimal weights of candidate variables                |
   class=class="str">"cmt">//+------------------------------------------------------------------+

「自助采样下的权重迭代怎么写」

这段代码是某加权模型里负责跑自助采样(bootstrap)并累加特征权重的核心函数。它接收目标分箱数、损失值引用和权重数组,先做好容量校验与归零,再进入多层循环。 外层按 m_num_boot 次重采样展开,每次先把 class_count 清零,用 Fisher-Yates 式的洗牌从 m_indices 里抽出不超过 m_bootsize 的子样本,并统计各目标分箱命中数。若某个分箱成员少于 2 个,会打印提示,说明分区数或自助样本量(Frel 相关参数)可能要调。 每次采样后调用 Optimize 拿到 m_trial_weights,把本次损失累加到 loss_value,再把临时权重叠加进 w。最终 w 是多次自助的平均倾向,开 MT5 把 m_num_boot 设成 200 对比 500,能直接看到 w 曲线稳定度变化,外汇与贵金属品种波动大,这类重采样对过拟合敏感,属高风险验证。 别把少于2成员的箱当噪声扔掉 原文提示分箱不足 2 人时只是 Print 警告而非中断,实盘前应先检查你自己的 m_bootsize 是否过小,否则权重会隐性偏向高频分箱。

MQL5 / C++
class="type">int calc_wt(class="type">int num_bins_target,class="type">class="kw">double &loss_value, class="type">class="kw">double &w[])
 {
  class="type">int ret,rand_error, class_count[] ;
  ret = class="num">0;
  if(ArrayResize(class_count,num_bins_target)!=num_bins_target || (w.Size()!=class="type">uint(m_data.Cols()-class="num">1) && ArrayResize(w,class="type">int(m_data.Cols()-class="num">1))!=class="type">int(m_data.Cols()-class="num">1)))
   {
    Print(__FUNCTION__, " error ", GetLastError());
    class="kw">return -class="num">1;
   }
  ArrayInitialize(w,class="num">0.0);
  loss_value = class="num">0.0 ;
  for(class="type">ulong i=class="num">0 ; i<m_data.Rows() ; i++)
   m_indices[i] = class="type">int(i) ;

  for(class="type">int ibootstrap=class="num">0 ; ibootstrap<m_num_boot; ibootstrap++)
   {
    Comment(" Bootstrap iteration ", ibootstrap+class="num">1);

    ArrayInitialize(class_count,class="num">0);
    class="type">int ii, j, k, m;
    ii = class="type">int (m_data.Rows()) ;
    while(ii > class="num">1)
     {
     m = class="type">int (m_data.Rows()) - ii ;
     if(m >= m_bootsize)
      break ;
     j = (class="type">int)(MathRandomUniform(class="num">0.0,class="num">1.0,rand_error) * ii) ;
     if(j >= ii)
      j = ii - class="num">1 ;
     k = m_indices[m] ;
     m_indices[m] = m_indices[m+j] ;
     m_indices[m+j] = k ;
     --ii ;
     ++class_count[m_target_bin[m_indices[m]]] ;
     }

    for(class="type">int i=class="num">0 ; i<num_bins_target ; i++)
     {
     if(class_count[i] < class="num">2)
      Print(__FUNCTION__, "  class at ", i, " has less than class="num">2 members. 考虑调整参数Frel(number of partitions or bootstrap sample size)");
     }

    ArrayInitialize(m_trial_weights,class="num">0.0);
    ret += Optimize(m_trial_weights);
    loss_value += PowellsMethod::GetFret() ;
    for(class="type">ulong i=class="num">0 ; i<m_data.Cols()-class="num">1 ; i++)
     w[i] += m_trial_weights[i] ;
    }

◍ 权重平滑与惩罚项的代码落地

上面这段类方法把 bootstrap 汇总得到的权重数组做归一化,再交给 Powell 优化器去最小化损失。注意 w[i] /= double(m_num_boot) 这一步:它把累计权重除以自助采样次数,得到的其实是平均权重,而非原始累加值。 func() 里对参数 p[i] 做了硬边界处理:当 p[i] > 4.0 时,工作权重走 exp(4.0) + p[i] - 4.0 的线性外推,并对超出部分平方累加进 pen;当 p[i] < -3.0 时对称处理。区间内才用 exp(p[i]) 保证权重恒正。这个 4.0 与 -3.0 的阈值直接限定了优化器可探索的权重对数空间。 loss() 先算 total_loss 并除以样本行数 m_data.Rows() 做平均,再按 m_reg_factor 对权重平方加权(L2 正则)。若你在 MT5 里把 m_reg_factor 设为 0.0,就退化为纯经验损失,过拟合风险会明显上升——外汇与贵金属品种上这种风险尤其高,参数敏感期可能放大回测与实盘的偏离。 直接把这段拷进你的 EA 框架,改 m_num_bootm_reg_factor 两个量,能在 30 秒内看到权重分布和损失值的变化倾向。

MQL5 / C++
for(class="type">ulong i=class="num">0 ; i<m_data.Cols()-class="num">1; i++)
      w[i] /= class="type">class="kw">double(m_num_boot) ;
   class="kw">return ret ;
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| function minimized by Powells optimization method               |
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="kw">virtual class="type">class="kw">double             func(const class="type">class="kw">double& p[])
    {
      class="type">class="kw">double pen = class="num">0.0 ;
      for(class="type">ulong i=class="num">0 ; i<m_data.Cols()-class="num">1 ; i++)
        {
         if(p[i] > class="num">4.0)
           {
            m_work_weights[i] = exp(class="num">4.0) + p[i] - class="num">4.0 ;
            pen += (p[i] - class="num">4.0) * (p[i] - class="num">4.0) ;
           }
         else
           if(p[i] < -class="num">3.0)
             {
              m_work_weights[i] = exp(-class="num">3.0) + p[i] + class="num">3.0 ;
              pen += (p[i] + class="num">3.0) * (p[i] + class="num">3.0) ;
             }
           else
             m_work_weights[i] = exp(p[i]) ;
        }
      class="kw">return (loss(m_work_weights) + pen) ;
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//| calculates the loss function                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">class="kw">double             loss(class="type">class="kw">double &w[])
    {
      class="type">class="kw">double totaloss = total_loss(w);
      totaloss/=class="type">class="kw">double(m_data.Rows());
      if(m_reg_factor>class="num">0.0)
        {
         for(class="type">ulong i=class="num">0;i<m_data.Cols()-class="num">1;i++)
            totaloss+=m_reg_factor*pow(w[i],class="num">2.0);
        }
      class="kw">return totaloss;
    }
class=class="str">"cmt">//+------------------------------------------------------------------+
   class=class="str">"cmt">//|  loss over all data                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
   class="type">class="kw">double             total_loss(class="type">class="kw">double &w[])
    {
      class="type">int category,first, other ;
      class="type">class="kw">double  distance, top, bottom, loss ;
      loss = class="num">0.0 ;

近邻边界的距离惩罚怎么写

这段逻辑在做一件事:对 bootstrap 抽出的每个样本,找同类和异类里离它最近的两个点,用两者距离差衡量分类边界清晰度。距离差越大,说明该类样本被推得离异类越远,边界越干净。 外层循环遍历 m_bootsize 个索引,other 是当前样本,category 是它的标签。top 和 bottom 先置为 DBL_MAX,准备分别承接「同类最近距离」和「异类最近距离」。 内层先跳过自身,用加权绝对值差累加 distance:权重 w[v] 乘上两样本在第 v 维的特征差。同类时刷新 top,异类时刷新 bottom,最终 distance = top - bottom。 惩罚项分两段:差大于 30.0 直接加线性距离,否则加 log(1+exp(distance)) 的平滑项。后者在距离接近 0 时梯度温和,避免边界样本被过度拉开。外汇与贵金属行情噪声大,这类边界度量在实盘特征上可能偏向过拟合,开 MT5 把 30.0 阈值和 w[] 权重打印出来比对几组 K 线片段更有感。

MQL5 / C++
for(class="type">int i=class="num">0; i<m_bootsize; i++)
  {
   other = m_indices[i] ;
   category = m_target_bin[other] ;
   top = bottom = DBL_MAX ;
   for(class="type">int iother=class="num">0 ; iother<m_bootsize; iother++)
     {
      first = m_indices[iother] ;
      if(first == other)
        class="kw">continue ;
      distance = class="num">0.0 ;
      for(class="type">ulong v=class="num">0 ; v<m_data.Cols()-class="num">1; v++)
        {
         distance += w[v] * fabs(m_data[other][v] - m_data[first][v]) ;
        }
      if(m_target_bin[first] == category)
        {
         if(distance < top)
            top = distance ;
        }
      else
        {
         if(distance < bottom)
            bottom = distance ;
        }
     }
   distance = top - bottom ;
   if(distance > class="num">30.0)
      loss += distance ;
   else
      loss += log(class="num">1.0 + exp(distance));
   }
 class="kw">return loss ;
 }
把特征扫描交给小布盯盘
这些诊断与特征相关性初筛,小布盯盘的AIGC已内置,打开对应品种页即可看到候选变量权重分布,你只需专注策略逻辑本身。

常见问题

核心区别在投票加权:标准k-NN按邻居多数类硬投,加权版按曼哈顿距离给每个邻居乘权重,MQL5里多写一层距离归一化即可。
能量函数若惩罚项偏弱,可能对历史样本拟合过紧;贵金属与外汇属高风险品种,建议用交叉验证看稳定性而非单次准确率。
概率上可以作特征门限参考,但需转成离散规则;倾向先做离线与在线权重一致性检查再接入实盘逻辑。
目前小布内置的是相关性诊断与变量初筛视图,完整FREL训练仍建议在MT5脚本内执行,结果可贴回小布做横向比对。
曼哈顿对异常跳点更不敏感,在价差突变的外汇tick上可能比欧氏更稳,但计算维度独立贡献时丢失了方向耦合信息。