MQL5中的逐步特征选择·进阶篇
🔬

MQL5中的逐步特征选择·进阶篇

(2/3)·传统逐步法为何在外汇特征工程里容易翻车,以及MQL5里的改进实现路径

偏理论 第 2/3 篇
很多人在MQL5里直接套用传统逐步特征选择,却没意识到单独无用的指标组合起来可能才是预测关键。把选举预期和趋势叠加,往往比单看市场方向更稳。先理解局限,再谈实现,能少走不少弯路。

◍ 逐步筛选里的数组拷贝与终止判断

这段逻辑处在逐步回归特征筛选的循环体内,核心是把当前轮次挑出的预测因子索引收进总表,并决定何时收工。 ArrayCopy 的偏移量用了 (n_so_far*(n_so_far+1))/2,这是三角数公式,保证每轮新增的 n_this_rep 个索引紧挨着之前写过的区域、不重叠。若返回负值说明拷贝失败,直接打印错误码并 return 1 中断流程。 当 n_so_far 累加到等于 m_max_num_preds(最大预测因子数,为整数强转)时,若开了 verbose 会打印完成提示并 break 跳出。外汇与贵金属市场高波动,这类多因子模型过拟合风险高,实盘前务必在 MT5 用历史数据跑通该返回路径。

MQL5 / C++
      }
      }
      if(ArrayCopy(m_var_indices,m_all_best_trials,(n_so_far*(n_so_far+class="num">1))/class="num">2,class="num">0,n_this_rep)<class="num">0)
      {
       Print(__FUNCTION__, " array copy error ", GetLastError());
       class="kw">return class="num">1;
      }
      if(m_verbose)
       Print(msg);
      ++n_so_far;
      if(n_so_far == class="type">int(m_max_num_preds))
      {
       if(m_verbose)
        Print(" Stepwise selection successfully completed ");
       break;
      }
   }
   class="kw">return class="num">0;
}

用交叉验证给特征子集打分

