下载MetaTrader 5
MQL5参考MQL5 程序测试交易策略 

测试交易策略

自动交易的理念是每周7天,全天候24小时的无间断自动交易。自动交易不会疲惫,受质疑或让人恐惧,它完全脱离了任何心理问题。只要清楚形式化交易规则并在算法中实施就已经足够,并且自动交易可以持续性无疲惫感的工作。但是,首先,您必须确保满足以下重要的两点:

  • EA交易根据交易系统的规则执行 交易操作
  • EA实施的交易策略,在历史记录上显示利润。

若要获得这些问题,我转到MetaTrader 5客户端的 策略测试

这部分涵盖程序测试的特点和策略测试的优化:

 

策略测试中的函数限制

客户端的策略测试中,对一些函数有操作限制。

Print() 和 PrintFormat() 函数

为了提高性能,优化自动交易参数时不能执行 Print()PrintFormat() 函数。除非在OnInit()处理程序内部使用这些函数。这可以使您在错误发生时,轻松找到错误的原因。

Alert(),MessageBox(),PlaySound(),SendFTP,SendMail(),SendNotification(),WebRequest() 函数

专为与“外部世界”互动而设计的 Alert()MessageBox()PlaySound()SendFTP()SendMail()SendNotification()WebRequest() 函数不在策略测试中执行。

 

订单号生成模式

EA交易是一个用MQL5编写的程序,每次运行都会反映一些外部事件 。EA有一个对应的函数(event handler),用于每次预定义事件。

NewTick 事件(价格变化)是EA的主要事件,因此,我们需要生成一个订单号序列来测试EA。MetaTrader 5客户端的策略测试中共实施三种订单号生成模式:

  • 每个订单号
  • 1 分钟 OHLC (分钟柱的OHLC 价)
  • 只有开盘价

最基本最详细的就是“每个订单号”模式,其他两种模式是该基本模式的简化,并且将被对应“每个订单号”模式进行描述。考虑这三种模式,以便于了解其中的不同。

"Every Tick"

金融工具的历史报价数据以打包分钟柱的形式从交易服务器转移到MetaTrader 5 客户端。发生请求和建设所需时间帧的详细信息可以从MQL5参考的组织数据访问 章节获得。

价格历史的最小元素就是分钟柱,从这里您可以获得四个价格值的信息:

  • Open - 分钟柱的开盘价;
  • High - 该分钟柱期间的最高价;
  • Low - 该分钟柱期间的最低价;
  • Close - 分钟柱的收盘价。

新的分钟开始时不会打开新的分钟柱(秒数归零),但是订单号产生时 - 价格变化至少一点时。数字显示新交易周的首个分钟柱,开盘时间是2011.01.10 00:00。我们在图表上看到的周五和周一的价格差,很普遍,因为汇率甚至周末的波动以反馈收到的新闻。

The price gap between Friday and Monday

对于该柱,我们只知道分钟柱在2011.01.10的零点零分打开,但是我们不知道秒数。它可以在00:00:12或00:00:36打开(新一天后的12或36秒)或在那分钟的任何其他时间。但是我们知道EURUSD在新分钟柱开盘时间的开盘价是1.28940 。

我们也不知道一秒钟内,何时收到受考虑分钟柱的收盘价的订单号。我们只知道一件事 - 分钟柱的最近收盘价。该分钟内,价格是1.28958。最高价和最低价出现的时间也不知道,但是我们知道最高价格和最低价格分别为 1.28958 和 1.28940水平。

要测试交易策略,我们需要一系列订单号,将在这里模拟EA交易。因此,对于每个分钟柱,我们都知道 4 个控制点,在这里价格可以清楚显示。如果一个柱只有4个订单号,那么就足够执行一个测试,但是通常订单交易量要大于4。

因此,在开盘价,最高价,最低价和收盘价之间,需要生成额外的订单号控制点。“每个订单号”的订单号生成模式的原则显示在 MetaTrader 5 程序端策略测试器中的订单号生成算法 ,数字显示如下。

Ticks generation algorithm

在“每个订单号”模式中测试时,EA的 OnTick() 函数在每个控制点都将会调用。每个控制点都是生成序列的订单号。EA将会接收模拟订单号的时间和价格,就如在线工作一样。

