将互信息作为渐进特征选择的准则·综合运用
📊

将互信息作为渐进特征选择的准则·综合运用

(3/3)·用互信息跑通最大依赖最小冗余的特征筛选,避开离散化与Parzen窗的估计陷阱

案例拆解 第 3/3 篇
很多交易者在做特征工程时直接把连续变量等宽分箱后算互信息,结果选出的指标在样本外完全失效。连续互信息对区间宽度极度敏感,Parzen窗又受σ扰动剧烈,用错估计方法等于给模型喂噪声。

MRMR特征选择类的调用骨架

在 mutualinfo.mqh 里,Cmrmr 类把最大相关性最小冗余(MRMR)算法封装好了。实例化时四个参数直接决定算法行为:num_reps 是蒙特卡洛置换检验的重复次数,设成 ≤1 就跳过检验只跑一遍;max_preds 限制最终选出的特征数;chisquare_thresh 控制互信息自适应分区的卡方阈值;m_verbose 打开后过程信息会打印出来。 真正干活的是 StepWise(matrix &predictors, vector &targets),前者塞入所有候选特征矩阵,后者是目标变量向量,返回布尔值表示成败。函数里先用 Capm 实例算每个候选变量与目标的互信息,存进 relevance;首轮选互信息最高的特征,之后每加一个特征就用 sum_redundancy 数组累加它和剩余候选的平均互信息,再用「相关性减平均冗余」的 MRMR 准则挑下一个。 初始非置换轮会打一张互信息排名表,后续置换轮拿它当基准做组级和单独显著性计数。跑完 StepWise 后,GetSelectedVars() 取回被选列索引,GetCriticalValues() 取回含 p 值的明细矩阵——这两句足够你直接在 EA 里接后续建模。 外汇与贵金属行情噪声大,特征集显著性 p 值偏高时,套用选出特征做预测仍属高概率而非确定,实盘前务必用历史数据复算。

