生成自定义事件

除标准事件外,终端还支持通过程序生成自定义事件,其本质和内容由 MQL 程序定义。这类事件将被添加到图表事件的通用队列中,并可由所有相关程序在 OnChartEvent函数中处理。

为自定义事件预留了 65536 个整数标识符的特殊范围:CHARTEVENT_CUSTOM 到 CHARTEVENT_CUSTOM_LAST(含首尾值)。换言之,自定义事件的 ID 必须为 CHARTEVENT_CUSTOM + n,其中 n 的取值范围为 0 到 65535。CHARTEVENT_CUSTOM_LAST 正好等于 CHARTEVENT_CUSTOM + 65535。

通过 EventChartCustom函数向图表发送自定义事件。

bool EventChartCustom(long chartId, ushort customEventId,
long lparam, double dparam, string sparam)

chartId 是事件接收图表的标识符,0 表示当前图表,customEventId是事件 ID(由 MQL 程序开发者选择)。该标识符会自动添加到 CHARTEVENT_CUSTOM 值中,并转换为整数类型。该值将作为第一个参数传递给OnChartEvent处理程序。EventChartCustom的其他参数与 OnChartEvent 中的标准事件参数对应,类型为 longdoublestring,可包含任意信息。

如果用户事件成功加入队列,函数返回 true;如果发生错误则返回 false(错误代码可通过 _LastError 获取)。

在即将进入本书最复杂且关键的交易自动化部分时,我们将着手解决交易机器人开发中的实际应用问题。在展示自定义事件功能的场景下,我们将转向交易环境的多货币(更广义的说,多交易品种)分析。

在前面的指标章节中,我们讨论了 多货币指标 ,但忽略了一个重要细节:尽管这些指标处理了不同交易品种的报价,但其计算过程仅在OnCalculate处理程序中启动,而仅当一个交易品种(即图表的工作交易品种)的新分时报价到时才会触发这个处理程序。实际上,其他金融工具的分时报价基本被跳过了。例如,如果指标应用于交易品种 A,则每次 A 的分时报价到来时,我们将仅获取其他品种(B、C、D)的最新已知分时报价,但很可能在此期间其他交易品种的分时报价而未进行处理。

如果将多货币指标应用于流动性最强的金融工具(即接收分时报价最频繁的金融工具),这种情况的影响就不那么显著了。然而,不同金融工具在全天不同时段可能存在速度差异,如果分析或交易算法需要对投资组合中所有金融工具的最新报价做出最快响应,则当前解决方案并不适用。

遗憾的是,在 MQL5 中,新分时报价到达的标准事件仅适用于当前图表的交易品种。在指标中,此时会调用 OnCalculate 处理程序,而在 EA 交易中则会调用 OnTick 处理程序。

因此,有必要设计一种机制,使 MQL 程序能够接收所有相关金融工具的分时报价通知。这正是自定义事件发挥作用的场景。当然,对于仅分析单一金融工具的程序,这并非必要。

现在,我们将开发一个名为 EventTickSpy.mq5的指标示例,该指标应用于特定交易品种 X,能够通过其 OnCalculate 函数使用EventChartCustom 发送分时报价通知。因此,在专门为接收此类通知而设计的 OnChartEvent处理程序中,将能够收集来自不同交易品种、不同指标实例的通知。

提供此示例仅用于演示目的。后续在研究多货币自动化交易时,我们将对该技术进行调整,以便在 EA 交易中更便捷地使用。

首先,我们将为该指标设计一个自定义事件编号。由于我们需要针对给定列表中的多个交易品种发送分时报价通知,因此可以在此选择不同的策略。例如,可以选择一个事件标识符,并分别在 lparamsparam 参数中传递交易品种在列表中的编号和/或交易品种名称。或者,你可以选取某个常量(大于或等于 CHARTEVENT_CUSTOM),通过将交易品种编号与该常量叠加,获得事件编号(这样我们就可以自由使用所有参数,特别是 lparamdparam,它们可用于传递 AskBid 价格或其他数据)。

我们将重点讨论使用单一事件代码的方案。我们将在 TICKSPY 宏中声明这个事件编号。这将作为默认值,用户可在必要时更改,以避免与其他程序发生冲突(尽管这种可能性较低)。