重要: “每个订单号”测试模式是最精确的,但是同时也是最耗时的。对于主要交易策略的初始测试,使用其他两个测试模式中的一个就已经够用。

"1 分钟 OHLC"

“每个订单号”模式是三个模式中最精确的,但是同时,也是最慢的。运行 OnTick() 处理程序在每个订单号都会产生,订单交易量可以非常巨大。对于一个策略,订单号价格波动序列并不重要,因为有更快更粗糙的模拟模式 - "1分钟 OHLC"。

在 "1 分钟 OHLC" 模式,订单号序列仅通过分钟柱的OHLC价格搭建,生成的控制点数量也会大量减少 - 因此,这是测试时间。在所有控制点上启动OnTick () 函数,这个可以通过OHLC分钟柱的价格来搭建。

拒绝在开盘价,最高价,最低价和收盘价之间生成额外的中间订单号,会导致出现从开盘价决定时刻起的价格发展的刚性决定论。这使得可以创建“测试盘”,它显示了一个不错的向上的测试平衡图形。

该大盘在代码库中显示的示例 - Grr-al.

The Grr-al Expert Advisor, it uses the special features of OHLC prices

数字显示的EA测试图形极具吸引力。它是如何获得的呢?我们知道一个分钟柱有4个价格,并且我们也知道第一个是开盘价,最后的是收盘价。两个价格中间,我们有最高价和最低价,而发生的顺序我们不得而知,但是我们知道,最高价大于或等于开盘价(而最低价小于或等于开盘价)。

它已足以确定接收开盘价的时间,然后分析下一个订单号以便决定我们此刻的价格 - 最高价还是最低价。如果价格低于开盘价,那么这是最低价并在此刻买入,下一个订单号将相当于最高价,在这里我们关闭购买打开卖出。下一个订单号是最后一个,就是收盘价,我们关闭上面的卖出交易。

如果此价格后,我们收到价格大于开盘价的订单号,那么交易顺序相反。在该“欺骗”模式处理分钟柱,并等候下一个。

当在历史记录测试这种EA时,一切进展都很顺利,但是一旦在线启用,真实情况就开始显露 - 平衡线保持稳定,但向下走。若要识破这个诡计,我们只需在“每个订单号”模式运行EA。

注意: 如果粗略测试模式下的EA测试结果(“1分钟OHLC”和“只有开盘价”)似乎很好,一定要在“每个订单号”模式再测试一次。

"只有开盘价"

在该模式下,订单号的生成基于选为测试的时间帧的OHLC价格。EA交易的OnTick() 函数只在柱开始的开盘价运行。鉴于这个特点,止损水平和挂单可能会在不同于指定价格的价格点触动(尤其在高时间帧测试时)。相反,我们有机会快速运行EA评估测试。

W1和MN1周期在“只有开盘价”订单号生成模式是个例外:这些时间帧的订单号在每日的OHLC价格生成,而非每周或每月的OHLC价格。

假定我们在“只有开盘价”模式测试EURUSD H1的EA交易。在这个情况下,测试间隔内,订单号总数(控制点)将不会超过4*小时柱数量。但是OnTick() 处理程序调用仅在打开小时柱的时候。 正确测试所需的检查在其他订单号时期出现(从EA“隐藏”)。

  • 计算所需预付款;
  • 启动止损和获利水平;
  • 启动挂单;
  • 移除过期的挂单。

如果没有持仓或挂单,我们无需在隐藏订单号执行这种检查,提高速度可能就是安静的本质。该“只有开盘价”模式很好的适应了测试策略,处理交易仅在柱的开始进行,而不使用挂单,以及止损和获利订单。对于这种策略的类,保护必要的测试精确性。

让我们使用来自标准组件的移动平均EA交易作为EA示例,它可以在任何模式下被测试。该EA按照所有决策都在开柱时制定的方式建立,并且立即执行交易,无需使用挂单。

在2010.09.01到2010.12.31期间运行EURUSD H1的EA测试,并比较图形。数字显示了所有三种模式测试报告的结余图。

The testing graph of the Moving Average.mq5 EA from the standard package does not depend on the testing mode

如您所见,不同测试模式的图形与标准组件的移动平均EA的图形完全相同。

