第三章 MQL5程序

要运行mql5程序,必须编译(点击编写(编译)按钮 或者 F7键)。编译过程应该没有错误(可能有一些 警告 错误,它们可供分析)。在这个过程中,会在相应的目录中创建一个具有相同文件名称,并以 EX5 作为扩展名的可执行文件。 终端安装目录下\MQL5\Experts, 终端安装目录下\MQL5\indicators 或 终端安装目录下\MQL5\scripts. 这个文件就是可以运行的MQL5程序。

MQL5程序的操作特性将在以下部分中描述:
• 程序运行 —— 调用预定义的事件处理程序的顺序。
• 测试交易策略 —— 在策略(EA交易)测试中 MQL5 程序的操作特点。
• 客户端事件 —— 事件描述,在程序中可以处理 ;
• 调用导入函数 —— 描述导入函数的顺序,允许参数,搜索细节和调用协议等。
• 运行时错误 —— 获得关于运行时和关键性错误的信息。

EA程序、自定义指标和脚本可以通过从 导航 窗口中拖放的方式来附加到一个打开的图表中。

要停止EA程序的操作,应该从图表中删除它。在图表上点击鼠标右键,在出关的上下文关联菜单中选择“EA交易系统”,然后从列表中选择EA并单击“删除”按钮。EA的操作也受到“自动交易”按钮状态的影响。

要停止自定义指标,从图表中移除即可。

直到从图表中移除才能运行自定义指标和EA交易;附加在EA交易和指标中的信息在客户端起始位置保存。

直到被明确地从图表中删除之前,自定义指标和EA程序会一直保持工作状态;有关EA程序 和 指标 的附加信息保存在客户端会话之间。

脚本只会执行一次,在操作完成 或 当前图表状态更改 或 客户端关闭时 自动删除。在重新启动客户终端脚本后,脚本不会启动,因为不会保存有关它们的信息。

在一个图表上,最多只能附加一个EA程序,一个脚本;但 指标 的数量无限。

# 3.1 程序运行

每个 脚本 和 每个 EA交易程序 都在各自的线程中运行。所有在一个 交易品种 上计算的指标,即使它们被附加到不同的图表上,也会在同一个线程中工作。因此,一个 交易品种上 的所有 指标 共享一个线程的资源。

与 交易品种 相关的所有其他操作,如 跳价处理 和 历史同步,也会与指标一起始终在同样的线程中执行。这意味着,如果在一个 指标 中执行了一个无限循环的动作,那么与其交易品种相关的所有其他事件将不会执行。

当运行一个EA交易程序时,请确保它有 一个实际的交易环境,并且可以 访问所需交易品种的时间周期 以及 历史数据,并在 终端 和 服务器 之间同步数据。对于所有这些过程,终端的启动延迟不超过5秒,在此之后,EA程序将开始使用可用的数据。因此,如果没有连接到服务器,这可能导致EA程序的启动延迟。

以下表格包含MQL5程序的简要概述:

程序 运行 注意
脚本 单独线程,脚本的线程数量等于脚本数 循环脚本不能破坏其他程序的运行
EA交易程序 单独线程,EA交易程序的线程数量等于EA交易的数量。 循环EA交易程序不能破坏其他程序的运行
指标 适用于一个交易品种上的所有指标共用一个线程。线程的数量等于指标交易品种的数量 一个指标中的无限循环将停止该交易品种上的所有其他指标

当一个MQL程序附加到一个图表上之后,它首先被上传到客户端电脑的内存中,然后进行全局变量初始化。如果 类 中的某些全局变量有构造函数,在全局变量初始化时则会调用构造函数。

之后,MQL程序开始等待客户端事件,每个MQL5程序应该至少有一个事件处理器,否则加载到内存的这个程序就不会执行任何操作。每一个事件处理程序有预定义名称,参数和返回类型。

类型 函数名称 参数 应用 注释
int OnInit EA交易和指标 初始化事件处理函数,允许返回void类型
void OnDeinit const int reason EA交易和指标 无法初始化事件处理函数
void OnStart 脚本 启动事件处理函数
int OnCalculate const int rates_total,
const int prev_calculated,
const datetime &Time[],
const double &Open[],
const double &High[],
const double &Low[],
const double &Close[],
const long &TickVolume[],
const long &Volume[],
const int &Spread[]
指标 计算所有价格的事件处理函数。
int OnCalculate const int rates_total, const int prev_calculated, const int begin, const double &price[] 指标 计算单个数据数组的事件处理函数指标不能同时有两个事件处理函数 只有一个事件处理函数处理数据数组
void OnTick EA交易 新跳价事件处理函数。当正在处理一个新的跳价事件时,不会接收到这种类型的其他事件。
void OnTimer EA交易和指标 定时器事件处理函数
void OnTrade EA交易 交易事件处理函数
double OnTester EA交易 测试事件处理函数
void OnChartEvent const int id,
const long &lparam,
const double &dparam,
const string &sparam
EA交易和指标 图表事件处理函数
void OnBookEvent const string &symbol_name EA交易和指标 预定义事件处理函数

客户端给打开的对应图表发送新事件。事件也可以由图表(图表事件)或MQL5程序(自定义事件)生成。通过设置图表属性: CHART_EVENT_OBJECT_CREATE 和 CHART_EVENT_OBJECT_DELETE, 可以 启用/禁用 图表上创建 或 删除图解对象的事件。 每个MQL5程序和每个图表都有自己的事件队列,在其中添加所有新的传入事件。

一个MQL程序仅接收来自它所运行的图表上的事件。所有事件按照它们接收的顺序先后处理。如果队列中已有 新跳价(NewTick)事件,或者该事件当前正被处理,那么新的新跳价(NewTick)事件就不能加入到MQL5程序队列中。同样,如果图表事件(ChartEvent) 已经加入队列,或该事件正被处理,那么此类的新事件也不会加入队列。计时器(timer)事件也以同样的方式处理 - 如果计时器(Timer)事件已经加入队列 或 正在被处理中,新的计时器(timer)事件就不会加入队列。 事件队列的容量有限但足够大,因此不太可能出现编写良好的程序队列溢出。在队列溢出的情况下,会丢弃新的事件,不会加入事件队列。

不建议使用无限循环来处理事件。此规则的例外可能是只处理单个启动(Start)事件的脚本。

程序库 不能处理任何事件。

在 指标 和 EA交易程序中 禁用的函数

指标,脚本和EA交易程序都是用MQL5编写的可执行程序。它们是专为不同类型的任务而设计的。因此,根据程序类型 ,对于某些函数的使用会有一些限制。以下函数在指标中被禁用:
• OrderCalcMargin();
• OrderCalcProfit();
• OrderCheck();
• OrderSend();
• SendFTP();
• Sleep();
• ExpertRemove();
• MessageBox().

专为指标设计的所有函数在EA交易程序 和 脚本中也被禁用:
• SetIndexBuffer();
• IndicatorSetDouble();
• IndicatorSetInteger();
• IndicatorSetString();
• PlotIndexSetDouble();
• PlotIndexSetInteger();
• PlotIndexSetString();
• PlotIndexGetInteger.

库 不是一个独立的程序,当有脚本、指标 或 EA程序这样的MQL5程序调用它时,在相关联的上下文中执行。因此,上述限制也适用于调用 库。

函数库 不处理任何事件。

指标的加载和卸载

在下列情况下,指标被加载:
• 拖曳 指标 附加到图表上;
• 客户端开启时(如果在上一次客户端关闭之前 指标 已附加到图表上);
• 装载模版时(如果在模板中指定了图表上的指标);
• 更改了图表配置文件(如果该指标附加在该图表配置中的某一个图表上);
• 改变图表的交易品种符号 和/或 时间框架,并之前已附加了 指标 时;
• 一个 指标 重新编译后,指标 重新附加到图表上;
• 改变了 指标 的 输入参量 。

在下列情况下,指标被卸载:
• 从图表中删除 指标;
• 客户端关闭(图表上附加的指标随之卸载);
• 装载模版时(原图表上附加的指标被卸载);
• 关闭 指标 附加的那个图表;
• 更改了图表配置文件(如果该指标附加在该图表配置中的某一个图表上);
• 图表中交易品种或者时间表改变,指标附加;
• 改变了 指标 的输入参数。

EA交易程序的加载和卸载

在下列情况下,EA交易程序被加载:
• 将一个EA交易程序附加到图表上;
• 客户端启动(如果在上一次客户端关闭之前 EA交易程序 已附加到图表上);
• 装载模版时(如果在模板中已经指定了图表上的 EA交易程序);
• 更改了图表配置文件(如果 EA交易程序 已附加在该图表配置中的某一个图表上);
• 连接帐户,即使帐户号是相同的(如果在之前帐户连接服务器时,EA交易程序已被附加到图表上)。

