MQL5 中的组合对称交叉验证·进阶篇
📐

MQL5 中的组合对称交叉验证·进阶篇

(2/3)· 优化完参数就直接上实盘?CSCV 能算出你那套 EA 被噪声骗进去的概率有多高

含代码示例偏理论 第 2/3 篇
把优化跑分最高的参数直接塞进实盘,是多数 MT5 使用者的默认动作。金融序列里的噪声会让这次「最优」大概率只是样本内幻觉,CSCV 正是把这种幻觉程度量出来的工具。

◍ CSCV概率函数的内存与分块初始化

CSCV 核心在于把历史样本拆成若干块做交叉验证,CalculateProbability 一上来先读矩阵维度:列数即绩效指标数(m_perfmeasures),行数即试验次数(m_trials),组合块数默认按 blocks/2*2 算,但若不足 4 就强制拉到 4,避免小样本下折分失效。 内存分配是硬门槛。代码对 m_indices、m_lengths、m_flags、m_data、is_perf、oos_perf 六个数组依次 ArrayResize,任一失败就 Print 错误并返回 -1.0,实盘跑前务必确认终端日志没这条报错,否则后面概率全是空值。 分块用累加游标实现:从 istart=0 起,每块长度 = 剩余列数 / 剩余块数,写完一块游标前移。前半段块 m_flags 标 1 作样本内(IS),后半段标 0 作样本外(OOS),num_less 计数器留着统计 OOS 表现低于中位数的频次,最终换算成策略非过拟合的概率。外汇与贵金属波动跳跃大,这种块切分对参数稳定性只是「可能」更可靠,不代表免亏。

MQL5 / C++
  ~Cscv(class="type">void);                                                                     class=class="str">"cmt">//destructor
   class="type">class="kw">double         CalculateProbability(class="kw">const class="type">ulong blocks, class="kw">const matrix &in_data,class="kw">const Criterion criterion, class="kw">const class="type">bool maximize_criterion);
  };
