创建 EA 交易优化的自定义标准·进阶篇
◍ 用 OnTester 喂给遗传算法一个评分
想在 MT5 策略测试器里跑自定义优化,得先认识 OnTester()。它是预定义函数,在每轮 EA 测试快结束时、OnDeinit() 调用之前自动触发,专门用来返回一个 double 类型的评分交给优化器。 关键点:必须用 OnTester() 就得在测试器里开启「Fast genetic based algorithm」优化模式,否则这个函数不会参与排序。遗传优化按降序排代内结果,最大值那组被认为最理想,最差的直接淘汰、不进下一代。 所以写自定义标准时思路要反过来——把你对 EA 的满意程度折算成一个越大越好的数值。比如你关心回撤和收益的混合表现,就在这个函数里算出一个综合分返回,测试器自然会往高分方向进化参数。外汇与贵金属品种波动剧烈,遗传优化只是概率性搜索,高分组合实盘仍可能失效。
「用 OnTester 自定义优化标准跑扇形 EA」
把前文的三均线扇形 EA 拿来做试验,核心改造是去掉指标调用、把计算挪出主循环,只留新柱触发时的信号判断。实测在 2009.06.01–2011.06.03 的 H1 区间上,回测速度比原版快了近三倍。 MT5 里想拿每次回测跑出的统计量做排序,只能从 OnTester() 或 OnDeinit() 调用 TesterStatistics()。原文给的空 OnTester 只 return 0.0,优化时等于没标准。 先试回收系数:用 STAT_PROFIT 除以 STAT_BALANCE_DD。这值 MT5 本身就有,写一遍只是演示钩子怎么挂。注意原文代码没做除零保护,真实 EA 里 max_dd 可能为 0,必须补判断。 更实用的是自拼标准:余额 ×(1/亏损) × 交易次数。亏损取倒数,等价于其他条件相同时亏损越小越好。用 MA1Period 在 100–2000 做遗传优化,最佳结果落在 980–1200 区间,图里能直接看到峰值。 对比最优与最差参数表,获利、亏损和交易次数都被同时考虑进去了,说明自定义标准确实在筛选。MA1Period=1106 的净值曲线在 1106 根 K 上走得很平,外汇和贵金属这类高杠杆品种回测好看不等于实盘稳,上 MT5 自己跑一遍才知深浅。
class="kw">input class="type">class="kw">double Lots = class="num">0.1; class="kw">input class="type">int MA1Period = class="num">200; class=class="str">"cmt">// period of the greatest moving average class="kw">input class="type">int MA2Period = class="num">50; class=class="str">"cmt">// period of the medium moving average class="kw">input class="type">int MA3Period = class="num">21; class=class="str">"cmt">// period of the smallest moving average class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// The handler of the event of completion of another test pass: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">class="kw">double OnTester() { class="kw">return(class="num">0.0); } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// The handler of the event of completion of another test pass: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">class="kw">double OnTester() { class="type">class="kw">double profit = TesterStatistics(STAT_PROFIT); class="type">class="kw">double max_dd = TesterStatistics(STAT_BALANCE_DD); class="type">class="kw">double rec_factor = profit/max_dd; class="kw">return(rec_factor); } class="type">class="kw">double OnTester() { class="type">class="kw">double param = class="num">0.0; class=class="str">"cmt">// Balance max + min Drawdown + Trades Number: class="type">class="kw">double balance = TesterStatistics(STAT_PROFIT); class="type">class="kw">double min_dd = TesterStatistics(STAT_BALANCE_DD); if(min_dd > class="num">0.0) { min_dd = class="num">1.0 / min_dd; } class="type">class="kw">double trades_number = TesterStatistics(STAT_TRADES); param = balance * min_dd * trades_number; class="kw">return(param); }
把优化标准封装成可跑的类
MT5 的云算力让多参数寻优有了实操可能,但本地类的计算必须快,否则一轮遗传优化就可能耗掉数小时。基类 TCustomCriterion 只做两件事:用 criterion_level 区分标准类型,把 GetCriterion 留成虚函数给子类重写。 正比类里,STAT_PROFIT 从负无穷(实际受保证金限制)到正无穷,STAT_TRADES 从 0 到无穷,都是值越大越优。TSimpleCriterion 的构造函数吃一个 ENUM_STATISTICS 枚举,GetCriterion 直接返回 TesterStatistics 对应值,零加工。 反比类如 STAT_BALANCE_DD 用 TSimpleDivCriterion 取倒数,代码里做了 temp>0.0 的除零保护,否则回测遇到 0 回撤会直接崩。TSimpleMinCriterion 带 _min 阈值,temp 小于下限就放弃该组参数——用来砍掉交易次数过少或亏损超标的组合很顺手。 容器 TCustomCriterionArray 基于 CArrayObj,GetCriterion 遍历所有标准相乘;一旦碰到负值立刻 break,避免两负得正污染排序。外汇与贵金属杠杆高,这类自定义标准只降低过拟合概率,不保证样本外表现。
class TCustomCriterion : class="kw">public CObject { class="kw">protected: class="type">int criterion_level; class=class="str">"cmt">// type of criterion class="kw">public: class="type">int GetCriterionLevel(); class="kw">virtual class="type">class="kw">double GetCriterion(); class=class="str">"cmt">// get value of the result of optimization }; class TSimpleCriterion : class="kw">public TCustomCriterion { class="kw">protected: ENUM_STATISTICS stat_param_type; class="kw">public: ENUM_STATISTICS GetCriterionType(); class=class="str">"cmt">// get type of optimized stat. parameter class="kw">public: class="kw">virtual class="type">class="kw">double GetCriterion(); class=class="str">"cmt">// receive optimization result value TSimpleCriterion(ENUM_STATISTICS _stat); class=class="str">"cmt">// constructor }; class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// Constructor: class=class="str">"cmt">//--------------------------------------------------------------------- TSimpleCriterion::TSimpleCriterion(ENUM_STATISTICS _stat) : stat_param_type( _stat ) { criterion_level = class="num">0; } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// Get the result of optimization: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">class="kw">double TSimpleCriterion::GetCriterion() { class="kw">return(TesterStatistics(stat_param_type)); } class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// Get value of the optimization result: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">class="kw">double TSimpleDivCriterion::GetCriterion() { class="type">class="kw">double temp = TesterStatistics(stat_param_type); if(temp>class="num">0.0) { class="kw">return(class="num">1.0/temp); } class="kw">return(class="num">0.0); } class TSimpleMinCriterion : class="kw">public TSimpleCriterion { class="type">class="kw">double min_stat_param; class="kw">public: class="kw">virtual class="type">class="kw">double GetCriterion(); class=class="str">"cmt">// receive optimization result value TSimpleMinCriterion(ENUM_STATISTICS _stat, class="type">class="kw">double _min); }; class=class="str">"cmt">//--------------------------------------------------------------------- class=class="str">"cmt">// Get value of the optimization result: class=class="str">"cmt">//--------------------------------------------------------------------- class="type">class="kw">double TSimpleMinCriterion::GetCriterion() { class="type">class="kw">double temp = TesterStatistics(stat_param_type); if(temp<this.min_stat_param) {
◍ 自定义优化判据的累乘实现
在 MT5 策略测试器的自定义优化框架下,若想把多个子目标合成一个总判据,累乘法比求和更不容易被单一指标刷分。下面这段类实现把数组里每个 TCustomCriterion 的 GetCriterion 结果连乘,任意一项 ≤0 就提前退出返回该负值。 代码里 temp 初值设为 1.0,遍历 this.Total() 个元素,用 temp *= 子对象判据。一旦 temp<=0.0 立即 return(temp),避免无意义的后续乘法。若数组为空则直接返回 0.0,对应无优化目标的退化情形。 开 MT5 新建一个 EA 骨架,把这类 TCustomCriterionArray 挂到优化流程里,能在多目标寻参时压住某分项亏损却总分虚高的陷阱。外汇与贵金属品种波动跳空频繁,这类自定义判据回测过拟合概率偏高,上线前务必用样本外数据复核。
class="kw">return(-class="num">1.0); } class="kw">return(temp); } class TCustomCriterionArray : class="kw">public CArrayObj { class="kw">public: class="kw">virtual class="type">class="kw">double GetCriterion( ); class=class="str">"cmt">// get value of the optimization result }; class="type">class="kw">double TCustomCriterionArray::GetCriterion() { class="type">class="kw">double temp = class="num">1.0; class="type">int count = this.Total(); if(count == class="num">0) { class="kw">return(class="num">0.0); } for(class="type">int i=class="num">0; i<count; i++) { temp *= ((TCustomCriterion*)(this.At(i))).GetCriterion(); if(temp <= class="num">0.0) { class="kw">return(temp); } } class="kw">return(temp); }
「把自定义标准塞进EA优化流程」
想在 MT5 策略测试器里跑自定义优化,核心是把容器类指针接进 EA 生命周期。先在头部把 CustomOptimisation.mqh 包含进来,再声明一个 TCustomCriterionArray 指针,真正的挂载发生在 OnInit:new 出来之后立刻用 CheckPointer 判空,失败直接 return(-1),EA 就不会继续初始化。 标准怎么加?示例里用了三个维度——最大获利 STAT_PROFIT、用余额回撤做除法约束 STAT_BALANCE_DD、以及最少 20 笔交易 STAT_TRADES 门槛(低于此数直接弃用该参数组)。这些 Add 调用写在 OnInit 的初始化段,OnTester 里 return criterion_Ptr.GetCriterion() 把综合评分交回测试器。 收尾别漏了 OnDeinit:用 CheckPointer 确认是 POINTER_DYNAMIC 后 delete 掉容器,避免句柄泄漏。代码逐行拆开看—— #include <CustomOptimisation.mqh> 引入自定义标准框架;TCustomCriterionArray* criterion_Ptr 声明容器指针;new 分配实例;判空失败返 -1;三个 Add 分别挂获利、回撤比、最小交易数(20.0) 标准;GetCriterion 返回评分;析构段检查动态指针后 delete。 实测用云代理跑这套 FanExpertSimple 逻辑,优化 13 分钟出结果。外汇与贵金属品种波动剧烈、杠杆高风险,云优化结果仅代表历史样本下的参数倾向,实盘前务必在本地复验。
class="macro">#include <CustomOptimisation.mqh> TCustomCriterionArray* criterion_Ptr; criterion_array = new TCustomCriterionArray(); if(CheckPointer(criterion_array) == POINTER_INVALID) { class="kw">return(-class="num">1); } criterion_Ptr.Add(new TSimpleCriterion(STAT_PROFIT)); criterion_Ptr.Add(new TSimpleDivCriterion(STAT_BALANCE_DD)); criterion_Ptr.Add(new TSimpleMinCriterion(STAT_TRADES, class="num">20.0)); class="kw">return(criterion_Ptr.GetCriterion()); if(CheckPointer(criterion_Ptr) == POINTER_DYNAMIC) { class="kw">delete(criterion_Ptr); }