在下列情况下,EA交易程序被卸载:
• 从图表中删除EA交易程序;
• 如果把一个新的EA交易程序添加到图表中,而图表上已经附加了另一个EA交易程序,则原来的(旧的)EA交易程序就会被卸载;
• 客户端关闭(EA交易程序 已附加到图表上);
• 装载模版时,(如果在模板中已经指定了图表上的 EA交易程序);
• 关闭附加了EA交易程序的图表;
• 更改了图表配置文件(如果 EA交易程序 已附加在该图表配置中的某一个图表上);
• 改变客户端连接的账号(如果在之前帐户连接服务器时,EA交易程序已被附加到图表上);
• 调用了ExpertRemove()函数。

如果一个附加了EA交易程序的图表,改变了 交易品种 或 时间框架,则 EA交易程序 不能 加载 或 卸载。在这种情况下,客户端随后会调用原 交易品种/时间框架上的函数OnDeinit() 和 新 交易品种/时间框架 上的函数OnInit()来处理程序 (如果它们是这样的话),全局变量 和 静态变量的值不会被重置。在初始化完成之前,已经接收了 EA交易程序 的所有事件(OnInit()函数)被跳过。

脚本的加载的卸载

脚本在附加到图表过后就直接被加载,在完成操作之后会直接被卸载,脚本不会调用OnInit() 和 OnDeinit() 函数。 当一个程序被卸载(从图表中删除)时,客户端将执行全局变量的初始化并删除事件队列。在这种情况下,反初始化意味着重置所有字符串类型的变量、重新分配动态数组对象的存储单元以及调用它们的析构函数。

当程序卸载后(从图表中删除),全局变量的客户端程序不能初始化并删除队列中的事件,这样所有的字符类型常量都会重设,动态数组对象 的存储单元会分配析构函数 也会调用。

为了更好地理解EA交易程序的操作,我们推荐编译如下的EA交易代码,并执行加载/卸载、更改模板、更改交易品种、更改时间框架等操作:

//+------------------------------------------------------------------+
//|                                                   TestExpert.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

class CTestClass
  {
public:
   CTestClass() { Print("CTestClass constructor"); }
   ~CTestClass() { Print("CTestClass destructor"); }
  };
CTestClass global;
//+------------------------------------------------------------------+
//| EA交易程序 初始化函数                                                     |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   Print("Initialisation");
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| EA交易程序 反初始化函数                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   Print("Deinitialisation with reason",reason);
  }
//+------------------------------------------------------------------+
//| EA交易程序 跳价函数                                                     |
//+------------------------------------------------------------------+
void OnTick()
  {
//---

  }
//+------------------------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

相关参考 客户端事件 , 事件处理程序

# 3.2 交易权限

自动交易

MQL5语言提供了一组专门用于开发自动交易系统的交易函数。无人为干涉,为自动交易而开发的程序称为EA交易程序 或 自动交易。若要在MetaEditor中创建EA交易程序,请启动MQL5向导并在以下两个选项中选择其中一个:

• EA交易(模板) —— 允许您创建一个具有现成的事件处理函数的模板,并通过编程的方式来补充所有必要的功能。
• EA交易 (生成) —— 通过选择必要的模块:交易信号模块、资金管理模块和追踪止损模块,让您逐步的开发一个成熟的交易机器人。(参考文档: MQL5 WIZARD:新版本https://www.mql5.com/zh/articles/275)

Alt text

(注意: MQL4 和 MQL5 的向导有差异,上图为 MQL4 向导)

Alt text

(注意: MQL4 和 MQL5 的向导有差异,上图为 MQL5 向导)

交易函数只能在EA交易和脚本中工作。指标不允许交易。

检查执行自动交易的权限

若要开发一个可靠的EA交易程序,能够在没有人为干预的情况下工作,则需要安排一套重要的检查。首先,我们应该以编程方式检查是否允许进行交易。这是开发任何自动化系统时必不可少的基本检查。

在客户端检查执行自动交易的权限

客户端设置为您提供了 允许 或 禁止 所有自动交易程序的权限。

Alt text

您可以在程序端标准面板直接切换自动交易选项:
•AutoTrading_enabled —— 启用自动交易,允许使用应用程序中的交易函数。

Alt text

•AutoTrading_disabled —— 禁用自动交易,运行中的应用程序不能执行交易函数。

Alt text

示例检查:

if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
   Alert("Check if automated trading is allowed in the terminal settings!");
1
2

检查某个运行中的EA/脚本 是否允许进行交易

您可以在启动时 允许 或 禁止 某个程序的自动交易。为此,请在程序属性中勾选特定的勾选框

Alt text

(注意: MT4 和 MT5 客户端上的设置有所不同,上图为 MT4 中选项面板)

Alt text

(注意: MT4 和 MT5 客户端上的设置有所不同,上图为 MT5 中选项面板)

示例检查:

if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
      Alert("Check if automated trading is allowed in the terminal settings!);
// 检查终端设置是否允许自动交易!
   else
     {
      if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
         Alert("Automated trading is forbidden in the program settings for ",__FILE__);
// 在程序设置中,自动交易是被禁止的
     }
1
2
3
4
5
6
7
8
9

检查是否允许当前账户的任何EA/脚本进行交易

在交易服务器端可以禁止自动交易。示例检查:

if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
  Alert("Automated trading is forbidden for the account ",AccountInfoInteger(ACCOUNT_LOGIN),
      " at the trade server side");// 在交易服务器端自动交易是禁止的
1
2
3

如果一个交易账户被禁止自动交易,EA交易/脚本的交易操作则不会执行。

检查当前账户是否允许进行交易

在某些情况下,交易账户可能被禁止任何交易操作 - 既不可以手动执行也不可以自动交易。投资者密码用于连接交易账户时的示例检查:

  if(!AccountInfoInteger(ACCOUNT_TRADE_ALLOWED))
      Comment("Trading is forbidden for the account ",AccountInfoInteger(ACCOUNT_LOGIN),
        ".\n Perhaps an investor password has been used to connect to the trading account.",
        "\n Check the terminal journal for the following entry:",
        "\n\'",AccountInfoInteger(ACCOUNT_LOGIN),"\': trading has been disabled - investor mode.");
1
2
3
4
5

AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) 在以下情况下可能返回false:
• 没有连接到交易服务器。可以使用TerminalInfoInteger(TERMINAL_CONNECTED)检查;
• 交易账户切换到只读模式 (发送至存档);
• 在交易服务器上禁止账户交易;
• 交易账户的连接在投资者(观察)模式。

相关参考 客户端属性, 账户属性, 运行MQL5程序的属性

# 3.3 客户端事件

初始化(OnInit))

在客户端装载了程序后(EA交易 或者 自定义指标)就会直接开始全局变量初始化进程,发送 初始化 事件,如果有OnInit()函数的话,将通过OnInit()函数处理初始化事件。这个事件也会在以下情况发生时产生:金融工具 和/或 图表的时间框架被改变后;该程序源码在MetaEditor编辑器中重新编译之后;EA交易 或者 自定义指标设置窗口中的输入参数改变后。在登录账户改变之后,EA交易也可以初始化,脚本没有初始化事件。

无法(反)初始化 (OnDeinit)

在全局变量初始化 和 程序(EA交易和自定义指标)加载之前,客户端向程序发送Deinit事件。有以下几种情况发生时,即会生成反初始化事件,当客户端关闭时,当图表关闭时,金融工具 和/或 图表的时间框架被改变后,程序重新编译成功时,当输入参数改变时,账户改变时。

无法初始化的原因可以从参数中获得,传递给OnDeinit()函数。运行的OnDeinit()函数被限制为2.5秒。如果在此期间函数尚未完成,则强制终止。脚本没有无法(反)初始化事件。

启动 (OnStart)

启动事件是加载脚本后激活的特殊事件。这个事件由OnStart函数处理。启动事件不适用于EA交易程序 或 自定义指标。

跳价 (OnNewTick)

如果客户端打开的图表上有到一个新报价,就会生成NewTick事件,它由附加在图表上的EA交易程序中的OnTick()函数处理。如果在收到新报价时正在处理前一个跳价的OnTick函数,那么新的报价将会被EA交易程序忽略,因为相应的事件不会进入队列。

当程序运行时接收到的所有新引号都将被忽略,直到OnTick()完成。在此之后,函数将仅在收到新报价后运行。无论是否允许自动交易(“自动交易”按钮 允许/禁止),都将生成NewTick事件。禁止自动交易只表示不允许从EA交易程序中发送交易请求,而EA交易程序是继续工作的。

通过按下对应的按钮来禁止自动交易不会阻止当前正在执行OnTick()函数。

计算 (OnCalculate)

计算事件只在指标完成初始化事件 并且 价格数据发生改变后开始生成,由 OnCalculate 函数处理。

计时器 (OnTimer)

