MetaTrader 5 中进行测试的原理

MetaQuotes | 16 十月, 2013

为什么我们需要 Strategy Tester(策略测试程序)

进行自动化交易的想法源自交易机器人能够不间断地每天 24 小时、每周 7 天工作。机器人不会疲劳、疑惑或恐惧,它完全不会出现任何心理问题。它足以清晰地对交易规则进行规范化,以各种算法实施它们,机器人已经准备好不知疲倦地工作。但是,首先您必须确信满足以下两个重要条件:

要获得这些问题的答案,我们求助于包含在 MetaTrader 5 客户端中的 Strategy Tester(策略测试程序)。


价格变动生成模式

Expert Advisor (EA) 是一种以 MQL5 语言编写的程序,在每次响应某些外部事件时运行。对于每个预定义事件,EA 都有一个对应的函数(事件处理程序)。

NewTick 事件(价格变动)是 EA 的主要事件,因此,我们需要生成价格变动序列来测试 EA。在 MetaTrader 5 客户端的策略测试程序中实施了 3 种价格变动生成模式:

基本且最详细的模式为 "Every tick"(每一价格变动)模式,其他两种模式是基本模式的简化,并且将通过与 "Every tick"(每一价格变动)模式相比较的形式来说明。为了理解它们之间的差异,请考虑所有三种模式。


"Every Tick"(每一价格变动)

金融工具的历史报价数据以封装的分钟指标柱的形式从交易服务器传输到 MetaTrader 5 客户端。可以从《MQL5 参考》的“组织数据访问”一章获取有关请求的出现和所需时间表的构造的详细信息。

价格历史记录的最小元素是分钟指标柱,您可以从该指标柱获取有关四种价格的信息:

新的一分钟指标柱在新的一分钟开始时并不会打开(秒数变为等于 0),而是在价格变动(至少一点)时打开。下图显示新的交易周的第一分钟指标柱,其开盘时间为 2011 年 1 月 10 日 00:00 点。我们在图上看到的星期五和星期一之间的价格缺口很常见,因为货币汇率甚至在周末也会依据即将出现的新闻而波动。

图 1. 星期五和星期一之间的价格缺口

对于此指标柱,我们仅知道一分钟指标柱是在 2011 年 1 月 10 日零时零分打开的,但是我们不知道秒数。它可能是在 00:00:12 或 00:00:36 打开的(新的一天开始后 12 秒或 36 秒),也有可能在该分钟内的任何一秒打开。但是我们确实知道 EURUSD 在新的一分钟指标柱开盘时的开盘价为 1.28940。

我们也不知道在一秒钟内何时收到对应于所考虑的一分钟指标柱的收盘价的价格变动。我们仅知道一件事情 - 一分钟指标柱的收盘价。对于这一分钟,价格为 1.28958。最高价和最低价的出现时间也未知,但是我们知道最高价和最低价分别为 1.28958 和 1.28940。

为了测试交易策略,我们需要一个据其模拟 EA 交易程序工作的价格变动序列。因此,对于每一分钟指标柱,我们明确知道价格的 4 个控制点。如果一个指标柱只有 4 个价格变动,则这些信息足以进行测试,但是价格变动数量通常大于 4。

因此,需要为在开盘价、最高价、最低价和收盘价之间的价格变动生成额外的控制点。在《MetaTrader 5 客户端策略测试程序中的价格变动生成算法》一文中说明了 "Every tick"(每一价格变动)价格变动生成模式的原理;以下是来自该文的一张图表。

图 2. 价格变动生成算法

用 "Every tick"(每一价格变动)模式进行测试时,在每个控制点都将调用 EA 的 OnTick() 函数。每个控制点是生成序列中的一个价格变动。EA 将接收模拟价格变动的时间和价格,正如其在线工作一样。

重要须知:"Every tick"(每一价格变动)测试模式是最准确的,但同时也最耗时。对于大多数交易策略的初步测试,通常使用其他两种测试模式中的一种就足够了。


"1 Minute OHLC"(一分钟指标柱 OHLC)

"Every tick"(每一价格变动)测试模式是三种模式中最准确的,但同时也最慢。每一价格变动都会有 OnTick() 处理程序的运行,而价格变动的数量可能非常大。对于价格在整个指标柱内都有变动的策略,没关系,有更快但更粗略的模拟模式 - "1 minute OHLC"(一分钟指标柱 OHLC)。

在 "1 minute OHLC"(一分钟指标柱 OHLC)模式中,价格变动序列仅按一分钟指标柱的开盘价 (O)、最高价 (H)、最低价 (L) 和收盘价 (C) 构造,生成的控制点的数量显著减少,测试时间也因而显著减少。OnTick () 函数的启动在按一分钟指标柱 OHLC 价格构造的所有控制点执行。

