将日历数据库传输到测试程序

日历仅在线时可供 MQL 程序使用,因此测试新闻交易策略会带来一些困难。一种解决方案是独立创建日历的某个镜像,即缓存,然后在测试程序内部使用它。缓存存储技术可以不同,例如文件或嵌入式 SQLite 数据库。在本节中,我们将展示一个使用文件的实现。

无论如何,在使用日历缓存时,请记住它对应于特定的时间点 X。在 X 之前发生的所有“旧”事件(财经报告)中,实际值已经设定,而在之后的事件中(相对于 X 的“未来”),没有实际值,并且在出现更新、更近期的缓存副本之前也不会有。换句话说,测试 X 右侧的指标和 EA 交易没有意义。至于 X 左侧的那些,你应该避免“前瞻性偏差”,也就是说,在每个特定新闻发布时间之前不要读取当前指标。

注意!在终端中请求日历数据时,所有事件的时间都会报告为考虑到服务器当前时区的时间,包括可能对“夏令时”进行的修正(通常这意味着时间戳增加 1 小时)。这样可以使新闻发布与在线报价时间同步。然而,过去的时钟变化(半年前、一年前或更早)仅显示在报价中,而不显示在日历事件中。整个日历数据库通过 MQL5 根据服务器的当前时区读取。因此,任何创建的日历存档都将包含那些在存储时与当时活动的 DST 模式(开启或关闭)相同的事件的正确时间戳。对于“相反”半年的事件,在读取存档后需要独立地进行一小时的调整。在下面的示例中,这种情况被忽略了。

我们将缓存类称为 CalendarCache 并将其放在名为 CalendarCache.mqh 的文件中。我们将需要在文件中保存日历库的所有 3 个表(MqlCalendarCountryMqlCalendarEventMqlCalendarValue)。MQL5 提供了函数 FileWriteArrayFileReadArray(参见 读写数组),它们可以直接将简单结构体数组写入文件和从文件读取。然而,在我们的案例中,3 个结构体中有 2 个没那么简单,因为它们有字符串字段。因此,我们需要一种单独存储字符串的机制,类似于我们已经在 CalendarFilter 类中使用过的那种(那里有一个字符串数组 stringCache,并且在筛选器中指明了该数组中所需字符串的索引)。

为了避免在同一个“字典”中遗漏来自不同“日历”结构体的字符串,我们将准备一个模板类 StringRef:类型参数 T 将是任何 MqlCalendar 结构体。这将为我们提供一个用于国家/地区的独立字符串缓存,以及一个用于事件类型的独立字符串缓存。

template<typename T>
struct StringRef
{
   static string cache[];
   int index;
   StringRef(): index(-1) { }
   
   void operator=(const string s)
   {
      if(index == -1)
      {
         PUSH(caches);
         index = ArraySize(cache) - 1;
      }
      else
      {
         cache[index] = s;
      }
   }
   
   string operator[](int x = 0const
   {
      if(index != -1)
      {
         return cache[index];
      }
      return NULL;
   }
   
   static bool save(const int handle)
   {
      FileWriteInteger(handleArraySize(cache));
      for(int i = 0i < ArraySize(cache); ++i)
      {
         FileWriteInteger(handleStringLen(cache[i]));
         FileWriteString(handlecache[i]);
      }
      return true;
   }
   
   static bool load(const int handle)
   {
      const int n = FileReadInteger(handle);
      for(int i = 0i < n; ++i)
      {
         PUSH(cacheFileReadString(handleFileReadInteger(handle)));
      }
      return true;
   }
};
   
template<typename T>
static string StringRef::cache[];

字符串通过使用 operator= 存储在 cache 数组中,并使用 operator[] 从中提取(带有一个总是被忽略的虚拟索引)。每个对象只存储字符串在数组中的索引。cache 数组被声明为静态,因此它将累积一个 T 结构体的所有字符串字段。可根据需要更改缓存方法,使结构体的每个字段都有其自己的数组,但这对于我们来说并不重要。

将数组写入文件和从文件读取由一对静态方法 saveload 执行:两者都以文件句柄作为参数。

考虑到 StringRef 类,我们描述一些结构体,它们复制了标准的日历结构体,但使用 StringRef 对象代替字符串字段。例如,对于 MqlCalendarCountry,我们得到 MqlCalendarCountryRef。标准结构体和修改后的结构体通过重载的运算符 '=' 和 '[]' 以类似的方式相互复制。