定时器 事件通过EA交易客户端定期执行,并通过EventSetTimer 函数被定时器激活。通常情况下,该函数通过OnInit调用。定时器事件过程通过OnTimer函数执行。在EA交易操作完成之后,有必要通过EventKillTimer函数删除定时器,通常叫做OnDeinit函数。

计时器事件是由客户端定期生成的,它是由EA交易程序中的EventSetTimer函数激活的。通常,这个函数由OnInit调用。计时器事件由OnTimer函数处理。在完成EA交易程序的操作之后,有必要使用EventKillTimer函数来销毁计时器,该函数通常在OnDeinit函数中调用。

交易 (OnTrade)

当交易服务器完成一个交易操作即会生成一个交易事件,交易事件通过 OnTrade() 函数处理,通常有如下一些操作:
• 发送、修改或者删除挂单;
• 因为资金不足 或者 过期 而取消挂单;
• 激活挂单;
• 打开,添加 或者 关闭 一个仓位头寸(或者部分仓位头寸);
• 修改打开的仓位头寸(改变 止损 和/或 止盈)。

交易事务(OnTradeTransaction)

当在交易账户上执行某些明确的操作时,其状态会发生变化。这样的行为包括:
• 在客户端中从任何MQL5应用使用 OrderSend 和 OrderSendAsync 函数发送交易请求及其后续的执行操作:
• 通过程序端图形界面发送交易请求及其后续的执行操作;
• 在服务器上激活挂单和止损订单;
• 在交易服务器上执行操作。

这些操作的结果会执行以下交易事务:
• 处理交易请求;
• 改变持仓订单;
• 改变订单历史记录;
• 改变交易历史记录;
• 改变持仓。

例如,当发送一个做多(buy)的市场订单时,它会被处理,账户中会创建一个对应的做多(buy)订单,然后该订单被执行 并 从打开持仓列表中移除,添加到历史订单,而对应的交易也会添加到历史记录中 并 创建一个新的仓位头寸。所有这些操作都是交易事务。在程序端收到这种事务就是交易事务事件。该事件通过 OnTradeTransaction 函数处理。

测试 (OnTester)

测试事件在EA交易测试历史数据完毕后产生,该事件通过 OnTester() 函数处理。

测试初始化 (OnTesterInit)

TesterInit事件是在策略测试器中第一次优化通过之前时生成的。TesterInit事件由OnTesterInit()函数处理。

测试通过(TesterPass)

当接收到新的数据帧时,将生成TesterPass事件。TesterPass事件由OnTesterPass()函数处理。

测试反初始化(TesterDeinit)

TesterDeinit 事件在EA交易优化结束后,在策略测试中生成。TesterDeinit事件通过OnTesterDeinit()函数处理。

图表事件(ChartEvent)

当用户在客户端使用图表时,即产生ChartEvent 事件,即以下情形发生时:
• 在图表中点击时,图表窗口处于焦点位置(最前面);
• 创建图解对象 ;
• 删除图解对象 ;
• 鼠标点击图表中的图解对象;
• 使用鼠标移动图解对象;
• 在LabelEdit中结束文本编辑;

还有一个是自定义的ChartEvent事件,可以由任何mql5程序通过EventChartCustom函数,发送给 EA程序。图表事件由OnChartEvent函数处理。

市场深度事件(BookEvent)

BookEvent 事件在市场深度改变后通过客户端产生;由 OnBookEvent函数处理。要为指定的交易品种生成BookEvent事件,需要使用MarketBookAdd函数将交易品种订阅到此事件。

要取消指定交易品种的BookEvent事件,必须调用MarketBookRelease函数。BookEvent事件是一个广播类型事件——它意味着仅为该事件订阅一个EA交易程序,以及拥有OnBookEvent事件处理程序的所有其他EA交易程序都将接受它。这就是为什么需要分析交易品种代码符号,它被传递给处理程序作为参数。

相关参考

事件处理程序 , 程序运行

# 3.4 资源

在MQL5程序中使用图形和声音 MQL5程序允许使用声音和图形文件:
• PlaySound() 可以播放一个声音文件;
• ObjectCreate() 允许使用图形对象 OBJ_BITMAP 和 OBJ_BITMAP_LABEL创建用户界面。 PlaySound()
调用 PlaySound() 函数的示例:

//+------------------------------------------------------------------+
//| 调用 标准的OrderSend()函数播放一个音频文件                            |
//+------------------------------------------------------------------+
void OrderSendWithAudio(MqlTradeRequest  &request, MqlTradeResult &result)
  {
  //--- 发送一个请求到服务器
   OrderSend(request,result);
   //--- 如果请求被采纳,播放音频文件 Ok.wav
   if(result.retcode==TRADE_RETCODE_PLACED) PlaySound("Ok.wav");
   //--- 如果失败,播放音频文件timeout.wav
   else PlaySound("timeout.wav");
  }
1
2
3
4
5
6
7
8
9
10
11
12

该示例显示了如何播放来自音频文件 'Ok.wav' 和 'timeout.wav'的声音,这些文件包含在标准的客户端安装程序包中。这些文件位于 terminal_directory\Sounds文件夹中。 在这里 terminal_directory 是指 客户端安装 的文件夹,MetaTrader 5 客户端在这里启动。客户端目录的位置可以通过以下方式,从mql5程序中找到:

//--- 文件夹,客户端存储数据的地方
   string terminal_path=TerminalInfoString(TERMINAL_PATH);
1
2

您不仅可以使用来自terminal_directory\Sounds文件夹的音频文件,还可以使用位于子文件夹terminal_data_directory\MQL5中的任何音频文件。 您可以通过点击 客户端 菜单“文件”---->"打开数据文件夹”或 使用编程方式找到客户端数据目录的位置:

//--- 文件夹,客户端存储数据的地方
   string terminal_data_path=TerminalInfoString(TERMINAL_DATA_PATH);
1
2

例,如果音频文件Demo.wav位于terminal_data_directory\MQL5\Files,那么调用PlaySound() 应该按以下方式编写:

//--- 播放来自terminal_directory_data\MQL5\Files\Demo.wav文件夹的Demo.wav
   PlaySound("\\Files\\Demo.wav");
1
2

请注意在代码注释中,文件路径使用“\”反斜杠,而在函数中使用“\"。 当指定路径时,通常使用 双反斜杠 作为分隔符,因为 单反斜杠 是编译器处理程序源代码中的字符串常数和字符常量时,使用的控制符号。 调用 PlaySound() 函数时,使用NULL参数,表示停止播放:

//--- 调用 PlaySound() 函数时,使用NULL参数,停止播放音频
   PlaySound(NULL);
1
2

ObjectCreate() EA交易程序的示例,使用ObjectCreate()函数创建一个图形标签 (OBJ_BITMAP_LABEL)

string label_name="currency_label";        // OBJ_BITMAP_LABEL 对象的名称
string euro      ="\\Images\\euro.bmp";    // 文件terminal_data_directory\MQL5\Images\euro.bmp的路径
string dollar    ="\\Images\\dollar.bmp";  // 文件terminal_data_directory\MQL5\Images\dollar.bmp的路径
//+------------------------------------------------------------------+
//| EA初始化函数                                                     |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 如果还未创建按钮OBJ_BITMAP_LABEL,请创建
   if(ObjectFind(0,label_name)<0)
     {
      //--- 尝试创建对象OBJ_BITMAP_LABEL
      bool created=ObjectCreate(0,label_name,OBJ_BITMAP_LABEL,0,0,0);
      if(created)
        {
         //--- 连接图表左上角的按钮
         ObjectSetInteger(0,label_name,OBJPROP_CORNER,CORNER_RIGHT_UPPER);
         //--- 现在设置对象属性
         ObjectSetInteger(0,label_name,OBJPROP_XDISTANCE,100);
         ObjectSetInteger(0,label_name,OBJPROP_YDISTANCE,50);
         //--- 重置上一个错误的代码为0
         ResetLastError();
         //--- 下载图片显示按钮的”按下“状态
         bool set=ObjectSetString(0,label_name,OBJPROP_BMPFILE,0,euro);
         //--- 测试结果
         if(!set)
           {
            PrintFormat("Failed to download image from file %s. Error code %d",euro,GetLastError());
           }
         ResetLastError();
         //--- 下载图片显示按钮的”未按下“状态
         set=ObjectSetString(0,label_name,OBJPROP_BMPFILE,1,dollar);

         if(!set)
           {
            PrintFormat("Failed to download image from file %s. Error code %d",dollar,GetLastError());
           }
         //--- 发送一个命令来刷新图表,这样按钮就会立即出现,而不需等到下一个跳价事件
         ChartRedraw(0);
        }
      else
        {
         //--- 创建对象失败,公告
         PrintFormat("Failed to create object OBJ_BITMAP_LABEL. Error code %d",GetLastError());
        }
     }
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 专家去初始化函数                                                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 删除图表对象
   ObjectDelete(0,label_name);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