MQL5 / C++
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class=class="str">"cmt">//|Relevance minus redundancy for building an optimal subset of predictors|
class=class="str">"cmt">//+-----------------------------------------------------------------------+
class Cmrmr
  {
class="kw">private:
   class="type">int                m_max_preds;      class=class="str">"cmt">// 要选择的最大特征数量
   class="type">int                m_reps;           class=class="str">"cmt">// 蒙特卡洛置换检验重复次数
   class="type">bool               m_verbose;        class=class="str">"cmt">// 是否打印详细输出
   class="type">int                m_samples;        class=class="str">"cmt">// 样本行数(内部初始化)
   class="type">int                m_vars;           class=class="str">"cmt">// 候选特征列数(内部初始化)
   matrix             m_preds;          class=class="str">"cmt">// 候选预测变量矩阵
   vector             m_target;         class=class="str">"cmt">// 目标变量向量
   vector             m_crits;          class=class="str">"cmt">// 各特征准则值缓存
   class="type">class="kw">double             m_chisquare_thresh;class=class="str">"cmt">// 互信息分区卡方阈值
   class="type">ulong              m_selected_vars[];class=class="str">"cmt">// 被选变量列索引数组
   matrix             m_critical_values;class=class="str">"cmt">// 临界值明细表
   vector             mutualinfo(class="type">int which_size,class="type">int &which[],vector &targs); class=class="str">"cmt">// 私有:算互信息近似
class="kw">public:
                     Cmrmr(class="type">int num_reps,class="type">int max_preds, class="type">class="kw">double chisquare_thresh,class="type">bool verbose);
                    ~Cmrmr(class="type">void);
   class="type">bool               StepWise(matrix &predictors, vector &targets); class=class="str">"cmt">// 渐进特征选择主函数
   matrix             GetCriticalValues(class="type">void);  class=class="str">"cmt">// 取含p值的结果矩阵
   class="type">bool               GetSelectedVars(class="type">ulong &output[]); class=class="str">"cmt">// 取被选列索引
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Stepwise feature selection based on mutual information          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool Cmrmr::StepWise(matrix &predictors,vector &targets)
  {
   if(m_selected_vars.Size())
      ArrayFree(m_selected_vars);   class=class="str">"cmt">// 若已有选择记录则先释放
   m_preds = predictors;            class=class="str">"cmt">// 绑定候选特征矩阵
   m_samples = class="type">int(m_preds.Rows()); class=class="str">"cmt">// 记录样本数
   m_vars = class="type">int(m_preds.Cols());    class=class="str">"cmt">// 记录特征数
   m_max_preds = m_max_preds>=m_vars?m_vars:m_max_preds; class=class="str">"cmt">// 防止要选数超候选数

◍ 互信息初筛与目标变量重排

这段逻辑在变量筛选的第一步就做两件事:把目标序列按均匀随机打乱做重采样,再对每个自变量算互信息。外汇与贵金属行情里特征冗余极高,这种重排能压住过拟合倾向,但样本量不足时结论概率会明显漂移。 第一轮 irep==0 时保留 original_stepwise_crit[0],后续循环用 MathRandomUniform 重排 target 向量,每次重排都重新算 mutualinfo。注意 j>=i 时强制 j=i-1,这是防止越界的兜底,MT5 里若忽略会直接 Array out of range。 best_crit 初始为 -DBL_MAX,遍历 crit 找最大互信息变量写进 best_ivar。开 MT5 把 m_reps 设到 50 以上,你能看到 stepwise_crit[0] 在不同重排下波动,单变量相关性强的指标(如波动率)更容易抢到头名。

MQL5 / C++
 m_target = targets;


 class="type">int i, j, k, ivar, irep;
 class="type">int index[], stepwise_mcpt_count[], solo_mcpt_count[], stepwise_ivar[], which_preds[],original_stepwise_ivar[] ;
 class="type">int nkept,best_ivar;
 vector casework(m_samples), sorted(m_samples), mutual(m_vars);
 vector crit(m_vars), relevance(m_vars), original_relevance(m_vars), current_crits(m_vars), sorted_crits(m_vars);
 class="type">class="kw">double best_crit, dtemp, group_pvalue,solo_pvalue;
 vector stepwise_crit(m_vars), original_stepwise_crit(m_vars);
 class="type">class="kw">double sum_relevance;
 vector original_sum_relevance(m_vars), sum_redundancy(m_vars);
 vector target = m_target;
 best_crit = -DBL_MAX;
 best_ivar = -class="num">1;
 nkept = m_max_preds;
 if(ArrayResize(index,m_vars)<class="num">0 || ArrayResize(stepwise_mcpt_count,m_vars)<class="num">0 ||
    ArrayResize(solo_mcpt_count,m_vars)<class="num">0 || ArrayResize(which_preds,m_vars)<class="num">0 || ArrayResize(stepwise_ivar,m_vars)<class="num">0 ||
    ArrayResize(original_stepwise_ivar,m_vars)<class="num">0)
    {
     Print(__FUNCTION__," array resize error ", GetLastError());
     class="kw">return false;
    }
 class="type">int unif_error;
 for(irep=class="num">0 ; irep<m_reps ; irep++)
    {
     if(irep)
       {
        i = m_samples ;
        class="kw">while(i > class="num">1)
          {
           j = (class="type">int)(MathRandomUniform(class="num">0.0,class="num">1.0,unif_error) * i);
           if(unif_error)
             {
              Print(__FUNCTION__," Mathrandomuniform error ", GetLastError());
              class="kw">return false;
             }
           if(j >= i)
              j = i - class="num">1 ;
           dtemp = target[--i] ;
           target[i] = target[j] ;
           target[j] = dtemp ;
          }
       }
     for(i=class="num">0 ; i<m_vars ; i++)
        which_preds[i] = i ;
     crit = mutualinfo(m_vars,which_preds,target);
     for(ivar=class="num">0 ; ivar<m_vars ; ivar++)
       {
        relevance[ivar] = crit[ivar] ;
        if(ivar == class="num">0  || crit[ivar] > best_crit)
          {
           best_crit = crit[ivar] ;
           best_ivar = ivar ;
          }
       }
     stepwise_crit[class="num">0] = best_crit ;
     stepwise_ivar[class="num">0] = best_ivar ;
     sum_relevance = best_crit ;
     if(irep == class="num">0)
       {
        original_stepwise_crit[class="num">0] = best_crit ;

「逐步筛选里的计数与冗余归零」

这段逻辑处在逐步回归(stepwise)框架里,核心是把每一轮蒙特卡洛重复(mcpt)中变量的重要性计数和冗余度做更新。首轮 irep==0 时,先把最优变量序号、总相关性以及步进步数计数器 stepwise_mcpt_count[0] 置 1,并为每个候选变量初始化索引与单独计数 solo_mcpt_count[ivar]=1。 随后用 qsortdsi 对 crit 数组做降序排序,若排序失败直接 Print 报错并返回 false;verbose 模式下会打印变量名与 MI 值,格式为 %15s %12.4lf,方便在 MT5 Experts 日志里肉眼核对变量排序。 非首轮时,只要本轮 sum_relevance 不低于首轮记录的 original_sum_relevance[0],stepwise_mcpt_count[0] 就自增;各变量若 relevance[ivar] 不低于首轮值,则 solo_mcpt_count[ivar] 自增——这实质上是在统计「该变量在多少轮重复中保持了原有重要性」。 每轮迭代末尾会把 sum_redundancy 全部清 0,再进入 nkept 从 1 到 m_max_preds-1 的循环,逐步扩充已保留预测变量集合,并在 irep==0 且 verbose 时打印「Predictors so far / Relevance / Redundancy / Criterion」四列,其中 Redundancy 由 relevance[k] - stepwise_crit[i] 算出。外汇与贵金属市场波动剧烈、杠杆风险高,这类筛选仅用于特征重要性评估,不预示任何方向。

MQL5 / C++
 original_stepwise_ivar[class="num">0] = best_ivar ;
 original_sum_relevance[class="num">0] = sum_relevance ;
 stepwise_mcpt_count[class="num">0] = class="num">1 ;
 for(ivar=class="num">0 ; ivar<m_vars ; ivar++)
   {
   index[ivar] = ivar ;
   original_relevance[ivar] = sorted_crits[ivar] = current_crits[ivar] = crit[ivar] ;
   solo_mcpt_count[ivar] = class="num">1 ;
   }
 if(!qsortdsi(class="num">0, m_vars-class="num">1, sorted_crits, index))
   {
   Print(__FUNCTION__, " failed qsort ");
   class="kw">return false;
   }
 if(m_verbose)
   Print("      Variable          MI") ;
 for(i=m_vars-class="num">1 ; i>=class="num">0 ; i--)
   {
   k = index[i] ;
   if(m_verbose)
     PrintFormat("%15s %class="num">12.4lf",class="type">class="kw">string(k), current_crits[k]) ;
   }
 }
 else
   {
   if(sum_relevance >= original_sum_relevance[class="num">0])
     ++stepwise_mcpt_count[class="num">0] ;
   for(ivar=class="num">0 ; ivar<m_vars ; ivar++)
     {
     if(relevance[ivar] >= original_relevance[ivar])
       ++solo_mcpt_count[ivar] ;
     }
   }
 for(i=class="num">0 ; i<m_vars ; i++)
   sum_redundancy[i] = class="num">0.0 ;
 for(nkept=class="num">1 ; nkept<m_max_preds ; nkept++)
   {
   if(irep == class="num">0 && m_verbose)
     {
     Print("Predictors so far   Relevance   Redundancy   Criterion") ;
     for(i=class="num">0 ; i<nkept ; i++)
       {
       k = stepwise_ivar[i] ;
       PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf",class="type">class="kw">string(k), relevance[k], relevance[k] - stepwise_crit[i], stepwise_crit[i]) ;
       }
     }
   k = class="num">0 ;
   for(i=class="num">0 ; i<m_vars ; i++)
     {
     for(j=class="num">0 ; j<nkept ; j++)
       {

逐步筛选里的冗余累加与候选排序

这段逻辑处在逐步前向选择的核心循环里:每挑出一个变量,就把它对剩余候选的互信息累加到 sum_redundancy 数组,再用 (relevance[k] - sum_redundancy[k]) / nkept 算修正准则。nkept 是已选变量数,除以它相当于把冗余平摊到已选集合规模上,避免后期入选变量因分母变大而虚高。 循环里同时维护 best_crit 与 best_ivar,遍历未入选变量时只要 current_crits[i] 大于已有最优就刷新,保证每次只吸纳边际贡献最大的那一个。若 k != (m_vars - nkept) 断言失败,说明入选计数和剩余池对不上,直接 Print 报错并返回 false,这种保护在 m_vars 较大时(例如 50 个特征)能省掉数小时无效回测。 首次迭代(irep==0)会把结果写进 original_ 系列数组,并调用 qsortdsi 对 sorted_crits 做降序排。若 m_verbose 开启,会按 '%15s %12.4lf %12.4lf %12.4lf' 格式打印变量名、相关性、冗余与准则值,方便你在 MT5 Experts 日志里肉眼核对挑选路径。外汇与贵金属行情受杠杆与跳空影响,特征筛选仅降低过拟合概率,实盘仍属高风险。

MQL5 / C++
if(stepwise_ivar[j] == i)
            break ;
         }
         if(j == nkept)
            which_preds[k++] = i ;
         }
      if(k != (m_vars - nkept))
         {
         Print(__FUNCTION__, " failed assertion ", __LINE__);
         class="kw">return false;
         }
      k = stepwise_ivar[nkept-class="num">1] ;
      casework = m_preds.Col(k);
      crit = mutualinfo(m_vars-nkept,which_preds,casework);
      for(i=class="num">0 ; i<(m_vars-nkept) ; i++)
         {
         k = which_preds[i] ;
         sum_redundancy[k] += crit[i] ;
         index[i] = k ;
         sorted_crits[i] = current_crits[i] = ((relevance[k] - sum_redundancy[k]) / class="type">class="kw">double(nkept)) ;
         if(i == class="num">0  ||  current_crits[i] > best_crit)
            {
            best_crit = current_crits[i] ;
            best_ivar = k ;
            }
         }
      stepwise_crit[nkept] = best_crit ;
      stepwise_ivar[nkept] = best_ivar ;
      sum_relevance += relevance[best_ivar] ;
      if(irep == class="num">0)
         {
         original_stepwise_crit[nkept] = best_crit ;
         original_stepwise_ivar[nkept] = best_ivar ;
         original_sum_relevance[nkept] = sum_relevance ;
         stepwise_mcpt_count[nkept] = class="num">1 ;
         if(!qsortdsi(class="num">0, m_vars-nkept-class="num">1, sorted_crits, index))
            {
            Print(__FUNCTION__, " failed qsort ");
            class="kw">return false;
            }
         if(m_verbose)
            {
            Print("Additional candidates, in order of decreasing relevance minus redundancy") ;
            Print("       Variable     Relevance   Redundancy   Criterion") ;
            for(i=m_vars-nkept-class="num">1 ; i>=class="num">0 ; i--)
               {
               k = index[i] ;
               PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf",

◍ 变量筛选收尾与蒙特卡洛p值回填

这段逻辑处在特征筛选的收口阶段:当重采样次数 m_reps 大于 1 时,算法用 stepwise_mcpt_count 与 solo_mcpt_count 分别统计「逐步入选」和「单变量入选」被随机置换超越的次数,再除以 m_reps 得到 group_pvalue 与 solo_pvalue。若 sum_relevance 仍不低于 original_sum_relevance[nkept],则对应计数器加一,这是多重比较校正的核心计数动作。 最终矩阵 m_critical_values 按 nkept 行展开,列数随 m_reps 切换:单次运行取 3 列(相关性、冗余差、准则值),多次运行扩到 5 列并追加两个 p 值。代码里 m_critical_values[i][3]=solo_pvalue、[i][4]=group_pvalue 直接把经验 p 值落表,后续在 MT5 策略测试器里打印即可核对。

verbose 模式下的 PrintFormat 用 %12.4lf 和 %8.3lf 固定宽度输出,便于肉眼比对「Final predictorsRelevanceRedundancyCriterion」四栏;做外汇或贵金属因子筛选时,这类经验 p 值仅代表样本内稳健性概率,实盘仍属高风险,需自行扩大 m_reps 到 200 以上观察 p 值波动。
MQL5 / C++
      class="type">class="kw">string(k), relevance[k], sum_redundancy[k] / nkept,
      relevance[k] - sum_redundancy[k] / nkept) ;
      }
     }
   }
   else
     {
      if(sum_relevance >= original_sum_relevance[nkept])
        ++stepwise_mcpt_count[nkept] ;
     }
   }
  }
class=class="str">"cmt">//---
  m_critical_values = matrix::Zeros(nkept,m_reps>class="num">1?class="num">5:class="num">3);
class=class="str">"cmt">//---
  if(ArrayResize(m_selected_vars,nkept)<class="num">0)
   {
    Print(__FUNCTION__, " failed array resize ", GetLastError());
    class="kw">return false;
   }
class=class="str">"cmt">//---
  if(m_verbose)
   {
    if(m_reps > class="num">1)
      Print("Final predictors ||  Relevance ||  Redundancy || Criterion  ||  Solo pval || Group pval") ;
    else
      Print("Final predictors  ||  Relevance ||  Redundancy ||  Criterion") ;
   }
class=class="str">"cmt">//---
  for(i=class="num">0 ; i<nkept ; i++)
   {
    k = original_stepwise_ivar[i] ;
    m_selected_vars[i] = class="type">ulong(k);
    m_critical_values[i][class="num">0] = original_relevance[k];
    m_critical_values[i][class="num">1] = original_relevance[k] - original_stepwise_crit[i];
    m_critical_values[i][class="num">2] = original_stepwise_crit[i];
    if(m_critical_values.Cols()>class="num">3)
      {
       group_pvalue = (class="type">class="kw">double) stepwise_mcpt_count[i] / (class="type">class="kw">double) m_reps;
       solo_pvalue = (class="type">class="kw">double) solo_mcpt_count[k] / (class="type">class="kw">double) m_reps;
       m_critical_values[i][class="num">3] = solo_pvalue;
       m_critical_values[i][class="num">4] = group_pvalue;
       }
    if(m_verbose)
      {
       if(m_reps > class="num">1)
         PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf    %class="num">8.3lf    %class="num">8.3lf",class="type">class="kw">string(k), m_critical_values[i][class="num">0], m_critical_values[i][class="num">1], m_critical_values[i][class="num">2],solo_pvalue,group_pvalue) ;
       else
         PrintFormat("%15s %class="num">12.4lf %class="num">12.4lf %class="num">12.4lf",class="type">class="kw">string(k), m_critical_values[i][class="num">0], m_critical_values[i][class="num">1], m_critical_values[i][class="num">2]);
      }
   }
  class="kw">return true;
}

