创建 EA 交易优化的自定义标准·进阶篇
📘

创建 EA 交易优化的自定义标准·进阶篇

第 2/3 篇

◍ 用 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 自己跑一遍才知深浅。

MQL5 / C++
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,避免两负得正污染排序。外汇与贵金属杠杆高,这类自定义标准只降低过拟合概率,不保证样本外表现。

MQL5 / C++
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 挂到优化流程里,能在多目标寻参时压住某分项亏损却总分虚高的陷阱。外汇与贵金属品种波动跳空频繁,这类自定义判据回测过拟合概率偏高,上线前务必用样本外数据复核。

MQL5 / C++
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 分钟出结果。外汇与贵金属品种波动剧烈、杠杆高风险,云优化结果仅代表历史样本下的参数倾向,实盘前务必在本地复验。

MQL5 / C++
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);
  }

常见问题

在 EA 里重写 OnTester 函数,返回你算出的综合评分(如风险调整后收益),优化器就会按这个值排序而不是默认利润。
把评分逻辑写进 OnTester,并在策略测试器开启遗传优化,每次回测结束框架自动调用你的标准给这组参数打分。
小布可以读取你的 EA 优化设置并给出判据诊断,也能在云端替你批量跑优化把结果整理成可对比的表。
累乘要求每项指标都达标,任一项接近零则总分趋零,能过滤掉单项极差但被平均掩盖的参数组合。
把评分计算抽成一个带 Calc 方法的类,EA 的 OnTester 里实例化并传交易历史进去,换品种只改构造参数即可。