MQL5 中的组合对称交叉验证·综合运用
(3/3)· 从矩阵切分到概率判读,用 CSCV 给策略优化填上最后一块防过拟合拼图
给均线EA挂上CSCV过拟合探针
把 MT5 自带的 Moving Average EA 改造成能跑 CSCV(组合对称交叉验证)过拟合概率测试的版本,核心思路是用「优化结果帧」在每根柱上采集收益,优化结束后拼成矩阵送进 Ccscv 类算 PBO。除了必须补上 OnTester 与 OnTesterDeinit 两个事件处理函数,还要在策略测试器里选「慢速完整算法」做全量优化,否则帧数据会漏。 头文件先 include Returns.mqh 和 CSCV.mqh:CReturns 负责逐柱收集收益,可算夏普、均值或总和,任一指标都能当性能判据;CSCV.mqh 里的 Ccscv 类才真正算概率。新增不可优化参数 NumBlocks 指定分区数,后面会看到它从 4 调到 16 时 PBO 的波动。 全局声明 CReturns 实例 colrets 以及 numrows / numcolumns 用于矩阵初始化。OnTesterInit 里用 ParameterGetRange 把四个可调参数的步进区间乘起来,得到待测参数集总数填给 numrows;OnTick 中调 colrets.OnNewtick() 抓每跳收益;OnTester 里用 FrameAdd 把收益数组塞进数据帧。 重头戏在 OnTesterDeinit:建 Ccscv 实例,循环读所有帧写进矩阵(向量做中转行),最后调 CalculateProbability(MathSum) 以总收益判最优集,并把 PBO 与已处理帧数打印到「专家」标签页。下面这组跨周期实测值得自己开 MT5 复现——分区数 时间框架 回溯测试过度拟合概率:4 每周 0.3333、4 每日 0.6666、4 每12小时 0.6666、8 每周 0.2、8 每日 0.8、8 每12小时 0.6、16 每周 0.4444、16 每日 0.8888、16 每12小时 0.6666。 最优也只是 PBO=0.2,其余普遍偏高,说明该 EA 丢到样本外数据集性能大概率衰减;调大分区并没有把原本差的分数救回来。外汇与贵金属市场高杠杆、跳空频繁,这类 PBO 高于 0.5 的 EA 实盘前务必用样本外数据再验一遍。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| MovingAverage_CSCV_DemoEA.mq5 | class=class="str">"cmt">//| Copyright class="num">2023, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2023, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <Returns.mqh> class="macro">#include <CSCV.mqh> class="macro">#include <Trade\Trade.mqh> sinput class="type">uint NumBlocks = class="num">4; CReturns colrets; class="type">ulong numrows,numcolumns; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterInit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterInit() { numrows=class="num">1; class=class="str">"cmt">//--- class="type">class="kw">string name="MaximumRisk"; class="type">bool enable; class="type">class="kw">double par1,par1_start,par1_step,par1_stop; ParameterGetRange(name,enable,par1,par1_start,par1_step,par1_stop); if(enable) numrows*=class="type">ulong((par1_stop-par1_start)/par1_step)+class="num">1; class=class="str">"cmt">//--- name="DecreaseFactor"; class="type">class="kw">double par2,par2_start,par2_step,par2_stop; ParameterGetRange(name,enable,par2,par2_start,par2_step,par2_stop); if(enable) numrows*=class="type">ulong((par2_stop-par2_start)/par2_step)+class="num">1; class=class="str">"cmt">//--- name="MovingPeriod"; class="type">long par3,par3_start,par3_step,par3_stop; ParameterGetRange(name,enable,par3,par3_start,par3_step,par3_stop); if(enable) numrows*=class="type">ulong((par3_stop-par3_start)/par3_step)+class="num">1; class=class="str">"cmt">//--- name="MovingShift"; class="type">long par4,par4_start,par4_step,par4_stop; ParameterGetRange(name,enable,par4,par4_start,par4_step,par4_stop); if(enable) numrows*=class="type">ulong((par4_stop-par4_start)/par4_step)+class="num">1; }
「回测里把收益序列喂给 CSCV」
EA 的主循环只在每笔报价触发时跑策略判断:有持仓就查平仓条件,空仓就查开仓条件,真正的统计活儿被挪到了回测收尾阶段。 OnTester 里先通过 colrets.GetReturns 把全部历史 bar 的收益率抽进数组,用 MathSum 求总收益作为回测返回值;同时用 FrameAdd 把这条收益序列按 MA_MAGIC 标识写进帧,供后面的分布检验读取。 OnTesterDeinit 才是重头戏:FrameFilter 筛出对应魔术号的帧后,用 FrameNext 逐帧把收益向量读出来,拼成 numrows × numcolumns 的 matrix。这里 numcolumns 直接取首帧的 passed_value(即数组长度),data_matrix 和 addvector 都按它Resize。 这套写法意味着你改了均线周期或魔术号,回测完就能在帧里拿到完整收益曲线,丢给 Cscv 类做交叉验证,评估策略过拟合概率。外汇与贵金属杠杆高,回测漂亮不代表实盘能复现,拿去跑之前先在小周期样本上验证帧写入是否成功。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- colrets.OnNewTick(); class=class="str">"cmt">//--- if(SelectPosition()) CheckForClose(); else CheckForOpen(); class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Tester function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- class="type">class="kw">double ret=class="num">0.0; class="type">class="kw">double array[]; class=class="str">"cmt">//--- if(colrets.GetReturns(ENUM_RETURNS_ALL_BARS,array)) { class=class="str">"cmt">//--- ret = MathSum(array); if(!FrameAdd(IntegerToString(MA_MAGIC),class="type">long(MA_MAGIC),class="type">class="kw">double(array.Size()),array)) { Print("Could not add frame ", GetLastError()); class="kw">return class="num">0; } class=class="str">"cmt">//--- } class=class="str">"cmt">//---class="kw">return class="kw">return(ret); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| TesterDeinit function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTesterDeinit() { class=class="str">"cmt">//---prob value numcolumns = class="num">0; class="type">class="kw">double probability=-class="num">1; class="type">int count_frames=class="num">0; matrix data_matrix=matrix::Zeros(numrows,class="num">1); vector addvector=vector::Zeros(class="num">1); Cscv cscv; class=class="str">"cmt">//---calculate if(FrameFilter(IntegerToString(MA_MAGIC),class="type">long(MA_MAGIC))) { class=class="str">"cmt">//--- class="type">ulong pass; class="type">class="kw">string frame_name; class="type">long frame_id; class="type">class="kw">double passed_value; class="type">class="kw">double passed_data[]; class=class="str">"cmt">//--- while(FrameNext(pass,frame_name,frame_id,passed_value,passed_data)) { class=class="str">"cmt">//--- if(!numcolumns) { numcolumns=class="type">ulong(passed_value); addvector.Resize(numcolumns); data_matrix.Resize(numrows,numcolumns); } class=class="str">"cmt">//--- if(addvector.Assign(passed_data))
◍ CSCV 概率跑出来的真实读数
上面这段收口代码把遍历完的向量塞进 data_matrix,随后调用 CSCV 的 CalculateProbability 做块间概率计算,最后把列数、行数和概率值一并打印出来。 从 EA 在 EURUSD H1 上的日志看,pass 数累计到 23520,最终概率落在 0.3333333333333333。这个数值接近 1/3,说明在随机切块的对照下,该均线策略的区分度并未显著优于随机——外汇与贵金属这类高波动品种,单凭这种回测概率很容易误判,务必上 MT5 自行复跑验证。 别把 0.33 直接读成「无效」 CSCV 给的是块间差异的概率度量,不是胜率。想确认策略有没有戏,得把 NumBlocks 调小或调大,看概率是否稳定偏离 0.5 再下结论。
{
data_matrix.Row(addvector,pass);
count_frames++;
}
class=class="str">"cmt">//---
}
}
else
Print("Error retrieving frames ", GetLastError());
class=class="str">"cmt">//---results
probability = cscv.CalculateProbability(NumBlocks,data_matrix,MathSum,true);
class=class="str">"cmt">//---output results
Print("cols ",data_matrix.Cols()," rows ",data_matrix.Rows());
Print("Number of passes processed: ", count_frames, " Probability: ",probability);
class=class="str">"cmt">//---
}
MovingAverage_CSCV_DemoEA(EURUSD,H1) Number of passes processed: class="num">23520 Probability: class="num">0.3333333333333333一点提醒
CSCV 评估的是优化后策略的过拟合概率(PBO),速度比蒙特卡罗替换快,也能榨干已有历史数据,但可靠性完全绑死在输入数据上。试验参数变动区间太窄,PBO 会偏低骗你;塞进大量不现实的参数组合,PBO 又会被抬到虚高。 时间框架也是暗坑:同一套 EA 在 H1 与 M5 优化出的 PBO 可能完全不同,因为参数选择被周期带着走。能跑的参数组合尽量铺满,才不容易漏判。 这套方法有个硬伤——EA 源码不可见就基本用不了。理论上可以给每种参数单独回测,但那跟蒙特卡罗一样磨人。附带三个文件可直接用:CSCV.mqh(6.9 KB)管核心验证、Returns.mqh(9.58 KB)收收益序列、MovingAverage_CSCV_DemoEA.mq5(11.24 KB)是改过的均线 EA 演示。外汇与贵金属波动剧烈、杠杆高风险大,任何 PBO 数字都只是概率倾向,别当免死金牌。