拒绝在开盘价、最高价、最低价和收盘价之间生成额外的中间价格变动,导致从确定开盘价时起在价格演变过程中刚性决定论的出现。这使创建“测试圣杯”成为可能,显示良好的向上测试平衡图。

在代码资料库 - Grr-al 中显示了一个此类“圣杯”的一个例子。

图 3. Grr-al EA 交易程序,使用 OHLC 价格的特点

该图显示了此 EA 测试的非常具有吸引力的图形。它是如何获得的?我们知道一分钟指标柱的 4 个价格,并且我们也知道第一个是开盘价,最后一个是收盘价。在这两个价格之间是最高价和最低价,并且最高价和最低价的出现顺序是未知的,但是知道最高价大于或等于开盘价(最低价小于或等于开盘价)。

这足以确定收到开盘价的时间,然后分析下一价格变动,以确定最高价或最低价是否在下一价格变动之前出现。如果价格低于开盘价,则我们具有最低价并且在此价格变动买入,下一价格变动将对应于最高价,在该价格我们将停止买入并开始卖出。下一价格变动是最后一个,即收盘价,我们在该价格停止卖出。

如果在此价格之后,我们收到价格高于开盘价的价格变动,则成交的顺序反转。在此“欺骗”模式下处理一分钟指标柱,并且等待下一指标柱。

用历史记录测试此类 EA 时,一切都很顺畅,但是一旦我们在线启动该 EA,则真相开始显现 - 平衡线保持稳定,但是趋势向下。为了曝露这一戏法,我们只需要在 "Every tick"(每一价格变动)模式中运行 EA。

注:如果在粗略测试模式("1 分钟 OHLC" 和 "仅开盘价")中的 EA 测试结果看起来非常好,请务必在 "Every tick" 模式中进行测试。


"Open Prices Only"(仅开盘价)

在此模式中,依据选择用于测试的时间表内的 OHLC 价格生成价格变动。EA 交易程序的 OnTick() 函数仅在指标柱开始时以开盘价运行。由于此特点,止损水平和待办可能在与指定价格不同的价格触发(尤其是在较大时间表内测试时)。而我们有机会快速运行 EA 交易程序的评估测试。

W1 和 MN1 周期是 "Open Price Only"(仅开盘价)模式下价格变动生成的例外:对于这些时间表,为每天的 OHLC 价格而不是每周或每月的 OHLC 价格生成价格变动。

假定我们以 "Open Price Only"(仅开盘价)模式在 EURUSD H1 上测试 EA 交易程序。在此情形中,价格变动(控制点)的总数将不超过测试周期内一小时指标柱的数量的 4 倍。但是仅在一小时指标柱开始处调用 OnTick() 处理程序。正确测试所需的检查在余下的价格变动上进行(对 EA 而言,这些价格变动是“隐藏”的)。

如果没有未平仓位或挂单,则我们不需要在这些隐藏的价格变动上执行这些检查,并且速度可能加快很多。这种 "Open prices only"(仅开盘价)模式非常适合仅在指标柱打开时处理成交、不使用挂单以及止损和获利委托的测试策略。对于此类策略,保持了必要的测试精度。

让我们以标准包中的 Moving Average Expert Advisor 作为 EA 的一个例子,该 EA 可在任何模式下测试。此 EA 的逻辑以这样的方式建立:所有决策都在指标柱打开时进行,立即执行成交,不使用挂单。

在 2010 年 9 月 1 日至 2010 年 12 月 31 日的时间范围内在 EURUSD H1 上运行 EA 测试,并且比较图形。下图显示了来自所有三种模式的测试报告的平衡图。


图. 4. 来自标准包的 Moving Average.mq5 EA 的测试图不依赖测试模式(单击图片可放大)

如您所见,对于来自标准包中的 Moving Average EA,不同模式中的图形完全相同。

在 "Open Prices Only"(仅开盘价)模式中有某些限制:

注:"Open prices only"(仅开盘价)模式具有最快的测试时间,但并不适合所有交易策略。依据交易系统的特性选择需要的测试模式。

为了对价格变动生成模式这一部分做出结论,让我们考虑一下针对 EURUSD,从 2011 年 11 月 1 日 21:00:00 至 2011 年 11 月 1 日 21:30:00 的两个 M15 指标柱的不同价格变动生成模式的视觉比较。

使用 WriteTicksFromTester.mq5 EA 将价格变动保存到不同的文件,并且这些文件名的末尾在 filenamEveryTick、filenameOHLC 和 filenameOpenPrice 输入参数中指定。

图 5. 我们可以为 WriteTicksFromTester Expert Advisor 指定价格变动的开始日期和结束日期(变量 start 和 end)