在OnInit()函数中,创建和设置了名为currency_label的图形对象。图形文件的路径设置在全局变量euro和dollar中,用双反斜杠作为分隔符:

string euro      ="\\Images\\euro.bmp";    // 文件terminal_data_directory\MQL5\Images\euro.bmp的路径
string dollar    ="\\Images\\dollar.bmp";  // 文件terminal_data_directory\MQL5\Images\dollar.bmp的路径
1
2

图象文件位于文件夹 terminal_data_directory\MQL5\Images。

对象 OBJ_BITMAP_LABEL 实际上是一个按钮,根据按钮的状态(按下 或 未按下)展示两个图像中的一个:euro.bmp 或 dollar.bmp。

Alt text

Alt text

(注意: 上图为 MT5 客户端中的对象编辑窗口 参数 标签页)

Alt text

(注意: 上图为 MT4 客户端中的对象编辑窗口 常用 标签页,与MT5中的窗口存在差异)

图形界面的按钮大小会根据图像的大小自动调整。在OBJ_BITMAP_LABEL对象上点击鼠标左键可以看到图像改变( 属性中必须设置"禁用选择")。OBJ_BITMAP对象以相同方式创建 - 它用于创建必要的背景图像。

Alt text

(注意: 上图为 MT5 客户端中的对象编辑窗口 普通 标签页)

OBJPROP_BMPFILE属性的值可以动态更改,该属性负责对象OBJ_BITMAP和OBJ_BITMAP_LABEL的外观。这允许为mql5程序创建各种交互式用户界面。

在mql5程序编译时将资源包含到可执行文件中

mql5程序可能需要许多不同的可下载资源,如不同格式的图像和声音文件的形式。为了避免在迁移MQL5可执行文件时,同时转移所有这些文件的需要,应该使用编译器的指令#resource指令:

#resource path_to_resource_file
1

#resource 指令告诉编译器保存在指定路径path_to_resource_file中的资源应该被包括到可执行EX5 文件。因此,所有这些必要的图像和音频文件可以直接放在EX5文件中,以便在您想要在不同程序端运行程序时,无需分别转移其中使用的文件。任何EX5文件都可以包含资源,而任何EX5程序都可以使用来自其他EX5程序的资源。

BMP 和 WAV 格式的文件在包含到EX5文件之前会自动压缩。这表示除了在MQL5中创建完整的程序之外,使用资源还可以在使用图形和声音时减少必要文件的总大小,这与MQL5程序编写的通常方式相比。

资源文件大小一定不要超过128Mb。

通过编译器搜索指定的资源

插入资源使用命令 #resource "<path to the resource file>"

#resource "<path_to_resource_file>"
1

字符串常量 <path_to_resource_file> 的长度不得超过63个字符。

编译器根据以下顺序在指定路径搜索资源:

• 如果单反斜杠""分隔符(写成"\")放在路径前面,它搜索目录terminal_data_directory\MQL5\涉及的资源。
• 如果没有反斜杠,则搜索资源编写所在的源文件的位置涉及的资源。

资源路径不能包括子字符串"..\" 和 ":\"。

资源包含的示例:

//--- 正确的资源规范
#resource "\\Images\\euro.bmp" // euro.bmp 位于 terminal_data_directory\MQL5\Images\
#resource "picture.bmp"        // picture.bmp 位于与源文件相同的目录
#resource "Resource\\map.bmp"  // 资源位于source_file_directory\Resource\map.bmp

//--- 错误的资源规范
#resource ":picture_2.bmp"     // 不能包括 ":"
#resource "..\\picture_3.bmp"  // 不能包括 ".."
#resource "\\Files\\Images\\Folder_First\\My_panel\\Labels\\too_long_path.bmp"
//超过 63 个字符
1
2
3
4
5
6
7
8
9
10

使用资源

资源名称

当资源使用#resource指令声明后,它可以用于程序的任何部分。资源的名称是:从行开头开始,不包含反斜杠的路径+文件名称,单反斜杠用于设置资源的路径。若要在代码中使用您自己的资源, 应该在资源名称的前面添加特殊符号 "::"。

示例:

//--- 注释中显示了资源规范和其名称的示例
#resource "\\Images\\euro.bmp"          // 资源名称 - Images\euro.bmp
#resource "picture.bmp"                 // 资源名称 - picture.bmp
#resource "Resource\\map.bmp"           // 资源名称 - Resource\map.bmp
#resource "\\Files\\Pictures\\good.bmp" // 资源名称 - Files\Pictures\good.bmp
#resource "\\Files\\Demo.wav";          // 资源名称 - Files\Demo.wav"
#resource "\\Sounds\\thrill.wav";       // 资源名称 - Sounds\thrill.wav"
...

//--- 使用资源文件
ObjectSetString(0,bitmap_name,OBJPROP_BMPFILE,0,"::Images\\euro.bmp");
...
ObjectSetString(0,my_bitmap,OBJPROP_BMPFILE,0,"::picture.bmp");
...
set=ObjectSetString(0,bitmap_label,OBJPROP_BMPFILE,1,"::Files\\Pictures\\good.bmp");
...
PlaySound("::Files\\Demo.wav");
...
PlaySound("::Sounds\\thrill.wav");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

应该注意的是,当将图像从资源设置到OBJ_BITMAP和OBJ_BITMAP_LABEL对象时,OBJPROP_BMPFILE属性的值不能手动修改。例如,为了创建OBJ_BITMAP_LABEL,我们使用资源euro.bmp 和 dollar.bmp。

#resource "\\Images\\euro.bmp";    // euro.bmp位于terminal_data_directory\MQL5\Images\
#resource "\\Images\\dollar.bmp";  // dollar.bmp位于terminal_data_directory\MQL5\Images\
1
2

当查看该对象属性时,我们将看到属性位图文件 (On) 和 位图文件 (Off) 无效,不能手动更改:

Alt text

使用其他mql5程序的资源

资源使用方面还有另一个优势 - 在任何mql5程序中,也可以使用另一个EX5文件的资源。因此,来自一个EX5文件的资源可以用在许多其他mql5程序。

若要使用来自另一个文件的资源名称,它应该被指定为 <path_EX5_file_name>::<resource_name> 例如,假设Draw_Triangles_Script.mq5 脚本在文件triangle.bmp中包含一个图像资源:

#resource "\\Files\\triangle.bmp"
1

然后,如果脚本自身使用该资源,则资源的名称将类似"Files\triangle.bmp",并且若要使用它,资源名称前应该添加"::"符号。

//--- 脚本 自身使用资源
ObjectSetString(0,my_bitmap_name,OBJPROP_BMPFILE,0,"::Files\\triangle.bmp");
1
2

若要使用另一个程序的相同资源,例如EA交易,我们需要添加资源名称路径到terminal_data_directory\MQL5\相对的EX5文件和脚本EX5文件的名称 - Draw_Triangles_Script.ex5。假设脚本位于标准文件夹 terminal_data_directory\MQL5\Scripts, 那么应该按照以下方式调用: 为了从另一个程序中使用相同的资源,例如,从一个EA交易程序中,我们需要向资源名称中添加一个到EX5文件的路径,该路径相对于terminal_data_directory\MQL5\以及脚本的EX5文件- Draw_Triangles_Script.ex5的名称。假设该脚本位于标准的文件夹terminal_data_directory\MQL5\Scripts\中,那么该调用应该写成以下方式:

//--- 在EA程序中使用脚本Draw_Triangles_Script.ex5中的资源
ObjectSetString(0,my_bitmap_name,OBJPROP_BMPFILE,0,"\\Scripts\\Draw_Triangles_Script.ex5::Files\\triangle.bmp");
1
2

如果从另一个EX5程序中调用资源时没有指定可执行文件的路径,那么编译器会在包含调用资源程序的相同文件夹中搜索可执行文件。这意味着如果EA交易程序调用来自Draw_Triangles_Script.ex5的资源时,没有指定路径,就像这样:

//--- 在EA程序中调用脚本Draw_Triangles_Script.ex5中的资源时未指定路径
ObjectSetString(0,my_bitmap_name,OBJPROP_BMPFILE,0,"Draw_Triangles_Script.ex5::Files\\triangle.bmp");
1
2

那么如果EA程序位于terminal_data_directory\MQL5\Experts\中,则编译器将在文件夹terminal_data_directory\MQL5\Experts\中搜索Draw_Triangles_Script.ex5文件。 使用包含资源的自定义指标 对于MQL5应用程序的操作,可能需要一个或多个自定义指标。它们都可以包含在可执行MQL5程序的代码中。将指标纳入资源可以简化应用程序的分布。 下面就是包含和使用位于terminal_data_folder\MQL5\Indicators\目录的SampleIndicator.ex5自定义指标的示例: 下面的示例,是一个包含和使用自定义指标 SampleIndicator.ex5的例子。自定义指标位于terminal_data_folder\MQL5\Indicators\ 文件夹中:

//+------------------------------------------------------------------+
//|                                                     SampleEA.mq5 |
//|                        Copyright 2013, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#resource "\\Indicators\SampleIndicator.ex5"
int handle_ind;
//+------------------------------------------------------------------+
//| EA初始化函数                                                     |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   handle_ind=iCustom(_Symbol,_Period,"::Indicators\\SampleIndicator.ex5");
   if(handle_ind==INVALID_HANDLE)
     {
      Print("Expert: iCustom call: Error code=",GetLastError());
      return(INIT_FAILED);
     }
//--- ...
   return(INIT_SUCCEEDED);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

当OnInit() 函数中的自定义指标创建一个或多个副本时,这种情况需要特别考虑。请谨记资源应该按照以下方式指明:<path_EX5_file_name>::<resource_name>。 例,如果SampleIndicator.ex5指标被包含在EA交易程序SampleEA.ex5 中,作为一个资源,在自定义指标初始化函数中调用iCustom()指定的路径如下: "\Experts\SampleEA.ex5::Indicators\SampleIndicator.ex5"。 当该路径明确设置时,SampleIndicator.ex5自定义指标被严格连接到EA交易程序SampleEA.ex5 ,失去独立工作的能力。 路径本身可以使用GetRelativeProgramPath() 函数接收。用法示例如下:

//+------------------------------------------------------------------+
//|                                              SampleIndicator.mq5 |
//|                        Copyright 2013, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property indicator_separate_window
#property indicator_plots 0
int handle;
//+------------------------------------------------------------------+
//| 自定义指标初始化函数                                                |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 提供自身链接的错误方式
//--- 字符串路径="\\Experts\\SampleEA.ex5::Indicators\\SampleIndicator.ex5";
//--- 接收自身链接的正确方式
  string path=GetRelativeProgramPath();
//--- 指标缓冲映射
   handle=iCustom(_Symbol,_Period,path,0,0);
   if(handle==INVALID_HANDLE)
     {
      Print("Indicator: iCustom call: Error code=",GetLastError());
      return(INIT_FAILED);
     }
   else Print("Indicator handle=",handle);
//---
   return(INIT_SUCCEEDED);
  }