在“只有开盘价”模式有一些限制:

  • 您不能使用 随机延迟执行模式
  • 在测试的EA交易,您不能访问低于用于测试/优化的时间帧数据。例如,如果您在H1周期运行测试/优化,您可以访问H2,H3,H4等等的数据,但不可以访问M30,M20,M10数据。另外,访问的高级时间帧也必须是测试时间帧的倍数。例如,如果您在M20运行测试,您不能访问M30的数据,但是可以访问H1。这些限制与不能获得较低的数据或测试/优化期间生成的非倍数时间帧有关。
  • 访问其他时间帧数据的限制也用于其他EA事宜的交易品种数据。在这个方面,每个交易品种的限制取决于测试/优化期间访问的第一个时间帧。假如,EURUSD H1测试期间,EA交易访问GBPUSD M20数据。这种情况下,EA交易将能够进一步使用EURUSD H1,H2,以及GBPUSD M20,H1,H2等等。

注意: "只有开盘价”模式是最快的测试方式,但是不适合所有的策略测试。在交易系统的特点上选择所需的测试模式。

要总结订单号生成模式,请考虑EURUSD不同订单号生成模式的视觉比较,2011.01.11 21:00:00 - 2011.01.11 21:30:00之间的两个M15柱。

使用WriteTicksFromTester.mq5 EA将订单号保存在不同的文件里并且这些文件名称的结尾指定了filenameEveryTick, filenameOHLC 和 filenameOpenPrice 导入参数

We can specify the starting and the ending dates of the ticks (the variables start and end) for the WriteTicksFromTester Expert Advisor

若要使用三个订单号序列(每个以下模式“每个订单号”,“1分钟OHLC”和“只有开盘价”)获得三个文件,EA在相应模式中启动三次。那么,来自这三个文件的数据展示在使用TicksFromTester.mq5指标的图表上。指标代码附属于该文章。

The tick sequence in the Strategy Tester of the MetaTrader 5 terminal in three different testing modes

默认情况下,MQL5语言的所有 文件操作 都在“文件箱”中完成,测试EA过程中只访问它自己的“文件箱”。为了使指标和EA测试期间使用来自一个文件夹的文件,我们使用 flag 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));
     }

若要阅读指标中的数据,我们也使用flag 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_SPREAD_FLOAT) 总是返回true。

另外,历史数据包含跳动量的值和交易量。若要存储和检索数据,我们使用特殊的 MqlRates 结构:

struct MqlRates
  {
   datetime time;         // 周期开始时间
   double   open;         // 开盘价
   double   high;         // 周期的最高价
   double   low;          // 周期的最低价
   double   close;        // 收盘价
   long     tick_volume;  // 跳动量
   int      spread;       // 点差
   long     real_volume;  // 交易量
  };

测试期间使用真实报价

使用真实报价进行测试和优化能够尽可能的贴近真实条件。除了基于分钟数据生成报价以外,还可以使用交易商累积的真实报价。也就是来自交易所和流通量提供商的报价。

为了确保测试的最大精确性,分钟柱也用于真实报价模式。这些柱可以检查和纠正报价数据。也可以使您避免测试器和客户端图表之间的差异。

测试器会对比报价数据和分钟柱参数:报价不应超过柱的最高价/最低价,同时初始价和最终价应该符合柱的开盘价/收盘价。交易量也会进行比对。如果检测到任何不匹配,该分钟柱的所有报价都将被放弃。用生成的报价来替代(类似“每个报价”模式)。

如果交易品种历史记录是没有报价数据的分钟柱,测试器会以“每个报价”模式生成报价。这可以在交易商报价数据不足的情况下,在测试器中绘制正确的图表。

如果交易品种历史记录没有分钟柱但存在分钟报价数据,那么该数据会被用于测试器。例如,使用最后价形成交易所的交易品种组。如果服务器只收到卖价/买价而没有最后价,则不会生成柱形图。测试器会使用这些报价数据,因为它们与分钟柱并不抵触。

报价数据可能因为种种原因不符合分钟柱,例如由于连接损耗或从源位置到客户端迁移数据失败。分钟柱的数据在测试中被认为更加可靠。