为了获得具有三个价格变动序列的三个文件(每个遵循 "Every tick"、"1 minute OHLC" 和 "Open prices only" 模式),以对应的模式启动 EA 三次,每次运行一次。然后,使用 TicksFromTester.mq5 指标将来自这三个文件的数据显示在图表中。本文附带了指标代码。


图 6. MetaTrader 5 客户端的策略测试程序中三种不同测试模式下的价格变动生成序列

默认情况下,MQL5 语言的所有文件操作都是在“文件沙箱”中进行的,并且在测试期间,EA 只能访问其自己的“文件沙箱”。为了让指标和 EA 在测试期间能够使用来自一个文件夹的文件,我们使用了标志 FILE_COMMON。来自 EA 的代码示例:

//--- 打开文件
   file=FileOpen(filename,FILE_WRITE|FILE_CSV|FILE_COMMON,";");
//--- 检查文件句柄
   if(file==INVALID_HANDLE)
     {
      PrintFormat("Error in opening of file %s for writing. Error code=%d",filename,GetLastError());
      return;
     }
   else
     {
      PrintFormat("The file will be created in %s folder",TerminalInfoString(TERMINAL_COMMONDATA_PATH));
     }

 为了在指标读取数据,我们也使用了标志 FILE_COMMON。这允许我们避免将必需的文件从一个文件夹手动转移到另一个文件夹。

//--- 打开文件
   int file=FileOpen(fname,FILE_READ|FILE_CSV|FILE_COMMON,";");
//--- 检查文件句柄
   if(file==INVALID_HANDLE)
     {
      PrintFormat("Error in open of file %s for reading. Error code=%d",fname,GetLastError());
      return;
     }
   else
     {
      PrintFormat("File will be opened from %s",TerminalInfoString(TERMINAL_COMMONDATA_PATH));
     }


点差的模拟

卖价和买价之间的价格差异称为点差。在测试期间,并没有对点差建模,而是从历史数据中获取点差。如果历史数据中的点差小于或等于零,则测试代理使用所请求历史数据当时的点差。

在策略测试程序中,点差始终被视为浮动的。即 SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) 始终返回 true。

此外,历史数据包含价格变动值和交易数量。对于数据的存储与检索,我们使用一个特殊的 MqlRates 结构:

struct MqlRates
  {
   datetime time;         // 柱形开始时间
   double   open;         // 开盘价
   double   high;         // 最高价
   double   low;          // 最低价
   double   close;        // 收盘价
   long     tick_volume;  // 每个价格变动的交易量
   int      spread;       // 点差
   long     real_volume;  // 市场交易量
  };

客户端全局变量

在测试期间,也模拟客户端全局变量,但是它们与可通过按 F3 键在客户端中查看的当前的客户端全局变量无关。这意味着在测试期间,客户端所有含有全局变量的操作都在客户端以外进行(在测试代理中)。


测试期间指标的计算

在实时模式中,每一次价格变动都会计算指标值。策略测试程序采用一种符合成本效益的模型来计算指标 - 仅在 EA 刚要运行之前重新计算指标。这意味着指标的重新计算是在调用 OnTick()、OnTrade() 和 OnTimer() 函数之前进行的。

在具体的事件处理程序中是否调用指标都没有关系,在调用事件处理程序之前将会重新计算所有指标(iCustom()IndicatorCreate() 函数创建了这些指标的句柄)。

因此,当在 "Every tick"(每一价格变动)模式下进行测试时,指标的计算在调用 OnTick() 函数之前进行。

如果在 EA 中使用 EventSetTimer() 函数打开了计时器,则将在每次调用 OnTimer() 处理程序之前重新计算指标。因此,使用以非最佳方式编写的指标时,测试时间会大幅增加。

测试期间加载历史记录

在开始测试过程之前,所测试交易品种的历史记录由客户端从交易服务器同步,并载入客户端。第一次,客户端加载一个交易品种的所有可用历史记录以免在以后请求这些数据。以后仅加载新数据。

测试代理在测试刚刚开始之后从客户端接收所测试交易品种的历史记录。如果在测试过程中使用其他工具的数据(例如,它是一个多货币 EA 交易程序),则测试代理在第一次调用此类数据期间从客户端请求需要的历史记录。如果在客户端中有历史数据,则它们将被立即传递到测试代理。如果数据不可用,则客户端向服务器请求并下载数据,然后将数据传递给测试代理。

要计算交易操作的交叉汇率,也需要其他工具的数据。例如,在入金货币为 USD 的 EURCHF 上测试一个策略时,在处理第一个交易操作之前,测试代理从客户端请求 EURUSD 和 USDCHF 的历史数据,尽管策略不包含对这些交易品种的直接使用。

