多条件筛选事件

正如本章前面章节所述,MQL5 API 允许你基于若干条件请求经济日历事件:

  • 按国家/地区(CalendarValueHistoryCalendarValueLast
  • 按频率(CalendarValueHistoryCalendarValueLast
  • 按事件类型 ID(CalendarValueHistoryByEventCalendarValueLastByEvent
  • 按时间范围(CalendarValueHistoryCalendarValueHistoryByEvent
  • 按自上次日历轮询以来的变更(CalendarValueLastCalendarValueLastByEvent
  • 按特定新闻的 ID (CalendarValueById)

这可以总结为以下函数表(在所有 CalendarValue 函数中,此处仅缺少用于获取单个特定值的 CalendarValueById)。

条件

时间范围

最新变更

国家/地区

CalendarValueHistory

CalendarValueLast

货币

CalendarValueHistory

CalendarValueLast

事件

CalendarValueHistoryByEvent

CalendarValueLastByEvent

这样的工具集涵盖了主要(但并非全部)流行的日历分析场景。因此,在实践中,常常需要在 MQL5 中实现自定义的筛选机制,尤其包括按以下条件请求事件:

  • 多个国家/地区
  • 多种货币
  • 多种事件类型
  • 事件的任意特性值(重要性、经济板块、报告期、类型、是否有预测、对汇率的估计影响、事件名称中的子字符串等)

为了解决这些问题,我们创建了 CalendarFilter 类 (CalendarFilter.mqh)。

由于内置 API 函数的特性,某些新闻特性被赋予了比其他特性更高的优先级。这包括国家/地区、货币和日期范围。它们可以在类的构造函数中指定,之后相应的特性便不能在筛选条件中动态更改。

这是因为该筛选器类后续将扩展新闻缓存功能,以便能够在测试程序中读取,而构造函数的初始条件实际上定义了可以进行进一步筛选的缓存上下文。例如,如果在创建对象时指定了国家代码 "EU",那么通过它请求美国或巴西的新闻显然没有意义。日期范围也类似:在构造函数中指定日期范围导致无法接收该日期范围之外的新闻。

我们也可以创建一个没有初始条件的对象(因为所有构造函数参数都是可选的),这样它就能够缓存和筛选整个日历数据库中的新闻(截至保存那一刻)。

此外,由于国家和货币现在几乎是唯一对应的(欧盟和欧元除外),它们通过单个参数 context 传递给构造函数:如果指定一个长度为 2 个字符的字符串,则表示国家代码(或国家组合);如果长度为 3 个字符,则表示货币代码。对于代码 "EU" 和 "EUR",欧元区是 "EU"(在有正式条约的国家内)的一个子集。在特殊情况下,如果关注非欧元区的欧盟国家,它们也可以用 "EU" 上下文来描述。如有必要,针对这些国家/地区货币(BGN、HUF、DKK、ISK、PLN、RON、HRK、CZK、SEK)新闻的更窄条件可以使用我们稍后将介绍的方法动态添加到筛选器中。然而,由于这些属于特殊情况,无法保证此类新闻一定会进入日历。

我们来开始研究这个类。

class CalendarFilter
{
protected:
   // initial (optional) conditions set in the constructor, invariants
   string context;    // country and currency
   datetime fromto// date range
   bool fixedDates;   // if 'from'/'to' are passed in the constructor, they cannot be changed
   
   // dedicated selectors (countries/currencies/event type identifiers)
   string country[], currency[];
   ulong ids[];
   
   MqlCalendarValue values[]; // filtered results
   
   virtual void init()
   {
      fixedDates = from != 0 || to != 0;
      if(StringLen(context) == 3)
      {
         PUSH(currencycontext);
      }
      else
      {
         // even if context is NULL, we take it to poll the entire calendar base
         PUSH(countrycontext);
      }
   }
   ...
public:
   CalendarFilter(const string _context = NULL,
      const datetime _from = 0const datetime _to = 0):
      context(_context), from(_from), to(_to)
   {
      init();
   }
   ...

为国家/地区和货币分配了两个数组:countrycurrency。如果在对象创建期间它们没有从 context 中填充,那么 MQL 程序将能够为多个国家/地区或货币添加条件,以便对它们执行组合新闻查询。

为了存储所有其他新闻特性的条件,在 CalendarFilter 对象中描述了 selectors 数组,其第二维等于 3。我们可以说这是一种表,其中每行有 3 列。

   long selectors[][3];   // [0] - property, [1] - value, [2] - condition

在第 0 个索引处,将存放新闻特性的标识符。由于这些特性分布在三个基础表(MqlCalendarCountryMqlCalendarEventMqlCalendarValue)中,它们使用广义枚举 ENUM_CALENDAR_PROPERTY (CalendarDefines.mqh) 的元素进行描述。

enum ENUM_CALENDAR_PROPERTY
{                                      // +/- means support for field filtering
   CALENDAR_PROPERTY_COUNTRY_ID,       // -ulong
   CALENDAR_PROPERTY_COUNTRY_NAME,     // -string
   CALENDAR_PROPERTY_COUNTRY_CODE,     // +string (2 characters)
   CALENDAR_PROPERTY_COUNTRY_CURRENCY// +string (3 characters)
   CALENDAR_PROPERTY_COUNTRY_GLYPH,    // -string (1 characters)
   CALENDAR_PROPERTY_COUNTRY_URL,      // -string
   
   CALENDAR_PROPERTY_EVENT_ID,         // +ulong (event type ID)
   CALENDAR_PROPERTY_EVENT_TYPE,       // +ENUM_CALENDAR_EVENT_TYPE
   CALENDAR_PROPERTY_EVENT_SECTOR,     // +ENUM_CALENDAR_EVENT_SECTOR
   CALENDAR_PROPERTY_EVENT_FREQUENCY,  // +ENUM_CALENDAR_EVENT_FREQUENCY
   CALENDAR_PROPERTY_EVENT_TIMEMODE,   // +ENUM_CALENDAR_EVENT_TIMEMODE
   CALENDAR_PROPERTY_EVENT_UNIT,       // +ENUM_CALENDAR_EVENT_UNIT
   CALENDAR_PROPERTY_EVENT_IMPORTANCE// +ENUM_CALENDAR_EVENT_IMPORTANCE
   CALENDAR_PROPERTY_EVENT_MULTIPLIER// +ENUM_CALENDAR_EVENT_MULTIPLIER
   CALENDAR_PROPERTY_EVENT_DIGITS,     // -uint
   CALENDAR_PROPERTY_EVENT_SOURCE,     // +string ("http[s]://")
   CALENDAR_PROPERTY_EVENT_CODE,       // -string
   CALENDAR_PROPERTY_EVENT_NAME,       // +string (4+ characters or wildcard '*')
   
   CALENDAR_PROPERTY_RECORD_ID,        // -ulong
   CALENDAR_PROPERTY_RECORD_TIME,      // +datetime
   CALENDAR_PROPERTY_RECORD_PERIOD,    // +datetime (like long)
   CALENDAR_PROPERTY_RECORD_REVISION,  // +int
   CALENDAR_PROPERTY_RECORD_ACTUAL,    // +long
   CALENDAR_PROPERTY_RECORD_PREVIOUS,  // +long
   CALENDAR_PROPERTY_RECORD_REVISED,   // +long
   CALENDAR_PROPERTY_RECORD_FORECAST,  // +long
   CALENDAR_PROPERTY_RECORD_IMPACT,    // +ENUM_CALENDAR_EVENT_IMPACT
   
   CALENDAR_PROPERTY_RECORD_PREVISED,  // +non-standard (previous or revised if any)
   
   CALENDAR_PROPERTY_CHANGE_ID,        // -ulong (reserved)
};

索引 1 将存储用于在选择新闻记录的条件中与它们进行比较的值。例如,如果要按经济板块设置筛选器,则在 selectors[i][0] 中写入 CALENDAR_PROPERTY_EVENT_SECTOR,并在 selectors[i][1] 中写入标准枚举 ENUM_CALENDAR_EVENT_SECTOR 的一个值。

最后,最后一列(第 2 个索引之下)保留用于比较选择器值与新闻中特性值的操作:所有支持的操作都总结在 IS 枚举中。

enum IS
{
   EQUAL,
   NOT_EQUAL,
   GREATER,
   LESS,
   OR_EQUAL,
   ...
};

我们在 TradeFilter.mqh中见过类似的方法。因此,我们将不仅能安排值的相等条件,还能安排不等或大于/小于关系的条件。例如,很容易想象一个针对 CALENDAR_PROPERTY_EVENT_IMPORTANCE 字段的筛选器,该字段应 GREATER(大于)CALENDAR_IMPORTANCE_LOW(这是标准 ENUM_CALENDAR_EVENT_IMPORTANCE 枚举的一个元素),这意味着选择重要性为“中等”和“高”的新闻。

下一个专为日历定义的枚举是 ENUM_CALENDAR_SCOPE。由于日历筛选通常与时间跨度相关,这里列出了最常用的时间跨度。

#define DAY_LONG     (60 * 60 * 24)
#define WEEK_LONG    (DAY_LONG * 7)
#define MONTH_LONG   (DAY_LONG * 30)
#define QUARTER_LONG (MONTH_LONG * 3)
#define YEAR_LONG    (MONTH_LONG * 12)
   
enum ENUM_CALENDAR_SCOPE
{
   SCOPE_DAY = DAY_LONG,         // Day
   SCOPE_WEEK = WEEK_LONG,       // Week
   SCOPE_MONTH = MONTH_LONG,     // Month
   SCOPE_QUARTER = QUARTER_LONG// Quarter
   SCOPE_YEAR = YEAR_LONG,       // Year
};

所有枚举都放在一个单独的头文件 CalendarDefines.mqh 中。

我们回到 CalendarFilter 类。selectors 数组的类型是 long,这适合存储几乎所有相关类型的值:枚举、日期和时间、标识符、整数,甚至经济指标值,因为它们在日历中以 long 数字的形式存储(以实际值的百万分之一为单位)。然而,如何处理字符串特性呢?

这个问题通过使用字符串数组 stringCache 来解决,所有在筛选条件中提及的行都将添加到此数组中。

class CalendarFilter
{
protected:
   ...
   string stringCache[];  // cache of all rows in 'selectors'
   ...

然后,我们可以在 selectors[i][1] 中轻松保存 stringCache 数组中元素的索引,而不是字符串值本身。

为了用筛选条件填充 selectors 数组,提供了几个 let 方法,特别是针对枚举:

class CalendarFilter
{
...
public:
   // all fields of enum types are processed here
   template<typename E>
   CalendarFilter *let(const E econst IS c = EQUAL)
   {
      const int n = EXPAND(selectors);
      selectors[n][0] = resolve(e); // by type E, returning the element ENUM_CALENDAR_PROPERTY
      selectors[n][1] = e;
      selectors[n][2] = c;
      return &this;
   }
   ...

对于指标的实际值:

   // the following fields are processed here:
   // CALENDAR_PROPERTY_RECORD_ACTUAL, CALENDAR_PROPERTY_RECORD_PREVIOUS,
   // CALENDAR_PROPERTY_RECORD_REVISED, CALENDAR_PROPERTY_RECORD_FORECAST,
   // and CALENDAR_PROPERTY_RECORD_PERIOD (as long)
   CalendarFilter *let(const long valueconst ENUM_CALENDAR_PROPERTY propertyconst IS c = EQUAL)
   {
      const int n = EXPAND(selectors);
      selectors[n][0] = property;
      selectors[n][1] = value;
      selectors[n][2] = c;
      return &this;
   }
   ...

以及对于字符串:

   // conditions for all string properties can be found here (abbreviated)
   CalendarFilter *let(const string findconst IS c = EQUAL)
   {
      const int wildcard = (StringFind(find"*") + 1) * 10;
      switch(StringLen(find) + wildcard)
      {
      case 2:
         // if the initial context is different from the country, we can supplement it with the country,
         // otherwise the filter is ignored
         if(StringLen(context) != 2)
         {
            if(ArraySize(country) == 1 && StringLen(country[0]) == 0)
            {
               country[0] = find// narrow down "all countries" to one (may add more)
            }
            else
            {
               PUSH(countryfind);
            }
         }
         break;
      case 3:
         // we can set a filter for a currency only if it was not in the initial context
         if(StringLen(context) != 3)
         {
            PUSH(currencyfind);
         }
         break;
      default:
         {
            const int n = EXPAND(selectors);
            PUSH(stringCachefind);
            if(StringFind(find"http://") == 0 || StringFind(find"https://") == 0)
            {
               selectors[n][0] = CALENDAR_PROPERTY_EVENT_SOURCE;
            }
            else
            {
               selectors[n][0] = CALENDAR_PROPERTY_EVENT_NAME;
            }
            selectors[n][1] = ArraySize(stringCache) - 1;
            selectors[n][2] = c;
            break;
         }
      }
      
      return &this;
   }

在针对字符串的方法重载中,请注意,长度为 2 或 3 个字符的字符串(如果它们没有模板星号 *,该星号是任意字符序列的替代品)分别进入国家/地区和符号(货币)数组,而所有其他字符串则被视作名称或新闻来源的片段,这两个字段都涉及 stringCacheselectors

该类还以特殊方式支持按事件类型(标识符)进行筛选。

protected:
   ulong ids[];           // filtered event types
   ...
public:
   CalendarFilter *let(const ulong event)
   {
      PUSH(idsevent);
      return &this;
   }
   ...

因此,优先筛选器(在 selectors 数组之外处理)的数量不仅包括国家/地区、货币和日期范围,还包括事件类型标识符。这样的构造决策是由于这些参数可以作为输入传递给某些日历 API 函数。我们以结构体数组(MqlCalendarValueMqlCalendarEventMqlCalendarCountry)中输出字段值的形式获取所有其他新闻特性。我们将根据 selectors 数组中的规则,基于这些结构体执行二次筛选。

所有 let 方法都返回一个指向对象的指针,这允许它们的调用可以链式进行。例如,像这样:

CalendarFilter f;
f.let(CALENDAR_IMPORTANCE_LOWGREATER// important and moderately important news
  .let(CALENDAR_TIMEMODE_DATETIME// only events with exact time
  .let("DE").let("FR"// a couple of countries, or, to choose from...
  .let("USD").let("GBP"// ...a couple of currencies (but both conditions won't work at once)
  .let(TimeCurrent() - MONTH_LONGTimeCurrent() + WEEK_LONG// date range "around" the current time
  .let(LONG_MINCALENDAR_PROPERTY_RECORD_FORECASTNOT_EQUAL// there is a forecast
  .let("farm"); // full text search by news titles

理论上,国家/地区和货币条件可以组合使用。但是,请注意,只能为国家/地区或货币之一(而非两者)设置多个值。在当前实现中,这两个上下文方面中的一个(两者任选其一)仅支持一个或零个值(即不对其进行筛选)。例如,如果选择了货币 EUR,则可以将新闻的搜索上下文缩小到仅德国和法国(国家代码 "DE" 和 "FR")。结果,欧洲央行和欧盟统计局的新闻将被丢弃,具体来说,意大利和西班牙的新闻也将被丢弃。然而,在这种情况下,指定 EUR 是多余的,因为德国和法国没有其他货币。

由于该类使用内置函数,其中 countrycurrency 参数使用逻辑“与”运算应用于新闻,因此请检查筛选条件的一致性。

在调用代码设置了筛选条件之后,有必要根据这些条件选择新闻。这就是公共方法 select 的作用(为简化起见,此处给出简化版)。

public:
   bool select(MqlCalendarValue &result[])
   {
      int count = 0;
      ArrayFree(result);
      if(ArraySize(ids)) // identifiers of event types
      {
         for(int i = 0i < ArraySize(ids); ++i)
         {
            MqlCalendarValue temp[];
            if(PRTF(CalendarValueHistoryByEvent(ids[i], tempfromto)))
            {
               ArrayCopy(resulttempArraySize(result));
               ++count;
            }
         }
      }
      else
      {
         // several countries or currencies, choose whichever is more as a basis,
         // only the first element from the smaller array is used
         if(ArraySize(country) > ArraySize(currency))
         {
            const string c = ArraySize(currency) > 0 ? currency[0] : NULL;
            for(int i = 0i < ArraySize(country); ++i)
            {
               MqlCalendarValue temp[];
               if(PRTF(CalendarValueHistory(tempfromtocountry[i], c)))
               {
                  ArrayCopy(resulttempArraySize(result));
                  ++count;
               }
            }
         }
         else
         {
            const string c = ArraySize(country) > 0 ? country[0] : NULL;
            for(int i = 0i < ArraySize(currency); ++i)
            {
               MqlCalendarValue temp[];
               if(PRTF(CalendarValueHistory(tempfromtoccurrency[i])))
               {
                  ArrayCopy(resulttempArraySize(result));
                  ++count;
               }
            }
         }
      }
      
      if(ArraySize(result) > 0)
      {
         filter(result);
      }
      
      if(count > 1 && ArraySize(result) > 1)
      {
         SORT_STRUCT(MqlCalendarValueresulttime);
      }
      
      return ArraySize(result) > 0;
   }

根据已填充的优先属性数组中的哪一个,该方法调用不同的 API 函数来轮询日历:

  • 如果 ids 数组已填充,则对所有标识符循环调用 CalendarValueHistoryByEvent
  • 如果 country 数组已填充且其大小大于货币数组,则调用 CalendarValueHistory 并遍历国家/地区
  • 如果 currency 数组已填充且其大小大于或等于国家/地区数组的大小,则调用 CalendarValueHistory 并遍历货币

每个函数调用都会填充一个临时的结构体数组 MqlCalendarValue temp[],该数组会顺序累积到 result 参数数组中。在根据主要条件(日期、国家/地区、货币、标识符)将所有相关新闻写入其中(如果存在主要条件)之后,一个辅助方法 filter 开始发挥作用,它根据 selectors 中的条件筛选该数组。在 select 方法的末尾,新闻条目按时间顺序排序,因为组合多个“日历”函数查询的结果可能会打乱顺序。排序是使用 SORT_STRUCT 宏实现的,这在 数组的比较、排序和搜索一节中已经讨论过。

对于新闻数组中的每个元素,filter 方法会调用工作方法 match,该方法返回一个布尔指示,表明该新闻是否符合筛选条件。如果不符合,则从数组中移除该元素。

protected:
   void filter(MqlCalendarValue &result[])
   {
      for(int i = ArraySize(result) - 1i >= 0; --i)
      {
         if(!match(result[i]))
         {
            ArrayRemove(resulti1);
         }
      }
   }
   ...

最后,match 方法会分析我们的 selectors 数组,并将其与传入的 MqlCalendarValue 结构体的字段进行比较。此处的代码以缩略形式提供。

 bool match(const MqlCalendarValue &v)
   {
      MqlCalendarEvent event;
      if(!CalendarEventById(v.event_idevent)) return false;
      
      // loop through all filter conditions, except for countries, currencies, dates, IDs,
      // which have already been previously used when calling Calendar functions
      for(int j = 0j < ArrayRange(selectors0); ++j)
      {
         long field = 0;
         string text = NULL;
         
         // get the field value from the news or its description
         switch((int)selectors[j][0])
         {
         case CALENDAR_PROPERTY_EVENT_TYPE:
            field = event.type;
            break;
         case CALENDAR_PROPERTY_EVENT_SECTOR:
            field = event.sector;
            break;
         case CALENDAR_PROPERTY_EVENT_TIMEMODE:
            field = event.time_mode;
            break;
         case CALENDAR_PROPERTY_EVENT_IMPORTANCE:
            field = event.importance;
            break;
         case CALENDAR_PROPERTY_EVENT_SOURCE:
            text = event.source_url;
            break;
         case CALENDAR_PROPERTY_EVENT_NAME:
            text = event.name;
            break;
         case CALENDAR_PROPERTY_RECORD_IMPACT:
            field = v.impact_type;
            break;
         case CALENDAR_PROPERTY_RECORD_ACTUAL:
            field = v.actual_value;
            break;
         case CALENDAR_PROPERTY_RECORD_PREVIOUS:
            field = v.prev_value;
            break;
         case CALENDAR_PROPERTY_RECORD_REVISED:
            field = v.revised_prev_value;
            break;
         case CALENDAR_PROPERTY_RECORD_PREVISED// previous or revised (if any)
            field = v.revised_prev_value != LONG_MIN ? v.revised_prev_value : v.prev_value;
            break;
         case CALENDAR_PROPERTY_RECORD_FORECAST:
            field = v.forecast_value;
            break;
         ...
         }
         
         // compare value with filter condition
         if(text == NULL// numeric fields
         {
            switch((IS)selectors[j][2])
            {
            case EQUAL:
               if(!equal(fieldselectors[j][1])) return false;
               break;
            case NOT_EQUAL:
               if(equal(fieldselectors[j][1])) return false;
               break;
            case GREATER:
               if(!greater(fieldselectors[j][1])) return false;
               break;
            case LESS:
               if(greater(fieldselectors[j][1])) return false;
               break;
            }
         }
         else // string fields
         {
            const string find = stringCache[(int)selectors[j][1]];
            switch((IS)selectors[j][2])
            {
            case EQUAL:
               if(!equal(textfind)) return false;
               break;
            case NOT_EQUAL:
               if(equal(textfind)) return false;
               break;
            case GREATER:
               if(!greater(textfind)) return false;
               break;
            case LESS:
               if(greater(textfind)) return false;
               break;
            }
         }
      }
      
      return true;
   }

equalgreater 方法几乎完全复制了我们之前使用筛选器类开发时所用的方法。

至此,筛选问题基本得到解决,也就是说,MQL 程序可以按以下方式使用 CalendarFilter 对象:

CalendarFilter f;
f.let()... // a series of calls to the let method to set filtering conditions
MqlCalendarValue records[]; 
if(f.select(records))
{
   ArrayPrint(records);
}

实际上,select 方法还有另一项重要功能,我们将其留作独立研究课题。

首先,在最终的新闻列表中,最好能在过去和未来之间以某种方式插入一个分隔符 (delimiter),以便于快速定位。理论上,这个功能对于日历来说极其重要,但出于某种原因,它在 MetaTrader 5 用户界面和 mql5.com 网站上都不可用。我们的实现能够在过去和未来之间插入一个空结构体,我们应该在视觉上将其显示出来(我们将在下面处理这个问题)。

其次,结果数组的大小可能会相当大(尤其是在选择设置的初始阶段),因此 select 方法还额外提供了限制数组大小 (limit) 的功能。这是通过移除距当前时间最远的元素来实现的。

因此,完整的方法原型如下所示:

bool select(MqlCalendarValue &result[],
   const bool delimiter = falseconst int limit = -1);

默认情况下,不插入定界符,数组也不会被截断。

前几段我们提到了筛选的一个附加子任务,即结果数组的可视化。CalendarFilter 类有一个特殊的方法 format,它将传入的 MqlCalendarValue &data[] 结构体数组转换为一个人类可读的字符串数组 string &result[]。该方法的代码可以在随附的文件 CalendarFilter.mqh 中找到。

bool format(const MqlCalendarValue &data[],
   const ENUM_CALENDAR_PROPERTY &props[], string &result[],
   const bool padding = falseconst bool header = false);

我们想要显示的 MqlCalendarValue 字段是在 props 数组中指定的。回想一下,ENUM_CALENDAR_PROPERTY 枚举包含了所有三个相关的日历结构体中的字段,因此 MQL 程序不仅可以自动显示特定事件记录中的经济指标,还可以显示其名称、特性、国家/地区或货币代码。所有这些都由 format 方法实现。

输出的 result 数组中的每一行都包含一个字段值(数字、描述、枚举元素)的文本表示。result 数组的大小等于输入结构体数量(在 data 中)与所显示字段数量(在 props 中)的乘积。可选参数 header 允许你在输出数组的开头添加一个包含字段(列)名称的行。padding 参数控制文本中额外空格的生成,以便于在等宽字体中显示表格(例如,在日志中)。

CalendarFilter 类还有另一个重要的公共方法:update

bool update(MqlCalendarValue &result[]);

其结构几乎完全重复了 select。然而,该方法调用的是 CalendarValueHistoryByEventCalendarValueHistory 函数,而不是 CalendarValueLastByEventCalendarValueLast。该方法的目的显而易见:查询日历中符合筛选条件的最近变更。但其运行需要一个变更 ID。类中确实定义了这样一个字段:它在 select 方法内部首次被填充。

class CalendarFilter
{
protected:
   ...
   ulong change;
   ...
public:
   bool select(MqlCalendarValue &result[],
      const bool delimiter = falseconst int limit = -1)
   {
      ...
      change = 0;
      MqlCalendarValue dummy[];
      CalendarValueLast(changedummy);
      ...
   }

CalendarFilter 类的一些细微之处仍然“处于幕后”,但我们将在后续章节中逐步讨论。

我们来实际测试一下这个筛选器:首先在一个简单的脚本 CalendarFilterPrint.mq5 中,然后在一个更实用的指标 CalendarMonitor.mq5 中。

在脚本的输入参数中,你可以设置上下文(国家/地区代码或货币)、时间范围以及用于按事件名称进行全文搜索的字符串,还可以限制结果新闻表的大小。

input string Context// Context (country - 2 characters, currency - 3 characters, empty - no filter)
input ENUM_CALENDAR_SCOPE Scope = SCOPE_MONTH;
input string Text = "farm";
input int Limit = -1;

根据这些参数,会创建一个全局筛选器对象。

CalendarFilter f(ContextTimeCurrent() - ScopeTimeCurrent() + Scope);

然后,在 OnStart 中,我们配置了几个额外的常量条件(事件的重要性为“中等”和“高”)以及是否存在预测(字段不等于 LONG_MIN),并将一个搜索字符串传递给对象。

void OnStart()
{
   f.let(CALENDAR_IMPORTANCE_LOWGREATER)
      .let(LONG_MINCALENDAR_PROPERTY_RECORD_FORECASTNOT_EQUAL)
      .let(Text); // with '*' replacement support
      // NB: strings with the character length of 2 or 3 without '*' will be treated
      // as a country or currency code, respectively

接下来,调用 select 方法,并使用 format 方法将得到的 MqlCalendarValue 结构体数组格式化为一个包含 9 列的表格。

 MqlCalendarValue records[];
   // apply the filter conditions and get the result
   if(f.select(recordstrueLimit))
   {
      static const ENUM_CALENDAR_PROPERTY props[] =
      {
         CALENDAR_PROPERTY_RECORD_TIME,
         CALENDAR_PROPERTY_COUNTRY_CURRENCY,
         CALENDAR_PROPERTY_EVENT_NAME,
         CALENDAR_PROPERTY_EVENT_IMPORTANCE,
         CALENDAR_PROPERTY_RECORD_ACTUAL,
         CALENDAR_PROPERTY_RECORD_FORECAST,
         CALENDAR_PROPERTY_RECORD_PREVISED,
         CALENDAR_PROPERTY_RECORD_IMPACT,
         CALENDAR_PROPERTY_EVENT_SECTOR,
      };
      static const int p = ArraySize(props);
      
      // output the formatted result
      string result[];
      if(f.format(recordspropsresulttruetrue))
      {
         for(int i = 0i < ArraySize(result) / p; ++i)
         {
            Print(SubArrayCombine(result" | "i * pp));
         }
      }
   }
}

表格的单元格连接成行并输出到日志中。

使用默认设置(即,针对所有国家和货币,事件名称中包含 "farm" 部分且重要性为“中等”和“高”),你可以得到类似这样的时间表。

Selecting calendar records...

country[i]= / ok

calendarValueHistory(temp,from,to,country[i],c)=2372 / ok

Filtering 2372 records

Got 9 records

TIME | CUR⁞ | NAME | IMPORTAN⁞ | ACTU⁞ | FORE⁞ | PREV⁞ | IMPACT | SECT⁞

2022.06.02 15:15 | USD | ADP Nonfarm Employment Change | HIGH | +128 | -225 | +202 | POSITIVE | JOBS

2022.06.02 15:30 | USD | Nonfarm Productivity q/q | MODERATE | -7.3 | -7.5 | -7.5 | POSITIVE | JOBS

2022.06.03 15:30 | USD | Nonfarm Payrolls | HIGH | +390 | -19 | +436 | POSITIVE | JOBS

2022.06.03 15:30 | USD | Private Nonfarm Payrolls | MODERATE | +333 | +8 | +405 | POSITIVE | JOBS

2022.06.09 08:30 | EUR | Nonfarm Payrolls q/q | MODERATE | +0.3 | +0.3 | +0.3 | NA | JOBS

― | ― | ― | ― | ― | ― | ― | ― | ―

2022.07.07 15:15 | USD | ADP Nonfarm Employment Change | HIGH | +nan | -263 | +128 | NA | JOBS

2022.07.08 15:30 | USD | Nonfarm Payrolls | HIGH | +nan | -229 | +390 | NA | JOBS

2022.07.08 15:30 | USD | Private Nonfarm Payrolls | MODERATE | +nan | +51 | +333 | NA | JOBS

 

现在我们看一下 CalendarMonitor.mq5 指标。它的目的是根据指定的筛选条件,在图表上向用户显示当前选定的事件。为了可视化这个表格,我们将使用已经熟悉的计分板类(Tableau.mqh,参见 期货订单的保证金计算一节)。该指标没有缓冲区和图表。

输入参数允许你设置时间窗口的范围 (scope),以及 CalendarFilter 对象的全局上下文,上下文可以是 Context 中的货币或国家/地区代码(默认为空,即无限制),或者通过布尔标志 UseChartCurrencies 来设置。该标志默认启用,建议使用它以便自动接收构成图表工作交易品种的那些货币的新闻。

input string Context// Context (country - 2 chars, currency - 3 chars, empty - all)
input ENUM_CALENDAR_SCOPE Scope = SCOPE_WEEK;
input bool UseChartCurrencies = true;

可以为事件类型、板块和重要性应用额外的筛选器。

input ENUM_CALENDAR_EVENT_TYPE_EXT Type = TYPE_ANY;
input ENUM_CALENDAR_EVENT_SECTOR_EXT Sector = SECTOR_ANY;
input ENUM_CALENDAR_EVENT_IMPORTANCE_EXT Importance = IMPORTANCE_MODERATE// Importance (at least)

Importance(重要性)设置的是选择的下限,而不是精确匹配。因此,默认值 IMPORTANCE_MODERATE 不仅会捕获中等重要性的事件,还会捕获高重要性的事件。

细心的读者会注意到这里使用了一些未知的枚举:ENUM_CALENDAR_EVENT_TYPE_EXT、ENUM_CALENDAR_EVENT_SECTOR_EXT、ENUM_CALENDAR_EVENT_IMPORTANCE_EXT。它们位于前面提到的 CalendarDefines.mqh 文件中,并且(几乎一对一地)与类似的内置枚举相吻合。唯一的区别是它们增加了一个表示“任何”值的元素。我们需要描述这样的枚举是为了简化条件的输入:现在每个字段的筛选器都使用一个下拉列表进行配置,你可以从中选择一个值或关闭筛选器。如果不是因为增加了枚举元素,我们就必须为每个字段在界面中输入一个逻辑上的“开/关”标志。

此外,输入参数允许你根据事件中是否存在实际值、预测值和前次指标来查询事件,以及通过搜索文本字符串 (Text) 来查询。

input string Text;
input ENUM_CALENDAR_HAS_VALUE HasActual = HAS_ANY;
input ENUM_CALENDAR_HAS_VALUE HasForecast = HAS_ANY;
input ENUM_CALENDAR_HAS_VALUE HasPrevious = HAS_ANY;
input ENUM_CALENDAR_HAS_VALUE HasRevised = HAS_ANY;
input int Limit = 30;

CalendarFiltertableau 对象在全局级别进行描述。

CalendarFilter f(Context);
AutoPtr<Tableaut;

请注意,筛选器只创建一次,而表格则由一个自动选择器表示,并将根据接收数据的大小动态重新创建。

筛选器设置是在 OnInit 中根据输入参数通过连续调用 let 方法完成的。

int OnInit()
{
   if(!f.isLoaded()) return INIT_FAILED;
   
   if(UseChartCurrencies)
   {
      const string base = SymbolInfoString(_SymbolSYMBOL_CURRENCY_BASE);
      const string profit = SymbolInfoString(_SymbolSYMBOL_CURRENCY_PROFIT);
      f.let(base);
      if(base != profit)
      {
         f.let(profit);
      }
   }
   
   if(Type != TYPE_ANY)
   {
      f.let((ENUM_CALENDAR_EVENT_TYPE)Type);
   }
   
   if(Sector != SECTOR_ANY)
   {
      f.let((ENUM_CALENDAR_EVENT_SECTOR)Sector);
   }
   
   if(Importance != IMPORTANCE_ANY)
   {
      f.let((ENUM_CALENDAR_EVENT_IMPORTANCE)(Importance - 1), GREATER);
   }
   
   if(StringLen(Text))
   {
      f.let(Text);
   }
   
   if(HasActual != HAS_ANY)
   {
      f.let(LONG_MINCALENDAR_PROPERTY_RECORD_ACTUAL,
         HasActual == HAS_SET ? NOT_EQUAL : EQUAL);
   }
   ...
   
   EventSetTimer(1);
   
   return INIT_SUCCEEDED;
}

最后,会启动一个辅助计时器。所有工作都在 OnTimer 中实现。

void OnTimer()
{
   static const ENUM_CALENDAR_PROPERTY props[] = // table columns
   {
      CALENDAR_PROPERTY_RECORD_TIME,
      CALENDAR_PROPERTY_COUNTRY_CURRENCY,
      CALENDAR_PROPERTY_EVENT_NAME,
      CALENDAR_PROPERTY_EVENT_IMPORTANCE,
      CALENDAR_PROPERTY_RECORD_ACTUAL,
      CALENDAR_PROPERTY_RECORD_FORECAST,
      CALENDAR_PROPERTY_RECORD_PREVISED,
      CALENDAR_PROPERTY_RECORD_IMPACT,
      CALENDAR_PROPERTY_EVENT_SECTOR,
   };
   static const int p = ArraySize(props);
   
   MqlCalendarValue records[];
   
almost one to one   f.let(TimeCurrent() - ScopeTimeCurrent() + Scope); // shift the time window every time
   
   const ulong trackID = f.getChangeID();
   if(trackID// if the state has already been removed, check for changes
   {
      if(f.update(records)) // request changes by filters
      {
         // if there are changes, notify the user
         string result[];
         f.format(recordspropsresult);
         for(int i = 0i < ArraySize(result) / p; ++i)
         {
            Alert(SubArrayCombine(result" | "i * pp));
         }
      // "fall through" further to update the table
      }
      else if(trackID == f.getChangeID())
      {
         return// calendar without changes
      }
   }
   
   // request a complete set of news by filters
   f.select(recordstrueLimit);
 
   // display the news table on the chart
   string result[];
   f.format(recordspropsresulttruetrue);
   
   if(t[] == NULL || t[].getRows() != ArraySize(records) + 1)
   {
      t = new Tableau("CALT"ArraySize(records) + 1p,
         TBL_CELL_HEIGHT_AUTOTBL_CELL_WIDTH_AUTO,
         CornerMarginsFontSizeFontNameFontName + " Bold",
         TBL_FLAG_ROW_0_HEADER,
         BackgroundColorBackgroundTransparency);
   }
   const string hints[] = {};
   t[].fill(resulthints);
}

如果我们在 EURUSD 图表上使用默认设置运行该指标,我们可以得到如下图所示的画面。

图表上已筛选和格式化的新闻集

图表上已筛选和格式化的新闻集