#define TICKSPY 0xFEED // 65261

该值特意设置为与首个允许的 CHARTEVENT_CUSTOM 值有较大间隔。

在指标首次(交互式)启动时,用户必须指定指标应追踪其分时报价的金融工具列表。为此,我们将描述输入字符串变量 SymbolList,其中包含以逗号分隔的交易品种列表。

用户事件的标识符在 message参数中设置。

最后,我们需要接收图表的标识符来传递该事件。为此,我们将提供Chart参数。用户不应编辑该参数:在手动启动指标的首个实例时,通过将指标附加到图表,即可隐式确定图表。在首个实例以编程方式启动的其他指标副本中,该参数将通过调用 ChartID函数(见下文)填充算法。

input string SymbolList = "EURUSD,GBPUSD,XAUUSD,USDJPY"// List of symbols separated by commas (example)
input ushort message = TICKSPY;                          // Custom message
input longchart = 0;                                     // Receiving chart (do not edit)

例如,在SymbolList参数中,会指定一个包含四个常用交易品种的列表。根据你的 Market Watch需求对其进行编辑。

OnInit处理程序中,我们将列表转换为交易品种的 Symbols 数组,然后在循环中,对数组中的所有交易品种(当前交易品种除外)运行相同的指标(通常存在这样的匹配情况,因为当前品种已由指标的初始副本处理)。

string Symbols[];
   
void OnInit()
{
   PrintFormat("Starting for chart %lld, msg=0x%X [%s]"ChartMessageSymbolList);
   if(Chart == 0)
   {
      if(StringLen(SymbolList) > 0)
      {
         const int n = StringSplit(SymbolList, ',', Symbols);
         for(int i = 0i < n; ++i)
         {
            if(Symbols[i] != _Symbol)
            {
               ResetLastError();
               // run the same indicator on another symbol with different settings,
               // in particular, we pass our ChartID to receive notifications back
               iCustom(Symbols[i], PERIOD_CURRENTMQLInfoString(MQL_PROGRAM_NAME),
                  ""MessageChartID());
               if(_LastError != 0)
               {
                  PrintFormat("The symbol '%s' seems incorrect"Symbols[i]);
               }
            }
         }
      }
      else
      {
         Print("SymbolList is empty: tracking current symbol only!");
         Print("To monitor other symbols, fill in SymbolList, i.e."
            " 'EURUSD,GBPUSD,XAUUSD,USDJPY'");
      }
   }
}

OnInit起始阶段,指标已启动实例的信息会显示在日志中,便于用户明确了解当前运行状况。

如果我们选择为每个交易品种使用独立事件代码的方案,则必须按如下方式调用 iCustom(在 message 中添加 i):

   iCustom(Symbols[i], PERIOD_CURRENTMQLInfoString(MQL_PROGRAM_NAME), "",
      Message + iChartID());

请注意,Chart参数的非零值表明该副本通过编程方式启动,且应监控单个交易品种,即图表的工作交易品种。因此,在运行从属副本时,我们无需传递交易品种列表。

在接收新分时报价时调用的 OnCalculate函数中,我们通过调用 EventChartCustomChart 图表发送Message 自定义事件。在这种情况下,lparam参数未被使用(等于 0)。在 dparam参数中,我们传递当前(最新)价格 price[0](这是 BidLast,具体取决于图表基于何种价格类型:它也是图表处理的最后一个分时报价的价格),并在 sparam 参数中传递交易品种名称。

int OnCalculate(const int rates_totalconst int prev_calculated,
   const intconst double &price[])
{
   if(prev_calculated)
   {
      ArraySetAsSeries(pricetrue);
      if(Chart > 0)
      {
         // send a tick notification to the parent chart
         EventChartCustom(ChartMessage0price[0], _Symbol);
      }
      else
      {
         OnSymbolTick(_Symbolprice[0]);
      }
   }
  
   return rates_total;
}

在指标的原始实例中(即 Chart参数为 0 的情况),我们直接调用一个特殊函数,即一种多资产分时报价处理程序OnSymbolTick。在这种情况下,无需调用 EventChartCustom:尽管此类消息仍会到达图表和该指标副本,但传输过程会消耗数毫秒时间,而且会无谓地增加队列负担。