在测试多货币策略之前,建议将所有必要的历史数据下载到客户端。这将避免与所需数据的下载有关的测试/优化延迟。例如,您可以通过打开适当的图表并将它们滚动到历史记录开始处来下载历史记录。《MQL5 参考》中的“组织数据存取”部分提供了一个强行将历史记录下载到客户端的例子。

客户端仅从交易服务器下载一次历史记录,在代理第一次从客户端请求所测试交易品种的历史记录时。以封装形式载入历史记录,以减少流量。
接着,测试代理以封闭形式从客户端接收历史记录。在下一次测试期间,测试程序不从客户端加载历史记录,因为在上一次运行测试程序之后,所需数据已经可用。

多货币测试

策略测试程序允许我们测试在多个交易品种上进行交易的策略。此类 EA 通常被称为多货币 EA,因为最初在以前的平台中,测试仅针对一种交易品种进行。在 MetaTrader 5 客户端的策略测试程序中,我们可以针对所有可用交易品种对交易进行建模。

测试程序在第一次调用交易品种数据期间从客户端(不是从交易服务器)加载所用交易品种的历史记录。

测试代理仅下载缺少的历史记录,以及少量边际以提供必要的历史数据来计算测试开始时的指标。对于时间表 D1 及更短时间,下载的历史记录的最小数量为一年。

因此,如果我们在 2010 年 11 月 1 日至 2010 年 12 月 1 日的时间范围内测试(测试一个月的数据),并且周期为 M15(每个指标柱等于 15 分钟),则客户端将被请求整个 2010 年该工具的历史记录。对于每周时间表,我们将请求 100 个指标柱的历史记录,大约为两年(一年有 52 周)。要在每月时间表内测试,代理将请求 8 年的历史记录(12 个月 x 8 年 = 96 个月)。

如果没有必需的指标柱,则测试的开始日期将自动从过去移到现在,以在开始测试之前提供必需的指标柱储备。

在测试期间也模拟 "Market Watch"(市场报价),用户可以从中获取交易品种信息

默认情况下,在测试开始时,在策略测试程序的 "Market Watch"(市场报价)中仅有一个交易品种 - 测试运行所在的交易品种。在引用时,所有必要的交易品种都自动连接到策略测试程序(不是客户端)的 "Market Watch"(市场报价)。

在开始测试多货币 EA 交易程序之前,必须在客户端的 "Market Watch"(市场报价)中选择测试所需的交易品种加载所需数据。在第一次调用“外来”交易品种之前,其历史数据将在测试代理和客户端之间自动同步。“外来”交易品种指除在其上运行测试的交易品种以外的交易品种

引用“其他”交易品种的数据出现在以下情况中:

第一次调用“其他”交易品种时,测试过程停止,并且从客户端向测试代理下载该交易品种/时间表的历史记录。同时,生成此交易品种的价格变动序列。

依据选择的价格变动生成模式,为每个交易品种生成单独的价格变动序列。您还可以通过在 OnInit() 处理程序中调用 SymbolSelect() 来显式请求所需交易品种的历史记录 - 历史记录的下载将在测试 EA 交易程序刚要开始之前立即进行。

因此,在 MetaTrader 5 客户端中无需额外的工作就能执行多货币测试。只需要在客户端中打开相应交易品种的图表。将从交易服务器自动更新所有需要的交易品种的历史记录,如果包含这些数据的话。


在策略测试程序中模拟时间

在测试期间,本地时间 TimeLocal() 始终等于服务器时间 TimeTradeServer()。而服务器时间始终等于对应于 GMT 时间的时间 - TimeGMT()。通过这种方式,在测试期间,所有这些函数显示相同的时间。

如果没有连接到服务器,则会故意在策略测试程序中让 GMT、本地和服务器时间没有区别。无论是否有连接,测试结果应该始终都是相同的。服务器时间的相关信息不会存储在本地,而是来自服务器。

 

策略测试程序中的 OnTimer() 函数

MQL5 提供处理时间事件的机会。OnTimer() 处理程序的调用与测试模式无关。

这意味着如果正在针对周期 H4 以 "Open prices only"(仅开盘价)模式运行测试,并且 EA 有一个设置为每秒钟调用一次的计时器,则在每个 H4 指标柱打开时,OnTick() 处理程序将被调用一次,并且 OnTimer() 处理程序将被调用 14400 次(3600 秒 x 4 小时)。视 EA 的逻辑而定,它的测试时间将会相应增加。

为了从给定计时器频率检查测试时间的依赖性,编写了一个不含任何交易操作的简单 EA。

