关于策略优化的一些简单想法(基础篇)
「从一次真实优化说起」
2015年8月3日,一名叫 Jose Miguel Soriano 的用户在 MT5 社区发了一篇关于策略优化的短帖,至今累计 2753 次浏览、13 条讨论。这类朴素开头在量化圈很常见:没有华丽回测曲线,只是把实盘里卡住的点摊开来讲。 外汇与贵金属策略做参数优化时,高风险首先来自过拟合——样本内漂亮、样本外崩盘是常态。MT5 自带的策略测试器能跑遗传优化,但遗传算法不等于免死金牌,种群收敛到的局部最优可能只是那段时间序列的巧合。 想验证一段逻辑是否经得起折腾,最直接的动作是开 MT5,把同一 EA 在 2020—2024 不同年份分段回测,看参数稳定性而不是单看总收益。若三年里最优参数漂移超过 30%,这套过滤条件大概率只是噪声捕捉器。
◍ 别只盯 EURUSD 一个品种跑 EA
多数交易者验证出一套稳定 EA 后,习惯直接挂在 EURUSD 图表上跑,很少去想它在别的货币对上是否产出更优。EURUSD 1H 跑得不顺时,有人会切到 EURJPY H4 试试,但这种手动换品种换周期的做法缺乏系统依据。 即便用上 64 位系统、回测速度不再是瓶颈,优化时的参数组合枚举量依然惊人。所有排列都会被 MT5 优化器跑一遍,最终报告里大量结果其实根本用不上,反而干扰判断。 外汇与贵金属属高风险品种,跨品种迁移策略前应先做小样本验证。本文给出的处理思路,目的就是压缩这些无效枚举、把注意力拉回到真正有区分度的组合上。
把 21 个周期砍到真正要测的那几个
MT5 原生抛出 21 种时间框架,从 M1、M2 到 MN1 全在里面。做 EA 优化时,若直接拿 ENUM_TIMEFRAMES 当输入参数,优化器会一次性铺开 21 组组合——多数组合只是浪费回测算力,尤其外汇和贵金属这种高杠杆品种,无效周期跑多了还会放大过拟合错觉。 实测中若策略在 M5 已显效,下一步只该去 M3 或 M6 附近收口验证,而不是把 H8、M12 这类冷门周期也拉进遗传优化。把枚举自己重写一遍,注释掉用不上的成员,优化参数列表立刻从 21 缩到 11 甚至更少,VPS 时长可能直接砍半。 下面这段代码就是收口做法:先定一个只含目标周期的 enum,再用函数把枚举映射回 ENUM_TIMEFRAMES,最后赋给全局变量 marcoTmp 供 EA 统一调用。注意最末两行都在给 marcoTmp 赋值,前一行走映射函数、后一行直接强转,实际工程里留一种即可,别两块都编译进去。 别把正态当圣经 周期优劣不是均匀分布的,M5 有效不代表 H4 必然有效;用自定义枚举做窄域优化,本质是主动引入先验,回测结论只对你圈定的那几个周期成立,换品种或切到未测周期可能完全失效。外汇与贵金属波动受事件驱动,窄周期优化结果需带「概率倾向」去看。
class="kw">input ENUM_TIMEFRAMES marcoTF= PERIOD_M5; enum mis_MarcosTMP { _M1= PERIOD_M1, _M5= PERIOD_M5, _M15=PERIOD_M15, class=class="str">"cmt">// _M20=PERIOD_M20, _M30=PERIOD_M30, _H1= PERIOD_H1, _H2= PERIOD_H2, _H4= PERIOD_H4, class=class="str">"cmt">// _H8= PERIOD_H8, _D1= PERIOD_D1, _W1= PERIOD_W1, _MN1=PERIOD_MN1 }; class="kw">input mis_MarcosTMP timeframe= _H1; class=class="str">"cmt">//----------------------------------------- DEFINE THE TIMEFRAME ---------------------------------------------------------- ENUM_TIMEFRAMES defMarcoTiempo(mi_MARCOTMP_CORTO marco) { ENUM_TIMEFRAMES resp= _Period; class="kw">switch(marco) { case _M1: resp= PERIOD_M1; class="kw">break; case _M5: resp= PERIOD_M5; class="kw">break; case _M15: resp= PERIOD_M15; class="kw">break; class=class="str">"cmt">//case _M20: resp= PERIOD_M20; class="kw">break; case _M30: resp= PERIOD_M30; class="kw">break; case _H1: resp= PERIOD_H1; class="kw">break; case _H2: resp= PERIOD_H2; class="kw">break; case _H4: resp= PERIOD_H4; class="kw">break; class=class="str">"cmt">//case _H8: resp= PERIOD_H8; class="kw">break; case _D1: resp= PERIOD_D1; class="kw">break; case _W1: resp= PERIOD_W1; class="kw">break; case _MN1: resp= PERIOD_MN1; } class="kw">return(resp); } ENUM_TIMEFRAMES marcoTmp= defMarcoTiempo(marcoTiempo); class=class="str">"cmt">//时间周期被定义为全局变量 ENUM_TIMEFRAMES marcoTmp= (ENUM_TIMEFRAMES)marcoTiempo;
「用整数参数绕开字符串不能优化的坑」
MT5 策略测试器里字符串变量没法直接当优化参数,但我们可以用一个 int 型输入(比如 selecDePar)当代理,再在函数里把它映射成货币对名称字符串。这样测试器按步长跑整数,EA 内部拿到的是 "EURUSD;GBPCHF" 这类组合,一次优化就能扫完多组标的。 想比较单品种就把 selecDePar 从 0 到 9 步长 1 跑;想测组合就拉到 15~22,对应代码里的 case 10 以后。实测某 EA 在 M5 周期、特定参数下,EURUSD+EURJPY+GBPCHF 三组同跑的结果明显好于单跑 EURJPY(后者回测很糟),但换到 H1/H2 周期同样组合就失效了。 多货币对系统里不能再用 Symbol()、Point()、Ask、Bid 这些隐性当前品种函数。所有报价相关调用都得把品种名当参数传进去,新 K 线判定也要按品种分别处理——EURUSD 可能比 USDJPY 早 2 秒走新柱,不能靠统一心跳误判。 优化结果会随你选的基准货币对不同而漂移,这点我找过技术支持也没拿到根因。所以落地时建议始终把这个 EA 验证过的组合之一塞进优化集,至少能确认优化器真在分析它,而不是静默跳过了。外汇和贵金属杠杆高,回测好不等于实盘能复制,参数过拟合风险得自己扛。
class="kw">input class="type">int selecDePar= class="num">0; class="type">class="kw">string cadParesFX= selecPares(selecDePar); class=class="str">"cmt">//------------------------------------- SELECT THE SET OF PAIRS ------------------------------------- class="type">class="kw">string selecPares(class="type">int combina= class="num">0) { class="type">class="kw">string resp="EURUSD"; class="kw">switch(combina) { case class="num">1: resp= "EURJPY"; class="kw">break; case class="num">2: resp= "USDJPY"; class="kw">break; case class="num">3: resp= "USDCHF"; class="kw">break; case class="num">4: resp= "GBPJPY"; class="kw">break; case class="num">5: resp= "GBPCHF"; class="kw">break; case class="num">6: resp= "GBPUSD"; class="kw">break; case class="num">7: resp= "USDCAD"; class="kw">break; case class="num">8: resp= "CADJPY"; class="kw">break; case class="num">9: resp= "XAUUSD"; class="kw">break; case class="num">10: resp= "EURJPY;USDJPY"; class="kw">break; case class="num">11: resp= "EURJPY;GBPJPY"; class="kw">break; case class="num">12: resp= "GBPCHF;GBPJPY"; class="kw">break; case class="num">13: resp= "EURJPY;GBPCHF"; class="kw">break; case class="num">14: resp= "USDJPY;GBPCHF"; class="kw">break; case class="num">15: resp= "EURUSD;EURJPY;GBPJPY"; class="kw">break; case class="num">16: resp= "EURUSD;EURJPY;GBPCHF"; class="kw">break; case class="num">17: resp= "EURUSD;EURJPY;USDJPY"; class="kw">break; case class="num">18: resp= "EURJPY;GBPCHF;USDJPY"; class="kw">break; case class="num">19: resp= "EURJPY;GBPUSD;GBPJPY"; class="kw">break;