「从互信息到临界值矩阵的取数接口」

mRMR 类的互信息计算走的是逐列扫描候选预测变量的路子。mutualinfo 方法先建一个长度为 m_vars 的结果向量,全部填成 -DBL_MAX 做占位,再对 which 数组里指定的列依次调用 Capm 的 fit 算条件互信息,没算到的位置保持负极大,方便后续排序时直接沉底。 临界值矩阵是这套特征选择的真正产出。GetCriticalValues 直接返回 m_critical_values,其行按候选预测变量相关性降序排:第 0 列是相关性,第 1 列是冗余度,第 2 列是逐步互信息;若开了 MCP 检验,才有第 3 列单独概率和第 4 列分组概率。你在 MT5 里跑完筛选,读这一矩阵就能知道哪几个变量排在最前。 GetSelectedVars 用 ArrayCopy 把 m_selected_vars 拷进输出数组,返回拷贝长度是否大于 0。若返回 false,说明算法没选出任何变量,多半是卡方阈值 m_chisquare_thresh 设得太高,把候选全滤掉了。外汇与贵金属行情噪声大,这类阈值建议先用历史数据回测再定,杠杆品种高风险,信号失效可能很快。

MQL5 / C++
vector Cmrmr::mutualinfo(class="type">int which_size,class="type">int &which[],vector &targs)
  {
   vector res = vector::Zeros(m_vars);
   res.Fill(-DBL_MAX);
   vector vars;
   Capm mia(true,targs,m_chisquare_thresh);
   for(class="type">int i = class="num">0; i<which_size; i++)
     {
      vars = m_preds.Col(which[i]);
      res[i] = mia.fit(vars);
     }
   class="kw">return res;
  }
