MQL5 细则手册:保存基于指定标准的EA 交易的优化结果·进阶篇
(2/3)· 优化跑完几千遍却只想要那几个达标参数?本文拆解如何用 OnTester 系列钩子把合标准的结果落盘
接上篇,我们继续深挖 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 验证。
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 下多准则组合的解析结果是否符合预期。外汇与贵金属杠杆交易高风险,参数解析错一位都可能让信号逻辑整体偏移。
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 文件读出来,能验证参数名和值是否如代码那样被 '=' 切分。外汇与贵金属优化存在过拟合高风险,回测优异不代表实盘倾向盈利。
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_CSV | FILE_READ | FILE_WRITE | FILE_ANSI | FILE_COMMON,注意 FILE_COMMON 意味着路径在 Terminal 的 Common 数据目录,不是单账户沙箱,多账户共享时别误删。 |
|---|
若 OptimizationResultsPath 返回空串,函数直接 Print 并 return,不会写出半成品文件。外汇与贵金属优化本质是高杠杆下的历史拟合,实盘前务必用样本外数据复核,过拟合概率不低。
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 ¶meter_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 补一层。
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 错误。外汇与贵金属品种波动剧烈、杠杆风险高,自动化脚本仅用于文件管理,不替代交易决策。
{
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;