MQL5 细则手册:保存基于指定标准的EA 交易的优化结果·进阶篇
🗂️

MQL5 细则手册:保存基于指定标准的EA 交易的优化结果·进阶篇

(2/3)· 优化跑完几千遍却只想要那几个达标参数?本文拆解如何用 OnTester 系列钩子把合标准的结果落盘

含代码示例偏理论 第 2/3 篇

接上篇,我们继续深挖 EA 优化时的数据留存问题。很多交易者让测试器跑完几千次传递,却只能靠肉眼在结果表里翻符合条件的那几条,事后想复算连参数都凑不齐。其实 MQL5 的测试事件钩子能在每次传递后直接把达标结果写进文件,只是多数人没碰过那几个函数。

◍ 在优化循环里逐帧抓取EA参数

MT5 的策略测试器在每次优化 pass 会生成一个 frame,里面同时塞着 EA 输入参数和评价指标。想自己落盘成报告,核心是先调 FrameNext 把统计值取出来,再用 FrameInputs 拿到这一 pass 的参数列表。 下面这段函数骨架演示了怎么在每次调用时给 passes_count 自增,并在第一遍 pass 时专门扫一遍参数列表、识别 CriterionSelectionRule 出现的位置来切换标志位。注意 parameters_list 的顺序是优化标记参数排在最前,遍历时从 i=0 开始即可。 只在 passes_count==1 时去解析准则字段,可以避免后续几千个 pass 重复处理表头,实测在 5000 次优化里能少跑近 4999 轮无用字符串查找。外汇与贵金属优化本质是高随机系统,回测优异不代表实盘概率占优,请自行在 MT5 验证。

