OnTester 事件

在完成对历史数据的 EA 交易测试(由用户启动的单独测试运行和优化期间由测试程序自动启动的多次运行之一)后,将生成 OnTester 事件。要处理 OnTester 事件,MQL 程序必须在其源代码中有相应的函数,但这不是必需的。即使没有 OnTester 函数,也可以依据标准准则成功优化 EA 交易。

该函数只能在 EA 交易中使用。

double OnTester()

该函数旨在计算 double 类型的某个值,用作自定义优化准则 (Custom max)。准则选择对于成功的遗传优化是重要的,同时它也允许用户估算和比较不同设置的效果。

在遗传优化中,结果在一代内按准则降序排列。也就是说,根据优化准则,具有最高值的结果认为是最好的。这种排序中最差的值随后被弃用,并且不参与下一代的形成。

请注意,只有在测试程序设置中选择了自定义准则时,才会考虑 OnTester 函数返回的值。OnTester 函数的可用性并不自动意味着其可被遗传算法使用。
 
无法通过 MQL5 API 来以编程方式判断用户在测试程序设置中选择了哪个优化准则。有时候,了解这一点非常重要,以便将自己的分析算法应用到后处理优化结果中。

在调用 OnDeinit 函数之前,内核仅在测试程序中调用此函数。

为了计算返回值,我们可以使用通过 TesterStatistics 函数及其任意计算获得的标准统计数据。

BandOsMA.mq5 EA 交易中,我们创建了 OnTester 处理程序,其考虑了几个指标:利润、止盈能力、交易次数和夏普比率。接下来,我们在计算每个指标的平方根后,将所有指标相乘。当然,在构建此类通用质量准则方面,每个开发人员可能有他们自己的偏好和想法。

double sign(const double x)
{
   return x > 0 ? +1 : (x < 0 ? -1 : 0);
}
   
double OnTester()
{
   const double profit = TesterStatistics(STAT_PROFIT);
   return sign(profit) * sqrt(fabs(profit))
      * sqrt(TesterStatistics(STAT_PROFIT_FACTOR))
      * sqrt(TesterStatistics(STAT_TRADES))
      * sqrt(fabs(TesterStatistics(STAT_SHARPE_RATIO)));
}

单元测试日志会显示包含 OnTester 函数值的行。

我们启动 2021 年 EURUSD,H1 的 EA 交易遗传优化,选择指标参数和止损大小(本书附带的 MQL5/Presets/MQL5Book/BandOsMA.set 文件)。为了检查优化的质量,我们还将包括从 2022 年初(5 个月)开始的递进测试。

首先,我们根据我们的准则进行优化。

如你所知,除了优化过程中使用的当前准则,MetaTrader 5 还在优化结果中保存了所有标准准则。这样,在优化完成后,可通过从带有表格的面板右上角的下拉菜单中选择某些准则来分析不同点的结果。因此,尽管我们根据自己的标准进行了优化,但我们也可以使用最有趣的内置复杂准则。

我们可以将优化表导出到一个 XML 文件中,首先使用我们选择的准则,然后使用复杂准则为文件命名(遗憾的是,只有一个准则被写入导出文件;重要的是不要改变两次导出之间的排序)。这使得在外部程序中可组合两个表并构建一个图表成为可能,在该图表上沿轴绘制两个准则;每个点表示一次运行中的准则组合。

自定义准则和复杂优化准则的比较

自定义准则和复杂优化准则的比较

在一个复杂准则中,我们观察到一个多层次结构体,因为它是根据一个有条件的公式计算的:在某处一个分支工作,在另一处另一个分支工作。我们的自定义准则总是使用相同的公式计算。我们还注意到在我们的准则中存在负值(这是意料之中的),并且复杂准则的声明范围为 0-100。

我们通过分析递进测试的值来检查我们的准则是否完善。

优化和递进测试期间的自定义准则值

优化和递进测试期间的自定义准则值

正如预期的那样,只有一部分良好的优化指标保持继续使用。但我们更感兴趣的不是准则,而是利润。我们来看看在优化与远期测试过程中盈利的分布情况。

优化和远期测试期间的利润

优化和远期测试期间的利润

