关于策略优化的一些简单想法·进阶篇
📘

关于策略优化的一些简单想法·进阶篇

第 2/2 篇

◍ 把分号串货币对拆成可遍历数组

EA 里经常用 "EURUSD;GBPUSD;USDJPY" 这种分号分隔的字符串来配置交易品种,但下单和判势都要逐个品种跑逻辑,所以必须先把字符串转成数组。下面这段 cargaPares 函数就是干这个的:传入字符串和引用数组,返回品种数量,空串时默认塞入当前图表品种 _Symbol。 函数先数分号个数加 1 得到 contPares,用 ArrayResize 定好数组长度,再逐字符拼接到 arrayPares[i],遇到分号就 i++ 切到下一个元素。实测 case 24 那种 7 品种串(EURUSD;EURJPY;USDJPY;GBPUSD;USDCHF;USDCAD;AUDUSD)会返回 7,数组下标 0~6 正好一一对应。 OnInit 里调一次 numSimbs=cargaPares(cadParesFX, ar_ParesFX) 就把全局 ar_ParesFX 填好了;OnTick 中若 nSimbs==1 只跑 ar_ParesFX[0],多品种则由 OnChartEvent 的自定义事件分流。外汇与贵金属杠杆高、滑点跳空频繁,多品种同跑要留意各品种点值差异和保证金占用。

MQL5 / C++
class="type">int cargaPares(class="type">class="kw">string cadPares, class="type">class="kw">string &arrayPares[])
{
   class="type">class="kw">string caract= "";
   class="type">int i= class="num">0, k= class="num">0, contPares= class="num">1, longCad= StringLen(cadPares);
   if(cadPares=="")
   {
      ArrayResize(arrayPares, contPares);
      arrayPares[class="num">0]= _Symbol;
   }
   else
   {
      for (k= class="num">0; k<longCad; k++) if (StringSubstr(cadPares, k, class="num">1)==";") contPares++;
      ArrayResize(arrayPares, contPares);
      ZeroMemory(arrayPares);
      for(k=class="num">0; k<longCad; k++)
      {
         caract= StringSubstr(cadPares, k, class="num">1);
         if (caract!=";") arrayPares[i]= arrayPares[i]+caract;
         else i++;
      }
   }
   class="kw">return(contPares);
}
class="type">class="kw">string ar_ParesFX[];
class="type">int numSimbs= class="num">1;
class="type">int OnInit()
{
   numSimbs= cargaPares(cadParesFX, ar_ParesFX);
   class="kw">return(INIT_SUCCEEDED);
}
class="type">void OnTick()
{
   class="type">class="kw">string simb="";
   class="type">bool entrar= (nSimbs==class="num">1);
   if(entrar)
   {
      simb= ar_ParesFX[class="num">0];
      gestionOrdenes(simb);
   }
   class="kw">return;
}
class="type">void OnChartEvent(class="kw">const class="type">int idEvento, class="kw">const class="type">long& lPeriodo, class="kw">const class="type">class="kw">double& dPrecio, class="kw">const class="type">class="kw">string &simbTick)
{
   class="type">bool entrar= nSimbs>class="num">1 && (idEvento>=CHARTEVENT_CUSTOM);
}

多品种基础信息与新K线判定的底层函数

做跨品种 EA 时,先得把每个交易标的的小数位、点值、买卖价这些属性抓准。下面这几个小函数就是干这个的:digitosSimb 用 SymbolInfoInteger 取 SYMBOL_DIGITS,返回 int 类型的小数位数;valorPunto 取 SYMBOL_POINT 得到该品种单点价格变动;precioAskBid 靠传入 ask 布尔值切换 SYMBOL_ASK 与 SYMBOL_BID,统一拿报价。 新蜡烛识别是事件驱动型策略的命门。nuevaVelaMD 用 static 数组 arrayHoraNV 按品种下标存上一次 K 线开始时间,iTime(simb,marcoTmp,0) 取当前柱开盘时间,两者不等就判为新柱。首次调用靠 primVez 把数组按 numSimbs 尺寸初始化,避免越界。 buscaCadArray 负责把品种名在 nombreParesFX 里找下标,找不到(codS<0)直接返回 false 不判新柱。外汇与贵金属杠杆高、滑点突变频繁,这类依赖时间精度的判断在极端行情可能漏帧,上 MT5 用 Print 打出 arrayHoraNV 内容验证最稳妥。