在该演示中,OnSymbolTick的唯一目的是在日志中输出交易品种名称和最新价格。

void OnSymbolTick(const string &symbolconst double price)
{
   Print(symbol" "DoubleToString(price,
      (int)SymbolInfoInteger(symbolSYMBOL_DIGITS)));
}

当然,在指标的接收(源)副本中,如果接收到我们发送的消息,同样会从OnChartEvent处理函数调用该函数。需注意,终端仅在交互式指标副本(即应用于图表的副本)中调用 OnChartEvent,而通过 iCustom 创建的“不可见”副本中不会显示该事件。

void OnChartEvent(const int id,
   const long &lparamconst double &dparamconst string &sparam)
{
   if(id >= CHARTEVENT_CUSTOM + Message)
   {
      OnSymbolTick(sparamdparam);
      // OR (if using custom event range):
      // OnSymbolTick(Symbols[id - CHARTEVENT_CUSTOM - Message], dparam);
   }
}

我们可以避免在事件中发送价格或交易品种名称,因为初始指标(启动该流程的指标)已知晓通用交易品种列表,因此可通过某种方式告知其列表中交易品种的编号。这可以通过在lparam参数中实现,或如前文所述,通过在用户事件的基准常量中添加一个数字来完成。随后,原始指标在接收事件时,可通过索引从数组中提取对应交易品种,并使用 SymbolInfoTick获取最新分时报价的全部信息,包括不同类型的价格。

我们将在 EURUSD 图表上以默认设置运行该指标,包括 "EURUSD,GBPUSD,XAUUSD,USDJPY" 测试列表。以下是日志内容:

16:45:48.745 (EURUSD,H1) Starting for chart 0, msg=0xFEED [EURUSD,GBPUSD,XAUUSD,USDJPY]
16:45:48.761 (GBPUSD,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.761 (USDJPY,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.761 (XAUUSD,H1) Starting for chart 132358585987782873, msg=0xFEED []
16:45:48.777 (EURUSD,H1) XAUUSD 1791.00
16:45:49.120 (EURUSD,H1) EURUSD 1.13068 *
16:45:49.135 (EURUSD,H1) USDJPY 115.797
16:45:49.167 (EURUSD,H1) XAUUSD 1790.95
16:45:49.167 (EURUSD,H1) USDJPY 115.796
16:45:49.229 (EURUSD,H1) USDJPY 115.797
16:45:49.229 (EURUSD,H1) XAUUSD 1790.74
16:45:49.369 (EURUSD,H1) XAUUSD 1790.77
16:45:49.572 (EURUSD,H1) GBPUSD 1.35332
16:45:49.572 (EURUSD,H1) XAUUSD 1790.80
16:45:49.791 (EURUSD,H1) XAUUSD 1790.80
16:45:49.791 (EURUSD,H1) USDJPY 115.796
16:45:49.931 (EURUSD,H1) EURUSD 1.13069 *
16:45:49.931 (EURUSD,H1) XAUUSD 1790.86
16:45:49.931 (EURUSD,H1) USDJPY 115.795
16:45:50.056 (EURUSD,H1) USDJPY 115.793
16:45:50.181 (EURUSD,H1) XAUUSD 1790.88
16:45:50.321 (EURUSD,H1) XAUUSD 1790.90
16:45:50.399 (EURUSD,H1) EURUSD 1.13066 *
16:45:50.727 (EURUSD,H1) EURUSD 1.13067 *
16:45:50.773 (EURUSD,H1) GBPUSD 1.35334

请注意,在记录来源的(交易品种、时间范围)列中,我们首先看到的是四个请求交易品种上启动的指标实例。

启动后,第一个分时报价是 XAUUSD,而非 EURUSD。随后各交易品种的分时报价以大致相同的频率交替出现。EURUSD 的分时报价标有星号,由此可以直观了解到,如果没有通知,将会遗漏多少其他交易品种的分时报价。

时间戳保存在左侧列中以供参考。

当同一交易品种连续两个事件的报价相同时,通常表明 Ask价发生了变化(此处未显示该价格)。

稍后,在深入研究 MQL5 交易 API 之后,我们将会在 EA 交易中运用相同的原理来实现多货币分时报价响应机制。