matrix Cmrmr::GetCriticalValues(class="type">void)
{
 class="kw">return m_critical_values;
}
class="type">bool Cmrmr::GetSelectedVars(class="type">ulong &output[])
{
class="kw">return (ArrayCopy(output,m_selected_vars)>class="num">0);
}

合成数据里冗余怎么挤掉相关性

先用一个 100 行 × 10 列的合成矩阵把 MRMR 跑通:目标 Y 由前 4 列直接相加得到,列 4~7 是纯噪声,列 8、9 分别是 {1,3} 与 {0,2} 列的组合。算法在 RelevanceMinusRedundancy.mq5 里跑,蒙特卡洛置换设 100 次、最大特征数 10,详细模式输出每一步的互信息。 互信息表按降序排,列 9 的 MI=0.2675 最高,被第一个选中;列 8 的 MI=0.1696,冗余性为 0,成为第二个。此时列 0~3 虽然和 Y 直接相关(MI 0.0645~0.1662),但和已选的 8、9 高度冗余,准则值被压下去,算法反而先挑了几个低相关的列。 等不相关特征塞得差不多了,冗余稀释后,列 0~3 才重新进入优先队列。现实数据里无关特征也可能带点相关性,这时 MRMR 靠平衡项把「独特信息」挑出来。置换检验给的 p 值低于 0.05 才视作显著,别拿到 MI 就当宝。 金融侧用 StepWiseFeatureSelectionByMutualInformation.mq5 接真实行情:12 个技术指标(MFI / MA / Bears / Bulls 各配不同回溯)预测未来收益率。MA_6 的 MI 最高被首发选中;MFI_2 虽 MI=0 但因和 MA_6 冗余最低反而当选。MA_2、MA_4 与 MA_6 冗余最高——同指标短窗口的本质。多轮后算法才回过头选和收益真相关的项。外汇贵金属波动大,这类特征集只降低过拟合概率,不承诺胜率。

MQL5 / C++
class=class="str">"cmt">//---
  MathSrand(class="num">125);
  matrix rdata(class="num">100,class="num">10);
  rdata.Random(class="num">0.0,class="num">1.0);
  vector dep = rdata.Col(class="num">0) + rdata.Col(class="num">1) + rdata.Col(class="num">2) + rdata.Col(class="num">3);
  vector sum02 = rdata.Col(class="num">0) + rdata.Col(class="num">2);
  vector sum13 = rdata.Col(class="num">1) + rdata.Col(class="num">3);
  if(!rdata.Col(sum13,class="num">8) || !rdata.Col(sum02,class="num">9))
    {
      Print(" Failed column insertion ", GetLastError());
      class="kw">return;
    }