MQL5 / C++
class="type">int digitosSimb(class="type">class="kw">string simb= NULL)
{
   class="type">int numDig= (class="type">int)SymbolInfoInteger(simb, SYMBOL_DIGITS);
   class="kw">return(numDig);
}
class=class="str">"cmt">//----------------------------------- POINT VALUE in price(Point())---------------------------------
class="type">class="kw">double valorPunto(class="type">class="kw">string simb= NULL)
{
   class="type">class="kw">double resp= SymbolInfoDouble(simb, SYMBOL_POINT);
   class="kw">return(resp);
}
class=class="str">"cmt">//--------------------------- precio ASK-BID  -----------------------------------------
class="type">class="kw">double precioAskBid(class="type">class="kw">string simb= NULL, class="type">bool ask= true)
{
   ENUM_SYMBOL_INFO_DOUBLE precioSolic= ask? SYMBOL_ASK: SYMBOL_BID;
   class="type">class="kw">double precio= SymbolInfoDouble(simb, precioSolic);
   class="kw">return(precio);
}
class=class="str">"cmt">//------------------------------------- NEW MULTI-CURRENCY CANDLESTICK -------------------------------------
class="type">bool nuevaVelaMD(class="type">class="kw">string simb= NULL, class="type">int numSimbs= class="num">1, ENUM_TIMEFRAMES marcoTmp= PERIOD_CURRENT)
{
       class="kw">static class="type">class="kw">datetime arrayHoraNV[];
       class="kw">static class="type">bool primVez= true;
       class="type">class="kw">datetime horaVela= iTime(simb, marcoTmp, class="num">0);      class=class="str">"cmt">//当前蜡烛线的开始时间
       class="type">bool esNueva= class="kw">false;
       class="type">int codS= buscaCadArray(simb, nombreParesFX);
       if(primVez)
       {
         ArrayResize(arrayHoraNV, numSimbs);
         ArrayInitialize(arrayHoraNV, class="num">0);
         primVez= class="kw">false;
       }
       esNueva= codS>=class="num">0? arrayHoraNV[codS]!= horaVela: class="kw">false;
       if(esNueva) arrayHoraNV[codS]= horaVela;
       class="kw">return(esNueva);
}

「用参数闸门拦掉无效优化组合」

跑 MT5 优化时,把 maxSpread 设成 0~600 以 20 间距遍历,再叠上其他参数,会生成大量根本不该测的组合。比如 EURUSD 经纪商平均点差一般小于 30,XAUUSD 小于 400;若 EURUSD 点差超 50 或 XAUUSD 点差小于 200,这类组合在逻辑上已无意义,硬跑只会烧掉本地算力和 MQL5 社区租用代理的资源。 做法是在 selecPares() 里给货币对编号:EURUSD 对应 0,XAUUSD 对应 9,再声明一个全局 bool 变量 paramCorrect 做闸门。仅当 paramCorrect 为 true 时 OnInit() 才真正初始化 EA;若为 false,直接返回 INIT_PARAMETERS_INCORRECT,策略测试器收到这个值后不会把该组合送进回测引擎,优化结果表里这一行记 0 并红色高亮。 比起在 OnTesterInit() 里用 ParameterGetRange()/ParameterSetRange() 拦组合,这种 OnInit 返回错误码的方式更简短也更稳。EA 终止原因还会通过输入参数传进 OnDeinit(),方便事后查为什么没跑——外汇与贵金属杠杆高、点差跳变频繁,这类无效组合若不清掉,优化成本可能倾向指数级放大。 下面这段是闸门核心逻辑,逐行拆一下:

bool paramCorrect= (selecDePar<9 && maxSpread<50)(selecDePar==9 && maxSpread>200);

// 第1行:定义布尔闸门。非黄金编号(<9)时点差须<50才合法;黄金编号(==9)时点差须>200才合法 int OnInit(){ ENUM_INIT_RETCODE resp= paramCorrect? INIT_SUCCEEDED: INIT_PARAMETERS_INCORRECT; // 第2-3行:OnInit 里先按 paramCorrect 决定返回码,合法就成功,不合法就报参数错 if (paramCorrect){ //... nSimbs= cargaPares(cadParesFX, nombreParesFX); //... EA初始化函数 } // 第4-7行:只有合法才执行加载货币对数组等初始化,不合法整段跳过 return(resp); } // 第8行:把 resp 返回给测试器 void OnDeinit(const int motivo){ if(paramCorrect){ //程序关闭函数 } infoDeInit(motivo); return; } // 第9-13行:退出时若曾合法初始化则跑清理,并把退出动机传给 infoDeInit 记录 string infoDeInit(int codDeInit){ string texto= "program initialization...", text1= "CIERRE por: "; // 第14-15行:infoDeInit 准备前缀,用于拼退出原因文案 switch(codDeInit){ case REASON_PROGRAM: texto= text1+"The EA finished its work with the ExpertRemove() function"; break; // 第16-17行:退出码0,EA自己调 ExpertRemove 结束 case REASON_ACCOUNT: texto= text1+"The account was changed"; break; // 第18行:退出码6,账户被切换 case REASON_CHARTCHANGE: texto= text1+"Symbol or timeframe change"; break; // 第19行:退出码3,图表品种或周期变了 case REASON_CHARTCLOSE: texto= text1+"The chart was closed"; break; // 第20行:退出码4,图表被关 case REASON_PARAMETERS: texto= text1+"Input parameters changed by the user"; break; // 第21行:退出码5,用户改了输入参数 case REASON_RECOMPILE: texto= text1+"The program was recompiled"; break; // 第22行:退出码2,程序被重编译 case REASON_REMOVE: texto= text1+"The program was deleted from the chart"; break; // 第23行:退出码1,EA从图表被删 case REASON_TEMPLATE: texto= text1+"Another chart template was used"; break; // 第24行:退出码7,换了图表模板 case REASON_CLOSE: texto= text1+"The terminal was closed"; break; } // 第25行:退出码9,终端被关闭;switch 结束

MQL5 / C++
class="type">bool paramCorrect= (selecDePar<class="num">9 && maxSpread<class="num">50) ||
					 (selecDePar==class="num">9 && maxSpread>class="num">200);