真实报价测试时请牢记以下特性:

  • 启动测试时,交易品种的分钟数据与报价数据同步。
  • 报价存储在策略测试器的交易品种缓存中。缓存大小不可超过128 000个报价。收到新报价时,旧数据会从缓存删除。但是,CopyTicks函数允许接收缓存外的报价(仅适用于真实报价测试的情况下)。这样,从测试器的报价数据库请求的数据完全类似于对应的客户端数据库。 该数据库不执行分钟柱更正。因此,这里的报价可能不同于缓存中存储的报价

客户端全局变量

测试期间,也会模仿客户端全局变量 ,但是它们与当前的 程序端全局变量无关,可以在程序端使用F3按钮查看这种情况。它意味着程序端全局变量的所有操作,在测试期间,都可以超出客户端进行(测试代理)。

测试期间的指标计算

在实时模式,每个订单号都会计算指标值。策略测试采用合算的计算指标模式 - 只在运行EA之前立即重新计算指标。它意味着指标重新计算在调用OnTick(), OnTrade() 和 OnTimer() 函数之前完成。

不管是否有要求在特定的事件处理程序调用指标,所有指标,通过iCustom()IndicatorCreate()函数创建的处理程序都将在调用事件处理程序之前重新计算。

因此,当在“每个订单号”模式下测试时,将在调用OnTick()函数之前计算指标。

如果计时器在使用EventSetTimer() 函数的EA上,那么指标将在每次调用OnTimer()处理程序之前进行重新计算。因此,使用指标的测试时间会大幅提高,以非理想的方式写入。

测试期间加载历史记录

将要测试的交易品种的历史记录在开始测试进程之前会通过交易服务器的程序端进行同步和加载。在第一次的时候,程序端会加载所有可用的交易品种历史记录,以避免随后的请求。之后只需加载新数据即可。

测试开始以后,测试代理直接从客户端接收要被测试的交易品种历史记录。如果在测试过程中使用了其他工具的数据(例如,多货币的EA交易),测试代理则在第一次调用该数据时从客户端请求所需的历史记录。如果程序端提供历史数据,它们会立刻传到测试代理。如果不提供数据,程序端会从服务器请求和下载数据,然后传到测试代理。

其他工具的数据也被需要用于计算交易操作的交叉汇率。例如,用存款货币USD测试EURCHF策略时,处理第一笔交易操作之前,测试代理要求来自客户端的EURUSD和USDCHF历史数据,虽然策略不包含直接使用调用这些交易品种。

测试多货币策略之前,建议下载所有必要的历史数据到客户端。这将有助于避免与下载这些所需数据有关的测试/优化延迟。您可以下载历史记录,例如,通过打开相应的图表并滚动到最开始的历史记录。MQL5参考的组织访问数据部分会提供强制加载历史记录到程序端的示例。

测试代理,反过来,会通过打包的形式接收来自程序端的历史记录。下一次测试期间,测试器就不会加载程序端的历史记录,因为所需数据已经从之前运行测试器的时候获得。

  • 程序端仅从交易服务器加载一次历史数据,首次时候,代理从程序端请求要被测试的交易品种的历史记录。历史记录以打包的形式加载以减少流量。
  • 订单号不会发送至网络,它们会在测试代理生成。

多货币测试

策略测试器允许我们执行多交易品种交易策略的测试。这种EA按惯例被称为多货币EA交易,因为最初,在早先的平台中,只能为一个单一的交易品种执行测试。而在MetaTrader 5程序端的策略测试器中,我们可以模拟所有可用交易品种的交易。

第一次调用交易品种数据时,测试器会自动加载来自客户端(而非交易服务器!)的所用交易品种的历史记录。

测试代理仅下载丢失的历史记录,以小预付款提供历史记录所需的数据,用于在测试开始时计算指标。对于时间帧D1和更少,下载历史最少量为一年。

因此,如果我们在2010.11.01-2010.12.01期间以M15(每柱等于15分钟)为周期运行测试(测试间隔一个月),那么程序端将会请求工具2010一整年的历史记录。对于周时间帧,我们将要求100柱的历史记录,这将是两年的时间(一年52周)。对于月时间帧的测试,代理将请求8年的历史记录(12月x 8年=96月)。