///....
//+------------------------------------------------------------------+
//| GetRelativeProgramPath                                           |
//+------------------------------------------------------------------+
string GetRelativeProgramPath()
  {
   int pos2;
//--- 接收应用程序的绝对路径
   string path=MQLInfoString(MQL_PROGRAM_PATH);
//--- 找出 "\MQL5\" 字符串的位置
   int    pos =StringFind(path,"\\MQL5\\");
//--- 子字符串未找到 - 错误
   if(pos<0)
      return(NULL);
//--- 跳过 "\MQL5" 目录
   pos+=5;
//--- 跳过多余的 '\' 交易品种
   while(StringGetCharacter(path,pos+1)=='\\')
      pos++;
//--- 如果这是一个资源,返回MQL5目录的相对路径
   if(StringFind(path,"::",pos)>=0)
      return(StringSubstr(path,pos));
//--- 为第一个MQL5子目录找到一个分隔符 (例如,MQL5\Indicators)
//--- 如果没找到,返回MQL5目录相对的路径
   if((pos2=StringFind(path,"\\",pos+1))<0)
      return(StringSubstr(path,pos));
//--- 返回子目录相对的路径(例如, MQL5\Indicators)
   return(StringSubstr(path,pos2+1));
  }
//+------------------------------------------------------------------+
//| 自定义指标重复函数                                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double& price[])
  {
//--- 返回prev_calculated值用于下次调用
   return(rates_total);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

资源变量 资源可以使用资源变量来声明并被当做适当类型的变量。声明格式:

#resource path_to_the_resource_file as resource_variable_type resource_variable_name
1

声明示例:

#resource "data.bin" as int ExtData[]           // 声明包含data.bin 文件数据的数字数组
#resource "data.bin" as MqlRates ExtData[]      // 声明包含data.bin文件数据的 简单结构 数组
//--- 字符串
#resource "data.txt" as string ExtCode          // 声明包含data.txt文件数据的字符串(支持ANSI,UTF-8 和 UTF-16 编码)
#resource "data.txt" as string ExtCode[]        // 声明包含data.txt文件字符串的数组(支持ANSI,UTF-8 和 UTF-16 编码)
//--- 图形资源
#resource "image.bmp" as bitmap ExtBitmap[]    // 声明包含BMP位图文件的一维数组,数组大小 = 高 * 宽
#resource "image.bmp" as bitmap ExtBitmap2[][] // 声明包含BMP位图文件的二维数组,数组大小 [高][宽]
1
2
3
4
5
6
7
8

在这样的声明中,资源数据只能通过变量来处理,通过"::<rsource name>"的自动寻址方式不起作用。

#resource "\\Images\\euro.bmp" as bitmap euro[][]
#resource "\\Images\\dollar.bmp"
//+------------------------------------------------------------------+
//|  OBJ_BITMAP_LABEL 对象使用资源创建函数   |
//+------------------------------------------------------------------+
void Image(string name,string rc,int x,int y)
  {
   ObjectCreate(0,name,OBJ_BITMAP_LABEL,0,0,0);
   ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
   ObjectSetString(0,name,OBJPROP_BMPFILE,rc);
  }
//+------------------------------------------------------------------+
//| 脚本程序起始函数                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- 输出存储在欧元资源变量的图像大小[宽,高]
   Print(ArrayRange(euro,1),", ",ArrayRange(euro,0));
//--- 改变欧元图像 - 在中间绘制红色横纹
   for(int x=0;x<ArrayRange(euro,1);x++)
      euro[ArrayRange(euro,1)/2][x]=0xFFFF0000;
//--- 使用资源变量创建图形资源
   ResourceCreate("euro_icon",euro,ArrayRange(euro,0),ArrayRange(euro,1),0,0,ArrayRange(euro,1),COLOR_FORMAT_ARGB_NORMALIZE);
//--- 创建欧元图形标签对象,并在此设置euro_icon资源图像
   Image("Euro","::euro_icon",10,40);
//--- 应用资源的另一个方法,我们不能绘制
   Image("USD","::Images\\dollar.bmp",15+ArrayRange(euro,1),40);
//--- 不提供解决euro.bmp资源的直接方法,因为它已经通过欧元资源变量进行声明
   Image("E2","::Images\\euro.bmp",20+ArrayRange(euro,1)*2,40); // 将会发生执行时间错误
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

脚本执行结果 —— 三个对象中只能创建两个OBJ_BITMAP_LABEL对象。第一个对象的图像在中间有红色条纹。

Alt text

应用资源的重要优势就是资源文件在编译成EX5可执行文件之前自动解压。因此,使用资源变量可以使您将全部必要数据直接放入可执行文件EX5,以及,相较于MQL5程序传统编写方式而言,减少文件的数量和总大小。

使用资源变量可以非常方便的在 市场 中发布产品。

特点
• 特殊的位图资源变量类型告诉编译器,这个资源是一个图像。这种变量接收为uint类型。
• 位图类型数组资源变量可以有两种维度。在这种情况下,数组大小定义为[图片高][图版片宽]。如果指定了一维数组,那么元素数量则等于 图片高*图片宽。
• 下载24-位图像时,阿尔法通道组件的全部图像像素设置为255.
• 下载没有阿尔法通道的32-位图时,阿尔法通道组件的全部图像像素设置为255。
• 下载有阿尔法通道的32-位图时,不以任何方式处理像素。
• 资源文件大小不能超过128 Mb。
• 通过存在的BOM(标题)自动编码检测执行字符串文件。如果没有BOM,则通过文件内容定义编码。支持ANSI,UTF-8和UTF-16编码文件。当阅读文件数据时,全部字符串转换为Unicode。

OpenCL程序

使用资源字符串变量可以极大的促进一些程序的开发。例如,您可以在独立的CL文件内编写一个OpenCL 程序的编码,然后将其包含为一个字符串放入您的MQL5程序资源。

#resource "seascape.cl" as string cl_program
...
int context;
if((cl_program=CLProgramCreate(context,cl_program)!=INVALID_HANDLE)
  {
   //--- 通过OpenCL程序执行进一步操作
  }
1
2
3
4
5
6
7

在这个示例中,如果没有使用cl_program资源变量,那么您需要编写整个代码作为一个大的字符串。 相关参考 ResourceCreate(), ResourceSave(), PlaySound(), ObjectSetInteger(), ChartApplyTemplate(), 文件函数

# 3.5 调用导入函数

在执行mql5程序时导入函数,客户端使用提前绑定。这意味着如果一个程序调用了一个导入的函数,那么对应的模块(ex5或dll)在程序加载时,同时被加载。MQL5和DLL库在调用模块的线程中执行。

不建议使用要加载的模块的完整路径名称,如:Drive:\Directory\FileName.Ext。MQL5库文件是从客户端目录下的\MQL5\ libraries文件夹中加载的。如果未找到 库 文件,则客户端将尝试从terminal_dir\experts 文件夹中加载。

系统 库(DLL) 文件是由操作系统规则加载的。如果库已经加载(例如,另一个EA交易,甚至来自另一个客户端,并行运行),那么它就会请求使用已经加载的 库 。否则,它将按以下顺序执行搜索:

1.从导入dll的那个模块所在的目录开始,模块可以是一个EA交易,脚本,指标或者EX5库;

2.从 客户端数据文件夹\MQL5\Libraries (TERMINAL_DATA_PATH\MQL5\Libraries)开始;
3.从MT5客户端启动的目录开始;
4.系统目录;
5.Windows 目录;
6.当前目录;
7.系统变量PATH指定的目录列表

如果DLL库中还使用另外的DLL工作,当没有找到第二个DLL时,第一个 库 不能装载。

在加载EA交易(脚本、指标)之前,会生成一个所有EX5库模块的公共列表。它将被加载的EA交易(脚本、指标)和 所有库 共同使用。因此,EX5库模块可以一次加载,多次使用。库使用EA交易的预定义变量(脚本、指标)。

导入 EX5库,按以下顺序执行搜索::

1.与导入EX5的EA交易(脚本、指标)的目录相对路径。
2.客户端数据文件夹\MQL5\Libraries;

3.在所有MT5客户端的共同目录中的MQL5\Libraries (Common\MQL5\Libraries)。

在MQL5程序中导入 DLL函数一定要确保定Windows API调用协议允许使用。为了确认这一点,在C或者C++程序源码文本中请添加关键字 __stdcall, 它是专门针对Microsoft(r)编译器的,该协议具有如下特性:

• 调用者(在我们的例子中是指一个mq5程序)应该“看到”一个函数的原型(从DLL中导入),以便正确地将参数组合到堆栈中;
• 调用者(在我们的例子中是mql5程序)将参数以相反的顺序放置到堆栈中,从右到左——按这个顺序,导入函数读取参数并传递给它的;

• 访客(在我们系统中是MQL5程序)以倒序方式将参量进行堆积,从右至左-在此命令中,输入函数从参量中传递;
• 参数是通过值传递的,除了通过引用显式传递的参数(在我们例子中,是字符串)
• 导入的函数通过读取传递给它的参数来独立清理堆栈。

在描述导入函数的原型时,可以使用默认参数。

如果相应的库无法加载,或者DLL禁止使用,或者没有找到导入的函数,那么EA交易就会在日志文件中使用消息“EA交易停止”来停止操作。在这种情况下,EA交易在重新初始化之前不会运行。一个EA交易被重新初始化,可能会因为以下2个原因: 重新编译 或 在其属性对话框中点击了OK(确认)按钮。

传递参量

所有 简单类型 的参数都是通过 值 传递的,除非明确表明通过 引用 传递。当传递一个字符串时,将传递复制的字符串的缓冲区地址;如果一个字符串是通过引用传递的,那么这个字符串的缓冲区的地址就不会被从DLL导入的函数复制。

结构 包括 动态数组,字符串,类 和其他复杂结构,如 枚举对象的静态或动态数组,不能被作为参数传递给导入函数。

当将数组传递给DLL时,数据缓冲区开始的地址总是能传递(不管AS_SERIES标帜是什么)。在DLL中的函数与AS_SERIES标帜无关,传递的数组是一个未定义长度的静态数组;另一个参数应该用于指定数组大小。

# 3.6 运行时间出错

客户端的执行子系统有机会保存错误代码,以防在MQL5程序运行期间发生。对于每个可执行的MQL5程序,都有一个预定义的变量_LastError。

在启动OnInit函数之前,_LastError变量将被重置。如果在计算过程中或内部函数调用过程中出现错误,_LastError变量将接受相应的错误代码。使用GetLastError()函数可以获得存储在该变量中的值。

有几个严重的错误,一旦发生,程序立即终止:

• 除以0;
• 超出数组范围;
• 使用错误 对象指针;

# 3.7 测试交易策略

自动交易的理念是每周7天,全天候24小时的无间断自动交易。自动交易不会疲惫,质疑自己 或 产生恐惧,它完全脱离了任何心理问题。它足够清晰地将交易规则形式化,并在算法中实现,机器人可以不知疲倦地工作。但首先,你必须确保满足以下两个重要条件:
• EA交易程序按照交易系统的规则执行交易操作;
• EA实施的交易策略,在历史数据记录上显示利润。

为了得到这些问题的答案,我们转到策略测试器,它包括在MetaTrader 4/5客户端中。

本节涵盖的内容是策略测试器中的程序测试和参数优化功能:

• 策略测试器中受限制的函数
• 报价跳动生成模式
• 模拟点差
• 测试期间使用真实报价跳动
• 客户端全局变量
• 测试期间的指标计算
• 测试期间加载历史记录
• 多币种测试
• 策略测试的模拟时间
• 测试中的图解对象
• 策略测试器中的 OnTimer() 函数
• 策略测试器中的 Sleep() 函数
• 策略测试器中的 Print() 函数
• 在数学计算中使用策略测试器进行优化问题
• 在“仅使用开(盘)价”模式中同步K线柱
• 策略测试器中的IndicatorRelease() 函数
• 策略测试器中的事件处理
• 测试代理
• 客户端和代理之间的数据交换
• 使用所有客户端的共享文件夹
• 使用 DLLs

策略测试中的函数限制

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

Print() 和 PrintFormat() 函数

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

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

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

订单号生成模式

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

NewTick(每次报价) 事件(价格变化)是EA的主要事件,因此,我们需要生成一个订单号序列来测试EA。MetaTrader 5客户端的策略测试器中共实施三种订单号生成模式:
• 每次报价
• 1 分钟 OHLC (分钟K线柱的OHLC 价)
• 仅使用开盘价

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

"每次报价"(Every Tick)

金融工具的历史报价数据从交易服务器转移到MetaTrader 5客户端,其形式是打包的分钟K线柱。有关请求时发生的详细信息 和 构建请求的时间框架 的详细信息,请参考 MQL5参考 的 组织数据存储 章节。

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

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

当新的分钟开始(秒数等于0)时,新的分钟K线柱并没有打开,但是当收到一个新的报价时——一个价格变化至少一个点。这个数字显示了新的交易周期的第一个分时,它的开盘时间是2011.01.10 00:00。我们在图表上看到的周五和周一之间的价格差距是很普遍的,因为即使是在周末,汇率也会随着收到的新闻消息而波动。

Alt text

The price gap between Friday and Monday 周五和周一之间的价格缺口

对于这个K线柱,我们只知道在2011年1月10日零点零分打开,但是我们不知道秒数。它可能是在00:00:12或00:00:36(新一天后的12或36秒)或在那一分钟内的任何其它时间内打开。但我们知道,在新的一分钟的开盘时,欧元的公开价格为1.28940。

我们也不知道(准确地说,在一秒内)对应这一分钟的K线柱的收盘价,是何时接收到的最后一次跳价的时间。我们只知道一件事——一分钟K线柱的最后收盘价。这一分钟的价格是1.28958。最高、最低的价格出现的时间也不清楚,但我们知道,最高和最低的价格分别是1.28958和1.28940。

要测试交易策略,我们需要一个报(跳)价序列,将在这里模拟EA交易。因此,对于每个分钟K线柱,我们都知道 4 个控制点,在这里,价格十分清楚并肯定。如果一个K线柱只有4个报价,那么就足够执行一个测试,但是通常报(跳)价的数量会大于4个。

因此,在开盘价,最高价,最低价和收盘价之间,需要生成额外的报(跳)价控制点。在MetaTrader 5终端的策略测试器中,“每次报价”的报(跳)价的生成模式的原理请参考 《METATRADER 5终端策略测试器中的订单生成算法》(https://www.mql5.com/zh/articles/75),如下图所示:

Alt text

跳价生成算法

在“每次报价”模式中测试时,EA的 OnTick() 函数在每个控制点都将会被调用。每个控制点都是来自报价序列生成的跳价。 EA将收到模拟时间的和价格,就像在线工作时一样。

重要提示: “每个报价”测试模式是最精确的,但同时也是最耗时的。对于大多数交易策略的初始测试,通常使用其他两种测试模式中的一种就足够了。
1

"1 分钟 OHLC"

“每次报价”模式是三种模式中最准确的模式,但同时也是最慢的模式。 OnTick()处理程序在每次价格跳动时都会运行,价格跳动的数量可能非常巨大。对于一个策略而言,在整个K线柱时间周期内,价格跳动的序列并不重要,有一个更快并更粗糙的模拟模式 - “1分钟OHLC”。

在 "1 分钟 OHLC" 模式,报价序列仅通过一分钟K线柱的OHLC价格搭建,生成的控制点数量也会大量减少 - 因此,测试时间也是如此。OnTick()函数的启动在所有控制点上执行,这些控制点由一分钟K线柱的OHLC的价格构成。

拒绝在开盘价,最高价,最低价和收盘价之间生成额外的中间报价,从开盘价格确定的那一刻起,它就会导致价格发展中出现刚性决定论。这使得创建一个“测试圣杯”成为可能,该测试圣杯显示了测试余额的上升图。

在代码库中介绍了这种“测试圣杯”的一个示例 - Grr-al. (https://www.mql5.com/zh/code/244)

Alt text

EA交易程序Grr-al ,它使用了OHLC价格的特殊性。

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

这已足以确定收到的开盘价的时间,然后分析下一个时间点的报价以确定我们此刻的价格 ---- 是最高价还是最低价。如果价格低于开盘价,那么这是一个最低价,应该即时在此价格上买入,下一个报价将会是最高价,在这里我们关闭平仓或卖出。下一个报价应该是最后一个,也就是收盘价,我们此时应该关闭交易。

如果在此价格之后,我们收到一个价格高于开盘价格的勾号,那么交易顺序是相反的。 在一分钟的K线柱内完成这个“作弊”模式的全部处理动作,然后等待下一个K线柱。

当在历史上测试这样的EA时,一切都会顺利进行,但一旦我们在线启动它,真相就会开始显现 - 余额曲线保持稳定,但方向是向下的。 为了揭示这个技巧,我们只需要在“每次报价”模式下运行EA。

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

"仅使用开(盘)价"

在该模式下,根据选定的时间框架的OHLC价格生成报(跳)价。EA交易的OnTick() 函数只在K线柱开始的开盘价运行。鉴于这个特点,止损价位 和 挂单 可能会在不同于指定价格的价格点触发(尤其在较高的时间框架上测试时)。相对而言,我们有机会快速的运行EA评估测试。

W1(月)和MN1(月)周期在“仅使用开(盘)价”报(跳)价生成模式是个例外:对于这些时间框架,是为每一天的OHLC价格而产生的,而不是一周 或 一个月的OHLC价格。

假设我们在“仅使用开(盘)价”模式下测试一个关于EURUSD H1的EA交易。在这种情况下,跳价的总数(控制点)在测试的间隔内不会超过4*小时数。但是OnTick()处理程序只在一个小时K线柱开盘时调用。正确测试所需的检查发生在其余的跳价上(即“隐藏”在EA中)。

• 计算所需预付款;
• 启动止损和止盈价位;
• 启动挂单;
• 移除过期的挂单。

如果没有打开的头寸 或 挂,我们不需要对隐藏的跳价进行这些检查,并且速度的提升可能是相当大的。这种“仅使用开(盘)价”模式非常适合于测试策略,该策略只在K线开盘时进行处理,不使用挂单,以及止损和止盈订单。对于此类策略,测试的必要准确性被保留下来。。

让我们使用来自标准客户端安装包中的 移动平均线EA交易程序作为示例,它可以在任何模式下被测试。这个EA交易程序的逻辑是:所有的决策都是在K线的开盘时做出的,而且交易是立即进行的,不需要使用挂单。

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

Alt text

移动平均线EA的测试图。来自标准客户端安装包的mq5 EA不依赖于测试模式。

如您所见,来自标准客户端安装包的移动平均线EA在不同测试模式下的图形完全相同。

在“仅使用开(盘)价”模式下有一些限制:

• 您不能使用 随机延迟执行模式。 (请参考 https://www.metatrader5.com/en/terminal/help/testing#trade_mode)

• 在测试的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等等。

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

作为“跳价”生成模式的小节,让我们来考虑一下对EURUSD的不同的“跳价”生成模式的一个可视化比较,从2011.01.11 21:00 -- 2011.01.11 21:30:00的区间上看两个M15框架的K线柱。

使用EA交易程序WriteTicksFromTester.mq5 将报价保存在不同的文件里并且在输入参数中指定了这些文件名的结尾包含了filenameEveryTick, filenameOHLC 和 filenameOpenPrice。

Alt text

我们可以为EA程序WriteTicksFromTester指定开始和结束日期(变量开始和结束)。

为了获得带有三个跳价序列的文件(对应于每一个模式“每次报价”、“1分钟OHLC”和“仅使用开(盘)价”)三个不同的文件,EA在相应模式下启动了三次,每次运行一次。然后,使用TicksFromTester.mq5 指标在图表上显示来自这三个文件的数据。指标的源代码附在本文中。

Alt text

在三种不同的测试模式下,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));
     }
1
2
3
4
5
6
7
8
9
10
11
12

若要读取指标中的数据,我们也使用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));
     }