class="type">int OnInit()
{  
   ENUM_INIT_RETCODE resp= paramCorrect? INIT_SUCCEEDED: INIT_PARAMETERS_INCORRECT;
   if (paramCorrect)
   {
      class=class="str">"cmt">//...
      nSimbs= cargaPares(cadParesFX, nombreParesFX);      class=class="str">"cmt">//返回包含EA所运行货币对的nombreParesFX数组
      class=class="str">"cmt">//... EA初始化函数
   }
   class="kw">return(resp);
}
class="type">void OnDeinit(class="kw">const class="type">int motivo)
{
   if(paramCorrect)
   {

      class=class="str">"cmt">//程序关闭函数

   }
   infoDeInit(motivo);
   class="kw">return;
}
class=class="str">"cmt">//+-------------------------------------- INFORMATION ABOUT THE PROGRAM SHUTDOWN----------------------------
class="type">class="kw">string infoDeInit(class="type">int codDeInit)
{                     class=class="str">"cmt">//告知程序关闭的原因
   class="type">class="kw">string texto= "program initialization...", text1= "CIERRE por: ";
   class="kw">switch(codDeInit)
   {
      case REASON_PROGRAM:    texto= text1+"The EA finished its work with the ExpertRemove() function"; class="kw">break;   class=class="str">"cmt">//class="num">0
      case REASON_ACCOUNT:    texto= text1+"The account was changed"; class="kw">break;                                 class=class="str">"cmt">//class="num">6
      case REASON_CHARTCHANGE: texto= text1+"Symbol or timeframe change"; class="kw">break;                             class=class="str">"cmt">//class="num">3
      case REASON_CHARTCLOSE:  texto= text1+"The chart was closed"; class="kw">break;                                   class=class="str">"cmt">//class="num">4
      case REASON_PARAMETERS:  texto= text1+"Input parameters changed by the user"; class="kw">break;                   class=class="str">"cmt">//class="num">5
      case REASON_RECOMPILE:   texto= text1+"The program was recompiled"; class="kw">break;                             class=class="str">"cmt">//class="num">2
      case REASON_REMOVE:      texto= text1+"The program was deleted from the chart"; class="kw">break;                 class=class="str">"cmt">//class="num">1
      case REASON_TEMPLATE:    texto= text1+"Another chart class="kw">template was used"; class="kw">break;                        class=class="str">"cmt">//class="num">7
      case REASON_CLOSE:       texto= text1+"The terminal was closed"; class="kw">break;                                class=class="str">"cmt">//class="num">9

◍ 把初始化失败原因打印出来

EA 在 OnStart 或 OnInit 里如果返回非零,MT5 会抛出 REASON_INITFAILED 事件,此时用 switch 把原因转成可读文本最省事。上面这段把 REASON_INITFAILED 映射成“The OnInit() handler returned non-zero value”,default 分支兜底为“Other reason”,最后 Print 输出并返回字符串。 直接在 MT5 里建个脚本跑这段,能从日志看到具体初始化失败文案,比光看报错码快。外汇与贵金属品种波动剧烈,EA 初始化失败可能让你错过行情或漏掉风控,建议每次部署前本地先验一遍返回码。 逐行看:case REASON_INITFAILED 给 texto 赋初始化失败说明并 break;default 捕获其余未知原因;右大括号收掉 switch;Print(texto) 打到终端;return(texto) 把文本交回调用方。

MQL5 / C++
    case REASON_INITFAILED:  texto= text1+"The OnInit() handler returned non-zero value"; class="kw">break;             class=class="str">"cmt">//class="num">8
    class="kw">default:                  texto= text1+"Other reason";
   }
   Print(texto);
   class="kw">return(texto);
}

别急着下结论

把时间周期优化和字符串型货币对参数解耦做进 EA,本质是用结构化编程绕开 MT5 优化器不支持字符串变量的限制,让回测不绑死在某一品种上。 顺手砍掉不合逻辑的参数组合,能直接少跑几千次无效 Pass,老机器风扇声都能小一截。 这些思路不新,但胜在从调试里抠出来的实在经验。外汇和贵金属杠杆高、滑点跳空频繁,任何参数方案都只是降低试错成本,不保证实盘能复现回测表现。 真要验证,开 MT5 把自己的 EA 货币对参数改成 input 枚举,再跑一轮针对性优化,比读十篇文都管用。

常见问题

用 StringSplit() 按分号切分存入 string 数组,遍历前先判空,避免空品种进循环导致初始化报错。
给每个品种存上根 K 线时间,循环中用 iTime 取当前时间比对,不等就标记新柱,注意不同周期要分别缓存。
可以,小布能按你设的参数闸门(如手数区间、止损下限)自动跳过无效组合,只把通过的组合丢进回测。
多是数组越界或品种数超预期,用 Print 把失败前的品种名和数组大小打出,对照输入字符串就能定位。
过度拟合概率高,应先看样本外和滑点下的衰减,别急着下结论,拿实盘模拟跑两周再定。