MQL5 中的组合对称交叉验证·进阶篇
(2/3)· 优化完参数就直接上实盘?CSCV 能算出你那套 EA 被噪声骗进去的概率有多高
◍ 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 表现低于中位数的频次,最终换算成策略非过拟合的概率。外汇与贵金属波动跳跃大,这种块切分对参数稳定性只是「可能」更可靠,不代表免亏。
~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 的分布,能直观判断你的组合筛选有没有偷看未来。
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 线。
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 只会被污染,看着低也不代表安全。做交叉验证前先确认参数空间跟真做单时的选法一致。