   struct MqlCalendarCountryRef
   {
      ulong id;
      StringRef<MqlCalendarCountryname;
      StringRef<MqlCalendarCountrycode;
      StringRef<MqlCalendarCountrycurrency;
      StringRef<MqlCalendarCountrycurrency_symbol;
      StringRef<MqlCalendarCountryurl_name;
      
      void operator=(const MqlCalendarCountry &c)
      {
         id = c.id;
         name = c.name;
         code = c.code;
         currency = c.currency;
         currency_symbol = c.currency_symbol;
         url_name = c.url_name;
      }
      
      MqlCalendarCountry operator[](int x = 0const
      {
         MqlCalendarCountry r;
         r.id = id;
         r.name = name[];
         r.code = code[];
         r.currency = currency[];
         r.currency_symbol = currency_symbol[];
         r.url_name = url_name[];
         return r;
      }
   };

请注意,第一种方法的赋值运算符具有来自 StringRef 的重载 '=',因此所有行都进入数组 StringRef<MqlCalendarCountry>::cache。在第二种方法中,'[]' 运算符调用可隐式获取字符串的地址,并直接从 StringRef 返回存储在该地址的 cache 数组中的字符串。

MqlCalendarEventRef 结构体以类似的方式定义,但其中只有 3 个字段(source_urlevent_codename)需要将 string 类型替换为 StringRef<MqlCalendarEvent>MqlCalendarValue 结构体不需要这样的转换,因为它没有字符串字段。

至此,准备阶段结束,你可以继续学习主要的缓存类 CalendarCache

基于总体考量,以及为了与已经开发的 CalendarFilter 类兼容,我们在缓存中描述指定上下文(国家/地区或货币)、存储事件的日期范围以及缓存生成时刻(时间 X,变量 t)的字段。

class CalendarCache
{
   string context;
   datetime fromto;
   datetime t;
   ...
   
public:
   CalendarCache(const string _context = NULL,
      const datetime _from = 0const datetime _to = 0):
      context(_context), from(_from), to(_to), t(0)
   {
      ...
   }

实际上,创建日历缓存时设置限制并没有太大意义。一个完整的缓存可能更实用,因为缓存大小并没有那么重要,到 2022 年中期,缓存大小也只有几十兆字节(这包括从 2007 年开始的历史数据以及计划到 2024 年的事件)。不过,功能被人为限制的演示程序可以适当采取限制措施。

很明显,缓存中应提供日历结构体数组来存储所有数据。

   MqlCalendarValue values[];
   MqlCalendarEvent events[];
   MqlCalendarCountry countries[];
   ...

最初,由 update 方法根据日历数据库来填充这些数组。

   bool update()
   {
      string country = NULLcurrency = NULL;
      if(StringLen(context) == 3)
      {
         currency = context;
      }
      else if(StringLen(context) == 2)
      {
         country = context;
      }
      
      Print("Reading online calendar base...");
      
      if(!PRTF(CalendarValueHistory(valuesfromtocountrycurrency))
         || (currency != NULL ?
            !PRTF(CalendarEventByCurrency(currencyevents)) :
            !PRTF(CalendarEventByCountry(countryevents)))
         || !PRTF(CalendarCountries(countries)))
      {
         // object is not ready, t = 0
      }
      else
      {
         t = TimeTradeServer();
      }
      return (bool)t;
   }

t 字段是缓存健康状况的标志,记录了填充数组的时间。

可以使用 save 方法,将填充的缓存对象写入文件。文件开头有一个标头 CALENDAR_CACHE_HEADER – 这是字符串 "MQL5 Calendar Cache\r\nv.1.0\r\n",它允许你在读取时确保格式正确。接下来,该方法“按原样”保存 contextfromtot 变量以及 values 数组。在数组本身之前,我们写入其大小,以便在读取时恢复它。