//--- 输入参数
input int      timer=1;              // 计时器的值,秒
input bool     timer_switch_on=true; // 打开计时器
//+------------------------------------------------------------------+
//| EA交易初始化函数                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---如果timer_switch_on==true, 运行计时器
   if(timer_switch_on)
     {
      EventSetTimer(timer);
     }
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| EA交易去初始化函数                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 停止计时器
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| 计时函数                                                              |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
// 句柄体为空,不做操作
  }
//+------------------------------------------------------------------+

以不同的时间参数值(Timer 事件的周期)测试时间指标。依据获得的数据,我们以 Timer 周期的函数绘制了测试时间。

图 7. 作为 Timer 周期的函数的测试时间

可以清晰地看到,在相同的其他条件下,在 EventSetTimer(Timer) 函数的初始化期间,参数 timer 的值越小,调用 OnTimer() 处理程序之间的周期 (Period) 就越小,测试时间 T 就越长。


策略测试程序中的 Sleep() 函数

在处理图形时,Sleep() 函数允许 EA 或脚本暂停 mql5 程序的执行一段时间。当请求尚未准备好的数据,并且您需要等待其就绪时,这非常有用。可以在“数据存储安排”部分找到一个使用 Sleep() 函数的详细例子。

测试过程并没有被 Sleep() 调用拖延。当您调用 Sleep() 时,生成的价格变动在指定的延迟时间内“播放”,这可导致挂单、停止等的触发。在调用 Sleep() 之后,在策略测试程序中的模拟时间增加在 Sleep 函数的参数中指定的时间间隔。

如果作为执行 Sleep() 函数的结果,策略测试程序中的当前时间超过测试期,则您将收到一条错误消息,指出 "Infinite Sleep loop detected while testing"(测试时检测到无限休眠循环)。如果您收到此错误信息,测试结果不会被拒绝,所有计算都按它们的最大值(成交数量、沉降等)进行,并且此测试结果被传递到客户端。

Sleep() 函数不会在 OnDeinit() 中工作,因为在其被调用之后,将保证测试时间超过测试间隔的范围。

图 7. 在  MetaTrader 5 客户端的策略测试程序中使用 Sleep() 函数的方案

图 8. 在 MetaTrader 5 客户端的策略测试程序中使用 Sleep() 函数的方案


使用策略测试程序优化数学计算中的问题

MetaTrader 5 客户端中的测试程序不仅可用于测试交易策略,还可用于数学计算。要使用它,必须选择 "Math calculations"(数学计算)模式:

在此情形中,将只调用三个函数:OnInit()、OnTester()、OnDeinit()。在 "Math calculations"(数学计算)模式中,策略测试程序不生成任何价格变动,也不下载历史记录。

如果您指定的开始日期晚于结束日期,则策略测试程序也将在 "Math calculations"(数学计算)模式下工作。

使用测试程序解决数学问题时,不会出现历史的上传和价格变动的生成。

在 MetaTrader 5 策略测试程序中解决的一个典型数学问题 - 搜索一个具有很多变量的函数的极值。

要解决该问题,我们需要:

编译 EA,打开 "Strategy Tester"(策略测试程序)窗口。在 "Input parameters"(输入参数)选项卡中,选择需要的输入变量,并通过为每个函数变量指定开始值、停止值和步长值定义一组参数值。

选择优化类型 - "Slow complete algorithm"(慢速完整算法,完整搜索参数空间)或 "Fast genetic based algorithm"(快速遗传算法)。对于函数极值的简单搜索,最好选择快速优化,但是如果您要计算整组变量的值,则最好使用慢速优化。