单靠一份训练集挑特征,过拟合概率会明显抬升,选出来的子集在实盘里可能很快失效。把样本拆成多折轮流训练与验证,能在不引入未来数据的前提下,压低过拟合倾向,让特征重要度评估更稳。 折数不是越多越好:折数上升,性能估计通常更准,但计算量近似线性增长。具体取几折要看样本规模和机器空闲算力,没有统一常数。 crossEval() 就是干这事的函数。它吃三个输入——候选预测器数量 num_vars、打乱后的目标向量 target、以及引用返回的决策变量 criterion(用于挑最合适特征子集)。跑完返回 0 表示成功。 内部先 fitModel() 在某一折区间训模型,再 evalModel() 在同区间算误差,所有折误差累加后,criterion 被写成 1.0 - error/m_samples。也就是说,criterion 越接近 1,该特征子集交叉验证表现越好。 fitModel() 和 evalModel() 在这里只是虚函数占位,必须在你自己的派生类里按用的模型类型实现,否则直接返回 false 或 EMPTY_VALUE,crossEval 会打印失败并退 1。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  evaluates a predictor set class="kw">using cross validation                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int                crossEval(class="type">class="kw">ulong num_vars,vector &target,class="type">class="kw">double &criterion)
  {
   class="type">class="kw">ulong ifold, n_remaining, test_start, test_stop ;
   class="type">class="kw">double error, evalresult ;
   n_remaining = m_samples;
   test_start = class="num">0;
   error = class="num">0.0;
   for(ifold = class="num">0; ifold<m_folds; ifold++)
     {
      test_stop = test_start + (n_remaining/(m_folds-ifold));
      if(!fitModel(num_vars,test_start,test_stop,target))
        {
         criterion = -DBL_MIN;
         Print(__FUNCTION__, " fit() failed ");
         class="kw">return class="num">1;
        }
      evalresult = evalModel(num_vars,test_start,test_stop,target);
      if(evalresult==EMPTY_VALUE)
        {
         criterion = -DBL_MIN;
         Print(__FUNCTION__, " evalModel() failed ");
         class="kw">return class="num">1;
        }
      error+=evalresult;
      n_remaining -= test_stop - test_start;
      test_start = test_stop;
     }
   criterion = class="num">1.0 - error/class="type">class="kw">double(m_samples);
   class="kw">return class="num">0;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| fit a model                                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">bool      fitModel(class="type">class="kw">ulong num_preds,class="type">class="kw">ulong row_start,class="type">class="kw">ulong row_stop, vector& targets) { class="kw">return class="kw">false;}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| evaluate a model                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">virtual class="type">class="kw">double    evalModel(class="type">class="kw">ulong num_preds,class="type">class="kw">ulong row_start,class="type">class="kw">ulong row_stop, vector& targets) { class="kw">return EMPTY_VALUE;}

「挑出集合里最管用的头号预测变量」

在逐步特征筛选里,第一根「柱子」必须靠遍历硬找。addFirstFeature() 由 addFeature() 调起,对候选池里每个变量做交叉验证,用派生类定义的标准打分,标准本身也决定了后面模型用哪种学习技术。 它先给 m_keptvars 个保留位灌入 -1.e60 这种极小值当占位,再逐个变量塞进 m_trial_vars[0] 跑 crossEval()。每次算出的 crit 会和当前最优列表从上往下比,一旦超过第 j 个就 break,然后把 j 之后的记录整体后挪一位,新变量插进 j 号位。 这套插入排序式维护保证遍历完 m_vars 个变量后,m_all_best_trials 头部就是表现最靠前的预测器。后面再调 addFeature() 走 addNewFeature() 增量引入,第一特征就此固定。 代码里 m_keptvars 决定保留几个优变量,默认若设小了(比如 3)可能漏掉次优但后续组合更强的因子;在 MT5 里把 m_keptvars 调到 10 左右再跑回测,能直观看到头部 trial 索引变化。外汇与贵金属波动噪声大,交叉验证样本外表现仅代表历史概率,实盘仍属高风险。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|    finds the first predictor in the set                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int                addFirstFeature(vector &targ, class="type">int bindex,class="type">class="kw">ulong rep_index)
  {
   class="type">class="kw">double crit;
   for(class="type">class="kw">ulong i = class="num">0; i<m_keptvars; i++)
     {
      m_all_best_crits[rep_index][i] = -class="num">1.e60;
      m_all_best_trials[bindex+i] = -class="num">1;
     }
   for(class="type">int i = class="num">0; i<class="type">int(m_vars); i++)
     {
      m_trial_vars[class="num">0] = i;
      if(crossEval(class="num">1,targ,crit))
        {
         Print(__FUNCTION__, " xval error ");
         class="kw">return class="num">1;
        }
      class="type">class="kw">ulong j;
      for(j = class="num">0; j<m_keptvars; j++)
        {
         if(crit > m_all_best_crits[rep_index][j])
           break;
        }
      if(j<m_keptvars)
        {
         for(class="type">long k = class="type">long(m_keptvars-class="num">2); k>=class="type">long(j); k--)
           {
            m_all_best_trials[bindex+k+class="num">1] = m_all_best_trials[bindex+k];
            m_all_best_crits[rep_index][k+class="num">1] = m_all_best_crits[rep_index][k];
           }
         m_all_best_trials[bindex+j] = i;
         m_all_best_crits[rep_index][j] = crit;
        }
     }
   class="kw">return class="num">0;
  }

◍ 逐步特征选择里的下一个预测器怎么挑

在 MT5 里做逐步特征选择,核心动作就是不断往模型里塞「下一个最值得用的预测器」。addNewFeature() 干的就是这事:它把当前已选的最优预测器集合,跟剩下的每个候选变量逐一配对,跑交叉验证,按指定标准打分,留下表现更好的组合。 这段代码先把上一轮保留下来的最优判据 m_prior_best_crits 从全局表 m_all_best_crits 里按 rep_index 取出来,同时用 ArrayCopy 把对应的变量组合拷进 m_prior_best_trials;原位置的全局判据被重置成 -1.e60,相当于清空待考察位。 紧接着它数出已经成型的「根组合」数量 nbest——循环里一旦碰到小于 -1.e59 的判据就 break,说明这一支再没已尝试组合。之后对每个根组合,遍历所有 m_vars 个候选变量,跳过那些早已在组合里的 ivar,把剩余变量拼成新的 m_trial_vars 送进去验证。 实盘接这套逻辑时,外汇与贵金属波动受消息面干扰大,交叉验证回测表现好只代表样本内倾向有效,换周期可能退化。建议先在 EURUSD 的 M15 上把 m_keptvars 设小一点(比如 8)跑通,再扩维度。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  looks for next predictor to include                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int                addNewFeature(vector& target,class="type">int &ind, class="type">int bindex,class="type">class="kw">ulong rep_index)
  {
   class="type">int i, j, k, ivar, ir ;
   class="type">int npred, n_already_tried,nbest, rootvars;
   nbest  = -class="num">1;
   class="type">class="kw">double crit;
   ind = npred = ind+class="num">1;
   for(i = class="num">0;i<class="type">int(m_keptvars); i++)
     {
     m_prior_best_crits[i] = m_all_best_crits[rep_index][i];
     if(ArrayCopy(m_prior_best_trials,m_all_best_trials,class="type">int(i*(npred-class="num">1)),bindex+class="type">int(i*(npred-class="num">1)),npred-class="num">1)<class="num">0)
       {
        Print(__FUNCTION__, " ArrayCopy error ", GetLastError());
        class="kw">return class="num">1;
       }
     m_all_best_crits[rep_index][i] = -class="num">1.e60;
     }
   n_already_tried = class="num">0;
   for(ir = class="num">0;ir<class="type">int(m_keptvars); ir++)
     {
     if(m_prior_best_crits[ir] < -class="num">1.e59)
       break;
     }
   nbest = ir;
   for(ir = class="num">0; ir<nbest; ir++)
     {
     rootvars = ir*(npred-class="num">1);
     for(ivar=class="num">0; ivar<class="type">int(m_vars); ivar++)
       {
       for(i = class="num">0;i<npred-class="num">1; i++)
         {
         if(m_prior_best_trials[rootvars+i] == ivar)
           break;
         }
       if(i < npred-class="num">1)
         class="kw">continue;
       k = class="num">0;
       for(i = class="num">0;i<class="type">int(m_vars); i++)
         {
         for(j=class="num">0;j<npred-class="num">1;j++)
           {
           if(m_prior_best_trials[rootvars+j] == i)
             {
              m_trial_vars[k++] = i;
              break;
             }
           }
         if(ivar == i)
           m_trial_vars[k++] = i;
         }
       for(i = class="num">0;i<n_already_tried; i++)
         {

特征组合去重与最优集插入的实现细节

这段逻辑处在特征选择循环的内层,核心任务是避免把已经评估过的预测变量组合重复跑一遍,同时把交叉验证表现更好的组合塞进固定长度的最优表里。 外层用 i 遍历已尝试组合,内层 j 从 0 到 npred-1 比对 m_trial_vars 与 m_already_tried 的对应位;只要有一位不同就 break,若 j 走到 npred 说明完全相等,直接跳出不再重复评估。 确认是新组合后,把它写进 m_already_tried 的第 n_already_tried 块,n_already_tried 自增,随后调 crossEval 做交叉验证。若返回非零,Print 报 xval error 并 return 1,说明本次评估失败。 最优表维护用 m_keptvars 控制容量。i 从 0 扫到 m_keptvars-1,遇到 crit 大于已有临界值就 break,定位插入位;之后把 i 之后的记录整体后移一位(j 从 m_keptvars-2 降到 i),腾出位置写入新 crit 与对应 trial 变量,ArrayCopy 失败则报 array copy error 并退出。 在 MT5 里把这段嵌进你自己的特征搜索函数时,重点核对 npred 与 m_keptvars 的实际值——若 m_keptvars 设得过小,高频出现的微优组合会很快把早先样本挤出表,可能削弱后续集成稳定性。外汇与贵金属特征筛选属高风险实验,回测优不代表实盘倾向盈利。

MQL5 / C++
for(j = class="num">0; j<npred; j++)
  {
   if(m_trial_vars[j] != m_already_tried[i*npred+j])
     break;
  }
 if(j == npred)
   break;
   }
  if(i < n_already_tried)
   class="kw">continue;
  for(i =class="num">0; i<npred; i++)
   m_already_tried[n_already_tried*npred+i]=m_trial_vars[i];
  ++n_already_tried;
  if(crossEval(class="type">class="kw">ulong(npred),target,crit))
   {
    Print(__FUNCTION__, " xval error ");
    class="kw">return class="num">1;
   }
  for(i = class="num">0; i< class="type">int(m_keptvars); i++)
   {
    if(crit > m_all_best_crits[rep_index][i])
      break;
   }
  if(i<class="type">int(m_keptvars))
   {
    for(j = class="type">int(m_keptvars-class="num">2); j>=i; j--)
     {
      m_all_best_crits[rep_index][j+class="num">1] = m_all_best_crits[rep_index][j];
      if(ArrayCopy(m_all_best_trials,m_all_best_trials,bindex+class="type">int((j+class="num">1)*npred),bindex+class="type">int(j*npred),npred)<class="num">0)
       {
        Print(__FUNCTION__, " array copy error ", GetLastError());
        class="kw">return class="num">1;
       }
     }
    m_all_best_crits[rep_index][i] = crit;
    if(ArrayCopy(m_all_best_trials,m_trial_vars,bindex+class="type">int(i*npred),class="num">0,npred)<class="num">0)
     {
      Print(__FUNCTION__, " array copy error ", GetLastError());
      class="kw">return class="num">1;
     }
   }
  }
  }
 class="kw">return class="num">0;
}

「逐步特征选择的基类接口与取数路径」

CStepwisebase 是个抽象基类,只管「从候选特征里迭代挑出预测力强的变量」这件事的骨架。它本身不绑定任何学习算法,派生类只要实现 fitModel() 和 evalModel() 两个方法,就能把任意模型和评价标准接进来。 对外真正能调用的入口是 stepwiseSelection(),它先校验数据、做标准化,再转去跑 stepwiseSearch()。想提前配参数就用 setParams(),把最后一个参数设 true,MT5 的 Experts 标签会直接打印选择过程,方便现场看每一步发生了什么。 结果怎么拿:getCriticalVals() 返回的是一个矩阵,列数取决于有没有开蒙特卡洛排列测试(MCP)。在 setParams() 里把 num_replications 设成大于 1 就开了 MCP,此时矩阵有 3 列——每行是一次最外层迭代,分别是标准值、『该性能纯属偶然』的 p 值、『本次新增特征带来的提升纯属偶然』的 p 值;不开 MCP 时只有 1 列标准值。 选出的特征子集数量用 getNumFeatureSets() 查,具体子集调 getFeatureSetAt(),传迭代索引和引用数组,就把那一轮选中的列下标拷给你。外汇与贵金属市场波动剧烈、杠杆风险高,这类特征选择仅降低过拟合概率,不保证样本外预测稳定。 下面这段是基类里 stepwiseSelection 与取数方法的原生实现,逐行拆一下关键逻辑: bool stepwiseSelection(matrix& predictors, vector& targets) 是主入口,先判 targets 长度得和 predictors 行数一致且非空,否则打印无效输入并返回 false。 随后 m_data = stdmat(predictors) 做标准化,m_target 用 (targets-均值)/(标准差+1e-10) 归一,分母加 1e-10 防零除。 m_vars、m_samples 记下列数和行数;接着一串 if 把 m_keptvars、m_folds、m_min/max_num_preds、m_reps 都夹到合法区间(比如折数不能小于 1、不能多于样本数)。 若 m_verbose 为真,Print 出保留变量数、CV 折数、最小最大选变量数、MCP 复制次数,这就是 Experts 标签里能看到的那几行。 最后 return (stepwiseSearch()==0),搜索成功返回 0 即 true。 matrix getCriticalVals(void) 直接 return m_decision_matrix,就是上面说的三列或一列矩阵。 bool getFeatureSetAt(ulong iteration_index, ulong &selectedvars[]) 按零基迭代索引取对应轮次选中的特征列下标,写进 selectedvars 数组,调用方拿去回溯哪几个原始特征被留了下来。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  main method to execute stepwise feature selection                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool                stepwiseSelection(matrix& predictors, vector& targets)
  {
   if(targets.Size()!=predictors.Rows() || !targets.Size())
     {
      Print(__FUNCTION__, " invalid inputs ");
      class="kw">return class="kw">false;
     }
   m_data = stdmat(predictors);
   m_target = (targets-targets.Mean())/(targets.Std()+class="num">1e-10);
   m_vars = predictors.Cols();
   m_samples = predictors.Rows();
   if(m_keptvars>m_vars || m_keptvars<class="num">1)
      m_keptvars = m_vars;
   if(m_folds<class="num">1)
      m_folds = class="num">1;
   if(m_folds>m_samples)
      m_folds = m_samples;
   if(m_min_num_preds==class="num">0 || m_min_num_preds>m_vars || m_min_num_preds>m_max_num_preds)
      m_min_num_preds = m_vars;
   if(m_max_num_preds==class="num">0 || m_max_num_preds>m_vars || m_max_num_preds<m_min_num_preds)
      m_max_num_preds = m_vars;
   if(m_reps<class="num">1)
      m_reps = class="num">1;
   if(m_verbose)
     {
      Print(" STEPWISE SELECTION PARAMETERS ");
      Print(" Number of retained candidate variables for each iteration ", m_keptvars);
      Print(" Number of folds in cross validation procedure ", m_folds);
      Print(" Final minimum number of selected predictors ", m_min_num_preds);
      Print(" Final maximum number of selected predictors ", m_max_num_preds);
      Print(" Number of replications for MCP test ", m_reps);
     }
   class="kw">return (stepwiseSearch()==class="num">0);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| get all crit values and corresponding p-values                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
matrix              getCriticalVals(class="type">void)
  {
   class="kw">return m_decision_matrix;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  get the selected features at a specific iteration of the process|
class=class="str">"cmt">//|  iterations denoted as zero based indices                        |
class=class="str">"cmt">//|  eg, first selected feature located at iteration index class="num">0         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool                getFeatureSetAt(class="type">class="kw">ulong iteration_index,class="type">class="kw">ulong &selectedvars[])
  {
让小布替你跑这套
这些诊断与增强特征筛选的逻辑,小布盯盘的AIGC模块已内置到品种分析页,打开对应外汇或贵金属页面就能看到候选特征组合的贡献评估,把重复劳动交给小布,你专注决策。

常见问题

它难以捕捉特征间的互补组合效应,且容易把市场噪声误判为有效信号导致过拟合,样本外表现常下滑。
可以,小布盯盘的品种页内置了增强逐步选择的诊断视图,直接给出交叉验证下的特征贡献,省去手写CStepwisebase类调试的时间。
低p值仅说明样本内系数非零的概率高,若数据含大量随机波动,算法可能拟合了噪声,样本外仍可能失效。
它在不做穷举搜索的前提下测试有潜力的特征组合,依托交叉验证标准剪枝,兼顾了组合发现与算力约束。
该类封装了增强算法的核心流程,便于对接不同监督学习器,是本文后续代码集成的基础骨架。