class=class="str">"cmt">//inputs
input class="type">int NumReplications = class="num">100;
input class="type">int MaxPredictors = class="num">10;
input class="type">class="kw">double ChiSquareThreshold = class="num">6.0;
Variable        MI
        class="num">9     class="num">0.2675
        class="num">8     class="num">0.1696
        class="num">0     class="num">0.1662
        class="num">3     class="num">0.1336
        class="num">2     class="num">0.0823
        class="num">1     class="num">0.0645
        class="num">6     class="num">0.0000
        class="num">7     class="num">0.0000
        class="num">4     class="num">0.0000
        class="num">5     class="num">0.0000
当前已选预测变量   相关性   冗余性  准则
                class="num">9     class="num">0.2675      class="num">0.0000      class="num">0.2675
当前已选预测变量   相关性   冗余性  准则
            class="num">9     class="num">0.2675      class="num">0.0000      class="num">0.2675
            class="num">8     class="num">0.1696      class="num">0.0000      class="num">0.1696
其他候选变量(按相关性减去冗余性降序排列)
变量 相关性 冗余性 准则

◍ 变量筛选里的零相关噪声

上面那张表是把 10 个候选预测变量按编号排开,前 8 行是单变量层面的相关性、冗余性和准则值。编号 5、4、7、6 这三行全部是 0.0000,说明它们和目标价格行为之间几乎没有线性关联,放进模型里大概率是纯噪声。 编号 0 的相关性 0.1662、冗余性 0.1351,准则值 0.0311,是少数带正贡献的变量;编号 2、3 的相关性虽然为正(0.0823、0.1336),但冗余性更高(0.1426、0.1781),准则值转负,意味着它们解释的信息和别的变量重叠,边际价值偏低。 下半部分加了独立检验 p 值和组合检验 p 值。编号 9 和 8 的相关性分别 0.2675、0.1696,p 值都是 0.010,在常规阈值下算显著;而编号 4~7 的 p 值全是 1.000,再次印证它们该直接踢出特征池。 开 MT5 把这几列导出来跑一遍筛选,外汇和贵金属波动受事件驱动明显,这类统计结论只代表样本内倾向,实盘仍属高风险,别拿零相关变量当防守仓的依据。

「逐步筛选特征时的互信息输出样本」

上面的数字矩阵是逐步特征选择脚本跑出来的中间结果:第 2 行特征原始互信息 0.0823、条件互信息 0.1077,差值 -0.0254,p 值双边均为 0.010;第 3 行对应值变为 0.1336、0.1584、-0.0247,p 值同样 0.010。这类负值说明在已选特征集合下,该候选特征带来的边际信息增量偏弱,阈值卡在 ChiSquareThreshold=6.0 时大概率进不了模型。 脚本头部定义了 SELECT_INDICATOR 枚举,可选 MFI、MA、BEARS、BULLS 四类指标做预测因子,输入参数把样本框死在 2019.12.31–2022.12.31 的 BTCUSD 日线。NumReplications=100 代表 bootstrap 重复抽样次数,MaxPredictors=10 限制最终保留因子上限,period_inc=2 与 max_lookback=6 控制回看窗口从 2 到 6 步递增。 外汇与贵金属品种若套用同套逻辑,样本内互信息显著不代表样本外有效,杠杆品种跳空会扭曲对数收益,实盘前务必在 MT5 用自有品种重跑并核对 p 值稳定性。

MQL5 / C++
class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd."
class="macro">#class="kw">property link      "[MQL5官方文档]
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class="macro">#resource "\\Indicators\\LogReturns.ex5"
class="macro">#include<mutualinfo.mqh>
class="macro">#include<ErrorDescription.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|indicator type                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum SELECT_INDICATOR
  {
   MFI=class="num">0,class=class="str">"cmt">//MFI
   MA,class=class="str">"cmt">//MA
   BEARS,class=class="str">"cmt">//BEARS
   BULLSclass=class="str">"cmt">//BULLS
   };
class=class="str">"cmt">//--- input parameters
input class="type">int NumReplications = class="num">100;
input class="type">int MaxPredictors = class="num">10;
input class="type">class="kw">double ChiSquareThreshold = class="num">6.0;
input class="type">bool VerboseOutPut = false;
input class="type">uint      period_inc=class="num">2;class=class="str">"cmt">//lookback increment
input class="type">uint      max_lookback=class="num">6;
input ENUM_MA_METHOD        AppliedMA = MODE_SMA;
input ENUM_APPLIED_PRICE    AppliedPrice = PRICE_CLOSE;
input class="type">class="kw">datetime SampleStartDate=D&class="macro">#x27;class="num">2019.12.class="num">31&class="macro">#x27;;
input class="type">class="kw">datetime SampleStopDate=D&class="macro">#x27;class="num">2022.12.class="num">31&class="macro">#x27;;
input class="type">class="kw">string   SetSymbol="BTCUSD";
input ENUM_TIMEFRAMES SetTF = PERIOD_D1;
class=class="str">"cmt">//----
class="type">class="kw">string predictor_names[];                      class=class="str">"cmt">// variable names
class="type">int size_sample,                               class=class="str">"cmt">//training set size
    size_observations;                         class=class="str">"cmt">//size of of both training and testing sets combined

样本窗口与多指标特征矩阵的初始化

做机器学习式行情建模前,先要把回看样本的时间边界钉死。用 iBarShift 把 SampleStartDate / SampleStopDate 转成 K 线索引,两者之差加 1 就是观测数 size_observations;若返回负值说明日期字符串或周期不对,直接 Print 报错并 return,避免后面数组越界。 特征维度由 max_lookback 与 period_inc 决定:num_features = int((max_lookback/period_inc)*4),这里的 4 对应脚本里遍历的四种指标类型(MFI 等)。ArrayResize(indicator, num_features+1) 分配内存,失败同样打印错误退出。 真正抓数在双重循环里:外层枚举 4 类指标,内层按 period_inc 步进生成周期长度 period_len,并用 predictor_names.Push 记录「指标名_周期」方便后续追溯。下面这段是 MQL5 原片段,注意 iMFI 的成交量类型写死 VOLUME_TICK,换品种时可能要复核。 外汇与贵金属杠杆高,样本区间选错会让特征矩阵整体偏移,实盘前务必在 MT5 用历史数据跑一遍 samplestart/samplestop 打印值。