class="type">class="kw">double Cscv::CalculateProbability(class="kw">const class="type">ulong blocks, class="kw">const matrix &in_data,class="kw">const Criterion criterion, class="kw">const class="type">bool maximize_criterion)
  {
class=class="str">"cmt">//---get characteristics of matrix
   m_perfmeasures = in_data.Cols();
   m_trials = in_data.Rows();
   m_combinations=blocks/class="num">2*class="num">2;
class=class="str">"cmt">//---check inputs
   if(m_combinations<class="num">4)
      m_combinations = class="num">4;
class=class="str">"cmt">//---memory allocation
   if(ArrayResize(m_indices,class="type">int(m_combinations))< class="type">int(m_combinations)||
      ArrayResize(m_lengths,class="type">int(m_combinations))< class="type">int(m_combinations)||
      ArrayResize(m_flags,class="type">int(m_combinations))<class="type">int(m_combinations)    ||
      ArrayResize(m_data,class="type">int(m_perfmeasures))<class="type">int(m_perfmeasures)      ||
      ArrayResize(is_perf,class="type">int(m_trials))<class="type">int(m_trials)                  ||
      ArrayResize(oos_perf,class="type">int(m_trials))<class="type">int(m_trials))
     {
       Print("Memory allocation error ", GetLastError());
       class="kw">return -class="num">1.0;
     }
class=class="str">"cmt">//---
   class="type">int is_best_index ;                    class=class="str">"cmt">//row index of oos_best parameter combination
   class="type">class="kw">double oos_best, rel_rank ;   class=class="str">"cmt">//oos_best performance and relative rank values
class=class="str">"cmt">//---
   class="type">ulong istart = class="num">0 ;
   for(class="type">ulong i=class="num">0 ; i<m_combinations ; i++)
     {
      m_indices[i] = istart ;           class=class="str">"cmt">// Block starts here
      m_lengths[i] = (m_perfmeasures - istart) / (m_combinations-i) ; class=class="str">"cmt">// It contains this many cases
      istart += m_lengths[i] ;          class=class="str">"cmt">// Next block
     }
class=class="str">"cmt">//---
   class="type">ulong num_less =class="num">0;                                class=class="str">"cmt">// Will count the number of time OOS of oos_best <= median OOS, for prob
   for(class="type">ulong i=class="num">0; i<m_combinations; i++)
     {
      if(i<m_combinations/class="num">2)              class=class="str">"cmt">// Identify the IS set
         m_flags[i]=class="num">1;
      else
         m_flags[i]=class="num">0;                    class=class="str">"cmt">// corresponding OOS set
     }
class=class="str">"cmt">//---
class="type">ulong ncombo;
   for(ncombo=class="num">0; ; ncombo++)
     {
       class=class="str">"cmt">//--- in sample performance calculated in this loop

「样本内择优与样本外排名的计算循环」

这段逻辑在做一件事:把多组绩效指标拆成样本内(in-sample)和样本外(out-of-sample)两部分,分别求准则值,再定位样本内最优系统在样本外的相对排名。外汇与贵金属品种上做这类交叉验证,过拟合风险极高,排名靠前不代表实盘能复现。 先看样本内部分:外层循环跑 m_trials 次系统,内层只挑 m_flags 为真的组合,把对应区间的 Flat 数据塞进 m_data,最后用 criterion 算出 is_perf。紧跟着的样本外块几乎对称,只是判断条件换成 !m_flags[ic],结果写进 oos_perf。 定位最优用的是 is_best_index = maximize_criterion?ArrayMaximum(is_perf):ArrayMinimum(is_perf);,即按是否最大化准则来取最大或最小下标,并取出对应的 oos_best。随后统计有多少套系统的样本外表现不差于 oos_best,得到 count。 相对排名 rel_rank = double(count)/double(m_trials+1),若 rel_rank<=0.5 则 num_less 自增——这步就是在累计“样本外未翻车”的次数。最后一段用 radix 进位思路翻转 m_flags,为下一轮组合重算腾位置。 把下面代码直接贴进 MT5 的 EA 或脚本里断点跟一遍,重点看 m_trials 设成 200 时 rel_rank 的分布,能直观判断你的组合筛选有没有偷看未来。

MQL5 / C++
for(class="type">ulong isys=class="num">0; isys<m_trials; isys++)
  {
   class="type">int n=class="num">0;
   for(class="type">ulong ic=class="num">0; ic<m_combinations; ic++)
     {
      if(m_flags[ic])
        {
         for(class="type">ulong i=m_indices[ic]; i<m_indices[ic]+m_lengths[ic]; i++)
         m_data[n++] = in_data.Flat(isys*m_perfmeasures+i);
        }
     }
   is_perf[isys]=criterion(m_data);
   }
class=class="str">"cmt">//--- out of sample performance calculated here
for(class="type">ulong isys=class="num">0; isys<m_trials; isys++)
  {
   class="type">int n=class="num">0;
   for(class="type">ulong ic=class="num">0; ic<m_combinations; ic++)
     {
      if(!m_flags[ic])
        {
         for(class="type">ulong i=m_indices[ic]; i<m_indices[ic]+m_lengths[ic]; i++)
         m_data[n++] = in_data.Flat(isys*m_perfmeasures+i);
        }
     }
   oos_perf[isys]=criterion(m_data);
   }
class=class="str">"cmt">//--- get the oos_best performing in sample index
is_best_index = maximize_criterion?ArrayMaximum(is_perf):ArrayMinimum(is_perf);
class=class="str">"cmt">//--- corresponding oos performance
oos_best = oos_perf[is_best_index];
class=class="str">"cmt">//--- count oos results less than oos_best
class="type">int count=class="num">0;
for(class="type">ulong isys=class="num">0; isys<m_trials; isys++)
  {
   if(isys == class="type">ulong(is_best_index) || (maximize_criterion && oos_best>=oos_perf[isys]) || (!maximize_criterion && oos_best<=oos_perf[isys]))
      ++count;
  }
class=class="str">"cmt">//--- calculate the relative rank
rel_rank = class="type">class="kw">double (count)/class="type">class="kw">double (m_trials+class="num">1);
class=class="str">"cmt">//--- cumulate num_less
if(rel_rank<=class="num">0.5)
   ++num_less;
class=class="str">"cmt">//---move calculation on to new combination updating flags array along the way
class="type">int n=class="num">0;
class="type">ulong iradix;
for(iradix=class="num">0; iradix<m_combinations-class="num">1; iradix++)
  {
   if(m_flags[iradix]==class="num">1)
     {
      ++n;
      if(m_flags[iradix+class="num">1]==class="num">0)
        {
         m_flags[iradix]=class="num">0;
         m_flags[iradix+class="num">1]=class="num">0;

用循环给基数位打旗标再算胜率比

上面这段是组合枚举里的收口逻辑:先按 iradix 长度跑一轮 for,给前 n 个基数位挂上 m_flags=1,其余补 0,用来标记当前这一组取舍。 当 iradix 恰好等于 m_combinations-1 时,说明已扫到倒数第二组组合,ncombo 自增后直接 break 跳出,避免无谓遍历。 最后一行把 num_less 转成 double 除以 ncombo 返回——这就是该轮样本里“小于”事件的经验概率,开 MT5 把 num_less 和 ncombo 打印出来,能直接看到在某外汇品种 M15 上回测时组合数与命中数的真实比值,外汇贵金属杠杆高,这个比例只反映历史样本、不预示下一根 K 线。

MQL5 / C++
for(class="type">ulong i=class="num">0; i<iradix; i++)
  {
    if(--n>class="num">0)
      m_flags[i]=class="num">1;
    else
      m_flags[i]=class="num">0;
  }
  class="kw">break;
   }
  }
 }
if(iradix == m_combinations-class="num">1)
  {
   ++ncombo;
   class="kw">break;
  }
 }
class=class="str">"cmt">//--- final result
  class="kw">return class="type">class="kw">double(num_less)/class="type">class="kw">double(ncombo);
 }