MQL5 / C++
class="type">void CreateOptimizationReport()
  {
   class="kw">static class="type">int passes_count=class="num">0;                   class=class="str">"cmt">// Pass counter
   class="type">int         parameters_count=class="num">0;              class=class="str">"cmt">// Number of Expert Advisor parameters
   class="type">int         optimized_parameters_count=class="num">0;    class=class="str">"cmt">// Counter of optimized parameters
   class="type">class="kw">string      string_to_write="";             class=class="str">"cmt">// String for writing
   class="type">bool        include_criteria_list=class="kw">false;    class=class="str">"cmt">// For determining the start of the list of parameters/criteria
   class="type">int         equality_sign_index=class="num">0;          class=class="str">"cmt">// The &class="macro">#x27;=&class="macro">#x27; sign index in the class="type">class="kw">string
   class="type">class="kw">string      name               ="";         class=class="str">"cmt">// Public name/frame label
   class="type">class="kw">ulong       pass               =class="num">0;          class=class="str">"cmt">// Number of the optimization pass at which the frame is added
   class="type">long        id                 =class="num">0;          class=class="str">"cmt">// Public id of the frame
   class="type">class="kw">double      value              =class="num">0.0;        class=class="str">"cmt">// Single numerical value of the frame
   class="type">class="kw">string      parameters_list[];              class=class="str">"cmt">// List of the Expert Advisor parameters of the "parameterN=valueN" form
   class="type">class="kw">string      parameter_names[];             class=class="str">"cmt">// Array of parameter names
   class="type">class="kw">string      parameter_values[];            class=class="str">"cmt">// Array of parameter values
class=class="str">"cmt">//--- Increase the pass counter
   passes_count++;
class=class="str">"cmt">//--- Place statistical values into the array
   FrameNext(pass,name,id,value,stat_values);
class=class="str">"cmt">//--- Get the pass number, list of parameters, number of parameters
   FrameInputs(pass,parameters_list,parameters_count);
class=class="str">"cmt">//--- Iterate over the list of parameters in a loop(starting from the upper one on the list)
class=class="str">"cmt">//    The list starts with the parameters that are flagged for optimization
   for(class="type">int i=class="num">0; i<parameters_count; i++)
     {
      class=class="str">"cmt">//--- Get the criteria for selection of results at the first pass
      if(passes_count==class="num">1)
        {
         class="type">class="kw">string current_value="";             class=class="str">"cmt">// Current parameter value
         class="kw">static class="type">int c=class="num">0,v=class="num">0,trigger=class="num">0; class=class="str">"cmt">// Counters and trigger
         class=class="str">"cmt">//--- Set a flag if you reached the list of criteria
         if(StringFind(parameters_list[i],"CriterionSelectionRule",class="num">0)>=class="num">0)
           {
            include_criteria_list=true;
            class="kw">continue;
           }

从参数串里拆出准则名和阈值

多准则信号引擎在遍历参数表时,靠一个 trigger 标志位在「读准则编号」和「读准则阈值」两种状态间切换。trigger=0 时解析出的是准则索引,trigger=1 时解析出的是该准则对应的比较值,两者成对写入 criteria[] 与 criteria_values[]。 代码里用 StringFind 定位 '=' 号、StringSubstr 截取右半段,再分别用 StringToInteger / StringToDouble 转类型。注意 equality_sign_index 要 +1,否则会含上 '=' 本身导致转换失败。 AND 模式下,只有当 i==parameters_count-1(即扫完最后一个参数)才调用 CalculateUsedCriteria(),统计实际生效的准则数。若 ParameterEnabledForOptimization 返回真,optimized_parameters_count 自增;仅在 passes_count==1 的首轮把参数名写进 parameter_names 数组做表头,避免每遍回测重复写入。 开 MT5 把这段塞进你的信号初始化函数,打印 criteria 与 criteria_values 的前 5 组,能直接看到 EURUSD 15M 下多准则组合的解析结果是否符合预期。外汇与贵金属杠杆交易高风险,参数解析错一位都可能让信号逻辑整体偏移。

MQL5 / C++
if(CriterionSelectionRule==RULE_AND && i==parameters_count-class="num">1)
   CalculateUsedCriteria();
if(include_criteria_list)
   {
   if(trigger==class="num">0)
      {
      equality_sign_index=StringFind(parameters_list[i],"=",class="num">0)+class="num">1;
      current_value =StringSubstr(parameters_list[i],equality_sign_index);
      criteria[c]=(class="type">int)StringToInteger(current_value);
      trigger=class="num">1;
      c++;
      class="kw">continue;
      }
   if(trigger==class="num">1)
      {
      equality_sign_index=StringFind(parameters_list[i],"=",class="num">0)+class="num">1;
      current_value=StringSubstr(parameters_list[i],equality_sign_index);
      criteria_values[v]=StringToDouble(current_value);
      trigger=class="num">0;
      v++;
      class="kw">continue;
      }
   }
if(ParameterEnabledForOptimization(parameters_list[i]))
   {
   optimized_parameters_count++;
   if(passes_count==class="num">1)
      {
      ArrayResize(parameter_names,optimized_parameters_count);

「从参数字符串到优化报告的文件写入链路」

EA 在优化遍历时,需要把每一趟 pass 的外部参数和统计值落盘。核心做法是先用 StringFind 定位 '=' 号,再用 StringSubstr 把参数名和参数值拆开:名取等号前段,值取等号后一位开始的后段。 拆出的 parameter_names 和 parameter_values 分别用 ArrayResize 按 optimized_parameters_count 扩容,避免越界。随后两个 for 循环拼接写入串:第一个循环把 stat_values 用 DoubleToString(stat_values[i],2) 转成保留 2 位小数的文本并加逗号;第二个循环追加优化参数值,且对最后一个元素不加分隔符,直接 break。 passes_count==1 时调 WriteOptimizationReport(parameter_names) 写表头,其余趟次只调 WriteOptimizationResults(string_to_write) 追加数据行。这样生成的 CSV 类报告,首行是字段名,后续每行是一次优化结果。 CalculateUsedCriteria 遍历 criteria 数组,凡不等于 C_NO_CRITERION 的计数加一,UsedCriteriaCount 就是实际启用的优化目标数。ParameterEnabledForOptimization 则靠定位 '=' 再解析 value/start/step/stop 来判断某个外部参数是否被设为可优化区间。 开 MT5 随便挂一个支持优化的 EA,在 EA 属性→输入参数里把某参数设成起始/步长/终止,跑一遍优化,用上述逻辑自己写个脚本把 results 文件读出来,能验证参数名和值是否如代码那样被 '=' 切分。外汇与贵金属优化存在过拟合高风险,回测优异不代表实盘倾向盈利。

MQL5 / C++
equality_sign_index=StringFind(parameters_list[i],"=",class="num">0);
parameter_names[i]=StringSubstr(parameters_list[i],class="num">0,equality_sign_index);
ArrayResize(parameter_values,optimized_parameters_count);
equality_sign_index=StringFind(parameters_list[i],"=",class="num">0)+class="num">1;
parameter_values[i]=StringSubstr(parameters_list[i],equality_sign_index);
for(class="type">int i=class="num">0; i<STAT_VALUES_COUNT; i++)
   StringAdd(string_to_write,DoubleToString(stat_values[i],class="num">2)+",");
for(class="type">int i=class="num">0; i<optimized_parameters_count; i++)
  {
   if(i==optimized_parameters_count-class="num">1)
     {
      StringAdd(string_to_write,parameter_values[i]);
      class="kw">break;
     }
   else
      StringAdd(string_to_write,parameter_values[i]+",");
  }
if(passes_count==class="num">1)
   WriteOptimizationReport(parameter_names);
WriteOptimizationResults(string_to_write);
class="type">void CalculateUsedCriteria()
  {
   UsedCriteriaCount=class="num">0;
   for(class="type">int i=class="num">0; i<ArraySize(criteria); i++)
     {
      if(criteria[i]!=C_NO_CRITERION)
         UsedCriteriaCount++;
     }
  }
class="type">bool ParameterEnabledForOptimization(class="type">class="kw">string parameter_string)
  {
   class="type">bool enable;
   class="type">long value,start,step,stop;
   class="type">int equality_sign_index=StringFind(parameter_string,"=",class="num">0);

◍ 把优化结果落盘成可比对 CSV

EA 跑完遗传优化后,最怕结果只躺在 MT5 终端里,换机器或重跑就丢了。上面这段把每次优化的关键指标和参数组合写进公共目录下的 CSV,方便你用 Excel 或 Python 横向比对数百组解。 WriteOptimizationReport 先拼表头:写死的前 8 列是 #,PROFIT,TOTAL DEALS,PROFIT FACTOR,EXPECTED PAYOFF,EQUITY DD MAX REL%,RECOVERY FACTOR,SHARPE RATIO,随后把传入的 parameter_names 数组逐个用逗号接在后面,最后一个参数不加逗号。这样无论你优化几个输入变量,表头都能自适应。

文件落在 CreateOptimizationResultsFolder 返回的目录,命名规则是 optimization_results1.csv(files_count 从 1 计)。打开方式用了 FILE_CSVFILE_READFILE_WRITEFILE_ANSIFILE_COMMON,注意 FILE_COMMON 意味着路径在 Terminal 的 Common 数据目录,不是单账户沙箱,多账户共享时别误删。

若 OptimizationResultsPath 返回空串,函数直接 Print 并 return,不会写出半成品文件。外汇与贵金属优化本质是高杠杆下的历史拟合,实盘前务必用样本外数据复核,过拟合概率不低。

MQL5 / C++
  ParameterGetRange(StringSubstr(parameter_string,class="num">0,equality_sign_index),
                     enable,value,start,step,stop);
class=class="str">"cmt">//--- Return the parameter status
  class="kw">return(enable);
  }
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//| Generating the optimization report file                             |
class=class="str">"cmt">//+--------------------------------------------------------------------+
class="type">void WriteOptimizationReport(class="type">class="kw">string &parameter_names[])
  {
  class="type">int files_count    =class="num">1; class=class="str">"cmt">// Counter of optimization files
class=class="str">"cmt">//--- Generate a header to the optimized parameters
  class="type">class="kw">string headers="#,PROFIT,TOTAL DEALS,PROFIT FACTOR,EXPECTED PAYOFF,EQUITY DD MAX REL%,RECOVERY FACTOR,SHARPE RATIO,";
class=class="str">"cmt">//--- Add the optimized parameters to the header
  for(class="type">int i=class="num">0; i<ArraySize(parameter_names); i++)
    {
      if(i==ArraySize(parameter_names)-class="num">1)
        StringAdd(headers,parameter_names[i]);
      else
        StringAdd(headers,parameter_names[i]+",");
    }
class=class="str">"cmt">//--- Get the location for the optimization file and
class=class="str">"cmt">//--- the number of files for the index number
  OptimizationResultsPath=CreateOptimizationResultsFolder(files_count);
class=class="str">"cmt">//--- If there is an error when getting the folder, exit
  if(OptimizationResultsPath=="")
    {
      Print("Empty path: ",OptimizationResultsPath);
      class="kw">return;
    }
  else
    {
      OptimizationFileHandle=FileOpen(OptimizationResultsPath+"\\optimization_results"+IntegerToString(files_count)+".csv",
                                      FILE_CSV|FILE_READ|FILE_WRITE|FILE_ANSI|FILE_COMMON,",");
      class=class="str">"cmt">//---
      if(OptimizationFileHandle!=INVALID_HANDLE)
        FileWrite(OptimizationFileHandle,headers);
    }
  }
class=class="str">"cmt">//+--------------------------------------------------------------------+
class=class="str">"cmt">//| Creating folders for optimization results                           |
class=class="str">"cmt">//+--------------------------------------------------------------------+
class="type">class="kw">string CreateOptimizationResultsFolder(class="type">int &files_count)
  {
  class="type">long   search_handle       =INVALID_HANDLE;      class=class="str">"cmt">// Search handle
  class="type">class="kw">string returned_filename   ="";                  class=class="str">"cmt">// Name of the found object(file/folder)
  class="type">class="kw">string path                ="";                  class=class="str">"cmt">// File/folder search location
  class="type">class="kw">string search_filter       ="*";                 class=class="str">"cmt">// Search filter(* - check all files/folders)

在公共目录里定位优化数据文件夹

把回测优化结果落盘时,第一道关是先确认终端公共数据目录下的 OPTIMIZATION_DATA 根目录以及对应 EA 的子目录是否存在。MT5 的公共目录路径由 COMMONDATA_PATH 给出,子目录结构通常是 Files\OPTIMIZATION_DATA\<EA名>\,这套层级不先核验,后续写文件会直接抛错。 下面这段逻辑先用 FileFindFirst 在公共 Files 区扫根目录,若首条返回就不是目标根目录,就靠 FileFindNext 循环补齐搜索,命中即置 root_folder_exists 并跳出。handle 拿不到时把 GetLastError 的描述打进日志,方便在终端 Journal 里直接看是哪一步空了或权限异常。 根目录确认后,把搜索路径拼成 root_folder+search_filter 再搜一轮,定位 EA 专属子目录,逻辑同前:首条命中就置 expert_folder_exists,否则继续 Next 直到找到或handle关闭。实测中若 COMMONDATA_PATH 下 Files 为空,FileFindFirst 返回 INVALID_HANDLE 的概率很高,这时候别迷信 EA 会自动建目录,得自己 mkdir 补一层。

MQL5 / C++
class="type">class="kw">string root_folder        ="OPTIMIZATION_DATA\\"; class=class="str">"cmt">// Root folder
class="type">class="kw">string expert_folder      =EXPERT_NAME+"\\";        class=class="str">"cmt">// Folder of the Expert Advisor
class="type">bool   root_folder_exists =class="kw">false;                     class=class="str">"cmt">// Flag of existence of the root folder
class="type">bool   expert_folder_exists=class="kw">false;                    class=class="str">"cmt">// Flag of existence of the Expert Advisor folder
class=class="str">"cmt">//--- Search for the OPTIMIZATION_DATA root folder in the common folder of the terminal
path=search_filter;
class=class="str">"cmt">//--- Set the search handle in the common folder of all client terminals \Files
search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
class=class="str">"cmt">//--- Print the location of the common folder of the terminal to the journal
Print("TERMINAL_COMMONDATA_PATH: ",COMMONDATA_PATH);
class=class="str">"cmt">//--- If the first folder is the root folder, flag it
if(returned_filename==root_folder)
  {
   root_folder_exists=true;
   Print("The "+root_folder+" root folder exists.");
  }
class=class="str">"cmt">//--- If the search handle has been obtained
if(search_handle!=INVALID_HANDLE)
  {
   class=class="str">"cmt">//--- If the first folder is not the root folder
   if(!root_folder_exists)
     {
      class=class="str">"cmt">//--- Iterate over all files to find the root folder
      class="kw">while(FileFindNext(search_handle,returned_filename))
        {
         class=class="str">"cmt">//--- If it is found, flag it
         if(returned_filename==root_folder)
           {
            root_folder_exists=true;
            Print("The "+root_folder+" root folder exists.");
            class="kw">break;
           }
        }
     }
   class=class="str">"cmt">//--- Close the root folder search handle
   FileFindClose(search_handle);
  }
else
  {
   Print("Error when getting the search handle "
         "or the "+COMMONDATA_PATH+" folder is empty: ",ErrorDescription(GetLastError()));
  }
class=class="str">"cmt">//--- Search for the Expert Advisor folder in the OPTIMIZATION_DATA folder
path=root_folder+search_filter;
class=class="str">"cmt">//--- Set the search handle in the ..\Files\OPTIMIZATION_DATA\ folder
search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
class=class="str">"cmt">//--- If the first folder is the folder of the Expert Advisor
if(returned_filename==expert_folder)
  {
   expert_folder_exists=true; class=class="str">"cmt">// Remember this
   Print("The "+expert_folder+" Expert Advisor folder exists.");
  }
class=class="str">"cmt">//--- If the search handle has been obtained
if(search_handle!=INVALID_HANDLE)
  {
   class=class="str">"cmt">//--- If the first folder is not the folder of the Expert Advisor
   if(!expert_folder_exists)

「顺手把优化结果目录补齐」

遍历 DATA_OPTIMIZATION 时若没撞见当前 EA 的专属文件夹,代码会把 expert_folder_exists 标 false,后面就靠 FolderCreate 在 ..\Files\OPTIMIZATION_DATA\ 下补一层以 EXPERT_NAME 命名的目录。 根目录不存在也同理:先 FolderCreate("OPTIMIZATION_DATA", FILE_COMMON),成功才把 root_folder_exists 翻成 true,失败直接 Print 错误并 return("") 中断,避免往空路径写文件。 这种自校验逻辑很实用——跑批量优化前让脚本自己把目录树建好,省得 FileWrite 因路径缺失报 5004 错误。外汇与贵金属品种波动剧烈、杠杆风险高,自动化脚本仅用于文件管理,不替代交易决策。

MQL5 / C++
{
 class=class="str">"cmt">//--- Iterate over all files in the DATA_OPTIMIZATION folder to find the folder of the Expert Advisor
 class="kw">while(FileFindNext(search_handle,returned_filename))
   {
   class=class="str">"cmt">//--- If it is found, flag it
   if(returned_filename==expert_folder)
     {
     expert_folder_exists=true;
     Print("The "+expert_folder+" Expert Advisor folder exists.");
     class="kw">break;
     }
   }
 }
class=class="str">"cmt">//--- Close the root folder search handle
FileFindClose(search_handle);
 }
 else
   Print("Error when getting the search handle or the "+path+" folder is empty.");
class=class="str">"cmt">//--- Generate the path to count the files
 path=root_folder+expert_folder+search_filter;
class=class="str">"cmt">//--- Set the search handle in the ..\Files\OPTIMIZATION_DATA\ folder of optimization results
 search_handle=FileFindFirst(path,returned_filename,FILE_COMMON);
class=class="str">"cmt">//--- If the folder is not empty, start the count
 if(StringFind(returned_filename,"optimization_results",class="num">0)>=class="num">0)
   files_count++;
class=class="str">"cmt">//--- If the search handle has been obtained
 if(search_handle!=INVALID_HANDLE)
  {
  class=class="str">"cmt">//--- Count all files in the Expert Advisor folder
  class="kw">while(FileFindNext(search_handle,returned_filename))
    files_count++;
  class=class="str">"cmt">//---
  Print("Total files: ",files_count);
  class=class="str">"cmt">//--- Close the Expert Advisor folder search handle
  FileFindClose(search_handle);
  }
 else
   Print("Error when getting the search handle or the "+path+" folder is empty");
class=class="str">"cmt">//--- Create the necessary folders based on the check results
class=class="str">"cmt">//		If there is no OPTIMIZATION_DATA root folder
 if(!root_folder_exists)
  {
  if(FolderCreate("OPTIMIZATION_DATA",FILE_COMMON))
    {
    root_folder_exists=true;
    Print("The root folder ..\Files\OPTIMIZATION_DATA\\ has been created");
    }
  else
    {
    Print("Error when creating the OPTIMIZATION_DATA root folder: ",
       ErrorDescription(GetLastError()));
    class="kw">return("");
    }
  }
class=class="str">"cmt">//--- If there is no Expert Advisor folder
 if(!expert_folder_exists)
  {
  if(FolderCreate(root_folder+EXPERT_NAME,FILE_COMMON))
    {
    expert_folder_exists=true;
让小布替你跑这套筛选
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到优化结果的达标标记与参数回看,不必自己搭框架落盘脚本。

常见问题

OnTesterInit 在优化开始时触发一次;OnTester 在每次传递后负责添加框架;OnTesterPass 在每次传递后取得框架;OnTesterDeinit 在优化结束时触发,用于收尾写盘。
可以,小布盯盘的品种页已集成优化结果诊断,达标传递的参数与指标值会直接标注,省去自己解析 *.mqd 档案的步骤。
FrameAdd 把单次传递的数据或数组写入框架档案,FrameNext 则在后续读取时逐个取出数字或整帧,配合 FrameInputs 可还原该帧对应的外部输入。
可能 LogOptimizationReport 未开启,或 CriterionSelectionRule 设定的条件在全部传递中均未满足,导致没有任何帧被显式落盘。
本篇实现支持指定多达三个标准,并通过规则参数定义是全部满足还是任一满足才写盘,具体上限取决于 EA 外部参数的设计。