   bool save(string filename = NULL)
   {
      if(!treturn false;
      
      MqlDateTime mdt;
      TimeToStruct(tmdt);
      if(filename == NULLfilename = "calendar-" +
         StringFormat("%04d-%02d-%02d-%02d-%02d.cal",
         mdt.yearmdt.monmdt.daymdt.hourmdt.min);
      int handle = PRTF(FileOpen(filenameFILE_WRITE | FILE_BIN));
      if(handle == INVALID_HANDLEreturn false;
      
      FileWriteString(handleCALENDAR_CACHE_HEADER);
      FileWriteString(handlecontext4);
      FileWriteLong(handlefrom);
      FileWriteLong(handleto);
      FileWriteLong(handlet);
      FileWriteInteger(handleArraySize(values));
      FileWriteArray(handlevalues);
      ...

对于 eventscountries 数组,则使用我们带有 "Ref" 后缀的包装结构体。辅助方法 storeevents 数组转换为一个简单结构体数组 erefs,其中字符串被替换为字符串字典 StringRef<MqlCalendarEvent> 中的数字。这样的简单结构体已经可以按常规方式写入文件,但为了后续读取它们,还需要保存字典的所有行(调用 StringRef<MqlCalendarEvent> ::save(handle))。Country 结构体以相同的方式转换并保存到文件。

      MqlCalendarEventRef erefs[];
      store(erefsevents);
      FileWriteInteger(handleArraySize(erefs));
      FileWriteArray(handleerefs);
      StringRef<MqlCalendarEvent>::save(handle);
      
      MqlCalendarCountryRef crefs[];
      store(crefscountries);
      FileWriteInteger(handleArraySize(crefs));
      FileWriteArray(handlecrefs);
      StringRef<MqlCalendarCountry>::save(handle);
      
      FileClose(handle);
      return true;
   }

前面提到的 store 方法非常简单:在该方法中,通过对元素的循环,在 MqlCalendarEventRefMqlCalendarCountryRef 结构体中执行重载的赋值运算符。

   template<typename T1,typename T2>
   void static store(T1 &array[], T2 &origin[])
   {
      ArrayResize(arrayArraySize(origin));
      for(int i = 0i < ArraySize(origin); ++i)
      {
         array[i] = origin[i];
      }
   }

为了将接收到的文件加载到缓存对象中,编写了一个镜像方法 load。该方法按相同的顺序将数据从文件读入变量和数组中,同时对事件类型和国家/地区的字符串字段执行反向转换。

   bool load(const string filename)
   {
      Print("Loading calendar cache "filename);
      t = 0;
      int handle = PRTF(FileOpen(filenameFILE_READ | FILE_BIN));
      if(handle == INVALID_HANDLEreturn false;
      
      const string header = FileReadString(handleStringLen(CALENDAR_CACHE_HEADER));
      if(header != CALENDAR_CACHE_HEADERreturn false// not our format
      
      context = FileReadString(handle4);
      if(!StringLen(context)) context = NULL;
      from = (datetime)FileReadLong(handle);
      to = (datetime)FileReadLong(handle);
      t = (datetime)FileReadLong(handle);
      Print("Calendar cache interval: "from"-"to);
      Print("Calendar cache saved at: "t);
      int n = FileReadInteger(handle);
      FileReadArray(handlevalues0n);
      
      MqlCalendarEventRef erefs[];
      n = FileReadInteger(handle);
      FileReadArray(handleerefs0n);
      StringRef<MqlCalendarEvent>::load(handle);
      restore(eventserefs);
      
      MqlCalendarCountryRef crefs[];
      n = FileReadInteger(handle);
      FileReadArray(handlecrefs0n);
      StringRef<MqlCalendarCountry>::load(handle);
      restore(countriescrefs);
      
      FileClose(handle);
      ... // something else will be here
   }

辅助方法 restore 在对 MqlCalendarEventRefMqlCalendarCountryRef 结构体中的元素进行循环时使用 '[]' 运算符的重载,以通过行号获取行本身并将其分配给标准的 MqlCalendarEventMqlCalendarCountry 结构体。

   template<typename T1,typename T2>
   void static restore(T1 &array[], T2 &origin[])
   {
      ArrayResize(arrayArraySize(origin));
      for(int i = 0i < ArraySize(origin); ++i)
      {
         array[i] = origin[i][];
      }
   }

在这个阶段,我们已经可以编写一个基于 CalendarCache 类的简单测试指标,对在线图表运行这个指标,并将该指标与日历缓存一起保存到文件中。然后可以在测试程序中,通过指标副本加载该文件,并接收完整的事件集。然而,这对于实际开发来说还不够。

事实是,为了快速访问数据,需要提供 indexing,这是编程中一个众所周知的概念,我们稍后将在关于数据库的章节中讨论它。理论上,我们可以使用内置的 SQLite 引擎来存储缓存,然后我们将“免费”获得索引,但我们稍后再讨论细节。

如果我们想象如何在我们的缓存中有效地实现标准日历函数的类似功能,那么索引的意义就很容易理解了。例如,在 CalendarValueById 函数中传递事件 ID。直接枚举 values 数组中的记录将非常耗时。因此,需要用某种“数据结构”来补充该数组,以便优化搜索。我们之所以将“数据结构”用引号括起来,因为它不是指编程语言的含义 (struct),而是泛指数据构建的体系结构。我们所谓的“数据结构”可以由不同的部分组成,并基于不同的组织原则。当然,额外的数据会需要内存,但用内存换取速度是编程中常见的做法。

建立索引最简单的解决方案是一个单独的二维数组,按升序排序,以便可以使用 ArrayBsearch 函数快速搜索。该二维数组的第二维仅需两个元素:索引 [i][0] 处存储排序所用的标识符,索引 [i][1] 处则存储该结构体在原数组中的序号。

另一个常用的概念是 hashing,它是将初始值转换为某些键(哈希值,整数)的过程,其方式是提供最小数量的冲突(不同初始数据的键匹配)。键的基本特性是其值的接近均匀的随机分布,因此它们可以用作预分配数组中的索引。为原始数据的单个元素计算哈希函数是一个快速的过程,实际上可以得到元素本身的地址。例如,众所周知的哈希映射数据结构就遵循这个原则。

如果两个原始值确实得到了相同的哈希值(尽管这种情况很少见),它们会为它们的键排成一个列表,并在列表内执行顺序搜索。然而,由于选择的哈希函数使得匹配数量很小,通常在计算出哈希值后搜索就能命中目标。

为了演示,我们将在 CalendarCache 类中同时使用这两种方法:哈希和二分搜索。

MetaTrader 5 软件包包含一组用于创建哈希映射的类 (MQL5/Include/Generic/HashMap.mqh),但我们将使用我们自己更简单的实现,其中只保留了使用哈希函数的原理。

通过哈希进行数据索引的方案

通过哈希进行数据索引的方案

在我们的案例中,只需对日历对象的标识符进行哈希就足够了。我们选择的哈希函数必须将标识符转换为特殊数组内的索引:日历结构体数组中标识符的位置将存储在该索引的单元格中。对于国家/地区、事件类型和特定新闻,都分配了其各自的数组。

   int id4country[];
   int id4event[];
   int id4value[];

它们的元素将条目的序号存储在相关数组 (countries, events, values) 中。

对于每个“重定向”数组,需要分配的结构体数量必须至少是数据库(和缓存)中相应结构体数量的 3 倍。由于这种冗余,我们最大限度地减少了哈希冲突的数量。据信,选择等于素数的大小可以实现最高效率。因此,该类有一个静态方法 size2prime,它根据源数据中的元素数量返回哈希“桶”数组(id4 数组之一)的推荐大小。

   static int size2prime(const int size)
   {
      static int primes[] =
      {
        175397193389,
        769154330796151,
        12289245934915798317,
        1966133932417864331572869,
        314573962914691258291725165843,
        50331653100663319201326611402653189,
        8053064571610612741
      };
      
      const int pmax = ArraySize(primes);
      for(int p = 0p < pmax; ++p)
      {
         if(primes[p] >= 2 * size)
         {
            return primes[p];
         }
      }
      return size;
   }

整个日历哈希过程在 hash 方法中描述。我们以 countries 结构体数组为例来看看它的开头,其他两个数组的处理方式类似。

因此,我们通过调用 size2prime,根据 countries 数组的大小计算出推荐的“素数”索引大小 id4country。最初,索引数组用值 -1 填充,也就是说,其所有元素都是空闲的。接下来,在遍历国家/地区的循环中,需要为每个下一个国家/地区标识符计算哈希值,并用于在 id4country 数组中找到一个空闲索引。这是辅助方法 place 的工作。

   bool hash()
   {
      Print("Hashing calendar...");
      ...
      const int c = PRTF(ArraySize(countries));
      PRTF(ArrayResize(id4countrysize2prime(c)));
      ArrayInitialize(id4country, -1);
      
      for(int i = 0i < c; ++i)
      {
         if(place(countries[i].idiid4country) == -1)
         {
            return false// failure
         }
      }
      ...
      return true// success
   }

place 内部的哈希函数是表达式 (MathSwap(id) ^ 0xEFCDAB8967452301) % n,其中 id 是我们的标识符,n 是索引数组的大小。因此,计算结果总是被缩减到 array[] 内的有效索引。选择哈希函数的原理是一个独立的主题,超出了本书的范围。

   int place(const ulong idconst int indexint &array[])
   {
      const int n = ArraySize(array);
      int p = (int)((MathSwap(id) ^ 0xEFCDAB8967452301) % n); // hash function
      int attempt = 0;
      while(array[p] != -1)
      {
         if(++attempt > n / 10// number of collisions - no more than 1/10 of the number
         {
            return -1// error writing to index array
         }
         p = (p + attempt) % n;
      }
      array[p] = index;
      return p;
   }

如果索引数组中 p 位置的单元格未被占用(等于 -1),我们立即将日历结构体的位置地址写入元素 [p]。如果单元格已被占用,我们尝试使用公式 p = (p + attempt) % n 来选择下一个,其中 attempt 是尝试次数的计数器(这是我们伪装的具有匹配哈希值的元素列表版本)。如果失败尝试次数达到原始数据的十分之一,索引将失败,但鉴于我们的索引数组的超大规模和哈希数据的已知性质(唯一标识符),这种情况实际上是不可能发生的。

对结构体数组进行哈希处理后,我们得到一个已填充的索引数组(其中有空闲空间,但这是有意设计的),通过它可以根据日历元素的标识符在结构体数组中找到相应结构体的位置。这是通过与 place 含义相反的 find 方法完成的。

   template<typename S>
   int find(const ulong idconst int &array[], const S &structs[])
   {
      const int n = ArraySize(array);
      if(!nreturn false;
      int p = (int)((MathSwap(id) ^ 0xEFCDAB8967452301) % n); // hash function
      int attempt = 0;
      while(structs[array[p]].id != id)
      {
         if(++attempt > n / 10)
         {
            return -1// error extracting from index array
         }
         p = (p + attempt) % n;
      }
      return array[p];
   }

我们展示一下它的实际运用。标准日历函数包括 CalendarCountryByIdCalendarEventById。当你需要在测试程序中测试 MQL 程序时,程序将无法直接访问这两个函数,但能够将日历缓存加载到 CalendarCache 对象中,因此它应该有类似的方法。

   bool calendarCountryById(ulong country_idMqlCalendarCountry &cnt)
   {
      const int index = find(country_idid4countrycountries);
      if(index == -1return false;
      
      cnt = countries[index];
      return true;
   }
   
   bool calendarEventById(ulong event_idMqlCalendarEvent &event)
   {
      const int index = find(event_idid4eventevents);
      if(index == -1return false;
      
      event = events[index];
      return true;
   }

它们使用 find 方法以及索引数组 id4countryid4event

但这些并不是日历最需要的功能。更常见的情况是,包含新闻策略的 MQL 程序需要的是 CalendarValueHistoryCalendarValueHistoryByEventCalendarValueLastCalendarValueLastByEvent 函数。它们提供了按时间、国家/地区或货币快速访问日历条目的功能。

因此,CalendarCache 类也应该提供类似的方法。这里我们将使用第二种“索引”方法―通过在已排序数组中进行二分搜索。

为了实现上述方法,我们向类中再添加 4 个二维数组,以建立新闻与事件类型、新闻与国家/地区、新闻与货币以及新闻与其发布时间之间的对应关系。

   ulong value2event[][2];    // [0] - event_id, [1] - value_id
   ulong value2country[][2];  // [0] - country_id, [1] - value_id
   ulong value2currency[][2]; // [0] - currency ushort[4]<->long, [1] - value_id
   ulong value2time[][2];     // [0] - time, [1] - value_id

在每行的第一个元素中,即索引 [i][0] 下,将分别记录事件 ID、国家/地区、货币或时间。在该系列的第二个元素中,即索引 [i][1] 下,将放置特定新闻的 ID。一次性填充所有数组后,将使用 ArraySort[i][0] 的值对数组进行排序。然后,我们可以按 ID(例如,按 event_id)在 value2event 数组中搜索所有此类新闻: ArrayBsearch 函数将返回第一个匹配元素的编号,随后是具有相同 event_id 的其他元素,直到遇到不同的标识符。第二个“列”中的顺序未定义(可以是任意的)。

基于排序快速搜索相关结构体

基于排序快速搜索相关结构体

这种不同类型结构体相互绑定的操作在 bind 方法中执行。每个“绑定”数组的大小与新闻数组的大小相同。通过循环遍历所有新闻,我们使用现成的索引数组和 find 方法进行快速寻址。

   bool bind()
   {
      Print("Binding calendar tables...");
      const int n = ArraySize(values);
      ArrayResize(value2eventn);
      ArrayResize(value2countryn);
      ArrayResize(value2currencyn);
      ArrayResize(value2timen);
      for(int i = 0i < n; ++i)
      {
         value2event[i][0] = values[i].event_id;
         value2event[i][1] = values[i].id;
         
         const int e = find(values[i].event_idid4eventevents);
         if(e == -1return false;
         
         value2country[i][0] = events[e].country_id;
         value2country[i][1] = values[i].id;
         
         const int c = find(events[e].country_idid4countrycountries);
         if(c == -1return false;
         
         value2currency[i][0] = currencyId(countries[c].currency);
         value2currency[i][1] = values[i].id;
         
         value2time[i][0] = values[i].time;
         value2time[i][1] = values[i].id;
      }
      ArraySort(value2event);
      ArraySort(value2country);
      ArraySort(value2currency);
      ArraySort(value2time);
      return true;
   }

就货币而言,使用 currencyId 函数从字符串获得的特殊数字会被作为标识符。

   static ulong currencyId(const string s)
   {
      union CRNC4
      {
         ushort word[4];
         ulong ul;
      } v;
      StringToShortArray(sv.word);
      return v.ul;
   }

现在我们终于可以展示 CalendarCache 类的整个构造函数了。

   CalendarCache(const string _context = NULL,
      const datetime _from = 0const datetime _to = 0):
      context(_context), from(_from), to(_to), t(0), eventId(0)
   {
      if(from > to// label that context is a filename
      {
         load(_context);
      }
      else
      {
         if(!update() || !hash() || !bind())
         {
            t = 0;
         }
      }
   }

在在线图表上启动时,使用默认参数创建的对象将收集所有日历信息 (update),对其建立索引 (hash),并链接这些表 (bind)。如果在任何阶段出现问题,变量 t 中的错误标志将为 0。如果成功,TimeTradeServer 函数的值将保留在那里(记住,该值是在 update 内部设置的)。这种随时可用的对象可以使用前面描述的 save 方法导出到文件。

在测试程序中启动时,应使用特殊的 fromto 参数组合 (from > to) 创建对象 ― 在这种情况下,程序会将 context 字符串视为文件名,并从中加载日历状态。最简单的方法是这样的:

CalendarCache calca("filename.cal"true);

load 方法内部,我们还将调用 hashbind 以使对象进入工作状态。

   bool load(const string filename)
   {
      ... // reading the file was shown earlier
      const bool result = hash() && bind();
      if(!resultt = 0;
      return result;
   }

CalendarValueLast 函数为例,我们展示了 calendarValueLast 方法(具有完全相同的原型)的等效实现。由于没有开放的软件 API 来读取在线日历变更表,缓存将使用当前的“服务器”时间作为变更标识符。假设我们可以使用 CalendarChangeSaver.mq5 服务保存的变更 ID 信息,但这种方法需要在开始测试之前进行长期的统计数据收集。因此,测试程序生成的“服务器”时间被认为是相当充分的替代方案。

当 MQL 程序首次以空标识符请求变更时,我们直接返回 TimeTradeServer 提供的值。

   int calendarValueLast(ulong &changeMqlCalendarValue &result[],
      const string code = NULLconst string currency = NULL)
   {
      if(!change)
      {
         change = TimeTradeServer();
         return 0;
      }
      ...

如果变更标识符已经不为零,我们继续算法的主要分支。

根据 codecurrency 参数的内容,我们找到国家/地区和货币的标识符。默认为 0,这意味着搜索所有变更。

      ulong country_id = 0;
      ulong currency_id = currency != NULL ? currencyId(currency) : 0;
      
      if(code != NULL)
      {
         for(int i = 0i < ArraySize(countries); ++i)
         {
            if(countries[i].code == code)
            {
               country_id = countries[i].id;
               break;
            }
         }
      }
      ...

接下来,使用传递的时间计数 change 作为搜索的开始,我们在 value2time 中找到所有新闻,直到新的、当前值 TimeTradeServer。在循环内部,我们使用 find 方法在 values 数组中查找相应 MqlCalendarValue 结构体的索引,并在必要时将关联事件类型的国家/地区和货币与期望值进行比较。所有符合标准的新闻项都写入 result 输出数组。

      const ulong past = change;
      const int index = ArrayBsearch(value2timepast);
      if(index < 0 || index >= ArrayRange(value2time0)) return 0;
      
      int i = index;
      while(value2time[i][0] <= (ulong)past && i < ArrayRange(value2time0)) ++i;
      
      if(i >= ArrayRange(value2time0)) return 0;
      
      for(int j = ij < ArrayRange(value2time0)
         && value2time[j][0] <= (ulong)TimeTradeServer(); ++j)
      {
         const int p = find(value2time[j][1], id4valuevalues);
         if(p != -1)
         {
            change = TimeTradeServer();
            if(country_id != 0 || currency_id != 0)
            {
               const int q = find(values[p].event_idid4eventevents);
               if(country_id != 0 && country_id != events[q].country_idcontinue;
               if(currency_id != 0)
               {
                  const int m = find(events[q].country_idid4countrycountries);
                  if(countries[m].currency != currencycontinue;
               }
            }
            
            PUSH(resultvalues[p]);
         }
      }
      
      return ArraySize(result);
   }

calendarValueHistorycalendarValueHistoryByEventcalendarValueLastByEvent 方法的实现原理类似(后者实际上将所有工作委托给前面讨论的 calendarValueLast 方法)。完整的源代码可以在附件文件 CalendarCache.mqh 中找到。

基于缓存类,创建一个派生类 CalendarFilter 是合乎逻辑的,该类在处理请求时将访问缓存而不是日历。

完成的解决方案在文件 CalendarFilterCached.mqh 中。由于缓存 API 是在标准 API 的基础上设计的,集成仅简化为将筛选器调用转发到缓存对象(自动指针 cache)。

class CalendarFilterCachedpublic CalendarFilter
{
protected:
   AutoPtr<CalendarCachecache;
   
   virtual bool calendarCountryById(ulong country_idMqlCalendarCountry &cntoverride
   {
      return cache[].calendarCountryById(country_idcnt);
   }
   
   virtual bool calendarEventById(ulong event_idMqlCalendarEvent &eventoverride
   {
      return cache[].calendarEventById(event_idevent);
   }
   
   virtual int calendarValueHistoryByEvent(ulong event_idMqlCalendarValue &temp[],
      datetime _fromdatetime _to = 0override
   {
      return cache[].calendarValueHistoryByEvent(event_idtemp_from_to);
   }
   
   virtual int calendarValueHistory(MqlCalendarValue &temp[],
      datetime _fromdatetime _to = 0,
      const string _code = NULLconst string _coin = NULLoverride
   {
      return cache[].calendarValueHistory(temp_from_to_code_coin);
   }
   
   virtual int calendarValueLast(ulong &_changeMqlCalendarValue &result[],
      const string _code = NULLconst string _coin = NULLoverride
   {
      return cache[].calendarValueLast(_changeresult_code_coin);
   }
   
   virtual int calendarValueLastByEvent(ulong event_idulong &_change,
      MqlCalendarValue &result[]) override
   {
      return cache[].calendarValueLastByEvent(event_id_changeresult);
   }
   
public:   
   CalendarFilterCached(CalendarCache *_cache): cache(_cache),
      CalendarFilter(_cache.getContext(), _cache.getFrom(), _cache.getTo())
   {
   }
   
   virtual bool isLoaded() const override
   {
 // readiness is determined by the cache
      return cache[].isLoaded();
   }
};

为了在测试程序中测试日历,我们创建一个新版本的指标 CalendarMonitor.mq5CalendarMonitorCached.mq5

主要区别如下。

我们假设某个缓存文件将以名称 "xyz.cal"(在文件夹 MQL5/Files 中)创建或已经创建,因此我们使用指令 tester_file将其连接到 MQL 程序。

#property tester_file "xyz.cal"

该指令确保将缓存传输到任何代理,包括分布式代理(然而,这对于 EA 交易而言比指标更相关)。可以使用新的输入变量 CalendarCacheFile 创建具有此名称(或其他名称)的缓存文件。如果用户将默认名称更改为其他名称,那么要在测试程序中工作,你将需要更正该指令(需要重新编译!),或者将文件传输到终端的共享文件夹(缓存类支持此功能,但“留作幕后”),然而,这样的文件不再可供远程代理使用。

input string CalendarCacheFile = "xyz.cal";

CalendarFilter 对象现在被描述为一个自动指针,因为根据指标运行的位置,它可以使用原始类 CalendarFilter 以及派生类 CalendarFilterCached

AutoPtr<CalendarFilterfptr;
AutoPtr<CalendarCachecache;

OnInit 的开头,有一个新的片段负责生成缓存和读取它。

int OnInit()
{
   cache = new CalendarCache(CalendarCacheFiletrue);
   if(cache[].isLoaded())
   {
      fptr = new CalendarFilterCached(cache[]);
   }
   else
   {
      if(MQLInfoInteger(MQL_TESTER))
      {
         Print("Can't run in the tester without calendar cache file");
         return INIT_FAILED;
      }
      else
      if(StringLen(CalendarCacheFile))
      {
         Alert("Calendar cache not found, trying to create '" + CalendarCacheFile + "'");
         cache = new CalendarCache();
         if(cache[].save(CalendarCacheFile))
         {
            Alert("File saved. Re-run indicator in online chart or in the tester");
         }
         else
         {
            Alert("Error: "_LastError);
         }
         ChartIndicatorDelete(00MQLInfoString(MQL_PROGRAM_NAME));
         return INIT_PARAMETERS_INCORRECT;
      }
      Alert("Currently working in online mode (no cache)");
      fptr = new CalendarFilter(Context);
   }
   CalendarFilter *f = fptr[];
   ... // continued without changes

如果已读取缓存文件,我们将获得完成的 CalendarCache 对象,该对象传递给 CalendarFilterCached 构造函数。否则,程序会检查它是在测试程序中运行还是在线运行。测试程序中缺少缓存是致命的情况。在常规图表上,程序会基于内置日历数据创建一个新对象,并将其保存在指定名称的缓存中。但如果文件名为空,指标将完全像原始指标一样工作,即直接与日历交互。

我们在 EURUSD 图表上运行该指标。用户将收到警告,提示未找到指定文件,并已尝试保存它。前提是终端设置中已启用日历,我们应该在日志中得到大致如下的行。下面是带有详细诊断信息的版本。可以通过在源代码中注释掉指令 #define LOGGING 来禁用详细信息。

Loading calendar cache xyz.cal
FileOpen(filename,FILE_READ|FILE_BIN|flags)=-1 / CANNOT_OPEN_FILE(5004)
Alert: Calendar cache not found, trying to create 'xyz.cal'
Reading online calendar base...
CalendarValueHistory(values,from,to,country,currency)=157173 / ok
CalendarEventByCountry(country,events)=1493 / ok
CalendarCountries(countries)=23 / ok
Hashing calendar...
ArraySize(countries)=23 / ok
ArrayResize(id4country,size2prime(c))=53 / ok
Total collisions: 9, worse:3, average: 2.25 in 4
ArraySize(events)=1493 / ok
ArrayResize(id4event,size2prime(e))=3079 / ok
Total collisions: 495, worse:7, average: 1.43478 in 345
ArraySize(values)=157173 / ok
ArrayResize(id4value,size2prime(v))=393241 / ok
Total collisions: 3511, worse:1, average: 1.0 in 3511
Binding calendar tables...
FileOpen(filename,FILE_WRITE|FILE_BIN|flags)=1 / ok
Alert: File saved. Re-run indicator in online chart or in the tester

现在,我们可以在测试程序中选择指标 CalendarMonitorCached.mq5,并根据历史数据动态查看新闻表的变化情况。

测试程序中使用日历缓存的新闻指标

测试程序中使用日历缓存的新闻指标

日历缓存的存在允许你根据新闻测试交易策略。我们将在下一节中讨论。