如果不是必要的柱,测试之前,测试开始日期将会自动从过去转换到现在 提供必要的柱储备。

测试期间,也模仿"市场报价" ,从哪一个能获得交易品种信息.

默认下,测试开始时,策略测试器的“市场报价”中只有一个交易品种 - 测试运行的交易品种。被提及时,所有必要的交易品种都自动连接策略测试器的“市场报价”(不是程序端)。

开始测试多货币EA之前,需要选择在程序端“市场报价”要求测试的交易品种和加载所需的数据 。首次调用“foreign”交易品种期间,历史记录会在测试代理和客户端之间自动同步。“foreign”交易品种是不同于测试运行的交易品种。

推荐“other”交易品种的数据出现在下列情况下:

  • 通过使用以下函数为交易品种/时间帧请求时间序列:

第一次调用“other”交易品种时,测试进程停止,下载历史记录用于交易品种/时间帧,从程序端到测试代理。同时,生成该交易品种的订单号序列。

根据所选的订单号生成模式,为每个交易品种生成一个独立的订单号序列。您也可以通过调用OnInit()处理程序的SymbolSelect()明确请求期待的交易品种的历史记录 - 下载历史记录会在测试EA交易之前立即完成。

因此,在MetaTrader 5客户端无需任何额外的努力去执行多货币测试。只需在客户端打开相应交易品种的图表。如果包括该数据,那么所有所需交易品种的历史记录将会从交易服务器自动上传。

策略测试的模拟时间

测试期间,本地时间 TimeLocal() 始终等于服务器时间 TimeTradeServer()。反过来,服务器时间也始终等于相应GMT时间的时间 - TimeGMT()。这样,所有这些函数会在测试期间同时展示。

如果没有连接服务器,由于GMT和本地时间没有差异,那么策略测试器中的服务器时间会刻意完成。无论连接与否,测试结果应该始终相同。有关服务器时间的信息也不会本地存储,而是来自服务器。

测试的图形对象

测试/优化期间图形对象无需标记。因此,当涉及测试/优化期间创建对象的属性时,EA交易将收到的是零值。

该限制不会应用于视觉模式的测试。

策略测试的OnTimer() 函数

MQL5 提供处理Timer事件的机会。无论何种测试模式都可以完成调用OnTimer() 处理程序。这意味着如果在“只有开盘价”的模式,H4的周期下运行,并且EA有一个每秒调用的Timer设置,那么每个H4柱开始的时候,都将会调用一次OnTick()处理程序,调用14400次调用OnTimer()处理程序(3600秒*4小时)。EA的总测试时间将会根据EA的逻辑进行增加。

为了根据给予的Timer频率的测试时间进行检查,我们已经创建了无需任何交易操作的简单EA。

//--- 输入参数
input int      timer=1;              // Timer 值, 单位为秒
input bool     timer_switch_on=true; // 定时开关
//+------------------------------------------------------------------+
//| 专家初始化函数                                                     |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 如果timer_switch_on==true则运行 Timer
   if(timer_switch_on)
     {
      EventSetTimer(timer);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 专家去初始化函数                                                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 停止 Timer
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer 函数                                                       |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
// 无操作,处理程序主体为空
  }
//+------------------------------------------------------------------+

  在timer参数不同的值下,获得测试时间测量(Timer事件的周期性)。在获得的数据上,我们绘制测试时间作为Timer周期函数。 .

Testing time as a function of Timer period

可以很清晰的看到,越小的参数timer,优化EventSetTimer(Timer)函数期间,OnTimer()处理程序之间的调用越少,而同样情况下,测试时间T越大。

策略测试器中的Sleep() 函数

使用图形工作时,Sleep() 函数允许EA或脚本暂停执行mql5-程序。这在请求未准备并需要等待其准备的数据时非常有用。使用Sleep()函数的详细示例可以在数据访问安排部分找到。

Sleep()调用不会停滞测试过程。当您调用Sleep()时,生成的订单号会在指定延迟内“播放”,这可能会导致触发挂单,止损等等。Sleep()调用以后,策略测试器的模拟时间会增加间隔,这会在Sleep函数的参数中指明。