此处的图呈现类似规律。在优化期盈利的 6850 轮测试中,有 3123 轮在远期测试中也实现了盈利(占比 45%)。而在前 1000 轮最优测试结果中,仅有 323 轮在远期测试中盈利,这一结果并不理想。因此,该 EA 交易需要做大量的工作来确定稳定的盈利设置。但也许是优化准则的问题呢?

我们重复优化,这次使用内置的复杂准则。

注意!MetaTrader 5 在优化过程中会在以下目录中生成 Tester/cache 优化缓存,即 opt 文件。当开始下一次优化时,它会寻找合适的缓存来继续优化。如果存在具有先前设置的缓存文件,则该过程不会从头开始,而是会考虑先前的结果。这允许你建立遗传优化链,假设你找到了最好的结果(毕竟,每个遗传优化都是一个随机过程)。
 
MetaTrader 5 没有将优化准则作为设置中的一个区分因素。根据前面所述,这在某些情况下可能是有用的,但它会干扰我们当前的任务。要进行纯实验,我们需要从头开始优化。因此,在使用我们的标准进行第一次优化之后,我们不能立即使用复杂准则启动第二次优化。
 
无法从终端界面禁用当前行为。因此,你应在任何文件管理器中手动删除或重命名(更改扩展名)以前的 opt 文件。稍后,我们将熟悉测试程序的预处理程序指令 tester_no_cache,该指令可以在特定 EA 交易的源代码中指定,允许你禁用缓存读取。

优化和远期测试期间的复合准则值比较采用以下形式。

优化和远期测试期间的复杂准则

优化和远期测试期间的复杂准则

以下是远期测试期间利润的稳定性。

优化和远期测试期间的利润

优化和远期测试期间的利润

在历史内的 5952 个正向结果中,只有 2655 个(也是约 45%)在远期测试中保持盈利但是在最初的 1000 个正向结果中,有 581 在远期测试中表现成功。

因此,我们已经看到,从技术角度来看,使用 OnTester 非常简单,但我们的准则比内置准则(其他条件不变)更差,尽管内置准则也并非完美。因此,无论是探索评估准则的公式本身,还是在不预知未来的情况下合理选择参数,OnTester 自身还有很多值得探索的地方。

至此,编程已逐渐演变为研究与科学探索,这超出了本书的范畴。但是我们将给出一个根据我们自己的度量指标(而不是现成的度量值)比较计算的准则示例:TesterStatistics。我们将讨论 R2 准则,也称为决策系数 (RSquared.mqh)。

我们创建一个通过余额曲线计算 R2 的函数。众所周知,当以固定手数交易时,理想的交易系统应以直线的形式显示余额。我们现在使用固定手数,因此更适合我们目前的情况。对于 R2,在可变手数的情况下,我们将稍后再做讨论。

最后,相对于建立在数据基础上的线性回归,R2 为数据方差的倒数。R2 值的范围为从负无穷大到 +1(尽管在我们的示例中不太可能出现大的负值)。显然,找到的线同时具有斜率的特性,因此,为了使代码通用化,我们将保存 R2A 结构体中的 R2 和角度的正切值作为中间结果。

struct R2A
{
   double r2;    // square of correlation coefficient
   double angle// tangent of the slope
   R2A(): r2(0), angle(0) { }
};

指标的计算是在 RSquared 函数中执行的,该函数将一个数据数组作为输入,并返回一个 R2A 结构体。