◍ PBO 数值怎么读才不踩坑

CSCV 这套流程只吐一个核心指标:PBO。按 Bailey 等人的定义,它量的是这样一种概率——在样本内调出来的最优参数集,放到样本外跑,表现还不如随机非最优参数集的中位数。数值越高,过拟合越重,策略拿到真实盘口失效的概率越大。 经验上 PBO 压到 0.1 以下才算干净,外汇与贵金属杠杆高、滑点跳变频繁,0.1 以上就得警惕样本内漂亮、样本外崩。 但 PBO 不是天然可信,它高度依赖你喂进去的参数网格。如果网格里塞了一堆实盘根本不会选的离谱组合,或者全挤在最优值旁边、远处空无一物,算出来的 PBO 只会被污染,看着低也不代表安全。做交叉验证前先确认参数空间跟真做单时的选法一致。

把过拟合诊断交给小布盯盘
小布盯盘的 AIGC 已内置 CSCV 类诊断视图,打开对应品种页即可看到策略优化后的过拟合概率带,你只管读结果做取舍。

常见问题

CSCV 本身不挑性能标准,净利润、夏普比率或任意比率都可用;差别只在采集的粒度数据——净利润要每根 K 线的盈亏,夏普要每根 K 线的收益序列,且每次测试采集长度须一致。
小布盯盘对应品种页已内置 AIGC 诊断,可基于优化输出直接给出过拟合概率估计,不必手工拼矩阵;重度自定义研究仍可走 MQL5 脚本。
N 一般取偶数且不宜过小,分块过细会让子样本长度变短、估计方差放大,N 取 10 到 20 在 EA 回测里较常见,具体看总柱数。
从 N 个分块里取 N/2 个作样本内、剩下 N/2 作样本外,所有组合对称枚举,使每对 ISS/OOS 容量相等且互补,从而消除长度偏倚。
0.8 倾向说明回测过拟合概率高,样本外表现可能明显衰减;但不等于绝对失效,应结合样本外表现衰减幅度与品种波动特征综合判断,外汇贵金属属高风险市场。