如果执行Sleep()函数的结果,策略测试器的当前时间检查测试时间,然后您将收到一个错误“测试时检测的无限睡眠循环”。如果您收到这个错误,测试结果会被驳回,在他们全交易量执行的所有计算(交易量,下降等),该测试的结果被传到程序端。

Sleep()函数 在 OnDeinit()不能工作,因为调用以后,测试时间将保证超过测试间隔范围。

The scheme of using the Sleep() function in the Strategy Tester of the MetaTrader 5 terminal

策略测试器中的Print() 函数

若要提高性能和节省流量,Print() 在本地代理和远程云代理测试期间不会工作。

OnInit()处理程序内使用Print() 除外。这允许您在发生时更容易找到错误的发生原因。

使用策略测试用于数学运算中的优化问题

使用MetaTrader 5 程序端的测试器,不仅能够测试交易策略,还能够用于数学运算。若要使用,必须选择“数学运算”模式:

math_calculations

在这种情况下,将只有三种函数能被调用:OnInit(),OnTester(),OnDeinit()。在“数学运算”模式,策略测试器不能生成任何订单号和下载历史记录。

如果您指定的开始日期大于结束日期,那么策略测试器也会以“数学运算”的模式工作。

当使用测试器解决数学问题时,上传历史记录和生成订单号都不会发生。

在MetaTrader 5策略测试器用于解决的典型数学问题 - 搜索带有许多变量的函数极值。

要解决它,我们需要:

  • 函数值的计算应该位于OnTester() 函数;
  • 函数参数必须被定义为EA交易的 导入变量

编译EA,打开“策略测试器”的窗口。在“导入参数”标签,选择所需的导入变量,通过为每个函数变量指定开始,停止和阶段值来定义参数值的设置。

选择优化类型 - “慢速完成算法”(全面搜索参数空间)或“快速遗传基础算法”。为了简单搜索函数极值,最好选择快速优化,但是如果您想要计算整个变量设置的值,那么最好使用慢速优化。

选择“数学运算”模式并使用“开始”按钮,运行优化程序。注意优化时,策略测试器将搜索OnTester函数的最大值。若要找到本地最小值,则从OnTester函数返回相反的所计算的函数值:

return(1/function_value);

需要检查function_value不等于零,因为否则的话,我们可以获得除数为零的严重错误

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

return(-function_value);

该选项不需要检查等于零的function_value,3D表现法的优化结果表面有同样的形状,但是普通的镜像。

作为示例,我们提供了sink() 函数:

sink_formula

用于找到该函数极值的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
//+------------------------------------------------------------------+
//| Tester函数                                                        |
//+------------------------------------------------------------------+
double OnTester()
  {
//---
   double sink=MathSin(x*x+y*y);
//---
   return(sink);
  }
//+------------------------------------------------------------------+

执行优化并以2D图形的形式查看 优化结果

The results of the full optimization of the sink (x*x+y*y) function as 2D-graph

给定参数组(x, y)的值越好,颜色饱和度越高。正如从查看sink()公式形式所期待的,它的值以中心为(0,0)形成同心圆。人们可以通过3D图形查看,sink()函数没有单独的全局极值:

3D Graph of Sink function

在“只开盘价”模式同步柱形

MetaTrader 5 客户端的测试器允许我们检查所谓的“多货币”EA。多货币EA - 是交易两个或两个以上交易品种的EA。

在多交易品种上进行交易的策略测试,在测试器上强加了一些额外的技术要求:

  • 为这些交易品种生成订单号;
  • 为这些交易品种计算指标的值;
  • 为这些交易品种计算预付款规定额;
  • 所有交易品种同步生成订单号序列。

按照所选的交易模式,策略测试器为每个工具生成和处理一个订单号序列。同时,打开每个交易品种的新柱 ,即使柱形在另一个交易品种打开。这意味着当多货币EA测试时,这种情况可能会产生(并经常产生),当一个工具时,一个新柱已经打开,而其他则不会。因此,测试时,一切都可能变为现实。

只要使用“每个订单号”和“1分钟OHLC”测试模式,测试器中历史记录的真实模拟就不会引起任何问题。对于这些模式来说,一个蜡烛图生成足够的订单号,以便能够等到同步来自不同交易品种的柱形。但是如果同步交易工具柱形是强制的,我们如何在“只开盘价”模式测试多货币策略?在该模式下,只在对应柱的开盘时间的一个订单号上调用EA。