1
2
3
4
5
6
7
8
9
10
11
12

模拟点差

Bid 与 Ask 之间的差价称为点差。在测试期间,点差不能模仿但可以从历史数据获得。如果在历史数据中点差小于或等于零,那么最后一个已知的(在生成的时刻)点差被测试代理使用。 在策略测试中,点差通常被认为都是浮动的。所以 SymbolInfoInteger(交易品种,SYMBOL_SPREAD_FLOAT) 总是返回TRUE。

此外,历史数据还包含了 跳价的数量 和 交易量。对于数据的存储和检索,我们使用一个特殊的mqlrate结构:

struct MqlRates
  {
   datetime time;         // 周期开始时间
   double   open;         // 开盘价
   double   high;         // 周期的最高价
   double   low;          // 周期的最低价
   double   close;        // 收盘价
   long     tick_volume;  // 跳动量
   int      spread;       // 点差
   long     real_volume;  // 交易量
  };
1
2
3
4
5
6
7
8
9
10
11

测试期间使用真实报价

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

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

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

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

如果 交易品种 的历史数据记录中没有的1分钟K柱,但是存在跳价(tick)数据,那么该数据会被用于策略测试器。例如,外汇品种的交易货币对使用的最后一次报价。如果从服务器只收到 Bid/Ask,而没有(时间框架内的)最后报价,则不会生成K线柱(没有收盘价)。策略测试器会使用这些报价数据,因为它们与1分钟K线柱并不抵触。