选择 "Math calculation"(数学计算模式,并单击 "Start"(开始)按钮来运行优化过程。注意,在进行优化测试时,策略测试程序将搜索函数的最大值。要查找局部最小值,从 OnTester 函数返回计算出来的函数值的反转值:

return(1/function_value);

必须确保 function_value 不等于零,否则我们会得到除以零的致命错误

还有另一种方式,这种方式更加方便,并且不会歪曲优化结果,它是本文的读者建议的:

return(-function_value);

此选项不需要确保 function_value 是否等于零,并且以 3D 表示的优化结果的表面具有相同的形状,但是原来的镜像。

作为一个例子,我们提供 sink() 函数:

用于查找此函数的极值的 EA 代码放在 OnTester() 中:

//+------------------------------------------------------------------+
//|                                                         Sink.mq5 |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//--- 输入参数
input double   x=-3.0; // start=-3, step=0.05, stop=3
input double   y=-3.0; // start=-3, step=0.05, stop=3
//+------------------------------------------------------------------+
//| 测试函数                                                               |
//+------------------------------------------------------------------+
double OnTester()
  {
//---
   double sink=MathSin(x*x+y*y);
//---
   return(sink);
  }
//+------------------------------------------------------------------+
执行优化,并以 2D 图形的形式查看优化结果

图 9. 以 2D 图形表示的 sink (x*x+y*y) 函数的完全优化结果

针对指定参数对 (x, y) 的值越好,颜色越饱和。如从 sink() 公式的形式角度来预期,其值构成以 (0,0) 为中心的多个同心圆。可以在 3D 图中看到,sink() 函数没有单一的全局极值:

Sink 函数的 3D 图形


"Open prices only"(仅开盘价)模式中指标柱的同步

MetaTrader 5 客户端中的测试程序允许我们检查所谓的“多货币” EA。多货币 EA 是一种在两个或多个交易品种上进行交易的 EA。

对在多个交易品种上进行交易的策略的测试,为测试程序施加几个额外的技术要求:

策略测试程序依据选择的交易模式对每个工具生成和播放价格变动序列。同时,针对每个交易品种新指标柱打开,无论在其他交易品种上指标柱是如何打开的。这意味着在测试多货币 EA 时,可能(并且通常)会出现对于某个工具,一个新的指标柱已经打开,而对于其他工具,新指标柱没有打开的情形。因此,在测试中,所有一切都如现实一样发生。

只要使用 "Every tick"(每一价格变动)和 "1 minute OHLC"(一分钟指标柱 OHLC)测试模式,测试程序中这种历史记录的真实模拟就不会造成任何问题。对于这些模式,为一个蜡烛图生成了足够多的价格变动,从而能够等待来自不同交易品种的指标柱发生同步。但是如果交易工具上指标柱的同步是强制的,我们如何在 "Open prices only"(仅开盘价)模式中测试多货币策略呢?在这种模式中,仅在一个价格变动上调用 EA,对应于指标柱打开的时间。

我们将通过一个例子来说明:如果我们在 EURUSD 上测试 EA,并且在 EURUSD 上已经打开了一个新的一小时蜡烛图,则我们可以轻易地确定以下事实:在 "Open prices only"(仅开盘价)模式中,事件 NewTick 对应于测试周期上打开指标柱的时刻。但是不能保证新的蜡烛图在 EA 使用的 USDJPY 交易品种上打开。

在一般情形下,这足以完成 OnTick() 函数的工作以及在下一价格变动检查 USDJPY 上新指标柱的出现。但是在 "Open prices only"(仅开盘价)模式下进行测试时,并没有其他价格变动,因此看起来这种模式不适合用于测试多货币 EA。但事实并不是这样的 - 不要忘记 MetaTrader 5 中的测试程序如同在真实交易中一样工作。您可以使用 Sleep() 函数等待其他交易品种上新的指标柱的打开!

EA Synchronize_Bars_Use_Sleep.mq5 的代码显示了一个在 "Open prices only"(仅开盘价)模式下同步指标柱的例子:

//+------------------------------------------------------------------+
//|                                   Synchronize_Bars_Use_Sleep.mq5 |
//|                        Copyright 2011, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//--- 输入参数
input string   other_symbol="USDJPY";
//+------------------------------------------------------------------+
//| EA交易初始化函数                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 检查交易品种
   if(_Symbol==other_symbol)
     {
      PrintFormat("You have to specify the other symbol in input parameters or select other symbol in Strategy Tester!");
      //--- 强制停止测试
      return(-1);
     }
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| EA的tick函数                                                          |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 静态变量,用于存储最近一个柱形的时间
   static datetime last_bar_time=0;
//--- 同步标识
   static bool synchonized=false;
//--- 如果静态变量没有初始化
   if(last_bar_time==0)
     {
      //---  如果是第一次调用,存储柱形的时间并退出
      last_bar_time=(datetime)SeriesInfoInteger(_Symbol,Period(),SERIES_LASTBAR_DATE);
      PrintFormat("The last_bar_time variable is initialized with value %s",TimeToString(last_bar_time));
     }
//--- 获取图表上交易品种最近柱形的开盘时间
   datetime curr_time=(datetime)SeriesInfoInteger(Symbol(),Period(),SERIES_LASTBAR_DATE);
//--- 如果时间不相等
   if(curr_time!=last_bar_time)
     {
      //--- 将柱形的开盘时间存储到静态变量中
      last_bar_time=curr_time;
      //--- 未同步
      synchonized=false;
      //--- 打印消息
      PrintFormat("A new bar has appeared on symbol %s at %s",_Symbol,TimeToString(TimeCurrent()));
     }
//--- 其他交易品种柱形的开盘时间
   datetime other_time;
//--- 循环检测直到其他交易品种的开盘时间和当前时间一致
   while(!(curr_time==(other_time=(datetime)SeriesInfoInteger(other_symbol,Period(),SERIES_LASTBAR_DATE)) && !synchonized))
     {
      PrintFormat("Waiting 5 seconds..");
      //---等待5秒并调用SeriesInfoInteger(other_symbol,Period(),SERIES_LASTBAR_DATE)
      Sleep(5000);
     }
//--- 柱形同步成功
   synchonized=true;
   PrintFormat("Open bar time of the chart symbol %s: is %s",_Symbol,TimeToString(last_bar_time));
   PrintFormat("Open bar time of the symbol %s: is %s",other_symbol,TimeToString(other_time));
//--- 不使用TimeCurrent(),用TimeTradeServer()
   Print("The bars are synchronized at ",TimeToString(TimeTradeServer(),TIME_SECONDS));
  }