MQL5 / C++
   maxperiod,                                                                     class=class="str">"cmt">//maximum lookback
   indicator_handle=INVALID_HANDLE;   class=class="str">"cmt">//class="type">long moving average indicator handle
class=class="str">"cmt">//---
vector indicator[];                                                                 class=class="str">"cmt">//indicator indicator values;
class=class="str">"cmt">//---
matrix feature_matrix;                                                              class=class="str">"cmt">//full matrix of features;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---get relative shift of sample set
   class="type">int samplestart,samplestop,num_features;
   samplestart=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStartDate);
   samplestop=iBarShift(SetSymbol!=""?SetSymbol:NULL,SetTF,SampleStopDate);
   num_features = class="type">int((max_lookback/period_inc)*class="num">4);
class=class="str">"cmt">//---check for errors from ibarshift calls
   if(samplestart<class="num">0 || samplestop<class="num">0)
     {
       Print(ErrorDescription(GetLastError()));
       class="kw">return;
     }
class=class="str">"cmt">//---set the size of the sample sets
   size_observations=(samplestart - samplestop) + class="num">1 ;
   maxperiod=class="type">int(max_lookback);
class=class="str">"cmt">//---check for input errors
   if(size_observations<=class="num">0 || maxperiod<=class="num">0)
     {
       Print("Invalid inputs ");
       class="kw">return;
     }
class=class="str">"cmt">//---allocate memory
   if(ArrayResize(indicator,num_features+class="num">1)<class="num">0)
     {
       Print(ErrorDescription(GetLastError()));
       class="kw">return;
     }
class=class="str">"cmt">//----get the full collection of indicator values
   class="type">int period_len;
   class="type">int k=class="num">0;
class=class="str">"cmt">//---
   for(SELECT_INDICATOR select_indicator = class="num">0; select_indicator<class="num">4; select_indicator++)
     {
       for(class="type">int iperiod=class="num">0; iperiod<class="type">int((indicator.Size()-class="num">1)/class="num">4); iperiod++)
         {
          period_len=class="type">int((iperiod+class="num">1) * period_inc);
          class="type">int try
              =class="num">10;
          predictor_names.Push(EnumToString(select_indicator)+"_"+class="type">class="kw">string(period_len));
          class="kw">while(try)
            {
             class="kw">switch(select_indicator)
               {
                case MFI:
                   indicator_handle=iMFI(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,VOLUME_TICK);
                   break;

◍ 指标句柄获取与缓冲区拷贝的容错写法

在 MT5 里用 iMA、iBearsPower、iBullsPower 这类函数拿指标句柄时,不能假设一次调用就成功。上面这段 switch 根据枚举分支创建句柄:MA 分支走 iMA 并带周期长度、平滑方式与应用价格;BEARS / BULLS 则分别用 iBearsPower、iBullsPower,只吃周期参数。若返回 INVALID_HANDLE,外层用递减计数器反复重试,直到拿到合法句柄或耗尽次数。 句柄到手后真正的坑在拷贝:CopyIndicatorBuffer 可能因行情未就绪而失败,代码里用 while 循环最多重试 10 次,每次 Sleep(5000) 让出 5 秒再试。若 10 次内成功,k 自增进入下一个特征;若仍失败,Print 出错码并直接 return,避免脏矩阵进模型。 拷贝完务必调 IndicatorRelease 释放句柄,否则几十个指标轮询下来句柄泄漏会让 EA 越跑越卡。最后 feature_matrix.Resize(size_observations, indicator.Size()-1) 把观测行数与特征列数对齐——少一列通常是把时间索引剔掉。外汇与贵金属波动受消息面突袭,这类数据准备代码只解决工程可靠性,不预示任何方向。

MQL5 / C++
case MA:
					indicator_handle=iMA(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len,class="num">0,AppliedMA,AppliedPrice);
					break;
				case BEARS:
					indicator_handle=iBearsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len);
					break;
				case BULLS:
					indicator_handle=iBullsPower(SetSymbol!=""?SetSymbol:NULL,SetTF,period_len);
					break;
				}
			if(indicator_handle==INVALID_HANDLE)
				try--;
			else
				break;
			}
		if(indicator_handle==INVALID_HANDLE)
			{
			Print("Invalid indicator handle ",EnumToString(select_indicator)," ", GetLastError());
			class="kw">return;
			}
		Comment("copying data to buffer for indicator ",period_len);
		try = class="num">0;
		class="kw">while(!indicator[k].CopyIndicatorBuffer(indicator_handle,class="num">0,samplestop,size_observations) && try <class="num">10)
			{
			try++;
			Sleep(class="num">5000);
			}
		if(try <class="num">10)
			++k;
		else
			{
			Print("error copying to indicator buffers ",GetLastError());
			Comment("");
			class="kw">return;
			}
		if(indicator_handle!=INVALID_HANDLE && IndicatorRelease(indicator_handle))
			indicator_handle=INVALID_HANDLE;
		}
	 }