我们将在一个示例上进行说明:如果我们在EA测试EURUSD,新的小时蜡烛图在EURUSD打开,那么我们可以很容易地辨认出这个事实 - “只开盘价”模式测试,事件NewTick 对应于测试周期的打开柱形时刻。但是不保证EA中使用的交易品种USDJPY打开的新蜡烛图。

正常情况下,它足以完成OnTick() 函数的工作和检查在下一个订单号出现的USDJPY新柱。但是当在“只开盘价”模式测试时,将不会有其他订单号,所以可能看起来这种模式不适合测试多货币EA。但是事实并非如此 - 不要忘了 MetaTrader 5 测试器的行为就如在现实生活一样。您可以等候直至使用Sleep()函数打开另一个交易品种的新柱!

EA Synchronize_Bars_Use_Sleep.mq5的代码,显示“只开盘价”模式的柱形同步示例:

//+------------------------------------------------------------------+
//|                                   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(INIT_PARAMETERS_INCORRECT);
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| EA订单号函数                                                      |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 静态变量,用于存储最近的柱形时间
   static datetime last_bar_time=0;
//--- sync 标识
   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;
//--- 循环,直至另一个交易品种开盘时间等于curr_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()后不能改变。在“只开盘价”模式运行EA,您将看到同步柱形的信息。

Synchronize_Bars_Use_Sleep_EA

如果您需要获得当前服务器时间,而不是最近订单号到达时间,请使用TimeTradeServer() 函数替代TimeCurrent()。

还有另一个同步柱形的方式 - 使用timer。这种EA的示例是Synchronize_Bars_Use_OnTimer.mq5,附于本文。

测试器的IndicatorRelease() 函数

完成单项测试以后,工具图表自动打开,显示完成的交易和EA中使用的指标。这有助于视觉上检查进出点,并且与指标的值进行比较。

注意: 在测试完成后自动打开的图表上显示的指标,会在测试完成后重新计算。即使这些指标应用在已测的EA上。

但是在某些情况下,程序员可能想要隐藏交易算法中包括的指标信息。例如,EA代码作为可执行文件被租用或出售,不提供源代码。为此,IndicatorRelease() 函数更合适。

