MQL5中的逐步特征选择(基础篇)
📘

MQL5中的逐步特征选择(基础篇)

第 1/3 篇

MQL5 里怎么一步步挑出有用特征

做策略的人常卡在一个点:指标堆得越多,过拟合越狠。MQL5 的逐步特征选择思路,就是每次只增删一个变量,看模型误差是降还是升,把拖后腿的特征踢掉。 这套流程在 MT5 里跑起来不复杂,核心是用 CTerminalInfo 或自定义数组记录每轮特征组合下的回测权益曲线斜率。2025 年 7 月社区里一篇 532 次浏览的统计分析帖提到,对 12 个候选特征做前向逐步筛选,最终留 4 个时样本外夏普掉得最慢。 外汇和贵金属波动受消息面扰动大,逐步选择只能降低维度灾难的概率,不保证实盘稳健,杠杆品种高风险先记牢。

「逐步特征选择到底卡在哪」

传统逐步特征选择从候选池里逐个单测变量,挑出单兵作战最强的先入模,再不断往里塞新特征测组合增益,直到预测或分类指标达标。这套流程在样本内往往漂亮,但拿到 unseen 数据容易塌——过拟合风险和忽略特征间非线性相互作用,是它两个老毛病。 本文要落地的增强算法来自 Timothy Masters 的《C++和CUDA C中的现代数据挖掘算法》,思路是修正上述缺陷,并在 MQL5 里写成可插拔模块,能直接挂到不同监督学习器上跑。 我们用一个样本回归任务做演示:同一批候选特征,传统法选出的子集在样本外 R² 倾向低于增强法选出的子集,证明换路子确实能改善变量筛选有效性。外汇与贵金属模型训练属高风险实验,回测结论不代表实盘概率。

◍ 逐步法挑特征为何在外汇模型里失灵

机器学习用的数据集常带非线性、时变与因子交织,传统逐步特征选择很难吃下这种复杂度。一个单独看没信息量的特征,配上特定互补特征后可能突然变高度预测——比如外汇预测里,把「即将举行的选举结果」和「当前市场趋势」叠在一起,比只靠趋势更稳;再叠上对经济环境与新政层的推演,样本外准确度还会再抬一截。 逐步法的第二个坑是噪声过拟合。每加一个变量,算法都可能把随机波动当信号,表面样本内绩效变好,样本外却垮掉。若只按样本内拟合判模型,往往会塞进多余指标,反而拖累实盘泛化——外汇与贵金属杠杆高、跳空频繁,这种虚胖模型一出信号就容易吃止损。 回归类逐步靠 p 值判显著性:零假设是系数为 0,用系数估计值与标准误算 t 分得出 p。但换到别的 ML 方法,要拿到可比 p 值得定制假设检验,落地成本陡增。更实用的做法应是模型无关、跨方法一致显著性度量。 改进算法做了三处关键改动:维护多个高潜力子集去试探组合,避免漏掉协同效应;用交叉验证成绩当准入门槛,收益递减就自动停手;每加一个特征算两个概率——当前集合表现纯属偶然的概率、最新这步提升是假象的概率。两者模型无关,可直接挂进各类 ML。下一段贴出 stepwise.mqh 里 CStepwiseBase 抽象类的骨架。

逐步搜索怎么在 MT5 里跑起来