//+------------------------------------------------------------------+

 请注意 EA 中的最后一行,该行显示在同步事实成立时的当前时间:

   Print("The bars synchronized at ",TimeToString(TimeTradeServer(),TIME_SECONDS));

要显示当前时间,我们使用 TimeTradeServer () 函数而不是 TimeCurrent ()。TimeCurrent() 函数返回上一个价格变动的时间,而该价格变动不会在使用 Sleep() 函数之后改变。在 "Open prices only"(仅开盘价)模式下运行 EA,您将看到一条有关指标柱同步的消息。

如果您需要获得当前服务器时间而不是上一个价格变动到达的时间,请使用 TimeTradeServer() 函数而不是 TimeCurrent() 函数。

还有另一种方式来同步指标柱 - 使用计时器。本文附带的 Synchronize_Bars_Use_OnTimer.mq5 提供了此类 EA 的一个例子。


测试程序中的 IndicatorRelease() 函数

在完成一次测试之后,工具的一个图表自动打开,显示完成的成交以及在 EA 中使用的指标。这有助于通过图形方式检查进入点和退出点,并将它们与指标的值进行比较。

注:显示在测试完成后自动打开的图表中的指标,是在测试完成后重新计算的。即使所测试的 EA 使用了这些指标。

但是在某些情形中,程序员可能希望隐藏有关在交易算法中涉及哪些指标的信息。例如,作为不提供源代码的执行文件出租或销售的 EA 代码。IndicatorRelease() 函数适合此目的。

如果客户端在客户端的 directory/profiles/templates 中设置了一个名为 tester.tpl 的模板,则该模板将被应用到打开的图表。如果没有该模板,则应用默认模板(default.tpl)。

IndicatorRelease() 函数最初用于在不再需要时释放指标的计算部分。这让您能够节省内存和 CPU 资源,因为每个价格变动都要调用指标计算。它的第二个目的是禁止在单次测试运行之后于测试图表上显示指标。

要禁止在测试之后于图表上显示指标,在 OnDeinit() 处理程序中通过指标的句柄调用 IndicatorRelease()。OnDeinit() 函数始终在测试完成之后并且在测试图表显示之前调用。

//+------------------------------------------------------------------+
//| EA交易去初始化函数                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   bool hidden=IndicatorRelease(handle_ind);
   if(hidden) Print("IndicatorRelease() successfully completed");
   else Print("IndicatorRelease() returned false. Error code ",GetLastError());
  }
为了禁止在单次测试完成之后于图表上显示指标,请在 OnDeinit() 处理程序中使用 IndicatorRelease() 函数。


测试程序中的事件处理

EA 中存在 OnTick() 处理程序并不是强制的,这样使其能够在 MetaTrader 5 测试程序中测试历史数据。对 EA 而言,包含以下至少一个函数处理程序就够了:

在一个 EA 中测试时,我们可以使用 OnChartEvent() 函数处理自定义事件,但是在指标中,不能在测试程序内调用此函数。即使指标具有 OnChartEvent() 事件处理程序并且此指标在所测试 EA 中使用,指标本身也不会接收任何自定义事件。

在测试期间,指标可通过 EventChartCustom() 函数生成自定义事件,并且 EA 能够在 OnChartEvent() 中处理此事件。


测试代理

MetaTrader 5 客户端中的测试是通过测试代理进行的。自动创建和启用本地代理。本地对象的默认数量对应于计算机内核的数量。

每个测试代理拥有其自己的 全局变量的副本,这些副本与客户端没有关系。客户端本身是调度程序,将任务分配到本地和远程代理。在一个 EA 上依据给定参数执行测试任务之后,代理将结果返回到客户端。一次测试仅使用一个代理。

代理在单独的文件夹中按工具名称存储从客户端收到的历史记录,因此,EURUSD 的历史记录存储在名为 EURUSD 的文件夹中。此外,按它们的来源区分工具的历史记录。用于存储历史记录的结构看起来如下所示:

tester_catalog\Agent-IPaddress-Port\bases\name_source\history\symbol_name

例如,来自服务器 MetaQuotes-Demo 的 EURUSD 的历史记录可以存储在文件夹 tester_catalog\Agent-127.0.0.1-3000\bases\MetaQuotes-Demo\EURUSD 中。

本地代理在测试完成之后进入待机模式,等待下一任务另外 5 分钟,这样不会浪费针对下一次调用的启动时间。只有在等待期结束后,本地代理才会关闭,并且从 CPU 内存中卸载。

如果用户提前完成测试(使用 "Cancel"(取消)按钮),或者客户端关闭,则所有本地代理将立即停止它们的工作并从内存中卸载。


客户端和代理之间的数据交换

当您运行测试时,客户端准备向代理发送若干参数块:

为每个参数块创建一个采用 MD5 哈希形式的数字指纹,该指纹被发送到代理。MD5 哈希对每一个参数块都是唯一的,其大小比其计算的信息量小很多倍。

代理收到参数块的哈希,并将它们与已经存在的哈希进行比较。如果给定参数块的指纹在代理中不存在,或者收到的哈希与现有哈希不同,则代理请求此参数块。这会减少客户端和代理之间的流量。

在测试之后,代理向客户端返回所有运行结果,这些结果显示在 "Test Results"(测试结果)和 "Optimization Results"(优化结果)选项卡中:收到的盈利、成交数量、夏普系数、OnTester() 函数的结果等。

在优化期间,客户端以小包的形式将测试任务分配给代理,每个包内含几个任务(每个任务指具有一组输入参数的单个测试)。这会减少客户端和代理之间的交换时间。

出于安全原因,代理从不会将从客户端获得的 EX5 文件(EA、指标、库等)记录到硬盘,因此,含有正在运行的代理的计算机不能使用发送的数据。所有其他文件,包括 DLL,都记录在沙箱中。在远程代理中,您不能使用 DLL 测试 EA。

客户端将测试结果添加到一个特殊的结果缓存中,以便在需要时快速存取它们。对于每一组参数,客户端搜索结果缓存中是否已经存在来自上一次运行的可用结果,以免重复运行。如果找不到具有此组参数的结果,则向代理分配进行测试的任务。

客户端和代理之间的所有流量都是加密的。


使用所有客户端的共享文件夹

所有测试代理都是相互隔离的,也与客户端是隔离的:每个代理有其自己的文件夹来记录其日志。此外,在代理的测试期间进行的所有文件操作都是在文件夹 agent_name/MQL5/Files 中进行的。但是,我们可以通过所有客户端的共享文件夹,在本地代理和客户端之间实施互动,如果在文件打开期间您指定标志 FILE_COMMON 的话:

//+------------------------------------------------------------------+
//| EA交易初始化函数                                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 所有客户端的共享文件夹
   common_folder=TerminalInfoString(TERMINAL_COMMONDATA_PATH);
//--- 打印此文件夹的名称
   PrintFormat("Open the file in the shared folder of the client terminals %s", common_folder);
//--- 在共享文件夹(由FILE_COMMON标识)下打开一个文件
   handle=FileOpen(filename,FILE_WRITE|FILE_READ|FILE_COMMON);
   ... further actions
//---
   return(0);
  }


使用 DLL

为了加速优化,我们不仅可以使用本地代理,也可以使用远程代理。在此情形下,对远程代理有某些限制。首先,远程代理不在它们的日志中显示 Print() 函数的执行结果以及有关建仓和平仓的消息。在日志中显示最少的信息以防止不正确编写的 EA 用消息破坏远程代理工作所在的计算机。

第二个限制 - 禁止在测试 EA 时使用 DLL。出于安全原因,在远程代理中绝对禁止调用 DLL。在本地代理中,只有在具备适当的“允许导入 DLL”权限时才允许在所测试的 EA 中调用 DLL。

图 10. MQL5 程序中的 "Allow import DLL"(允许导入 DLL)选项

注:当使用收到的需要允许 DLL 调用的 EA(脚本、指标)时,您应意识到风险;若您在客户端的设置中允许此选项,则这些风险由您承担。无论将如何使用 EA,用于测试或用于在图表上运行。


总结

本文介绍基础知识,这些知识将帮助您快速掌握在 MetaTrader 5 客户端中测试 EA:

策略测试程序的主要任务是让 MQL5 程序员用最少的工作量确保所需的数据精确度。开发人员已经做了很多工作,因此您不必仅仅是为了用历史数据测试您的交易策略而重写您的代码。知道测试的基础知识就已足够,正确编写的 EA 在测试程序中以及在线模式中都能对图表进行相同的处理。