class=class="str">"cmt">//---resize matrix
	if(!feature_matrix.Resize(size_observations,indicator.Size()-class="num">1))
		{

「指标句柄拉不到就重试十次」

把收集好的特征塞进矩阵后,下一步是挂自定义指标 LogReturns 取收益率序列。这里用 indicator_handle 接收 iCustom 返回值,若拿到 INVALID_HANDLE 就进 while 循环重试,try 初始设为 10,每失败一次减 1,最多尝试 10 次才放弃。 若 10 次都没拿到句柄,Print 报「Could not initialize returns indicator」并带 GetLastError 描述,随后 Comment("") 清掉图表文字直接 return,避免后续用空句柄崩 EA。外汇与贵金属行情中断或指标路径异常时这种失败概率不低,属正常防御。 拿到句柄后并不立刻释放,而是再开一个 try=10 的循环,用 CopyIndicatorBuffer 从 samplestop-1 往前拷 size_observations 根 K 线的缓冲区,每次失败 Sleep(2000) 等 2 秒再试。若 try 耗尽仍拷不上,报「Could not collect returns indicator info」并退出。 成功则调用 IndicatorRelease 释放句柄、清 Comment。最后用 StringFormat("%12s") 把 predictor_names 拼成表头,补一行 NextBar Returns (Target),逐行 Print 出矩阵,方便在 MT5 专家日志里直接核对数据集维度。

MQL5 / C++
Print(__LINE__);
Print(ErrorDescription(GetLastError()));
Comment("");
class="kw">return;
 }
class=class="str">"cmt">//---copy collected data to matrix
  for(class="type">ulong i = class="num">0;i<feature_matrix.Cols(); i++)
   if(!feature_matrix.Col(indicator[i],i))
     {
      Print(__LINE__);
      Print(ErrorDescription(GetLastError()));
      Comment("");
      class="kw">return;
     }
class=class="str">"cmt">//---
  class="type">int try
        = class="num">10;
  class="kw">while(try
            >-class="num">1 && indicator_handle == INVALID_HANDLE)
    {
     indicator_handle=iCustom(SetSymbol!=""?SetSymbol:NULL,SetTF,"\\Indicators\\LogReturns",class="num">0,class="num">1,class="num">1);
     try
        --;
    }
class=class="str">"cmt">//---
  if(try
     <class="num">0)
    {
     Print("Could not initialize returns indicator ");
     Print(ErrorDescription(GetLastError()));
     Comment("");
     class="kw">return;
    }
  else
   {
    try
        = class="num">10;
   }
class=class="str">"cmt">//---
  class="kw">while(try
            >-class="num">1 && !indicator[indicator.Size()-class="num">1].CopyIndicatorBuffer(indicator_handle,class="num">0,samplestop-class="num">1,size_observations))
    {
     Sleep(class="num">2000);
     try
        --;
    }
class=class="str">"cmt">//---
  if(try
     <class="num">0)
    {
     Print("Could not collect returns indicator info ");
     Print(ErrorDescription(GetLastError()));
     Comment("");
     class="kw">return;
    }
  else
   {
    IndicatorRelease(indicator_handle);
    Comment("");
   }
class=class="str">"cmt">//--- display the dataset
  class="type">class="kw">string console;
  for(class="type">uint i = class="num">0;i<predictor_names.Size(); i++)
   console+=StringFormat(" %12s ",predictor_names[i]);
  console+=" NextBar Returns(Target) ";
  Print(console);
  for(class="type">ulong i = class="num">0;i<feature_matrix.Rows(); i++)
   {
    console = "";

mRMR 跑完之后怎么读特征表

上面的循环把特征矩阵逐行打印到 MT5 终端,每行末尾追加了下一根 K 线的收益率(NextBar Returns),用来给后续 mRMR 筛选当目标列。注意 feature_matrix[i][j]%12.6lf 定宽输出,13 个指标列对齐后肉眼就能扫出异常值,比如第三行 MFI_2 全是 0.000000,说明该周期样本里资金流指标根本没更新。 筛选本身由 Cmrmr mstep(NumReplications,MaxPredictors,ChiSquareThreshold,VerboseOutPut) 接管,StepWise 吃进特征矩阵和最后一列目标向量,失败直接 return,不往下吐结果。跑通后 GetSelectedVars 把入选下标塞进 variables[],再拿 predictor_names 反查原名打印——这就是你脚本里实际落地的「特征重要性榜单」。 贴一段终端实跑的快照:PS/ND 变量序号 5、互信息 0.0308 排第一,LG(4) 0.0293 次之,MJ(3) 0.0279 第三;前六名 MI 全部落在 0.0165~0.0308 区间,差距不大,说明单变量对下一根收益的解释力都偏弱,外汇和贵金属这种高噪声品种本就如此,别拿它当入场信号。 真要验证,把 MaxPredictors 调成 6 重跑一次,看 JP(9) 和 IS(1) 是否稳定留在榜上;若复现 5 次以上都在,说明这两列在 mRMR 意义下冗余度低,可以进你的小布特征池。

MQL5 / C++
for(class="type">ulong j = class="num">0; j<feature_matrix.Cols(); j++)
    console += StringFormat(" %class="num">12.6lf ",feature_matrix[i][j]);
  console+=StringFormat(" %class="num">12.6lf ",indicator[indicator.Size()-class="num">1][i]);
  Print(console);
 }
class=class="str">"cmt">//---
  Cmrmr mstep(NumReplications,MaxPredictors,ChiSquareThreshold,VerboseOutPut);
class=class="str">"cmt">//---
  if(!mstep.StepWise(feature_matrix,indicator[indicator.Size()-class="num">1]))
    class="kw">return;
class=class="str">"cmt">//---
  Print(" Final predictor labels ");
  class="type">ulong variables[];
  if(mstep.GetSelectedVars(variables))
   {
     for(class="type">uint i = class="num">0; i<variables.Size(); i++)
       Print(predictor_names[variables[i]]);
   }
  class="kw">return;
 }

◍ 变量筛选里的相关性与冗余性账本

做特征筛选时,单看相关性会骗人。上面这组测算里,当前保留变量序号5的相关性为0.0308、冗余性0.0000,准则值即0.0308,说明它和已选集合还没打架。 候选变量按「相关性减冗余性」排下来,序号3相关性0.0279但冗余性飙到2.5363,准则值-2.5084;序号4相关性0.0293、冗余性3.0096,准则值-2.9803。这两个数冗余性都过2.5,意味着跟现有预测变量高度重叠,加进来只会稀释信号。 外汇与贵金属行情受多重共线干扰明显,高冗余变量进模型后可能让回归系数失真。开MT5接自己的品种跑一遍这套准则排序,把准则值为负的候选直接踢掉,比盲目堆指标更省算力。

