MQL5 细则手册:保存基于指定标准的EA 交易的优化结果(基础篇)
把达标参数自动存盘的测试器技巧
MT5 策略测试器的优化结果默认只留在内存里,关掉终端或切到别的品种就容易丢。想在批量跑参时把符合自己标准的那一组 EA 设置留下来,得走测试器自带的筛选与保存通道,而不是手动抄表。 实测在 2014 年那版 MT5 测试器里,单轮优化可输出并落盘的候选组合数量受「优化结果最大保留数」限制,默认上限常见为 100 条;超过的部分会被覆盖,所以先调高这个上限再跑长周期网格,才不会漏掉边缘但稳健的参数。 操作上,跑完优化后在「优化结果」标签页用右键菜单的「保存为预设」或「导出为 xml」都能落盘。外汇与贵金属品种点差和滑点敏感,回测达标不代表实盘同效,保存的参数仅作进一步 forward test 的起点,存在过拟合可能。
「把每趟优化结果落盘」
在 MT5 的策略测试器里跑 EA 参数优化时,默认只给你看最终靠前的几组解,中间每一趟传递(pass)的具体输入与产出其实都被丢弃了。想做自己的稳健性分析,就得在优化过程中把每一趟的结果连同当时用的参数一起写进文件。 做法是在 EA 里加一段逻辑:当外部参数指定的某个条件成立时,把该趟的优化目标值(比如净利润、夏普类指标)和对应的输入参数组合写入一个文本或 CSV 文件。这样一轮优化跑完,你就拿到一份完整样本,而不是测试器摘要里那几行。 外汇与贵金属品种波动大、点差跳变频繁,这类优化结果仅反映历史样本特征,实盘复制存在较高偏差风险,用作参数分布观察更稳妥。
◍ 用框架函数把优化结果筛进Excel
在 MT5 里做参数优化,光跑完 10 万次传递没用,关键是怎么把符合条件的结果落盘。MQL5 给了四个测试器钩子:OnTesterInit() 标记优化开始,OnTester() 每次传递后写框架,OnTesterPass() 取框架,OnTesterDeinit() 收尾关文件。框架本质是存在 MQL5/Files/Tester 下的 *.mqd 数据结构,优化中能动态读,跑完也能回读。 直接加 LogOptimizationReport 外部参数控制是否写文件,再配 CriterionSelectionRule 决定 AND 还是 OR 逻辑——比如盈利系数>1 且 回收系数>2 才存。外部参数里这几个输入要补进 EA:NumberOfBars=2、Lot=0.1、TakeProfit=100、StopLoss=50、TrailingStop=10,都是原 EA 已有的基础设置。 踩坑点:OnTester() 里必须调 FrameAdd() 塞框架,否则 OnTesterPass() 和 OnTesterDeinit() 在日志里哑火;而且若外部参数没改动,MT5 会从缓存直接喂结果给测试器,绕过那两个函数,别当成 bug。 实跑验证:用 MQL5 云网络分布式算,101,000 次传递 5 分钟跑完,本地单机可能要数天。筛选后只 719 个结果写进文件,Excel 打开就能按列挑参数组合。外汇/贵金属这类高杠杆品种,优化结果仅代表历史样本特征,实盘概率可能偏移,别直接当仓位依据。 FileFunctions.mqh 里 GetTestStatistics() 负责填数组,FrameNext() 取当前框架值,FrameInputs() 拿参数列表——注意优化标记的参数排在最前,跟 EA 外部参数顺序无关。CreateOptimizationReport() 每次新建带索引的文件,不删旧档,WriteOptimizationResults() 按 AND/OR 调 AccessCriterionAND/OR 决定写不写。
class=class="str">"cmt">//--- External parameters of the Expert Advisor class="kw">input class="type">int NumberOfBars = class="num">2; class=class="str">"cmt">// Number of one-direction bars sinput class="type">class="kw">double Lot = class="num">0.1; class=class="str">"cmt">// Lot class="kw">input class="type">class="kw">double TakeProfit = class="num">100; class=class="str">"cmt">// Take Profit class="kw">input class="type">class="kw">double StopLoss = class="num">50; class=class="str">"cmt">// Stop Loss class="kw">input class="type">class="kw">double TrailingStop = class="num">10; class=class="str">"cmt">// Trailing Stop
把优化报告筛选条件写进EA参数
做 MT5 策略优化时,跑完几千组参数最怕的就是人工翻结果。这段输入变量设计让你可以直接在 EA 外部参数里定义「哪些统计指标达标才写文件」,不用进代码改逻辑。 核心是两个枚举:CRITERION_RULE 决定多条件之间是 AND(全满足才写)还是 OR(任一满足就写);ENUM_STATS 列出了可筛选的 7 类统计量,从总利润、成交笔数、盈利因子到夏普比率,编号 1~7,编号 0 代表不筛选。 实际用法是把 LogOptimizationReport 设 true,Criterion_01~03 各自挑一个统计量并填 CriterionValue,比如 Criterion_01 选 C_STAT_PROFIT_FACTOR=3、值填 1.5,配合 RULE_AND 就只导出盈利因子≥1.5 的组。外汇与贵金属杠杆高,回测筛选过的参数实盘仍可能失效,需先开 MT5 用历史数据验证。 全局里 criteria[3] 数组就是存这三组条件的容器,OptimizationFileHandle 管文件句柄,UsedCriteriaCount 记实际启用了几个条件——这些不用手填,EA 初始化时自己算。
class="kw">input class="type">bool Reverse = true; class=class="str">"cmt">// Position reversal sinput class="type">class="kw">string delimeter=""; class=class="str">"cmt">// -------------------------------- sinput class="type">bool LogOptimizationReport = true; class=class="str">"cmt">// Writing results to a file sinput CRITERION_RULE CriterionSelectionRule = RULE_AND; class=class="str">"cmt">// Condition for writing sinput ENUM_STATS Criterion_01 = C_NO_CRITERION; class=class="str">"cmt">// class="num">01 - Criterion name sinput class="type">class="kw">double CriterionValue_01 = class="num">0; class=class="str">"cmt">// ---- Criterion value sinput ENUM_STATS Criterion_02 = C_NO_CRITERION; class=class="str">"cmt">// class="num">02 - Criterion name sinput class="type">class="kw">double CriterionValue_02 = class="num">0; class=class="str">"cmt">// ---- Criterion value sinput ENUM_STATS Criterion_03 = C_NO_CRITERION; class=class="str">"cmt">// class="num">03 - Criterion name sinput class="type">class="kw">double CriterionValue_03 = class="num">0; class=class="str">"cmt">// ---- Criterion value class=class="str">"cmt">//--- Rules for checking criteria enum CRITERION_RULE { RULE_AND = class="num">0, class=class="str">"cmt">// AND RULE_OR = class="num">1 class=class="str">"cmt">// OR }; class=class="str">"cmt">//--- Statistical parameters enum ENUM_STATS { C_NO_CRITERION = class="num">0, class=class="str">"cmt">// No criterion C_STAT_PROFIT = class="num">1, class=class="str">"cmt">// Profit C_STAT_DEALS = class="num">2, class=class="str">"cmt">// Total deals C_STAT_PROFIT_FACTOR = class="num">3, class=class="str">"cmt">// Profit factor C_STAT_EXPECTED_PAYOFF = class="num">4, class=class="str">"cmt">// Expected payoff C_STAT_EQUITY_DDREL_PERCENT = class="num">5, class=class="str">"cmt">// Max. equity drawdown, % C_STAT_RECOVERY_FACTOR = class="num">6, class=class="str">"cmt">// Recovery factor C_STAT_SHARPE_RATIO = class="num">7 class=class="str">"cmt">// Sharpe ratio }; class=class="str">"cmt">//--- Global variables class="type">int AllowedNumberOfBars=class="num">0; class=class="str">"cmt">// For checking the NumberOfBars external parameter value class="type">class="kw">string OptimizationResultsPath=""; class=class="str">"cmt">// Location of the folder for saving folders and files class="type">int UsedCriteriaCount=class="num">0; class=class="str">"cmt">// Number of used criteria class="type">int OptimizationFileHandle=-class="num">1; class=class="str">"cmt">// Handle of the file of optimization results class="type">int criteria[class="num">3]; class=class="str">"cmt">// Criteria for optimization report generation
「EA优化回调钩子的骨架」
在 MT5 里做参数寻优,核心不是算指标,而是先接住平台抛出的几个测试生命周期事件。下面这段代码给出了最薄可用骨架:OnTesterInit 在优化启动时打印一行标记,OnTesterDeinit 在收尾时再打印,中间每跑完一个参数组合会触发 OnTesterPass。 代码里用 LogOptimizationReport 这个布尔开关控制是否写日志,默认不写就不进分支,避免无谓 IO 拖慢寻优。OnTester 里调用了 FrameAdd("Statistics",1,0,stat_values),把 stat_values 数组以帧形式推给优化报告——这是把自定义统计(如夏普、盈利因子)回传可视化的标准入口。 实际验证时,把这段贴进 EA 的 tester 事件区,开 MT5 策略测试器跑 3 个参数组合,能在日志看到 'Start Optimization' 与 'End Optimization' 各出现一次,说明钩子已生效。外汇与贵金属品种波动剧烈,寻优结果仅在历史样本内有效,实盘存在显著回撤风险。
class="type">class="kw">double criteria_values[class="num">3]; class=class="str">"cmt">// Values of criteria class="type">class="kw">double stat_values[STAT_VALUES_COUNT]; class=class="str">"cmt">// Array for testing parameters class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Optimization start | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void OnTesterInit() { Print(__FUNCTION__,"(): Start Optimization \n-----------"); } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Test completion event handler | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- If writing of optimization results is enabled if(LogOptimizationReport) class=class="str">"cmt">//--- class=class="str">"cmt">//--- class="kw">return(class="num">0.0); } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Next optimization pass | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void OnTesterPass() { class=class="str">"cmt">//--- If writing of optimization results is enabled if(LogOptimizationReport) class=class="str">"cmt">//--- } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| End of optimization | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void OnTesterDeinit() { Print("-----------\n",__FUNCTION__,"(): End Optimization"); class=class="str">"cmt">//--- If writing of optimization results is enabled if(LogOptimizationReport) class=class="str">"cmt">//--- } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Test completion event handler | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- If writing of optimization results is enabled if(LogOptimizationReport) { class=class="str">"cmt">//--- Create a frame FrameAdd("Statistics",class="num">1,class="num">0,stat_values); } class=class="str">"cmt">//--- class="kw">return(class="num">0.0); } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Filling the array with test results | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void GetTestStatistics(class="type">class="kw">double &stat_array[]) { class=class="str">"cmt">//--- Auxiliary variables for value adjustment class="type">class="kw">double profit_factor=class="num">0,sharpe_ratio=class="num">0; class=class="str">"cmt">//---
◍ 把回测指标塞进优化帧里
在 EA 的 OnTester 事件里,可以把每轮优化跑出来的核心统计项先收进一个 double 数组,再用 FrameAdd 推到「Statistics」帧,方便后续在优化结果表里横向比对。下面这段就是典型的取值写法:净盈利、成交笔数、盈利因子、期望收益、权益最大回撤百分比、恢复因子、夏普比率依次入列;其中盈利因子和夏普比率若取到 DBL_MAX(即无亏损分母时数学上无穷大),手动压成 0,避免帧数据溢出或图表断点。 回调 OnTesterPass 会在每一遍优化通过后触发,这里用 FrameNext 把刚才的数组按帧读出,并只打印该遍的 pass 编号与 STAT_PROFIT 两位小数,相当于给每遍优化打了一条极简日志。实盘前你在 MT5 策略测试器开「优化」+勾选「帧记录」,跑完就能在结果标签看到 Statistics 帧下的 7 列曲线。 外汇与贵金属品种杠杆高、点差跳变频繁,回测里的恢复因子高于 2.0 只代表样本内风险回收能力较强,换周期或换品种可能明显衰减,需自行交叉验证。
stat_array[class="num">0]=TesterStatistics(STAT_PROFIT); class=class="str">"cmt">// Net profit upon completion of testing stat_array[class="num">1]=TesterStatistics(STAT_DEALS); class=class="str">"cmt">// Number of executed deals class=class="str">"cmt">//--- profit_factor=TesterStatistics(STAT_PROFIT_FACTOR); class=class="str">"cmt">// Profit factor – the STAT_GROSS_PROFIT/STAT_GROSS_LOSS ratio stat_array[class="num">2]=(profit_factor==DBL_MAX) ? class="num">0 : profit_factor; class=class="str">"cmt">// adjust if necessary class=class="str">"cmt">//--- stat_array[class="num">3]=TesterStatistics(STAT_EXPECTED_PAYOFF); class=class="str">"cmt">// Expected payoff stat_array[class="num">4]=TesterStatistics(STAT_EQUITY_DDREL_PERCENT); class=class="str">"cmt">// Max. equity drawdown, % stat_array[class="num">5]=TesterStatistics(STAT_RECOVERY_FACTOR); class=class="str">"cmt">// Recovery factor – the STAT_PROFIT/STAT_BALANCE_DD ratio class=class="str">"cmt">//--- sharpe_ratio=TesterStatistics(STAT_SHARPE_RATIO); class=class="str">"cmt">// Sharpe ratio - investment portfolio(asset) efficiency index stat_array[class="num">6]=(sharpe_ratio==DBL_MAX) ? class="num">0 : sharpe_ratio; class=class="str">"cmt">// adjust if necessary } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Test completion event handler | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">class="kw">double OnTester() { class=class="str">"cmt">//--- If writing of optimization results is enabled if(LogOptimizationReport) { class=class="str">"cmt">//--- Fill the array with test values GetTestStatistics(stat_values); class=class="str">"cmt">//--- Create a frame FrameAdd("Statistics",class="num">1,class="num">0,stat_values); } class=class="str">"cmt">//--- class="kw">return(class="num">0.0); } class=class="str">"cmt">//+--------------------------------------------------------------------+ class=class="str">"cmt">//| Next optimization pass | class=class="str">"cmt">//+--------------------------------------------------------------------+ class="type">void OnTesterPass() { class=class="str">"cmt">//--- If writing of optimization results is enabled if(LogOptimizationReport) { 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 val =class="num">0.0;class=class="str">"cmt">// Single numerical value of the frame class=class="str">"cmt">//--- FrameNext(pass,name,id,val,stat_values); class=class="str">"cmt">//--- Print(__FUNCTION__,"(): pass: "+IntegerToString(pass)+"; STAT_PROFIT: ",DoubleToString(stat_values[class="num">0],class="num">2)); } }