报(跳)价数据可能因为种种原因与1分钟K线柱不相符,当将数据从一个源传输到客户端时,由于连接丢失或其他故障。1分钟K线柱的数据在测试中被认为更加可靠。

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

客户端全局变量

在测试期间,客户端的全局变量也被模拟,但是它们与终端的当前全局变量没有关系,在终端中可以使用F3按钮来查看。这意味着在测试期间,所有与终端的全局变量的操作都发生在客户机终端(在测试代理中)之外。

测试期间的指标计算

在实时模式,每次跳价都会计算指标值。策略测试器采用采用了一种成本效益的模型来计算指标 —— 这些指标只在EA运行之前重新计算。这意味着在OnTick()、OnTrade()和OnTimer()函数调用之前,就会对指标进行重新计算。

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

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

如果计时器在EA中,使用EventSetTimer()函数,那么将在OnTimer()处理程序的每个调用之前重新计算指标。因此,使用一个非最优的指标,可以大大提高测试时间。

测试期间加载历史记录

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

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

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

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

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

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

多货币测试

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

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

测试代理只下载缺失的历史记录,并提供少量的空白以提供历史上必要的数据,以便在测试开始时计算指标。对于时间框架D1和更小的框架,下载历史的最小容量是一年。

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

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

测试期间,也模拟"市场报价"窗口 ,以获得交易品种信息.

默认情况下,测试开始时,策略测试器的“市场报价”中只有一个交易品种 —— 当运行测试的交易品种被提及时,所有必要的交易品种都会自动的连接到 策略测试器 的“市场报价”窗口中(不是客户端的“市场报价”窗口 )。

在开始测试多货币EA之前,有必要在终端的“市场报价”中选择测试所需的交易品种,并加载所需的数据。首次调用“外汇”交易品种时,历史记录会在测试代理和客户端之间自动同步。“外汇”交易品种是指测试运行的交易品种。
1

在下列情况下,请参考“其他”交易品种的资料:

• 当在交易品种/时间帧上使用技术指标函数 和 IndicatorCreate() 时;
• 请求其他交易品种的“市场报价”数据:
1.SeriesInfoInteger
2.Bars
3.SymbolSelect
4.SymbolIsSynchronized
5.SymbolInfoDouble
6.SymbolInfoInteger
7.SymbolInfoString
8.SymbolInfoTick
9.SymbolInfoSessionQuote
10.SymbolInfoSessionTradeM
11. MarketBookAdd
12.MarketBookGet
• 通过使用以下函数为交易品种/时间帧请求时间序列:
1.CopyBuffer
2.CopyRates
3.CopyTime
4.CopyOpen
5.CopyHigh
6.CopyLowM
7.CopyClose
8.CopyTickVolume
9.CopyRealVolume
10.CopySpread
在第一次调用“其他”交易品种时,测试进程停止,并将历史记录从终端下载到测试代理。与此同时,生成该交易品种的报价序列。

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

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