如果程序端以 tester.tpl的名称在客户端的directory/profiles/templates设置模板,那么它将被应用于打开的图表。如果没有,则会使用默认模板(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()。

测试器中的事件处理

The presence of the EA的OnTick() 处理程序并不是强制性的,要在MetaTrader 5测试器测试历史数据。EA中至少包括一个以下函数就已经足够:

在EA测试时,我们可以使用OnChartEvent()函数处理自定义事件,但是在指标中,该函数不能在测试器中调用。即使指标拥有OnChartEvent()事件处理程序并且该指标用在测试的EA,指标本身仍然不能接收任何自定义事件。

测试期间,指标可以使用EventChartCustom()函数生成自定义事件,并且EA可以用OnChartEvent()处理该事件。

除了这些事件,与测试和优化相关的特殊事件也在策略测试器生成。

  • Tester - 该事件在完成EA历史数据测试后生成。Tester 事件使用OnTester()函数处理。该函数仅在测试EA时被使用,主要意于计算用作自定义极大极小标准的值,用于导入参数的遗传优化。
  • TesterInit - 该事件在第一次通过之前在策略测试器优化开始时生成。TesterInit事件使用 OnTesterInit() 函数处理。优化开始期间,该处理程序的EA交易自动加载于测试器指定交易品种和周期的独立程序端图表上,接收 TesterInit事件。该函数用于在优化开始之前启动EA交易,以便进一步处理优化结果
  • TesterPass - 该事件在接收新数据框时生成。TesterPass事件使用OnTesterPass() 函数处理。优化期间,该处理程序的EA交易自动加载于测试器指定交易品种和周期的独立程序端图表上,并在收到框架时接收TesterPass事件。 该函数用于"立时"动态处理 优化结果 无需等候完成。使用FrameAdd()函数添加框架,该函数可以在单次通过结束后用OnTester() 处理程序调用。
  • TesterDeinit - 该事件在结束策略测试器的EA交易优化后生成。TesterDeinit 事件使用OnTesterDeinit() 函数处理。该处理程序的EA交易在优化开始时自动加载于图表,而在完成后接收TesterDeinit。该函数用于最后处理 所有 优化结果

测试代理

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内存。

如果提前完成测试,从用户方面(”取消“按钮),以及关闭客户端,所有本地代理会立即停止工作,释放所占用的内存。

程序端和代理之间的数据交换

当您运行测试时,客户端已准备向代理发送一些的参数模块:

  • 用于测试的导入参数(模拟模式,测试间隔,工具,优化准则,等等)
  • 选定的”市场报价“交易品种列表
  • 交易品种测试规则(合约大小,获准的市场预付款用于设置止损和获利,等等)
  • 要被测试的EA交易和其导入参数的值
  • 附加文件信息(程序库,指标,数据文件 - # property tester_ ...)

tester_indicator

string

"indicator_name.ex5"格式的自定义指标名称。如果对应的参数通过常量字符串设置,那么需要测试的指标通过调用iCustom()函数自动定义。针对所有其他情况(使用IndicatorCreate() 函数或使用设置指标名称的参数中的非常量字符串)需要该属性

tester_file

string

带有扩展指证的测试器文件名,双引号(常量字符串)。指定文件将被传到测试器。如果有必要,要被测试的导入文件,必须始终指明。

tester_library

string

带有扩展名的程序库名称,双引号。程序库可以有dll或ex5扩展名。需要测试的程序库被自动定义。然而,如果用自定义指标使用任何程序库,则需要该属性。

每个参数模块,都以MD5-hash的形式创建数字指纹,它会被发送至代理。MD5-hash每次设置都是独一无二的,其交易量比计算的信息数量少很多倍。

代理接收散列模块,将其与现有的进行比较。如果代理中没有给定的参数模块指纹,或接收的散列模块不同于现有的,那么代理请求该模块参数。这会减少程序端和代理之间的流量。

测试之后,代理返回程序端,运行的所有结果显示在“测试结果”和“优化结果”标签:所获利润,成交量,夏普系数,OnTester() 函数的结果,等等。

优化期间,程序端以小包形式分发测试任务给代理,每包都包含几个任务(每个任务意味着一系列导入参数的单独测试)。这减少了程序端和代理之间的交换时间。

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

测试结果通过程序端添加到特殊的结果缓存(结果缓存),以便使用时可以快速访问。对于每个参数设置,程序端都会搜索结果缓存,获得之前运行提供的结果,为了避免重新运行。如果无法找到这种参数的结果,代理会被给予任务引导测试。

程序端和代理之间的所有流量加密。

订单号并不是通过网络发送,它们在测试生成代理。

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

所有测试代理都来自客户端,相互独立:每个代理都有其自己的日志记录文件夹。另外,代理测试期间的所有操作都发生在agent_name/MQL5/Files文件夹。然而,如果打开文件期间您指定标识FILE_COMMON ,那么我们可以通过客户端的共享文件夹实施本地代理和客户端的互动:

//+------------------------------------------------------------------+
//| 专家初始化函数                                                     |
//+------------------------------------------------------------------+
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 flag指定)
   handle=FileOpen(filename,FILE_WRITE|FILE_READ|FILE_COMMON);
   ... further actions
//---
   return(INIT_SUCCEEDED);
  }

使用DLLs

所要加速优化,我们不仅可以使用本地,还可以使用 远程代理。这种情况下,对远程代理会有一些限制。首先,在日志中远程代理不显示Print()函数的执行结果,持仓和平仓的信息。日志中仅显示最少的信息,来防止在远程代理工作的电脑上,错误地编写EA。

另一个限制 - 测试EA时禁止使用DLL。出于安全考虑,远程代理完全禁止调用DLL。在本地代理,也只在相应的“允许导入DLL”权限下,允许在测试EA中调用DLL。

The option "Allow import DLL" in mql5-programs

注意: 当使用从EA(脚本,指标)接收的需要允许DLL调用时,您应该意识到在程序端设置中允许该选项时您要承担的风险。无论EA如何使用 - 是用于测试还是运行图表。

 


更新: 2017.03.30