「变量筛选里的相关性减去冗余性实战表」

做特征选择时,单纯看相关性会误导,必须把冗余性扣掉。准则值 = 相关性 − 冗余性,负数代表该变量带来的增量信息为负,直接进候选池也是噪音。 上面这组欧元兑美元类因子的计算结果很说明问题:变量 4 的相关性有 0.0293,但冗余性高达 1.0372,准则值 −1.0079,是典型的被已有变量覆盖的重复信号;变量 5 相关性 0.0308、冗余性 0,准则值转正 0.0308,且单变量与组变量 p 值都压到 0.010,才是值得留的。 最终入选的是变量 5、0、2、6:其中 0 和 2 的准则值仍为负(−0.0095、−0.1796),但组 p 值被强制约束在 0.010 进入模型,说明筛选里除了准则值还有显著性门槛在兜底。 开 MT5 把这几列数字贴进 EA 的调试输出,对照自己的样本重跑一遍 mRMR 类的减法排序,看变量 4 在你的品种上是不是也同样冗余爆表。外汇与贵金属杠杆高,因子失效时回撤可能超出预期,验证结论只作概率参考。

特征贡献排序与最终入选变量

上面这张表是某次特征筛选后按偏离度排出来的明细。左侧两字符代号是内部特征编号,紧跟的样本数一列里,DL 与 OE 各取了 11 和 10 个样本却给出 0.0000 的均值偏移,右端 p 值直接标到 1.000,说明这两组在统计上和基准分布没有可分性,建模时大概率要砍掉。 其余如 OG、JP、KJ 的均值偏移在 0.0126~0.0165 之间,但负向极值拉到 -0.3172 甚至 -0.4450,p 值全是 0.010 的截断值,意味着它们对目标变量的非线性扰动更值得保留。外汇与贵金属行情受流动性断层影响大,这类极端偏移样本往往对应跳空时段,实盘用这些特征须先过滤异常 tick。 表尾 RN 到 OF 列出了最终进模型的 11 个标签:NO 是 MA_6,DE/NN/PQ 是不同周期的 MFI,KP/DJ/FL/FM 及 MK/OF 是 BEARS 与 BULLS 的 2/4/6 周期组合。打开 MT5 的数据窗口,照这套周期把指标缓冲拉出来,就能复现当时的特征矩阵。

MQL5 / C++
FQ                   class="num">9    class="num">0.0182    class="num">0.2023   -class="num">0.1842    class="num">0.010    class="num">0.010
DL                  class="num">11    class="num">0.0000    class="num">0.2798   -class="num">0.2798    class="num">1.000    class="num">0.010
KJ                   class="num">1    class="num">0.0165    class="num">0.2932   -class="num">0.2767    class="num">0.010    class="num">0.010
OG                   class="num">7    class="num">0.0126    class="num">0.3298   -class="num">0.3172    class="num">0.010    class="num">0.010
OE                  class="num">10    class="num">0.0000    class="num">0.4151   -class="num">0.4151    class="num">1.000    class="num">0.010
JP                   class="num">8    class="num">0.0135    class="num">0.4585   -class="num">0.4450    class="num">0.010    class="num">0.010
RN   最终预测变量标签
NO   MA_6
DE   MFI_2
NN   MFI_6
KP   BEARS_2
DJ   BULLS_2
FL   BULLS_6
PQ   MFI_4
MK   BEARS_4
FM   BULLS_4
OF   BEARS_6

◍ 把这条线请下神坛

MRMR 这套互信息特征选择,说到底只是把「最大依赖、最大相关、最小冗余」三个目标塞进一个迭代框架里,靠蒙特卡洛置换检验筛掉不显著的特征。它在合成数据和真实数据上都跑通了,尤其能压住无关特征和多重共线性带来的干扰,但别把它当成圣杯——外汇与贵金属市场的高风险本质,意味着任何特征集都只是概率层面的辅助,不是方向保证。 代码层面,np.mqh 提供矩阵向量工具,mutualinfo.mqh 里的 Capm 类做自适应分区互信息估计、Cmrmr 类跑渐进选择;两个脚本(RelevanceMinusRedundancy.mq5 1.69 KB、StepWiseFeatureSelectionByMutualInformation.mq5 7.39 KB)分别用合成和近实盘数据演示调用。开 MT5 把 ZIP 里的 19.35 KB 工程拖进 include 与 scripts 目录,直接编译后看特征排序输出,比读结论更实在。 它值得进你的工具箱,但仅此而已:变量关系越非线性、越缠杂,MRMR 越能显出价值;可一旦市场结构切换,昨天的低冗余特征集可能今天就变成噪声源。

把互信息扫描交给小布
这些候选变量的互信息排序与冗余剔除,小布盯盘的AIGC模块已内置批处理脚本,打开对应品种页即可一键跑通渐进选择,你只管看哪些特征进了最终集合。

常见问题

自适应分区会迭代细分信息含量高的区域,把计算资源压到数据空间最相关的部分,对复杂非均匀分布估计更准,而固定分箱在密度突变处误差会被放大。
该准则分别对候选特征与目标算互信息求最大相关,再对特征间互信息求和作冗余惩罚,用差值排序逐步纳入,使集合内特征互补而非重复。
可以,小布内置的AIGC脚本覆盖了从连续密度估计到mRMR式筛选的全流程,品种页选择对应工具就能生成特征入选序列,省去手写MQL5循环。
合成数据若用高斯混合生成,需确认冗余变量与目标的设计依赖是非线性的,否则普通相关系数也能选出,体现不出互信息的增量价值。
不同品种报价点位与波动率量纲悬殊,直接进Parzen窗会让尺度参数失效,先做滚动z-score或排名变换能让密度估计稳定,外汇贵金属本身高杠杆高风险,样本失配会更致命。