策略测试的模拟时间

在测试过程中,本地时间TimeLocal()总是等于服务器时间TimeTradeServer()。反过来,服务器的时间总是等于格林尼治时间(GMT)的时间。这样,所有这些函数在测试期间都显示相同的时间。

如果没有连接到服务器,那么在策略测试器中,GMT和服务器时间之间的差异就会被刻意地消除。无论是否有连接,测试结果都应该是相同的。有关服务器时间的信息不是本地存储的,而是从服务器获取的。

测试的图形对象

在测试/优化过程中,图形对象没有被绘制出来。因此,当在测试/优化过程中引用创建对象的属性时,EA交易程序将得到零值。

该限制不会应用于可视化模式的测试。
1

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

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

为了从定时器的给定频率中检查测试时间的依赖性,我们创建了一个简单的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);
  }
//+------------------------------------------------------------------+
//| EA反初始化函数                                                   |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 停止 Timer
   EventKillTimer();
  }
//+------------------------------------------------------------------+
//| Timer 函数                                                       |
//+------------------------------------------------------------------+
void OnTimer()
  {
//---
// 无操作,处理程序主体为空
  }
//+------------------------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

测试时间测量是在计时器参数的不同值(计时器事件的周期性)中进行的。在获得的数据中,我们将测试时间作为定时器函数的周期。

Alt text

测试时间作为定时器函数的周期。

可以清楚地看到,在EventSetTimer(timer)函数的初始化过程中,在其它条件相同的情况下,计时器参数越小,较小的是OnTimer()处理程序的调用之间的周期(期间),而较大的是测试时间T。

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

在使用图形工作时,Sleep() 函数允许EA或脚本暂停执行mql5-程序。这在请求数据时是有用的,在请求时还没有准备好,您需要等到它准备好。在 组织数据存储 部分可以找到使用Sleep()函数的详细示例。

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

如果作为Sleep()函数执行的结果,策略测试器的当前时间超过了选定的测试周期,那么您将会收到一个错误“在测试时检测到的无限睡眠循环”。如果您收到这个错误,测试结果不会被拒绝,所有的计算指令都是在他们的全部数量(交易数量,价格平静,等等)中执行,并且测试的结果被传递到终端。

Sleep()函数在OnDeinit()中不能工作,因为它被调用后,测试时间将必然超出测试间隔的范围。

Alt text

在MetaTrader 5终端的策略测试器中使用Sleep()函数的方案

使用策略测试器优化数学计算问题(MT4中无此功能)

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

Alt text

数学计算

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

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

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

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

要解决这个问题,我们需要:
• 函数值的计算应该位于OnTester() 函数;
• 函数参数必须被定义为EA交易的 输入变量 ;
编译EA,打开“策略测试器”的窗口。在“输入(参数)”标签,选择所需的输入变量,通过为每个变量输入开始,停止和阶段值来定义参数值的设置。

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

选择“数学运算”模式并点击“开始”按钮,运行优化程序。请注意,在优化期间,策略测试器将搜索OnTester函数的最大值。要查找局部最小值,请从OnTester函数返回计算得到的函数值的倒数:

return(1/function_value);
1

需要检查函数值不能等于零,否则的话,我们可能获得 除数为零 的严重错误 。

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

return(-function_value);
1

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

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

img

用于找到该函数极值的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);
  }
//+------------------------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

Alt text

sink函数(x * x + y * y)的全面优化结果如二维图所示

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

给定的一对参数(x,y)的值越好,颜色饱和度越高。 从sink()公式的形式来看,它的值形成了以(0,0)为中心的同心圆。人们可以在3D图中看到,sink()函数没有单个全局极值:

Alt text

Sink函数的三维图形 在“仅使用开(盘)价”模式下同步K线柱

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

在多交易品种上进行交易的策略测试,在测试器上强加了一些额外的技术要求:
• 为这些交易品种生成每次跳价;
• 为这些交易品种计算指标的值;
• 为这些交易品种计算需要的保证金预付款;
• 为所有交易品种同步生成报价序列。

按照所选的交易模式,策略测试器为每个金融工具(交易品种)生成和处理一个报价序列。每个交易品种的新的K线柱会在同一时刻打开(开盘) ,即使在另一个交易品种上K线柱(可能)已经打开(开盘)了。这意味着当多货币EA测试时,这种情况可能会产生(并经常产生),当一个金融工具有一个新K线柱已经打开(开盘),而其他交易品种还没有打开(开盘)。因此,在测试时,这一切都可能变为现实。

只要使用“每次报价”和“1分钟OHLC”测试模式,测试器中历史数据的真实模拟就不会引起任何问题。对于这些模式来说,一个蜡烛图已生成足够的报价信息,能够同步来自不同交易品种的K线柱。但是如果同步交易品种的K线柱是强制的,我们如何在“仅使用开(盘)价”模式测试多货币策略?在该模式下,只在对应K线柱的开盘时间的一个报价上调用EA。

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

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

EA Synchronize_Bars_Use_Sleep.mq5的代码,显示“仅使用开(盘)价”模式的K线柱形同步示例:

//+------------------------------------------------------------------+
//|                                   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));
  }
//+------------------------------------------------------------------+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70

请注意EA中的最后一行,它显示了同步的事实建立时的当前时间:

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

要显示当前时间,我们使用 TimeTradeServer() 函数而非 TimeCurrent()。 TimeCurrent()函数返回最近的跳价时间,该时间在使用Sleep()后不能改变。在“仅使用开(盘)价”模式运行EA,您将看到同步K线柱的信息。

Alt text

Synchronize_Bars_Use_Sleep_EA 的输出信息

如果您需要获得当前服务器的时间,而不是最新的一次报价到达的时间,请使用TimeTradeServer() 函数替代TimeCurrent()。

还有另一个同步K线柱的方式 —— 使用计时器事件 timer。这种EA的示例是Synchronize_Bars_Use_OnTimer.mq5,请参考本文附件。

测试器中的IndicatorRelease() 函数

策略测试器完成一次测试后,自动打开交易品种的图表,显示完成的交易和EA中使用的指标,这有助于直观地检查进出场的点位,并将其与指标的值进行比较。

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

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

如果在客户终端的目录/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());
  }
1
2
3
4
5
6
7
8
9
10

若要在单项测试完成后禁止在图表上显示指标,请通过OnDeinit()处理程序使用函数IndicatorRelease()。

测试器中的事件处理

要在MetaTrader 5测试器测试历史数据,在EA中,OnTick()处理程序不是强制的。EA中至少包括以下函数中一个的就已经足够:
•OnTick() —— 收到一个新跳价的事件处理程序;
•OnTrade() —— 交易事件处理程序;
•OnTimer() —— 收到一个计时器信号的事件处理程序;
•OnChartEvent() —— 客户事件处理程序。
在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
1

例如,服务器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哈希对于每一组来说都是唯一的,它的体积比计算它的信息量要小很多倍。

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

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

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

由于安全原因,代理从终端(EA、指示器、库等)中获取的ex5文件从来没有记录到硬盘上,因此有运行代理的计算机不能使用发送的数据。所有其他文件,包括DLL,都被记录在沙箱中。在远程代理中,不能使用DLL测试EAs。

测试结果被终端添加到一个特殊的结果缓存中(结果缓存),以便在需要时快速访问它们。对于每一组参数,终端会搜索结果缓存,以避免重复运行。如果没有找到这样一组参数的结果,那么代理就会被赋予进行测试的任务。

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

每次跳价并不是通过网络发送,它们是代理在测试中生成的。
1

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

所有测试代理都来自客户端,相互独立:每个代理都有其自己的日志记录文件夹。另外,代理测试期间的所有操作都发生在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 标帜)
   handle=FileOpen(filename,FILE_WRITE|FILE_READ|FILE_COMMON);
   ... further actions
//---
   return(INIT_SUCCEEDED);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

使用DLLs

为了加快优化,我们不仅可以使用本地,还可以使用远程代理。在这种情况下,远程代理有一些限制。首先,远程代理不显示在日志中执行Print()函数的结果,关于头寸的打开和关闭的消息。在日志中显示了最少的信息,以防止错误地写入EAs,从而使远程代理工作的计算机与消息一起工作。

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

Alt text

勾选 “允许DLL导入”选项

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

注意:当使用第三方EA(脚本、指标)时,如果需要允许DLL调用,您应该意识到,当您在终端设置中勾选允许该选项时所承担的风险。与如何使用EA无关 —— 意即,用于测试 或 在图表上运行,都存在风险。
1