传统逐步特征选择容易卡在局部最优,这套做法同时跟踪多个有希望的特征子集,靠多次排列复制避开穷尽搜索,计算量更可控。主流程在 stepwiseSearch() 里:先初始化记录试验的数据结构,再循环加特征,直到预测器数量触顶或性能提升掉到阈值以下。 循环里每轮会把目标变量随机打乱若干次(由 num_replications 控制),通过 addFeature() 塞入一个新变量,用交叉验证算表现。若加变量反而掉点,算法可能提前终止;若持续变好,就一直加到 max_num_predictors 为止,最终索引写进 m_var_indices。 超参数直接决定搜索行为:num_kept 管每轮留几个最优候选(至少 1),num_folds 折数越大估计越准但越慢,min/max_num_predictors 框定模型复杂度上下限,verbose_output 控制是否在「专家」标签吐过程日志。这些都靠 setParams() 一次性塞进去。 下面这段是参数注入与搜索骨架的原文,逐行看能少踩坑:setParams() 里对非法值做了兜底,比如 num_kept 为 0 时强制取 1;stepwiseSearch() 开头先按 m_max_num_preds*m_vars*m_keptvars 算 trial 长度并 reset(),数组扩容失败就 Print 错误并 return,避免后面越界。外汇与贵金属特征建模波动大、过拟合风险高,参数请先在历史数据上小样本验证再上实盘。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  set the stepwise selection parameters                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void                 setParams(class="type">class="kw">ulong num_kept, class="type">class="kw">ulong num_folds, class="type">class="kw">ulong min_num_predictors, class="type">class="kw">ulong max_num_predictors,class="type">long num_replications,class="type">bool verbose_output)
  {
   m_keptvars = num_kept>class="num">0?num_kept:class="num">1;          class=class="str">"cmt">// 保留候选数,若传入0则兜底为1
   m_folds = num_folds>class="num">0?num_folds:ULONG_MAX;   class=class="str">"cmt">// 交叉验证折数,0则设为最大ulong
   m_min_num_preds = min_num_predictors;        class=class="str">"cmt">// 最小预测器数下限
   m_max_num_preds = max_num_predictors;        class=class="str">"cmt">// 最大预测器数上限
   m_reps = num_replications>class="num">0?num_replications:class="num">1; class=class="str">"cmt">// 排列测试次数,0则取1
   m_verbose = verbose_output;                  class=class="str">"cmt">// 是否输出详细日志
   class="kw">return;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| coordinates main stepwise search operation                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int                  stepwiseSearch(class="type">void)
  {
   class="type">int done = class="num">1;                                class=class="str">"cmt">// 返回码,默认已完成
   class="type">int btrials;
   m_trial_length = m_max_num_preds*m_vars*m_keptvars; class=class="str">"cmt">// 试验数组总长
   reset();                                     class=class="str">"cmt">// 重置内部状态
   if(!m_prior_best_crits.Resize(m_keptvars) || !m_all_best_crits.Resize(m_reps,m_keptvars))
     {
      Print(__FUNCTION__," data structure resizing error ", GetLastError());
      class="kw">return done;                              class=class="str">"cmt">// 二维标准值数组扩容失败则退出
     }
   if(ArrayResize(m_all_best_trials,class="type">int(m_reps*m_max_num_preds*m_keptvars))<class="num">0  ||
       ArrayResize(m_already_tried,class="type">int(m_trial_length))<class="num">0 ||
       ArrayResize(m_trial_vars,class="type">int(m_max_num_preds))<class="num">0 ||
       ArrayResize(m_prior_best_trials,class="type">int(m_max_num_preds*m_keptvars))<class="num">0)
     {
      Print(__FUNCTION__,"  error resizing array ", GetLastError());
      class="kw">return done;                              class=class="str">"cmt">// 任一试验数组扩容失败则退出
     }
   ArrayInitialize(m_all_best_trials,-class="num">1);       class=class="str">"cmt">// 全部试验索引初始化为-class="num">1
   ArrayInitialize(m_already_tried,-class="num">1);         class=class="str">"cmt">// 已尝试标记清-class="num">1
   ArrayInitialize(m_trial_vars,-class="num">1);            class=class="str">"cmt">// 本轮变量索引清-class="num">1
   ArrayInitialize(m_prior_best_trials,-class="num">1);     class=class="str">"cmt">// 历史最优试验清-class="num">1
   vector target;
   class="type">int n_so_far =class="num">0;                             class=class="str">"cmt">// 已选特征计数
   class="type">int rval, rem, j,n_this_rep = class="num">0;
   class="type">int mcpt_mod_count, mcpt_change_count;
   mcpt_mod_count = mcpt_mod_count = mcpt_change_count = -class="num">1; class=class="str">"cmt">// 排列计数初始化
   class="type">class="kw">double temp,prior_crit;
   class="type">class="kw">double original_crit, original_change, new_crit;
   original_crit = original_change = new_crit = -DBL_MIN;    class=class="str">"cmt">// 性能基准置最小
   if(m_verbose)
     {

「重采样循环里的特征增量逻辑」

这段逻辑跑在一个死循环里,每次迭代先锁定目标列索引数组 target,再按 m_reps 设定的重复次数做自助采样。rep 大于 0 时用 17*rep+11 作种子喂给 unif,对样本做 Fisher-Yates 式的就地重排,rem 从 m_samples 递减到 1。 重排后 btrials 直接算成 rep*m_max_num_preds*m_keptvars,作为本轮候选试错上限。若 n_so_far 为 0 则先验准则置为 -1.e60,否则取 m_all_best_crits[rep][0] 作对比基线。 addFeature 返回后立刻校验:done 为真或实际新增数不等于 n_so_far+1 就打印内部错误并 return 1。当 rep 为 0、已选变量数不低于 m_min_num_preds、且本轮最优准则没超过先验时,把 m_prior_best_crits 抄回并做 ArrayCopy,偏移量用 btrials+i*n_so_far 定位,复制失败同样 return 1。外汇与贵金属市场波动剧烈,这类特征选择代码仅用于历史样本建模,实盘信号失效概率偏高。

MQL5 / C++
if(m_reps>class="num">1)
     Print("Criterion  || New pval  || Diff pval  || Column Indices");
   else
     Print("Criterion  || Column Indices");
   }
 class="kw">while(true)
   {
    target = m_target;
    for(class="type">class="kw">ulong rep=class="num">0; rep<m_reps; rep++)
     {
      if(rep)
        {
         rval = class="num">17*class="type">int(rep) + class="num">11;
         unif(rval);
         unif(rval);
         rem = class="type">int(m_samples);
         class="kw">while(rem>class="num">1)
           {
            j = (class="type">int)(unif(rval))*rem;
            if(j>=rem)
               j = rem-class="num">1;
            temp = target[--rem];
            target[rem] = target[j];
            target[j] = temp;
           }
        }
      btrials = class="type">int(rep*m_max_num_preds*m_keptvars);
      if(n_so_far == class="num">0)
         prior_crit = -class="num">1.e60;
      else
         prior_crit = m_all_best_crits[rep][class="num">0];
      n_this_rep = n_so_far;
      done = addFeature(target,n_this_rep,btrials,rep);
      if(done || n_this_rep!=(n_so_far+class="num">1))
        {
         Print(__FUNCTION__, " internal error ");
         class="kw">return class="num">1;
        }
      if(m_all_best_crits[rep][class="num">0]<=prior_crit && n_so_far>=class="type">int(m_min_num_preds) && !rep)
        {
         for(class="type">class="kw">ulong i = class="num">0 ; i<m_keptvars; i++)
           {
            m_all_best_crits[rep][i] = m_prior_best_crits[i];
            if(ArrayCopy(m_all_best_trials,m_prior_best_trials,btrials+class="type">int(i*n_so_far),class="type">int(i*n_so_far),n_so_far)<class="num">0)
              {
               Print(__FUNCTION__, " ArrayCopy error ", GetLastError());
               class="kw">return class="num">1;
              }
           }
        }
      if(m_verbose)

◍ 多重复现下的显著性判定落点

上面这段逻辑跑在变量筛选的收口阶段:当新加入的变量让性能劣化时,直接打印早退信息并返回 0,不再往下写决策矩阵。这一步能避免把无效变量带进后续回测,外汇与贵金属样本外环境波动大,无效变量更容易在实盘制造伪信号。 如果重复次数 m_reps 大于 1,代码会算两个 p 值:mod_pval = mcpt_mod_count / m_reps,change_pval = mcpt_change_count / m_reps。前者衡量新变量不劣于原始判据的重复比例,后者衡量边际改善不弱于首轮的比例,二者都落在 [0,1] 区间,值越高代表该变量在多重复现中越稳。 矩阵落盘时,m_decision_matrix 被 Resize 成 n_so_far+1 行;若 m_reps>1 则留 3 列(原始判据、mod_pval、change_pval),否则只留 1 列。开 MT5 把 m_verbose 设 true,就能在日志里看到形如 0.12345678 0.85000000 0.70000000 的行,据此判断哪个变量值得保留。 别把高 p 值当进场指令 这俩比例只是变量筛选的统计证据,不是交易信号。贵金属杠杆高、滑点跳空频繁,即便 mod_pval 到 0.9,也只在样本内倾向有效,实盘仍可能失效。

MQL5 / C++
Print(__FUNCTION__, " procedure terminated early because adding a new variable caused performance degradation");
class="kw">return class="num">0;
}
if(prior_crit < class="num">0.0)
   prior_crit = class="num">0.0;
new_crit = m_all_best_crits[rep][class="num">0];
if(new_crit<class="num">0.0)
   new_crit = class="num">0.0;
if(rep == class="num">0)
   {
   original_crit = new_crit;
   original_change = new_crit - prior_crit;
   mcpt_mod_count = mcpt_change_count = class="num">1;
   }
else
   {
   if(new_crit >= original_crit)
      ++mcpt_mod_count;
   if(new_crit - prior_crit >= original_change)
      ++mcpt_change_count;
   }
}
if(n_so_far == class="num">0)
   mcpt_change_count = mcpt_mod_count;
if(!m_decision_matrix.Resize(n_so_far+class="num">1,m_reps>class="num">1?class="num">3:class="num">1,class="num">100) || ArrayResize(m_var_indices,class="type">int(m_var_indices.Size())+n_this_rep,class="num">100)<class="num">0)
   {
   Print(__FUNCTION__, " container resize error ", GetLastError());
   class="kw">return class="num">1;
   }
class="type">class="kw">string msg;
if(m_reps>class="num">1)
   {
   class="type">class="kw">double mod_pval = (class="type">class="kw">double) mcpt_mod_count / (class="type">class="kw">double) m_reps;
   class="type">class="kw">double change_pval = (class="type">class="kw">double) mcpt_change_count / (class="type">class="kw">double) m_reps;
   if(m_verbose)
      msg = StringFormat("%class="num">10.8lf %class="num">10.8lf %class="num">10.8lf  ", original_crit,mod_pval,change_pval);
   m_decision_matrix[n_so_far][class="num">0] = original_crit;
   m_decision_matrix[n_so_far][class="num">1] = mod_pval;
   m_decision_matrix[n_so_far][class="num">2] = change_pval;
   }
else
   {
   msg = m_verbose?StringFormat("%class="num">10.8lf", original_crit):NULL;
   m_decision_matrix[n_so_far][class="num">0] = original_crit;
   }
if(m_verbose)
   {
   for(class="type">int i = class="num">0; i<n_this_rep; i++)
      {
      msg += StringFormat(" %s",IntegerToString(m_all_best_trials[i]));

常见问题

逐步法按单次增量显著性贪心筛选,外汇噪声大易过拟合;建议配合重采样多重复现,只保留跨循环稳定的特征。
常见是重采样嵌套过深或每次拟合开销过大;可先缩小候选集、降低重复次数再做增量验证。
可以,小布能按你给的候选因子自动做多重复现筛选,并直接列出跨样本显著的特征清单,省去手写循环。
看该特征加入后模型在验证折叠上的稳定增益;若多次重采样增益波动大就舍去。
高风险品种建议调高判定门槛并拉长复现次数,避免把随机波动当有效信号。