开发多币种 EA 交易(第 7 部分):根据前向时间段选择组·进阶篇
「前向筛选到底算不算作弊」
做品种和参数分组时,一个绕不开的纠结是:我们能不能拿“未来一段已知走势”去挑组?如果挑组时已经瞄着那段前向行情,还指望换到新周期也能复现类似表现,这算不算自我欺骗?更实际的问题是,这种选法在下一阶段真能明显跑赢“完全不看前向”的盲选吗? 用贝叶斯思路能把问题拆清楚:先假定“考虑前向挑选能改善后续结果”为主假设,反之为备选;跑完实验再用新数据修正信心。具体可设计对照——一组在 2018–2023 全段(IS)优化后直接选优,另一组用 2018–2022 做 IS、拿 2023 做 OOS 验证,只留两期都稳的组。后者大概率不会和前者选同一批,绝对收益可能稍差,但多过了一年样本外考验。 这里有个语义坑:当你用历史前向段参与挑选,那段就不再是 OOS 而是实质 IS,只是名字还叫前向;真要评估实盘,得再挪出一段“前向之后”的新窗口。外汇与贵金属杠杆高、样本外失效快,这类分组实验结论只代表概率倾向,不构成稳定盈利保证。 我们手头有 2018–2023 数据想押 2024,两种路径结果谁更好,只能让 MT5 把两组都跑一遍 2024 真实 tick 才知道。任何一组后续好坏,还夹着行情大环境等无关变量,某一季策略整体背时,选组方法差异可能根本显不出来。
◍ 第一种选法下的参数分组与跨年验证
第一种方法的核心是先对 2018–2023 整段做单副本策略优化,再按前文规则把参数拆成八组,从中筛出表现占优的组。原文在 2018–2023 主段与 2024 年三个月前向段分别跑了 OptGroupClusterExpert.mq5 的组选择,图 8、图 9 显示:主段挑出的优组在 2024 前向总体为正,但组间差异很大。 为做细查,取排名最前的组,令缩放因子 scale_ = 10 / 2.04 ≈ 5,丢进测试器跑 2023 主段与 2024 三个月前向。图 10 结果看,这组参数在 2024 并未出现明显的余额曲线下行倾向,但样本仅三个月,不足以对 EA 后续行为下结论。外汇与贵金属品种波动剧烈,这种短前向即便不亏也仅是概率上的温和信号。 先把这组基线记牢,后面直接和第二种选法跑出的数值对照,才能判断哪种分组逻辑更抗过拟合。
用前向验证挑出稳健参数组
把 2018–2022 年优化结果里标准化利润最好的一组拿出来,不回头看 2018 年图,直接以 2023 年做测试主区间、2024 年头三个月做前向区间。实测里 2023 年回撤约 1820 美元,比主测试期算出的 1000 美元高出近两倍,说明样本外表现会明显偏离。 筛选思路是算主期与前向期的参数比值:交易次数比 TradesRatio = OOS_Trades / IS_Trades,越接近 1 越稳;利润因周期长度不同,用 ResultRatio = OOS_ForwardResultCorrected * 5 / IS_BackResult 修正五年与一年的差距。把所有比值对 1 的偏离求和得 SumDiff,同时取两期最大回撤率算仓位缩放 Scale = 10 / MAX(OOS_EquityDD, IS_EquityDD)。 只保留 Scale 大于 SumDiff 的组,用 Res = Scale / SumDiff 降序排,靠前的组两期表现更相似、回撤也更小。挑出榜首组后,把同簇索引的组再排一遍,迭代选出四组硬编码进新 EA。 新建 OptGroupForwardClusterExpert.mq5,删掉 OnTesterInit()、OnTesterDeinit() 和优化用输入,在 OnInit() 里写死 strGroups 与 scales。2023–2024 实测余额曲线整体向上,3–7 月横盘但回撤没破预期上限;2024 仅三个月样本偏短。和第二种以外的旧选法比,结果相近,但本算法可日后自动化。外汇与贵金属市场高风险,样本外结果仅代表历史概率。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">// Load strategy parameter sets class="type">int totalParams = LoadParams(fileName_, params); class=class="str">"cmt">// If nothing is loaded, report an error if(totalParams == class="num">0) { PrintFormat(__FUNCTION__" | ERROR: Can&class="macro">#x27;t load data from file %s.\n" "Check that it exists in data folder or in common data folder.", fileName_); class="kw">return(INIT_PARAMETERS_INCORRECT); } class=class="str">"cmt">// Selected set groups class="type">class="kw">string strGroups[] = {"class="num">55,class="num">12,class="num">3,class="num">35,class="num">48,class="num">54,class="num">16,class="num">40", "class="num">11,class="num">54,class="num">33,class="num">30,class="num">62,class="num">6,class="num">10,class="num">23", "class="num">50,class="num">15,class="num">8,class="num">34,class="num">2,class="num">36,class="num">4,class="num">9", "class="num">26,class="num">42,class="num">25,class="num">22,class="num">36,class="num">51,class="num">53,class="num">0" }; class=class="str">"cmt">// Scaling factors for selected set groups class="type">class="kw">double scales[] = {class="num">4.16, class="num">3.40, class="num">3.33, class="num">2.76 }; class=class="str">"cmt">// Set parameters in the money management class CMoney::DepoPart(expectedDrawdown_ / class="num">10.0); CMoney::FixedBalance(fixedBalance_); class=class="str">"cmt">// Create an EA handling class="kw">virtual positions expert = new CVirtualAdvisor(magic_, "SimpleVolumes_OptGroupForwardCluster"); CVirtualStrategyGroup *groups[ArraySize(strGroups)]; FOREACH(strGroups, { class=class="str">"cmt">// Form the class="type">class="kw">string from the parameter set indices separated by commas class="type">class="kw">string strIndexes = strGroups[i];
「把参数串拆成多策略实例」
这段逻辑干的事很直接:先按逗号把索引字符串切成数组,再逐个取出对应参数集、生成策略对象塞进容器。StringSplit 的分隔符是英文逗号,索引数组 indexes[] 每一项都是字符串数字,需用 StringToInteger 转回整型才能定位 params 行。 CSVStringGet 取的是 params 里第 0 到第 11 列的子串,相当于剥离了簇编号只留纯参数。CSimpleVolumesStrategy(param) 每次 new 一个实例,APPEND 进 strategies 数组;FOREACH 宏跑完,一个 cluster 内的所有策略就齐了。 groups[i] 用 CVirtualStrategyGroup 把这批策略加一个 scales[i] 权重打包,最后 expert.Add 把总组挂进 EA。整段返回 INIT_SUCCEEDED 表示初始化链路通了。开 MT5 把这段贴进 EA 的 OnInit 尾部,改 params 列数边界(那个 11)就能验证不同参数表宽度下的加载行为,外汇与贵金属品种下务必先开模拟盘跑,实盘波动可能让策略组权重失真。
class=class="str">"cmt">// Turn the class="type">class="kw">string into the array class="type">class="kw">string indexes[]; StringSplit(strIndexes, &class="macro">#x27;,&class="macro">#x27;, indexes); class=class="str">"cmt">// Create and fill the array of all strategy instances CVirtualStrategy *strategies[]; FOREACH(indexes, { class=class="str">"cmt">// Remove the cluster number from the parameter set class="type">class="kw">string class="type">class="kw">string param = CSVStringGet(params[StringToInteger(indexes[i])], class="num">0, class="num">11); class=class="str">"cmt">// Add a strategy with a set of parameters with a given index APPEND(strategies, new CSimpleVolumesStrategy(param)) }); class=class="str">"cmt">// Add the strategy to the next group of strategies groups[i] = new CVirtualStrategyGroup(strategies, scales[i]); }); class=class="str">"cmt">// Form and add the group of strategy groups to the EA expert.Add(CVirtualStrategyGroup(groups, scale_)); class="kw">return(INIT_SUCCEEDED); }
◍ 别急着下结论
这次只拿了 2024 年前三个月做前向对比,样本太短。前面已经观察到,该策略的余额曲线可能在零轴附近横摆好几个月,所以单看这季度根本分不清是增长前夜还是长期 flat。 把主段往后再推一年也行:主段从 2017 起、二法选组放在 2022,对比窗就变成 2023 全年加 2024 初。但更直接的做法是混搭——一部分组用方法一挑,另一部分用方法二挑,塞进同一个 EA。 真正没搞清楚的是单机容量上限:一个 EA 里堆多少个策略实例,才不会把服务器 CPU 吃满、不让内存爆掉。这点留到下一篇拆。 附件里只多了一个 OptGroupForwardClusterExpert.mq5,其余文件沿用上一篇,没动过。外汇与贵金属本身高波动高杠杆,这类多组合并实盘前务必在 MT5 策略测试器跑资源占用。