R2A RSquared(const double &data[])
{
   int size = ArraySize(data);
   if(size <= 2return R2A();
   double xydiv;
   int k = 0;
   double Sx = 0Sy = 0Sxy = 0Sx2 = 0Sy2 = 0;
   for(int i = 0i < size; ++i)
   {
      if(data[i] == EMPTY_VALUE
      || !MathIsValidNumber(data[i])) continue;
      x = i + 1;
      y = data[i];
      Sx  += x;
      Sy  += y;
      Sxy += x * y;
      Sx2 += x * x;
      Sy2 += y * y;
      ++k;
   }
   size = k;
   const double Sx22 = Sx * Sx / size;
   const double Sy22 = Sy * Sy / size;
   const double SxSy = Sx * Sy / size;
   div = (Sx2 - Sx22) * (Sy2 - Sy22);
   if(fabs(div) < DBL_EPSILONreturn R2A();
   R2A result;
   result.r2 = (Sxy - SxSy) * (Sxy - SxSy) / div;
   result.angle = (Sxy - SxSy) / (Sx2 - Sx22);
   return result;
}

为了优化,我们需要一个标准值,此处的角度是很重要的,因为负斜率的平滑资金曲线可能仍会获得较高 R2 值。因此我们将再编写一个函数,该函数将对任何具有负角度的 R2 估算值取相反数。我们采用 R2 模的值,因为在非常差的(分散的)数据不适合我们的线性模型的情况下,该值本身可以是负的。因此,我们必须防止出现负负得正的情况。

double RSquaredTest(const double &data[])
{
   const R2A result = RSquared(data);
   const double weight = 1.0 - 1.0 / sqrt(ArraySize(data) + 1);
   if(result.angle < 0return -fabs(result.r2) * weight;
   return result.r2 * weight;
}

此外,我们的准则考虑了系列的大小,其对应于交易次数。因此,交易次数的增加需要增加指标。

有了这个工具,我们将在 EA 交易中实现资金曲线计算函数,并为该函数找到 R2。最后,我们将该值乘以 100,从而将标度转换为内置复杂准则的范围。

#define STAT_PROPS 4
   
double GetR2onBalanceCurve()
{
   HistorySelect(0LONG_MAX);
   
   const ENUM_DEAL_PROPERTY_DOUBLE props[STAT_PROPS] =
   {
      DEAL_PROFITDEAL_SWAPDEAL_COMMISSIONDEAL_FEE
   };
   double expenses[][STAT_PROPS];
   ulong tickets[]; // only needed because of the 'select' prototype, but useful for debugging
   
   DealFilter filter;
   filter.let(DEAL_TYPE, (1 << DEAL_TYPE_BUY) | (1 << DEAL_TYPE_SELL), IS::OR_BITWISE)
      .let(DEAL_ENTRY,
      (1 << DEAL_ENTRY_OUT) | (1 << DEAL_ENTRY_INOUT) | (1 << DEAL_ENTRY_OUT_BY),
      IS::OR_BITWISE)
      .select(propsticketsexpenses);
   
   const int n = ArraySize(tickets);
   
   double balance[];
   
   ArrayResize(balancen + 1);
   balance[0] = TesterStatistics(STAT_INITIAL_DEPOSIT);
   
   for(int i = 0i < n; ++i)
   {
      double result = 0;
      for(int j = 0j < STAT_PROPS; ++j)
      {
         result += expenses[i][j];
      }
      balance[i + 1] = result + balance[i];
   }
   const double r2 = RSquaredTest(balance);
   return r2 * 100;
}

OnTester 处理程序中,我们可使用条件编译指令下的新标准,因此我们需要取消源代码开头的 #define USE_R2_CRITERION 指令的注释。

double OnTester()
{
#ifdef USE_R2_CRITERION
   return GetR2onBalanceCurve();
#else
   const double profit = TesterStatistics(STAT_PROFIT);
   return sign(profit) * sqrt(fabs(profit))
      * sqrt(TesterStatistics(STAT_PROFIT_FACTOR))
      * sqrt(TesterStatistics(STAT_TRADES))
      * sqrt(fabs(TesterStatistics(STAT_SHARPE_RATIO)));
#endif      
}

我们删除之前的优化结果(带缓存的 opt 文件),并启动 EA 交易的新优化:根据 R2 准则。

当比较 R2 准则和复杂准则的值时,我们可以说它们之间的“趋同性”提高了。

自定义准则 R2 与复杂内置准则的比较

自定义准则 R2 与复杂内置准则的比较

对于相应的参数组,优化窗口中和远期测试的 R2 标准的值如下所示。

优化和远期测试期间的 R2 标准

优化和远期测试期间的 R2 标准

此处是如何将过去和未来的利润结合在一起的说明。

优化和远期测试期间的利润

R2 优化和远期测试期间的利润

统计数据如下:在最近 5582 轮盈利的测试中,有 2638 次(47%)在远期测试中仍保持盈利;而在前 1000 次盈利最高的测试中,有 566 次在远期测试中盈利,这一表现与内置复杂准则相当。

如上所述,统计数据为下一次更智能的优化阶段提供了原始资料,这不仅仅是一项编程任务。我们将专注于优化的其他纯编程方面。