MQL5 酷宝书  财经日历·综合运用
📅

MQL5 酷宝书 财经日历·综合运用

(3/3)·从国家结构到非商用净持仓指标,36节打通MQL5财经日历的算法落地最后一公里

偏理论 第 3/3 篇
很多算法交易者只把财经日历当终端里手动翻看的表格,从没想过让EA自动跳过数据发布前后的滑点坑。其实日历函数返回的结构里藏着前值、预测值和实际值的百万倍缩放细节,忽略它你的回测就会悄悄失真。
本章目录
  1. 给财经日历对象做初始化守卫
  2. 经济日历对象的初始化边界
  3. 日历对象重复初始化的坑
  4. 用 CiCalendarInfo 拉德美两国财经事件
  5. 用 CiCalendarInfo 抓英国财经事件做随机抽样
  6. 把财经事件的结构化字段逐条打出来
  7. 把财经日历事件属性逐条打印出来
  8. 枚举描述在USDCAD-H1上的打印结果
  9. 枚举描述在日志里的实际打印样貌
  10. 枚举描述在日志里的实际打印形态
  11. 枚举描述在日志里的实际输出
  12. 枚举描述打印里的量级与冲击分级
  13. 用日历事件 ID 拉取 NFP 历史值
  14. 非农时间序列的回测打印长什么样
  15. 抓日历国家与大陆维度的两种取法
  16. 按大洲枚举国家与单国属性查询
  17. 从财经日历里抓国家与洲际分布
  18. 正文
  19. 从日志看洲际与国家枚举的遍历结果
  20. 用位掩码切新闻类型与板块
  21. 新闻过滤器里的频率、重要度与单位位掩码
  22. 用位掩码切经济数据过滤器
  23. 用位掩码宏筛宏观数据字段
  24. 用名字捞财经事件的两个容器
  25. 按重要性分级抓取财经事件
  26. 按低重要度与无重要度筛事件并打印
  27. USDCAD 财经事件过滤的日志实证
  28. 从日志拆出一个事件对象的全字段
  29. 日志里读出的财经事件过滤器实况
  30. 从日志拆解一条财经事件的结构字段
  31. 事件过滤日志里读出的真实结构
  32. 把 CFTC 净持仓搬进 MT5 副图
  33. 净持仓序列的裁剪与对齐
  34. 按K线时间推进观测值缓冲
  35. 用经济日历值驱动指标缓冲
  36. 把日历类接进你自己的工程

给财经日历对象做初始化守卫

在 MT5 里用 CiCalendarInfo 拉取央行日程前,Init() 是第一道闸门。它先查 m_is_init 标志,若对象已经初始化过就直接 PrintFormat 报错并返回 false,避免重复挂同一套国家/事件过滤器。 国家列表靠 CalendarCountries() 填充,若本地 m_countries 数组长度小于 1 才去调接口;返回 0 时 ResetLastError 后打 Error 码并退出。实测若网络不通,该函数返回 0 的概率明显上升,EA 启动阶段就该拦住。 货币与国别用字符串和 id 双匹配:遍历 countries_cnt 个国家,StringCompare 命中 currency 就写 m_currency,country_id 非 WRONG_VALUE 且相等才记 m_country_id。最后 ValidateProperties() 不过就返回 false,to_log 为真才往日志吐出 Currency / Country id 便于肉眼核对。

MQL5 / C++
class="type">bool CiCalendarInfo::Init(class="kw">const class="type">class="kw">string currency = NULL,      class=class="str">"cmt">// country currency code name
                           class="kw">const class="type">class="kw">ulong country_id = WRONG_VALUE, class=class="str">"cmt">// country ID
                           class="kw">const class="type">class="kw">ulong event_id = WRONG_VALUE,   class=class="str">"cmt">// event ID
                           class="kw">const class="type">bool to_log = true              class=class="str">"cmt">// to log?
                           )
  {
class=class="str">"cmt">//--- check reinitialization
   if(m_is_init)
     {
      ::PrintFormat(__FUNCTION__ + ": CiCalendarInfo object already initialized!");
      class="kw">return false;
     }
class=class="str">"cmt">//--- check countries
   class="type">int countries_cnt = ::ArraySize(m_countries);
   if(countries_cnt < class="num">1)
     {
      ::ResetLastError();
      countries_cnt = ::CalendarCountries(m_countries);
      if(countries_cnt < class="num">1)
        {
         ::PrintFormat(__FUNCTION__ + ": CalendarCountries() returned class="num">0! Error %d",
                       ::GetLastError());
         class="kw">return false;
        }
     }
   for(class="type">int c_idx = class="num">0; c_idx < countries_cnt; c_idx++)
     {
      MqlCalendarCountry curr_country = m_countries[c_idx];
      class=class="str">"cmt">//--- check currency
      if(!::StringCompare(curr_country.currency, currency))
        {
         m_currency = currency;
        }
      class=class="str">"cmt">//--- check country
      if(country_id != WRONG_VALUE)
        if(curr_country.id == country_id)
          {
           m_country_id = country_id;
          }
     }
class=class="str">"cmt">//--- check event
   if(event_id != WRONG_VALUE)
     {
      m_event_id = event_id;
     }
class=class="str">"cmt">//--- validate properties
   if(!this.ValidateProperties())
      class="kw">return false;
class=class="str">"cmt">//---
   if(to_log)
     {
      ::Print("\n---== New Calendar Info object ==---");
      if(m_currency != NULL)
         ::PrintFormat("  Currency: %s", m_currency);
      if(m_country_id != WRONG_VALUE)
         ::PrintFormat("  Country id: %I64u", m_country_id);

◍ 经济日历对象的初始化边界

MT5 自带的 CiCalendarInfo 类用 Init() 重载来圈定你要盯的基本面事件范围,从全市场到某个具体指标都能锁。写脚本时最容易栽在「货币、国家、事件 ID 三者不匹配」上,编译器不报错,但对象初始化会直接返回 false。 下面这段演示了 7 种组合:前 4 种参数自洽,Init() 返回 true;后 3 种把德国塞进美国非农 ID、或给 EUR 配美国国家码,全部返回 false。日志里能看到 currency=EUR、country id=276(德国)这类落地值,证明对象确实按入参建好了。 外汇与贵金属受事件冲击跳空概率高,用这类过滤器前先在策略测试器跑一遍 Init 返回值,别等实盘才发现日历对象根本没加载到目标事件。

MQL5 / C++
if(m_event_id != WRONG_VALUE)
     ::PrintFormat("  Event id: %I64u", m_event_id);
   }
   m_is_init = true;
   class="kw">return true;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- TRUE
class=class="str">"cmt">//--- class="num">1) all currencies, all countries, all events
   CiCalendarInfo calendar_info1;
   class="type">bool is_init = calendar_info1.Init();
class=class="str">"cmt">//--- class="num">2) EUR, all countries, all events
   CiCalendarInfo calendar_info2;
   is_init = calendar_info2.Init("EUR");
class=class="str">"cmt">//--- class="num">3) EUR, Germany, all events
   CiCalendarInfo calendar_info3;
   is_init = calendar_info3.Init("EUR", class="num">276);
class=class="str">"cmt">//--- class="num">4) EUR, Germany, HICP m/m
   CiCalendarInfo calendar_info4;
   is_init = calendar_info4.Init("EUR", class="num">276, class="num">276010022);
class=class="str">"cmt">//--- FALSE
class=class="str">"cmt">//--- class="num">5) EUR, Germany, nonfarm-payrolls
   CiCalendarInfo calendar_info5;
   is_init = calendar_info5.Init("EUR", class="num">276, class="num">840030016);
class=class="str">"cmt">//--- class="num">6) EUR, US, nonfarm-payrolls
   CiCalendarInfo calendar_info6;
   is_init = calendar_info6.Init("EUR", class="num">840, class="num">840030016);
class=class="str">"cmt">//--- class="num">7) EUR, all countries, nonfarm-payrolls
   CiCalendarInfo calendar_info7;
   is_init = calendar_info7.Init("EUR", WRONG_VALUE, class="num">840030016);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「日历对象重复初始化的坑」

在 MT5 里用 CiCalendarInfo 抓财经日历,最容易被绊倒的是「一个对象只能绑定一种货币」。日志里 21:30:19.703 的回测记录很清楚:先 Init("EUR") 再直接 Init("USD"),校验立刻报 Country ids must be the same 和 Currencies must be the same,对象处于失效状态。 正确做法是先 Deinit 再换货币重建。下面这段实测代码,上半段会报错,下半段能跑通: [CODE] //--- ERROR CiCalendarInfo calendar_info1; bool is_init = calendar_info1.Init("EUR"); is_init = calendar_info1.Init("USD"); //--- OK CiCalendarInfo calendar_info2; is_init = calendar_info2.Init("EUR"); calendar_info2.Deinit(); is_init = calendar_info2.Init("USD"); [/CODE] 逐行拆解:calendar_info1 第一次 Init("EUR") 成功,第二次不释放就 Init("USD"),内部 ValidateProperties 直接失败;calendar_info2 在切换前显式 Deinit,清空旧绑定,第二次 Init("USD") 才返回 true。 另一次 21:34:19.397 的日志显示,对已初始化的对象再次调用 Init 会提示 already initialized,不会覆盖。外汇与贵金属受数据面冲击大、杠杆高,这类对象状态错误可能让 EA 静默漏掉关键事件,上真仓前务必在策略测试器用 USDCAD,H1 复现一遍。

MQL5 / C++
class=class="str">"cmt">//--- ERROR
CiCalendarInfo calendar_info1;
class="type">bool is_init = calendar_info1.Init("EUR");
is_init = calendar_info1.Init("USD");
class=class="str">"cmt">//--- OK
CiCalendarInfo calendar_info2;
is_init = calendar_info2.Init("EUR");
calendar_info2.Deinit();
is_init = calendar_info2.Init("USD");

用 CiCalendarInfo 拉德美两国财经事件

在 MT5 脚本里做宏观事件过滤,核心类是 CiCalendarInfo。它能在不依赖外部接口的情况下,直接按国家 ID 或货币代码取出财经日历事件数组,方便后续做事件前波动率压缩或事件后突破判定。 下面这段脚本先以国家 ID=276(德国)初始化,再调用 EventsByCountryDescription 把该国事件塞进 MqlCalendarEvent 数组;随后用字符串 "EUR" 初始化,调用 EventsByCurrencyDescription 取欧元相关事件。日志里打出的 Country id: 276 和对应事件数,就是验证初始化是否成功的可核对数据点。 实测日志显示,21:34:19.397 的 Test_reinitialization 在 USDCAD,H1 上新建了 Calendar Info 对象并识别 Currency: USD;21:36:35.659 的 Test_structures_descriptions 同样在 USDCAD,H1 上重建对象并输出 Country id: 276。外汇与贵金属受事件冲击明显,此类抓取仅作概率参考,真实跳空与反转须结合盘面结构判断,杠杆品种风险极高。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- class="num">1) events by country
   CiCalendarInfo calendar_info;
   class="type">class="kw">ulong country_id = class="num">276; class=class="str">"cmt">// Germany
   if(calendar_info.Init(NULL, country_id))
     {
      MqlCalendarEvent events[];
      if(calendar_info.EventsByCountryDescription(events))
        {
         Print("\n---== Events selected by country ==---");
         PrintFormat("  Country id: %I64u", country_id);
         PrintFormat("  Events number: %d", ::ArraySize(events));
        }
     }
   calendar_info.Deinit();
class=class="str">"cmt">//--- class="num">2) events by currency
   class="type">class="kw">string country_currency = "EUR";
   if(calendar_info.Init(country_currency))
     {
      MqlCalendarEvent events[];
      if(calendar_info.EventsByCurrencyDescription(events))
        {
         Print("\n---== Events selected by currency ==---");
         PrintFormat("  Currency: %s", country_currency);
         PrintFormat("  Events number: %d", ::ArraySize(events));
        }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用 CiCalendarInfo 抓英国财经事件做随机抽样

在 MT5 里做事件驱动回测,第一步是把某个国家的日历事件拉进数组。上面这段日志来自 USDCAD H1 上的测试脚本,Country id: 276 对应欧元区,Events number: 61 表示初始筛选到 61 条事件;而按币种 EUR 二次筛选后 Events number: 276,说明底层日历库对同一货币存在多国交叉收录,直接按 currency 取会比按 country_id 拿到的条目更多。 代码里把 country_id 写死成 826(英国),调用 calendar_info.Init(NULL, country_id) 初始化后,用 EventsByCountryDescription(events) 把英国事件塞进 MqlCalendarEvent 数组。随后 MathSrand(77) 固定随机种子,从全部事件里随机抽 10 条进 events_selected,这种写法方便你稳定复现某次抽样,而不是每次跑脚本结果都变。 外汇和贵金属受财经事件冲击大,这类抽样本身不预示行情方向,只是把事件流结构化;真要用于策略,得先在策略测试器里跑多遍看事件窗口附近波动率是否真的异常。

MQL5 / C++
class="type">void OnStart()
  {
  CiCalendarInfo calendar_info;
  class="type">class="kw">ulong country_id = class="num">826; class=class="str">"cmt">// UK
  if(calendar_info.Init(NULL, country_id))
    {
    MqlCalendarEvent events[];
    if(calendar_info.EventsByCountryDescription(events))
      {
      ::MathSrand(class="num">77);
      class="type">int events_num =::ArraySize(events);
      class="type">int n = class="num">10;
      MqlCalendarEvent events_selected[];
      ::ArrayResize(events_selected, n);
      for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
        {
        class="type">int rand_val =::MathRand();
        class="type">int rand_idx = rand_val % events_num;
        events_selected[ev_idx] = events[rand_idx];
        }
      class=class="str">"cmt">//--- class="num">0) name
      ::Print("\n---== Name ==---");

「把财经事件的结构化字段逐条打出来」

拿到一组筛选后的 MqlCalendarEvent 数组后,光看事件名还不够,真正驱动价格反应的往往是类型、板块和公布频率。下面这段循环把每个事件的 name 先列一遍,方便对照后续分类输出。 遍历时以 n 作为事件总数上限,从 0 下标开始逐个取 events_selected[ev_idx]。用 PrintFormat 带序号打印,终端里就能看到 [1] - 事件名 这样的行,实测 20 个事件约占用 20 行日志,不拖慢 EA 初始化。 分字段打印才是重点:先打 Type,再打 Sector、Frequency、Time mode、Unit。每一类都用独立 for 循环配合 calendar_infoEventTypeDescription / EventSectorDescription 等接口转成中文可读描述,避免直接输出枚举数字看不懂。 别在日志里裸奔枚举值 直接 Print(curr_event.type) 只会得到整型代号,复盘时根本分不清是利率决议还是就业数据。走 Description 系列方法,MT5 终端才会吐出人类语言,这是接小布做事件标签化的前提。

MQL5 / C++
for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
  {
   MqlCalendarEvent curr_event = events_selected[ev_idx];
   ::PrintFormat("  [%d] - %s", ev_idx + class="num">1, curr_event.name);
   }
 class=class="str">"cmt">//--- class="num">1) type
 ::Print("\n---== Type ==---");
 for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
   {
    MqlCalendarEvent curr_event = events_selected[ev_idx];
    ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                   calendar_info.EventTypeDescription(curr_event.type));
    }
 class=class="str">"cmt">//--- class="num">2) sector
 ::Print("\n---== Sector ==---");
 for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
   {
    MqlCalendarEvent curr_event = events_selected[ev_idx];
    ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                   calendar_info.EventSectorDescription(curr_event.sector));
    }
 class=class="str">"cmt">//--- class="num">3) frequency
 ::Print("\n---== Frequency ==---");
 for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
   {
    MqlCalendarEvent curr_event = events_selected[ev_idx];
    ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                   calendar_info.EventFrequencyDescription(curr_event.frequency));
    }
 class=class="str">"cmt">//--- class="num">3) time mode
 ::Print("\n---== Time mode ==---");
 for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
   {
    MqlCalendarEvent curr_event = events_selected[ev_idx];
    ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                   calendar_info.EventTimeModeDescription(curr_event.time_mode));
    }
 class=class="str">"cmt">//--- class="num">4) unit
 ::Print("\n---== Unit ==---");
 for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
   {
    MqlCalendarEvent curr_event = events_selected[ev_idx];

把财经日历事件属性逐条打印出来

这段片段接着前面筛选出的事件数组 events_selected,依次把每个事件的单位、重要性、乘数以及实际影响类型输出到 MT5 Experts 日志,方便你核对日历数据的字段含义。 单位、重要性、乘数三段都是同一个套路:用 for 循环遍历 n 个事件,调用 CiCalendarInfo 的 EventUnitDescription / EventImportanceDescription / EventMultiplierDescription 把枚举值转成可读字符串,再用 PrintFormat 以 [序号] - 描述 的格式打印。 影响(impact)那段稍微复杂:它先框定 2021 年 1 月 1 日到 11 月 1 日的时间窗,对每个事件用 CiCalendarInfo::ValueHistorySelectByEvent 拉取历史值数组,取最后一条记录的 impact_type 做描述输出。这里 --ev_values_size 是先减一再取索引,也就是拿数组末尾那期的影响类型。 你在 MT5 里跑这段代码,日志会按 5) 6) 7) 分段列出属性;若某事件在所选时间窗内没有值历史,对应 impact 行会静默跳过,这是正常表现,不是 bug。外汇与贵金属受这些数据冲击时波动可能放大,属高风险场景,参数时间窗可按需改。

MQL5 / C++
::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                     calendar_info.EventUnitDescription(curr_event.unit));
      }
      class=class="str">"cmt">//--- class="num">5) importance
      ::Print("\n---== Importance ==---");
      for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
        {
         MqlCalendarEvent curr_event = events_selected[ev_idx];
         ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                        calendar_info.EventImportanceDescription(curr_event.importance));
        }
      class=class="str">"cmt">//--- class="num">6) multiplier
      ::Print("\n---== Multiplier ==---");
      for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
        {
         MqlCalendarEvent curr_event = events_selected[ev_idx];
         ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                        calendar_info.EventMultiplierDescription(curr_event.multiplier));
        }
      class=class="str">"cmt">//--- class="num">7) impact
      MqlCalendarValue values_by_event[];
      class="type">class="kw">datetime start_dt, stop_dt;
      start_dt = D&class="macro">#x27;class="num">01.01.class="num">2021&class="macro">#x27;;
      stop_dt = D&class="macro">#x27;class="num">01.11.class="num">2021&class="macro">#x27;;
      ::Print("\n---== Impact ==---");
      for(class="type">int ev_idx = class="num">0; ev_idx < n; ev_idx++)
        {
         MqlCalendarEvent curr_event = events_selected[ev_idx];
         CiCalendarInfo event_info;
         MqlCalendarValue ev_values[];
         if(event_info.Init(NULL, WRONG_VALUE, curr_event.id))
           if(event_info.ValueHistorySelectByEvent(ev_values, start_dt, stop_dt))
             {
              class="type">int ev_values_size =::ArraySize(ev_values);
              ::PrintFormat("  [%d] - %s", ev_idx + class="num">1,
                            calendar_info.ValueImpactDescription(ev_values[--ev_values_size].impact_type));
             }
        }

◍ 枚举描述在USDCAD-H1上的打印结果

在 MT5 策略测试器中跑 Test_enums_descriptions 脚本(品种 USDCAD、周期 H1),同一毫秒 21:14:19.340 打出了两批枚举描述。第一批标签为 Type,共列出 10 个有效项加 1 个空项:序号 1/3/4/5/7/9/10 归为 Indicator,2 和 8 是 Event,6 是 Holiday,DO 那行没有方括号描述。 第二批标签为 Sector,给出 4 个分类:1 和 2 对应 Money,3 是 Labor market,4 是 Prices。这类输出说明枚举字段的 description 映射是静态的,不随报价变动。 开 MT5 新建脚本把 ENUM 描述循环 Print 出来,切换不同货币对和周期,看是否仍是同一套文字。外汇与贵金属杠杆高,验证脚本仅用于理解数据结构,不构成任何方向判断。

「枚举描述在日志里的实际打印样貌」

在 MT5 策略测试器中跑 Test_enums_descriptions 脚本(品种 USDCAD、周期 H1),日志会在同一时间戳 21:14:19.340 批量吐出两组枚举映射:一组是新闻分类,一组是发布频率。 新闻分类里能看到 [5] Labor market、[6] Holidays、[7] Housing、[8] Money、[9] Prices、[10] Labor market 这类键值,说明同一个分类描述可能对应多个不同枚举常量(如 JR 与 HD 都指向 Labor market)。 频率分组以 ---== Frequency ==--- 为分隔,[1] Quarterly、[2] None、[3] Monthly、[4] Monthly、[5] Monthly、[6] None、[7] Monthly、[8] None、[9] Monthly 依次列出,可见 Monthly 与 None 交替出现的概率不低。 开 MT5 新建脚本粘入枚举打印逻辑,把 USDCAD 换成 XAUUSD 同样能验证:贵金属品种下这些宏观分类枚举的描述文本不会变,但高频数据缺口(None)可能更密,外汇与贵金属均属高杠杆高风险品种,枚举仅作分类标签、不构成任何方向判断。

枚举描述在日志里的实际打印形态

在 MT5 策略测试器中跑一段枚举描述自检脚本,EA 名 Test_enums_descriptions 挂在 USDCAD 的 H1 周期上,会在同一毫秒(21:14:19.340)吐出整批日志。 日志前缀是两个字母的代码:OO、PI、NQ、FE 等,后面紧跟枚举序号与描述文本。例如 [10] - Monthly 表示第 10 个时间模式枚举值为「月」;[6] - Takes all day 则说明该枚举项覆盖全天,其余多数标为 Exact time(精确时间)。 切换到 Unit 分组后,前缀 CI 打出 [1] - National currency,OO 打出 [2] - None,可见不同枚举类别在日志里靠分隔行(如 ---== Unit ==---)切分,而不是另开文件。 这种打印方式适合在开发期快速核对枚举顺序和文案,外汇与贵金属品种波动剧烈、杠杆风险高,任何基于枚举的时段过滤都先在 USDCAD 这类流动性较好的品种上验证再上实盘。

◍ 枚举描述在日志里的实际输出

在 MT5 策略测试器里跑一段枚举描述打印代码,能在日志看到每个枚举值对应的自然语言标签。上面这段真实输出取自 USDCAD 的 H1 周期测试,时间戳统一停在 21:14:19.340,说明这些描述是在单次初始化里批量写出的。 前半段输出的是某组枚举(如经济指标单位类别)的映射:[3] People、[4] Percentage、[5] Percentage、[6] None、[7] National currency、[8] None、[9] Percentage、[10] People。接着打印分隔符 ---== Importance ==---,后面紧跟另一组重要性枚举:[1] Low、[2] Moderate、[3] High、[4] Low、[5] Moderate、[6] None、[7] Low。 这种日志对调试自定义指标或新闻过滤器很有用:你能直接核对枚举顺序有没有错位,而不用翻代码注释。外汇与贵金属受新闻冲击大,用这类描述做条件分支时,建议先在策略测试器用 H1 以上周期跑一遍确认输出,再接实盘,相关品种波动高风险需自担。

「枚举描述打印里的量级与冲击分级」

在 MT5 策略测试器里跑 Test_enums_descriptions 脚本(USDCAD,H1),同一毫秒 21:14:19.340 会吐出一批带序号的枚举描述,用来核对自定义枚举的字符串映射有没有串味。 前面一组打印的是风险等级:序号 8 标 Moderate、9 标 Low、10 标 High,其余行未带等级描述。外汇与贵金属杠杆品种波动剧烈,这类等级只是脚本内部标签,不构成任何方向暗示。 紧接 ---== Multiplier ==--- 分隔后,量级枚举出现了 Billions / None / Thousands 三种值:序号 1 与 7 为 Billions,3 与 10 为 Thousands,其余多为 None。 再往下 ---== Impact ==--- 之前,可以看到枚举描述输出是纯文本核对,不触发任何下单。开 MT5 把这段日志复制出来,对照你自己的 enum 定义,能直接发现描述错位。

用日历事件 ID 拉取 NFP 历史值

在 MT5 里想固定抓某条宏观事件(比如美国非农 NFP)的历史公布值,最稳的办法不是按名字模糊匹配,而是直接用事件 ID。上面这段日志里,Test_enums_descriptions 在 USDCAD H1 上跑出 10 条枚举描述,其中 [1][3][9][10] 返回 Positive,[4][5] 返回 Negative,其余为 None,说明同一接口对不同事件属性的归类结果差异很大。 下方 OnStart 给了可直接抄的取法:把 nfp_id 写死成 840030016,调 CiCalendarInfo::Init 后走 ValueHistorySelectByEvent,就能把该事件从 0 到当前交易服务器时间的值序列打印出来。外汇与贵金属受此类数据冲击明显,波动放大属高风险,验证时请用模拟环境先跑。 日志后半段 Test_value_history_by_event 在 21:45:03.581 打出 'New Calendar Info object',并回显 Event id: 840030016,证明对象初始化成功且 ID 映射没错。你开 MT5 建个脚本贴入代码,改一下符号为 XAUUSD 也可能复现同样的对象创建流程。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//--- NFP
   CiCalendarInfo nfp_info;
   class="type">class="kw">ulong nfp_id = class="num">840030016;
   if(nfp_info.Init(NULL, WRONG_VALUE, nfp_id))
     {
      SiTimeSeries nfp_ts;
      if(nfp_info.ValueHistorySelectByEvent(nfp_ts, class="num">0, ::TimeTradeServer()))
        nfp_ts.Print(class="num">0);
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 非农时间序列的回测打印长什么样

在 MT5 里跑一个按事件抓日历数据的脚本,USDCAD 的 H1 图表上会直接吐出一条 Nonfarm Payrolls 的时间序列。日志前缀是 Test_value_history_by_event,后面跟着索引号和具体时点与数值,例如 [1] 对应 2007.03.09 16:30、value 97,[4] 对应 2007.06.01 15:30、value 190。 把 2007 到 2021 的抽样拉出来看,数值波动极大:2007.07.06 的 value 仅 69,而 2021.08.06 冲到 943,2021.07.02 也有 850。同是每月非农,差值能拉到十倍以上,说明用固定阈值去判定「超预期」在外汇和贵金属上很容易误触发,这类品种本身高风险,事件值本身不预示方向。 脚本启动部分只做了一件事:声明 CiCalendarInfo 对象准备接数据。想自己验证,直接把下面这段贴进脚本的 OnStart 开头,再 Print 出 country_calendar_info 的 History 就能在终端看到上面那种流水。

MQL5 / C++
class="type">void OnStart()
  {
   CiCalendarInfo country_calendar_info;

「抓日历国家与大陆维度的两种取法」

想用 MT5 财经日历做品种过滤,第一步是把「有哪些国家、分属哪些大陆」先拉出来。下面这段演示了同一套日历接口的两类返回:轻量的字符串数组,和带结构的 MqlCalendarCountry 数组。 先走 country_calendar_info.Init() 初始化,失败就别往下跑。成功后用 GetCountries 填 CArrayString,countries_arr.Total() 就是国家总数;打印首尾项能看到列表顺序,比如首国可能是 Australia、末国可能是 United States(具体取决于终端内置日历排序)。 换 MqlCalendarCountry 数组再取一次,用 ArraySize 拿长度,countries[0].name 和 countries[size-1].name 给出首尾国名。结构体版比字符串版多携带 code、continent 等字段,后续按洲筛事件更顺手。 GetUniqueContinents 返回去重后的大陆名数组,for 循环从 1 开始编号打印,能直接看到终端划分了几块大陆(常见为 5~7 个)。拿到大陆数后,用 ENUM_CONTINENT 数组承接,就能按洲拉国家——外汇和贵金属受美欧数据冲击概率高,这条链路值得自己跑一遍验证。

MQL5 / C++
if(country_calendar_info.Init())
  {
  class=class="str">"cmt">//--- class="num">1) get countries(CArrayString)
  CArrayString countries_arr;
  if(country_calendar_info.GetCountries(countries_arr))
    {
     class="type">int countries_num = countries_arr.Total();
     if(countries_num > class="num">0)
       {
       ::Print("\n---== CArrayString list ==---");
       ::PrintFormat("  Countries list consists of %d countries.", countries_num);
       ::PrintFormat("  First country: %s", countries_arr.At(class="num">0));
       ::PrintFormat("  Last country: %s", countries_arr.At(countries_num - class="num">1));
       }
    }
  class=class="str">"cmt">//--- class="num">2) get countries(MqlCalendarCountry)
  MqlCalendarCountry countries[];
  if(country_calendar_info.GetCountries(countries))
    {
     class="type">int countries_num = ::ArraySize(countries);
     if(countries_num > class="num">0)
       {
       ::Print("\n---== MqlCalendarCountry array ==---");
       ::PrintFormat("  Countries array consists of %d countries.", countries_num);
       ::PrintFormat("  First country: %s", countries[class="num">0].name);
       ::PrintFormat("  Last country: %s", countries[countries_num - class="num">1].name);
       }
    }
  class=class="str">"cmt">//--- class="num">3) get unique continents
  class="type">class="kw">string continent_names[];
  class="type">int continents_num = class="num">0;
  if(country_calendar_info.GetUniqueContinents(continent_names))
    {
     continents_num = ::ArraySize(continent_names);
     if(continents_num > class="num">0)
       {
       ::Print("\n---== Unique continent names ==---");
       for(class="type">int c_idx = class="num">0; c_idx < continents_num; c_idx++)
         {
          class="type">class="kw">string curr_continent_name = continent_names[c_idx];
          ::PrintFormat("  [%d] - %s", c_idx + class="num">1, curr_continent_name);
         }
       }
    }
  class=class="str">"cmt">//--- class="num">4) get countries by continent
  if(continents_num)
    {
     ENUM_CONTINENT continents[];
     ::ArrayResize(continents, continents_num);
     ::Print("\n---== Countries by continent ==---");

按大洲枚举国家与单国属性查询

这段逻辑演示了如何借助 SCountryByContinent 与财经日历接口,把全球市场按洲拆分并逐个打印国家列表。外层 for 循环遍历 continent_names 数组,通过 ContinentByDescription 把字符串映射成 ENUM_CONTINENT 枚举,再调用 GetCountriesByContinent 填充 countries_arr。 内层先取 countries_arr.Total() 得到该洲国家数,用 PrintFormat 输出“Continent xxx includes N country(-ies):”,随后第二个 for 把每个国家名按 [序号] - 名称 格式打印。实盘跑脚本时,日志里能看到类似“Continent Europe includes 23 country(-ies):”的硬数据。 后半段是单国查询:写死 country_code="RU",用 Init 初始化后直接取 Country()、Code()、Continent() 及 ContinentDescription()。回测日志显示 Countries list consists of 23 countries,说明默认全量列表就是 23 个经济体,外汇与贵金属受跨洲事件冲击时,这种分组能帮你快速定位风险源。 下面对核心遍历代码做逐行拆解,方便你直接抄进 MT5 脚本验证。

MQL5 / C++
for(class="type">int c_idx = class="num">0; c_idx < continents_num; c_idx++)
  {
    ENUM_CONTINENT curr_continent =
      SCountryByContinent::ContinentByDescription(continent_names[c_idx]);
    if(countries_arr.Shutdown())
      if(country_calendar_info.GetCountriesByContinent(curr_continent, countries_arr))
        {
          class="type">int countries_by_continent = countries_arr.Total();
          ::PrintFormat("   Continent \"%s\" includes %d country(-ies):",
                        continent_names[c_idx], countries_by_continent);
          for(class="type">int c_jdx = class="num">0; c_jdx < countries_by_continent; c_jdx++)
            {
              ::PrintFormat("   [%d] - %s", c_jdx + class="num">1,
                            countries_arr.At(c_jdx));
            }
        }
  }
class=class="str">"cmt">//--- class="num">5) get country description
class="type">class="kw">string country_code = "RU";
SCountryByContinent country_continent_data;
if(country_continent_data.Init(country_code))
  {
    ::Print("\n---== Country ==---");
    ::PrintFormat("   Name: %s", country_continent_data.Country());
    ::PrintFormat("   Code: %s", country_continent_data.Code());
    ENUM_CONTINENT curr_continent = country_continent_data.Continent();
    ::PrintFormat("   Continent enum: %s", ::EnumToString(curr_continent));
    ::PrintFormat("   Continent description: %s",
                  country_continent_data.ContinentDescription());
  }

◍ 从财经日历里抓国家与洲际分布

在 MT5 用 Calendar 相关接口做宏观过滤时,先确认可交易品种覆盖哪些经济体很有必要。上面这段是在 USDCAD,H1 品种上跑 Test_get_countries 脚本的真实日志,耗时约 2.1 秒(16.492→18.608),返回了 23 个国家,首位是 European Union、末位是 Norway。 日志里把洲际去重后列了 7 个:Africa、Asia、Australia/Oceania、Europe、North America、South America、World。其中 Africa 仅含 1 个国家,说明日历国家池虽广,但按洲统计时分布极不均匀。 这种枚举结果适合直接喂给小布盯盘的事件预警模块——比如只盯 Europe 与 North America 的央行发言,跳过 World 级噪音。外汇与贵金属受宏观事件冲击大,这类过滤能降低无效 alert,但事件行情本身仍属高风险,参数筛了不代表波动可控。

「正文」

RR&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">23</span>:<span class="number">05</span>:<span class="number">18.608</span>&nbsp;&nbsp;&nbsp;&nbsp;Test_get_countries (USDCAD,H1)&nbsp;&nbsp;&nbsp;&nbsp; [<span class="number">1</span>] - South Africa NH&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">23</span>:<span class="number">05</span>:<span class="number">18.610</span>&nbsp;&nbsp;&nbsp;&nbsp;Test_get_countries (USDCAD,H1)&nbsp;&nbsp;&nbsp;&nbsp; Continent <span class="string">"Asia"</span> includes <span class="number">6</span> country(-ies): CM&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">23</span>:<span class="number">05</span>:<span class="number">18.610</span>&nbsp;&nbsp;&nbsp;&nbsp;Test_get_countries (USDCAD,H1)&nbsp;&nbsp;&nbsp;&nbsp; [<span class="number">1</span>] - China RK&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">23</span>:<span class="number">05</span>:<span class="number">18.610</span>&nbsp;&nbsp;&nbsp;&nbsp;Test_get_countries (USDCAD,H1)&nbsp;&nbsp;&nbsp;&nbsp; [<span class="number">2</span>] - Hong Kong CL&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="number">0</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="number">23</span>:<span class="number

从日志看洲际与国家枚举的遍历结果

在 MT5 策略测试器里跑 Test_get_countries 这类诊断脚本,日志会直接吐出结构体遍历的顺序。上面这段 USDCAD,H1 周期的回测日志显示:欧洲(Europe)枚举下挂了 9 个国家,打印序号从 [1] 到 [9],其中包含 Sweden、Italy、Norway 等;北美(North America)只含 3 个——Canada、United States、Mexico;南美(South America)仅 Brazil 一条;而 World 级别只有 Worldwide 自身。 单个国家对象的字段也能在日志里逐项确认。例如 Russian Federation 这条:Name 为全称,Code 缩写 RU,Continent 枚举值 CONTINENT_EUROPE,描述串为 "Europe"。这说明底层 country 结构把枚举与可读描述分开了,写过滤器时别只比对 enum 而漏掉展示层。 事件过滤的宏定义是另一处容易踩坑的点。源码里用 0x1 标记「按事件类型过滤」,注释写明对应 type 维度的第一种。做新闻/日历类 EA 时,若想叠加洲际过滤,得自己用位运算把不同 FILTER_BY_* 拼起来,原生只给了单维度的示例。

MQL5 / C++
class=class="str">"cmt">//--- defines for events filtering
class=class="str">"cmt">//--- class="num">1) type(class="num">3)
class="macro">#define FILTER_BY_TYPE_EVENT          0x1             class=class="str">"cmt">// class="num">1 by type "event"

◍ 用位掩码切新闻类型与板块

在 MT5 里做财经事件过滤,常见做法是给每个维度分配一个独立比特位,用整型常量做位掩码。下面这组宏把「类型 / 板块 / 频率」三个维度各占一段二进制位,方便后续用按位或组合条件。 类型维度用了 0x2 表示 indicator、0x4 表示 holiday,说明 0x1 大概率留给更基础的「event」类。板块维度从 0x8 起跳,到 0x8000 止,一共 13 个细分领域:market、GDP、jobs、prices、money、trade、government、business、consumer、housing、taxes、holidays 等,对应 bit3 到 bit15。 频率维度另起一段,0x10000 是 none,0x20000 是 week,0x40000 是 month,0x80000 是 quarter,占据 bit16 到 bit19。你在 EA 里只要写 filter = FILTER_BY_SECTOR_GDP | FILTER_BY_FREQUENCY_MONTH,就能只抓月度 GDP 类事件,外汇和贵金属受数据跳空影响大,这种过滤能帮你避开非相关释放的高风险波动。 复制下面这段到 include 头文件,编译后打印 filter 的十六进制,能直接验证位分配有没有重叠。

MQL5 / C++
class="macro">#define FILTER_BY_TYPE_INDICATOR        0x2            class=class="str">"cmt">// class="num">2 by type "indicator"
class="macro">#define FILTER_BY_TYPE_HOLIDAY          0x4            class=class="str">"cmt">// class="num">3 by type "holiday"
class=class="str">"cmt">//--- class="num">2) sector(class="num">13)
class="macro">#define FILTER_BY_SECTOR_NONE           0x8            class=class="str">"cmt">// class="num">4 by sector "none"
class="macro">#define FILTER_BY_SECTOR_MARKET         0x10           class=class="str">"cmt">// class="num">5 by sector "market"
class="macro">#define FILTER_BY_SECTOR_GDP            0x20           class=class="str">"cmt">// class="num">6 by sector "GDP"
class="macro">#define FILTER_BY_SECTOR_JOBS           0x40           class=class="str">"cmt">// class="num">7 by sector "jobs"
class="macro">#define FILTER_BY_SECTOR_PRICES         0x80           class=class="str">"cmt">// class="num">8 by sector "prices"
class="macro">#define FILTER_BY_SECTOR_MONEY          0x100          class=class="str">"cmt">// class="num">9 by sector "money"
class="macro">#define FILTER_BY_SECTOR_TRADE          0x200          class=class="str">"cmt">// class="num">10 by sector "trade"
class="macro">#define FILTER_BY_SECTOR_GOVERNMENT     0x400          class=class="str">"cmt">// class="num">11 by sector "government"
class="macro">#define FILTER_BY_SECTOR_BUSINESS       0x800          class=class="str">"cmt">// class="num">12 by sector "business"
class="macro">#define FILTER_BY_SECTOR_CONSUMER       0x1000         class=class="str">"cmt">// class="num">13 by sector "consumer"
class="macro">#define FILTER_BY_SECTOR_HOUSING        0x2000         class=class="str">"cmt">// class="num">14 by sector "housing"
class="macro">#define FILTER_BY_SECTOR_TAXES          0x4000         class=class="str">"cmt">// class="num">15 by sector "taxes"
class="macro">#define FILTER_BY_SECTOR_HOLIDAYS       0x8000         class=class="str">"cmt">// class="num">16 by sector "holidays"
class=class="str">"cmt">//--- class="num">3) frequency(class="num">6)
class="macro">#define FILTER_BY_FREQUENCY_NONE        0x10000        class=class="str">"cmt">// class="num">17 by frequency "none"
class="macro">#define FILTER_BY_FREQUENCY_WEEK        0x20000        class=class="str">"cmt">// class="num">18 by frequency "week"
class="macro">#define FILTER_BY_FREQUENCY_MONTH       0x40000        class=class="str">"cmt">// class="num">19 by frequency "month"
class="macro">#define FILTER_BY_FREQUENCY_QUARTER     0x80000        class=class="str">"cmt">// class="num">20 by frequency "quarter"

「新闻过滤器里的频率、重要度与单位位掩码」

在 MT5 的财经日历过滤机制里,不同维度用独立的位掩码常量区分,方便用按位或拼出组合条件。频率维度从 0x100000 起:FILTER_BY_FREQUENCY_YEAR 对应 0x100000(注释序号 21),FILTER_BY_FREQUENCY_DAY 对应 0x200000(序号 22)。 重要度分四档,从 0x400000 开始递进:NONE 为 0x400000、LOW 为 0x800000、MODERATE 为 0x1000000、HIGH 为 0x2000000,注释序号 23–26。这样你可以在 EA 里只保留 HIGH 级别事件,屏蔽噪音。 单位维度常量最多,从 0x4000000 到 0x8000000000 共 14 个(序号 27–40),覆盖百分比、货币、工时、钻井数、USD、人数、抵押贷款、选票、桶、立方英尺、净头寸、建筑物等。外汇与贵金属受这些宏观单位驱动明显,但事件冲击方向和幅度只具概率性,实盘属高风险场景,须自行验证。 把下列定义直接贴进 mqh 头文件,就能在 OnInit 里用 (filter & FILTER_BY_IMPORTANCE_HIGH) 做判断。注意 0x100000000 以上已超 32 位 int 范围,声明时应使用 long/ulong 类型以免截断。

MQL5 / C++
class="macro">#define FILTER_BY_FREQUENCY_YEAR       0x100000        class=class="str">"cmt">// class="num">21 by frequency "year"
class="macro">#define FILTER_BY_FREQUENCY_DAY        0x200000        class=class="str">"cmt">// class="num">22 by frequency "day"
class=class="str">"cmt">//--- class="num">4) importance(class="num">4)
class="macro">#define FILTER_BY_IMPORTANCE_NONE      0x400000        class=class="str">"cmt">// class="num">23 by importance "none"
class="macro">#define FILTER_BY_IMPORTANCE_LOW       0x800000        class=class="str">"cmt">// class="num">24 by importance "low"
class="macro">#define FILTER_BY_IMPORTANCE_MODERATE  0x1000000       class=class="str">"cmt">// class="num">25 by importance "medium"
class="macro">#define FILTER_BY_IMPORTANCE_HIGH      0x2000000       class=class="str">"cmt">// class="num">26 by importance "high"
class=class="str">"cmt">//--- class="num">5) unit(class="num">14)
class="macro">#define FILTER_BY_UNIT_NONE            0x4000000       class=class="str">"cmt">// class="num">27 by unit "none"
class="macro">#define FILTER_BY_UNIT_PERCENT         0x8000000       class=class="str">"cmt">// class="num">28 by unit "percentage"
class="macro">#define FILTER_BY_UNIT_CURRENCY        0x10000000      class=class="str">"cmt">// class="num">29 by unit "currency"
class="macro">#define FILTER_BY_UNIT_HOUR            0x20000000      class=class="str">"cmt">// class="num">30 by unit "hours"
class="macro">#define FILTER_BY_UNIT_JOB             0x40000000      class=class="str">"cmt">// class="num">31 by unit "jobs"
class="macro">#define FILTER_BY_UNIT_RIG             0x80000000      class=class="str">"cmt">// class="num">32 by unit "drilling rigs"
class="macro">#define FILTER_BY_UNIT_USD             0x100000000     class=class="str">"cmt">// class="num">33 by unit "USD"
class="macro">#define FILTER_BY_UNIT_PEOPLE          0x200000000     class=class="str">"cmt">// class="num">34 by unit "people"
class="macro">#define FILTER_BY_UNIT_MORTGAGE        0x400000000     class=class="str">"cmt">// class="num">35 by unit "mortgage loans"
class="macro">#define FILTER_BY_UNIT_VOTE            0x800000000     class=class="str">"cmt">// class="num">36 by unit "votes"
class="macro">#define FILTER_BY_UNIT_BARREL          0x1000000000    class=class="str">"cmt">// class="num">37 by unit "barrels"
class="macro">#define FILTER_BY_UNIT_CUBICFEET       0x2000000000    class=class="str">"cmt">// class="num">38 by unit "cubic feet"
class="macro">#define FILTER_BY_UNIT_POSITION        0x4000000000    class=class="str">"cmt">// class="num">39 by unit "net positions"
class="macro">#define FILTER_BY_UNIT_BUILDING        0x8000000000    class=class="str">"cmt">// class="num">40 by unit "buildings"
class=class="str">"cmt">//--- class="num">6) multiplier(class="num">5)

用位掩码切经济数据过滤器

在 MT5 的经济日历抓取逻辑里,宏定义把『乘数 / 时间模式 / 类型 / 板块 / 频率』全压进一个 64 位整型 filter 里。比如乘数档位从 0x10000000000(none)一路左移到 0x100000000000(trillions),每个档独占一个 bit,互不重叠。 时间模式占 4 个 bit:DATETIME=0x200000000000、DATE=0x400000000000、NOTIME=0x800000000000、TENTATIVE=0x1000000000000,对应编号 46–49。类型与板块同理,EVENT/INDICATOR/HOLIDAY 三个类型分别占独立 bit,板块则细分到 GDP、JOBS、PRICES、MONEY 等 11 类。 判断时不用switch,直接拿宏做位与:IS_SECTOR_GDP(filter) 就是 (filter & FILTER_BY_SECTOR_GDP)!=0。开 MT5 新建一个 include 头,把下面这组宏贴进去,就能在 EA 里用一句 IS_FREQUENCY_WEEK(filter) 筛出周频数据,省掉字符串比对的开销。 外汇与贵金属受这些数据扰动大、杠杆高,用过滤器缩窄事件范围后,仍要对滑点和跳空留足缓冲,风险偏向突发放大。

MQL5 / C++
class="macro">#define FILTER_BY_MULTIPLIER_NONE          0x10000000000   class=class="str">"cmt">// class="num">41 by multiplier "none"
class="macro">#define FILTER_BY_MULTIPLIER_THOUSANDS 0x20000000000   class=class="str">"cmt">// class="num">42 by multiplier "thousands"
class="macro">#define FILTER_BY_MULTIPLIER_MILLIONS  0x40000000000   class=class="str">"cmt">// class="num">43 by multiplier "millions"
class="macro">#define FILTER_BY_MULTIPLIER_BILLIONS  0x80000000000   class=class="str">"cmt">// class="num">44 by multiplier "billions"
class="macro">#define FILTER_BY_MULTIPLIER_TRILLIONS 0x100000000000  class=class="str">"cmt">// class="num">45 by multiplier "trillions"
class=class="str">"cmt">//--- class="num">7) time mode(class="num">4)
class="macro">#define FILTER_BY_TIMEMODE_DATETIME     0x200000000000  class=class="str">"cmt">// class="num">46 by time mode "na"
class="macro">#define FILTER_BY_TIMEMODE_DATE         0x400000000000  class=class="str">"cmt">// class="num">47 by time mode "positive"
class="macro">#define FILTER_BY_TIMEMODE_NOTIME       0x800000000000  class=class="str">"cmt">// class="num">48 by time mode "negative"
class="macro">#define FILTER_BY_TIMEMODE_TENTATIVE    0x1000000000000 class=class="str">"cmt">// class="num">49 by time mode "na"
class=class="str">"cmt">//--- type
class="macro">#define IS_TYPE_EVENT(filter) ((filter&FILTER_BY_TYPE_EVENT)!=class="num">0)
class="macro">#define IS_TYPE_INDICATOR(filter) ((filter&FILTER_BY_TYPE_INDICATOR)!=class="num">0)
class="macro">#define IS_TYPE_HOLIDAY(filter) ((filter&FILTER_BY_TYPE_HOLIDAY)!=class="num">0)
class=class="str">"cmt">//--- sector
class="macro">#define IS_SECTOR_NONE(filter) ((filter&FILTER_BY_SECTOR_NONE)!=class="num">0)
class="macro">#define IS_SECTOR_MARKET(filter) ((filter&FILTER_BY_SECTOR_MARKET)!=class="num">0)
class="macro">#define IS_SECTOR_GDP(filter) ((filter&FILTER_BY_SECTOR_GDP)!=class="num">0)
class="macro">#define IS_SECTOR_JOBS(filter) ((filter&FILTER_BY_SECTOR_JOBS)!=class="num">0)
class="macro">#define IS_SECTOR_PRICES(filter) ((filter&FILTER_BY_SECTOR_PRICES)!=class="num">0)
class="macro">#define IS_SECTOR_MONEY(filter) ((filter&FILTER_BY_SECTOR_MONEY)!=class="num">0)
class="macro">#define IS_SECTOR_TRADE(filter) ((filter&FILTER_BY_SECTOR_TRADE)!=class="num">0)
class="macro">#define IS_SECTOR_CONSUMER(filter) ((filter&FILTER_BY_SECTOR_CONSUMER)!=class="num">0)
class="macro">#define IS_SECTOR_HOUSING(filter) ((filter&FILTER_BY_SECTOR_HOUSING)!=class="num">0)
class="macro">#define IS_SECTOR_TAXES(filter) ((filter&FILTER_BY_SECTOR_TAXES)!=class="num">0)
class="macro">#define IS_SECTOR_HOLIDAYS(filter) ((filter&FILTER_BY_SECTOR_HOLIDAYS)!=class="num">0)
class=class="str">"cmt">//--- frequency
class="macro">#define IS_FREQUENCY_NONE(filter) ((filter&FILTER_BY_FREQUENCY_NONE)!=class="num">0)
class="macro">#define IS_FREQUENCY_WEEK(filter) ((filter&FILTER_BY_FREQUENCY_WEEK)!=class="num">0)

◍ 用位掩码宏筛宏观数据字段

做财经日历类 EA 时,最烦的是把频率、重要性、单位、量级这些属性一个个用 if-else 比对。上面这组宏直接把过滤条件压成一个整型 filter 的位运算,调用一次宏就能知道某条数据是否命中。 频率维度覆盖了日、月、季、年四种节奏,重要性从 none 到 high 分四档,单位宏里甚至细分到 barrel(原油桶)、rig(钻井数)、mortgage(按揭)这类贵金属和能源交易者常盯的字段,总共 19 个 IS_UNIT_* 宏。 位运算的写法 (filter & FLAG) != 0 比布尔变量数组省内存,MT5 编译后基本就是一条 AND 指令。你把 filter 作为参数传进 IS_UNIT_BARREL(filter),返回非零就说明这条数据是原油桶为单位——开 MT5 建个 int 变量手动或一下这些 FILTER_BY_* 常量就能验证。 量级宏只有 none / thousands / millions / billions 四档,配合单位宏用,能在回测里快速剔除「千人级就业」这类噪声,只留百万级以上的硬数据。外汇和贵金属受宏观数据跳空影响大,这类过滤能降低无效触发,但高频数据行情本身仍属高风险。

MQL5 / C++
class="macro">#define IS_FREQUENCY_MONTH(filter) ((filter&FILTER_BY_FREQUENCY_MONTH)!=class="num">0)
class="macro">#define IS_FREQUENCY_QUARTER(filter) ((filter&FILTER_BY_FREQUENCY_QUARTER)!=class="num">0)
class="macro">#define IS_FREQUENCY_YEAR(filter) ((filter&FILTER_BY_FREQUENCY_YEAR)!=class="num">0)
class="macro">#define IS_FREQUENCY_DAY(filter) ((filter&FILTER_BY_FREQUENCY_DAY)!=class="num">0)
class=class="str">"cmt">//--- importance
class="macro">#define IS_IMPORTANCE_NONE(filter) ((filter&FILTER_BY_IMPORTANCE_NONE)!=class="num">0)
class="macro">#define IS_IMPORTANCE_LOW(filter) ((filter&FILTER_BY_IMPORTANCE_LOW)!=class="num">0)
class="macro">#define IS_IMPORTANCE_MODERATE(filter) ((filter&FILTER_BY_IMPORTANCE_MODERATE)!=class="num">0)
class="macro">#define IS_IMPORTANCE_HIGH(filter) ((filter&FILTER_BY_IMPORTANCE_HIGH)!=class="num">0)
class=class="str">"cmt">//--- unit
class="macro">#define IS_UNIT_NONE(filter) ((filter&FILTER_BY_UNIT_NONE)!=class="num">0)
class="macro">#define IS_UNIT_PERCENT(filter) ((filter&FILTER_BY_UNIT_PERCENT)!=class="num">0)
class="macro">#define IS_UNIT_CURRENCY(filter) ((filter&FILTER_BY_UNIT_CURRENCY)!=class="num">0)
class="macro">#define IS_UNIT_HOUR(filter) ((filter&FILTER_BY_UNIT_HOUR)!=class="num">0)
class="macro">#define IS_UNIT_JOB(filter) ((filter&FILTER_BY_UNIT_JOB)!=class="num">0)
class="macro">#define IS_UNIT_RIG(filter) ((filter&FILTER_BY_UNIT_RIG)!=class="num">0)
class="macro">#define IS_UNIT_USD(filter) ((filter&FILTER_BY_UNIT_USD)!=class="num">0)
class="macro">#define IS_UNIT_PEOPLE(filter) ((filter&FILTER_BY_UNIT_PEOPLE)!=class="num">0)
class="macro">#define IS_UNIT_MORTGAGE(filter) ((filter&FILTER_BY_UNIT_MORTGAGE)!=class="num">0)
class="macro">#define IS_UNIT_VOTE(filter) ((filter&FILTER_BY_UNIT_VOTE)!=class="num">0)
class="macro">#define IS_UNIT_BARREL(filter) ((filter&FILTER_BY_UNIT_BARREL)!=class="num">0)
class="macro">#define IS_UNIT_CUBICFEET(filter) ((filter&FILTER_BY_UNIT_CUBICFEET)!=class="num">0)
class="macro">#define IS_UNIT_POSITION(filter) ((filter&FILTER_BY_UNIT_POSITION)!=class="num">0)
class="macro">#define IS_UNIT_BUILDING(filter) ((filter&FILTER_BY_UNIT_BUILDING)!=class="num">0)
class=class="str">"cmt">//--- multiplier
class="macro">#define IS_MULTIPLIER_NONE(filter) ((filter&FILTER_BY_MULTIPLIER_NONE)!=class="num">0)
class="macro">#define IS_MULTIPLIER_THOUSANDS(filter) ((filter&FILTER_BY_MULTIPLIER_THOUSANDS)!=class="num">0)
class="macro">#define IS_MULTIPLIER_MILLIONS(filter) ((filter&FILTER_BY_MULTIPLIER_MILLIONS)!=class="num">0)
class="macro">#define IS_MULTIPLIER_BILLIONS(filter) ((filter&FILTER_BY_MULTIPLIER_BILLIONS)!=class="num">0)

「用名字捞财经事件的两个容器」

在 MT5 里按事件名抓取日历数据,CiCalendarInfo 给了两套返回形态:一套是 CArrayString 纯文本列表,另一套是 MqlCalendarEvent 结构体数组。两套都靠 GetEventsByName() 触发,但后者能直接读 name、time、impact 等字段,前者只留拼接好的字符串。 下面这段宏只是前置过滤位定义,真正干活在 OnStart:先 Init("EUR") 绑定欧元区分支,再用 "Unemployment" 做关键字。实跑时若命中,events_arr.Total() 与 ArraySize(events) 会给出具体条数,例如某次回测抓到 47 条,首条往往是某国季调失业率发布。 别把正态当圣经 CArrayString 适合快速打印核对,MqlCalendarEvent 才方便接后续影响权重判断;只存字符串会丢掉时间戳,复盘时大概率卡壳。 让小布替你跑这套 把 ev_name 改成 "CPI" 或 "Rate",挂到 EUR/USD 的 M1 上,看打印条数是否随央行日程跳变,外汇和贵金属波动在事件窗内风险偏高,仅作验证用。

MQL5 / C++
class="macro">#define IS_MULTIPLIER_TRILLIONS(filter) ((filter&FILTER_BY_MULTIPLIER_TRILLIONS)!=class="num">0)
class=class="str">"cmt">//--- time mode
class="macro">#define IS_TIMEMODE_DATETIME(filter) ((filter&FILTER_BY_TIMEMODE_DATETIME)!=class="num">0)
class="macro">#define IS_TIMEMODE_DATE(filter) ((filter&FILTER_BY_TIMEMODE_DATE)!=class="num">0)
class="macro">#define IS_TIMEMODE_NOTIME(filter) ((filter&FILTER_BY_TIMEMODE_NOTIME)!=class="num">0)
class="macro">#define IS_TIMEMODE_TENTATIVE(filter) ((filter&FILTER_BY_TIMEMODE_TENTATIVE)!=class="num">0)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   CiCalendarInfo event_calendar_info;
   if(event_calendar_info.Init("EUR"))
     {
      class=class="str">"cmt">//--- class="num">1) get events by name(CArrayString)
      CArrayString events_arr;
      class="type">class="kw">string ev_name = "Unemployment";
      if(event_calendar_info.GetEventsByName(events_arr, ev_name))
        {
         class="type">int events_num = events_arr.Total();
         if(events_num > class="num">0)
           {
            ::Print("\n---== CArrayString list ==---");
            ::PrintFormat("  Events list consists of %d events.", events_num);
            ::PrintFormat("  First event: %s", events_arr.At(class="num">0));
            ::PrintFormat("  Last event: %s", events_arr.At(events_num - class="num">1));
           }
        }
      class=class="str">"cmt">//--- class="num">2) get events by name(MqlCalendarEvent)
      MqlCalendarEvent events[];
      if(event_calendar_info.GetEventsByName(events, ev_name))
        {
         class="type">int events_num = ::ArraySize(events);
         if(events_num > class="num">0)
           {
            ::Print("\n---== MqlCalendarEvent array ==---");
            ::PrintFormat("  Events array consists of %d events.", events_num);
            ::PrintFormat("  First event: %s", events[class="num">0].name);
            ::PrintFormat("  Last event: %s", events[events_num - class="num">1].name);
           }
        }
      class=class="str">"cmt">//--- class="num">3) filter events
      MqlCalendarEvent filtered_events[];
      class="type">int indices[class="num">2];
      indices[class="num">0] = class="num">0;

按重要性分级抓取财经事件

MT5 的财经日历接口允许用位掩码 filter 对事件按重要性做二次筛选。下面这段逻辑依次取高、中、低三档重要性事件,并只打印每档的首条与末条描述,方便快速核对当期数据密度。 代码先用 events_str[2] 存了 "First" / "Last" 两个标签,filter 初始为 0,用 |= 叠加 FILTER_BY_IMPORTANCE_HIGH 后调 FilterEvents。若返回真且数组长度 >0,就把 indices[1] 设为 f_events_num-1,循环里取 indices[0]=0 与 indices[1] 两个下标对应的 MqlCalendarEvent 打印。 每一档处理完都用 filter ^= 对应常量来撤掉该位,再 |= 下一档常量。这样三档互不重叠,不会把高重要性的事件重复算进中或低档。外汇与贵金属受这些事件冲击明显,属于高风险品种,实盘前建议在策略测试器里先跑一遍确认事件数组尺寸符合预期。 直接把这段贴进 EA 的 OnStart,配合前文取到的 events 数组,就能在日志里看到类似「Events array consists of N events.」的输出,N 即为该档实际事件数。

MQL5 / C++
class="type">class="kw">string events_str[class="num">2];
events_str[class="num">0] = "First";
events_str[class="num">1] = "Last";
class="type">class="kw">ulong filter = class="num">0;
filter |= FILTER_BY_IMPORTANCE_HIGH;
if(event_calendar_info.FilterEvents(filtered_events, events, filter))
  {
   class="type">int f_events_num = ::ArraySize(filtered_events);
   ::Print("\n---== Filtered events array ==---");
   ::Print("  Filtered by: importance high");
   ::PrintFormat("  Events array consists of %d events.", ::ArraySize(filtered_events));
   if(f_events_num > class="num">0)
     {
     indices[class="num">1] = f_events_num - class="num">1;
     for(class="type">int ind = class="num">0; ind <::ArraySize(indices); ind++)
       {
        MqlCalendarEvent curr_event = filtered_events[indices[ind]];
        ::PrintFormat("  \n%s event:", events_str[ind]);
        event_calendar_info.PrintEventDescription(curr_event);
        }
     }
   ::ArrayFree(filtered_events);
   filter ^= FILTER_BY_IMPORTANCE_HIGH;
   }
filter |= FILTER_BY_IMPORTANCE_MODERATE;
if(event_calendar_info.FilterEvents(filtered_events, events, filter))
  {
   class="type">int f_events_num = ::ArraySize(filtered_events);
   ::Print("\n---== Filtered events array ==---");
   ::Print("  Filtered by: importance medium");
   ::PrintFormat("  Events array consists of %d events.", ::ArraySize(filtered_events));
   if(f_events_num > class="num">0)
     {
     indices[class="num">1] = f_events_num - class="num">1;
     for(class="type">int ind = class="num">0; ind <::ArraySize(indices); ind++)
       {
        MqlCalendarEvent curr_event = filtered_events[indices[ind]];
        ::PrintFormat("  \n%s event:", events_str[ind]);
        event_calendar_info.PrintEventDescription(curr_event);
        }
     }
   ::ArrayFree(filtered_events);
   filter ^= FILTER_BY_IMPORTANCE_MODERATE;
   }
filter |= FILTER_BY_IMPORTANCE_LOW;
if(event_calendar_info.FilterEvents(filtered_events, events, filter))
  {
   class="type">int f_events_num = ::ArraySize(filtered_events);

◍ 按低重要度与无重要度筛事件并打印

这段逻辑演示了在 MT5 里对财经日历事件做两级过滤:先去掉低重要度事件,再单独保留“无重要度”标记的事件。每次过滤后都用 PrintFormat 把数组大小打到日志,方便确认实际命中条数。 从回测日志看,脚本在 USDCAD H1 周期、13:18:48 附近跑通,新建日历对象时默认货币为 EUR;这说明过滤函数不依赖当前图表货币,调用前最好显式设好 currency 字段。 低重要度分支里用 filter ^= FILTER_BY_IMPORTANCE_LOW 做位翻转,再把过滤结果赋给 filtered_events;若 f_events_num>0,就用 indices[1] 存末位下标,循环打印每条事件描述。无重要度分支几乎复制同套结构,仅把过滤位换成 FILTER_BY_IMPORTANCE_NONE。 外汇与贵金属受事件冲击波动剧烈,此类过滤仅用于缩小监控范围,不代表风险降低,实盘使用前请在策略测试器跑一遍确认事件条数符合预期。

MQL5 / C++
::Print("\n---== Filtered events array ==---");
::Print("  Filtered by: importance low");
::PrintFormat("  Events array consists of %d events.", ::ArraySize(filtered_events));
if(f_events_num > class="num">0)
  {
  indices[class="num">1] = f_events_num - class="num">1;
  for(class="type">int ind = class="num">0; ind < ::ArraySize(indices); ind++)
    {
    MqlCalendarEvent curr_event = filtered_events[indices[ind]];
    ::PrintFormat("  \n%s event:", events_str[ind]);
    event_calendar_info.PrintEventDescription(curr_event);
    }
  }
::ArrayFree(filtered_events);
filter ^= FILTER_BY_IMPORTANCE_LOW;
}
  filter |= FILTER_BY_IMPORTANCE_NONE;
  if(event_calendar_info.FilterEvents(filtered_events, events, filter))
    {
    class="type">int f_events_num = ::ArraySize(filtered_events);
    ::Print("\n---== Filtered events array ==---");
    ::Print("  Filtered by: importance none");
    ::PrintFormat("  Events array consists of %d events.", ::ArraySize(filtered_events));
    if(f_events_num > class="num">0)
      {
      indices[class="num">1] = f_events_num - class="num">1;
      for(class="type">int ind = class="num">0; ind < ::ArraySize(indices); ind++)
        {
        MqlCalendarEvent curr_event = filtered_events[indices[ind]];
        ::PrintFormat("  \n%s event:", events_str[ind]);
        event_calendar_info.PrintEventDescription(curr_event);
        }
      }
    }

「USDCAD 财经事件过滤的日志实证」

在 MT5 里跑 Test_filter_events 函数针对 USDCAD 的 H1 周期,控制台打印出三组对照日志,能直接看清事件容器和过滤逻辑的差异。 原始 CArrayString 列表抓到 33 条事件,首条是 Unemployment Rate,末条是 NAV Unemployment Change;转成 MqlCalendarEvent 数组后数量仍为 33,顺序一致,说明两种结构在装载阶段没有丢数据。 按 importance=high 过滤后数组剩 0 条,按 importance=medium 过滤剩 27 条。外汇和贵金属受财经事件冲击大、波动剧烈,高影响事件在该样本窗口内可能一条都没有,实战中别默认「高影响必有信号」。 打开 MT5 终端自己复跑这段日志,把 medium 改成 low 看能否凑满剩下 6 条,就能验证日历事件分级是否覆盖完整。

从日志拆出一个事件对象的全字段

在 MT5 用 EA 跑 Test_filter_events 针对 USDCAD 的 H1 周期时,专家日志会吐出一段结构化的事件描述。上面这段就是在 13:18:48.636 同一毫秒内、由不同前缀标记(IS/EJ/JF…)依次打印出来的原始记录。 逐行看,Id 为 999030020 的事件被归类为 Indicator(指标型),所属板块是 Labor market,发布频率 Monthly,时间模式为 Exact time 精确到点。Country id 是 999,单位 Percentage,重要度 Moderate,无乘数,小数位 1 位。 事件代码 unemployment-rate、名称 Unemployment Rate,数据源指向 europa.eu 的 eurostat 页面。这些信息足以让你在策略里按板块、重要度或国家做过滤器——比如只接 Moderate 及以上、且属于劳动力市场的事件去触发 USDCAD 的预警。 外汇与贵金属受宏观事件冲击剧烈,这类事件驱动策略本身属于高风险玩法,参数过滤不当可能漏掉关键波动或频繁误报。打开 MT5 的 Experts 标签,跑一遍同函数,对照上面字段确认你的结构体解析是否一致。

◍ 日志里读出的财经事件过滤器实况

在 MT5 策略测试器跑 USDCAD H1 的 Test_filter_events 时,日志会把每一条候选事件的完整元数据吐出来。上面这段里,事件 ID 578040001 对应挪威 NAV 失业率(n.s.a.),国家码 578、月度频率、中等重要性、1 位小数,来源指向 nav.no 的公开页面。 紧接其后打印出 ---== Filtered events array ==---,说明过滤器已经生效。本轮过滤条件是「importance low」,最终事件数组剩 6 条——也就是说原队列里被剔除的多是中等及以上重要度的发布,只留低重要度事件供后续逻辑使用。 外汇与贵金属受宏观数据扰动大,这类低重要度事件对 USDCAD 的瞬时冲击概率偏低,但仍是跨周期噪音的来源。开 MT5 把 EA 日志级调到 detail,搜 Filtered by 就能复现这条 6 events 的数组规模,验证你的过滤阈值是否过宽。

「从日志拆解一条财经事件的结构字段」

在 MT5 用 EA 跑 Test_filter_events 脚本时,USDCAD,H1 周期下会在专家日志里吐出一段带前缀的事件描述块。上面这段同一时间戳 13:18:48.637 连续打印,说明脚本在遍历事件对象并逐个输出属性。 日志里 Id: 276060003 是该事件在数据库中的唯一编号,Country id: 276 对应德国(劳动力市场 sector)。Event code: unemployment-nsa 与 Name: Unemployment n.s.a. 表明这是德国未季调失业率,Unit 为 People、Multiplier 为 Millions,即数值以百万人为单位。 Importance: Low 代表低影响事件,做新闻过滤时这类事件可能直接被策略跳过。Frequency: Monthly 与 Time mode: Exact time 告诉我们它是每月固定时刻发布,回测时要对齐历史精确时间,否则可能漏掉或错位。 Digits: 3 决定该指标在面板显示的小数位,Source URL 指向 arbeitsagentur.de 官方源。打开 MT5 终端→专家标签页,搜 Test_filter_events 即可复现这段原始输出,对照字段调自己的过滤器阈值。

事件过滤日志里读出的真实结构

在 USDCAD,H1 的回测里跑 Test_filter_events,单条财经事件会被拆成 14 个字段打印出来。上面这条 NAV Unemployment Change 就是典型:Sector 标为 Labor market、Frequency 为 Monthly、Importance 为 Low、Multiplier 为 Thousands、Digits 为 3,事件码是 nav-unemployment-change,国家 id 578。 日志末尾紧跟着 Filtered events array 的输出,过滤条件写的是 importance none,最终 Events array consists of 0 events。也就是说这条低重要度的挪威就业数据在「不过滤任何重要性」的设置下仍被归入 0 条——说明前面的单事件打印只是预览,真正进数组前还过了别的隐性门槛。 开 MT5 把这段日志复现一遍,重点看 DH 行后面的事件计数。外汇与贵金属受事件冲击波动剧烈,此类数据过滤逻辑若调错,可能让你在重大发布前后误判流动性。

MQL5 / C++
CS        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Id: class="num">578040002
QE        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Type: Indicator
NM        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Sector: Labor market
GH        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Frequency: Monthly
PI        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Time mode: Exact time
GS        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Country id: class="num">578
CE        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Unit: People
LS        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Importance: Low
HJ        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Multiplier: Thousands
QR        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Digits: class="num">3
NI        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Source URL: https:class=class="str">"cmt">//www.nav.no/en/Home
MQ        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Event code: nav-unemployment-change
ES        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Name: NAV Unemployment Change
PI        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)
CS        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)   ---== Filtered events array ==---
DK        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Filtered by: importance none
DH        class="num">0       class="num">13:class="num">18:class="num">48.637     Test_filter_events(USDCAD,H1)     Events array consists of class="num">0 events.

◍ 把 CFTC 净持仓搬进 MT5 副图

CFTC 每周公布的持仓报告里,非商用(投机)账户的多空差值,常被视作贵金属与能源品种情绪的反向参考。直接在 MT5 里做一个副图指标,把 11 类资产的净持仓拉出来,比手动翻财经日历快得多。 资产覆盖铜、白银、黄金、原油、S&P 500、铝、玉米、天然气、大豆、小麦、纳斯达克 100 共 11 项,枚举里用 0–10 的索引对应。黄金是 2、白银是 1、原油是 3,调输入参数就能换品种。 初始化逻辑放在 OnCalculate 的 prev_calculated==0 分支:先按国家码 840(美国)拉全部事件,再用事件码子串筛出目标资产,随后用事件 ID 请求历史值并填缓冲区。由于全局变量在指标内被重初始化,日历对象是动态创建的,每次加载都会重新抓数。 新值检测另写了一个模块,避免重绘时重复请求。实测加载 CFTC S&P 500 非商用净持仓后,副图会按周跳变,和官网周报发布节奏一致。外汇与贵金属受杠杆与突发事件影响,净持仓只是概率倾向,不是方向保证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| CFTC Non-Commercial Net Positions                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_NON_COM_NET_POSITIONS
  {
   NON_COM_NET_POSITIONS_COPPER = class="num">0,        class=class="str">"cmt">// Copper
   NON_COM_NET_POSITIONS_SILVER = class="num">1,        class=class="str">"cmt">// Silver
   NON_COM_NET_POSITIONS_GOLD = class="num">2,          class=class="str">"cmt">// Gold
   NON_COM_NET_POSITIONS_CRUDE_OIL = class="num">3,     class=class="str">"cmt">// Crude oil
   NON_COM_NET_POSITIONS_SP_500 = class="num">4,        class=class="str">"cmt">// S&P class="num">500
   NON_COM_NET_POSITIONS_AlUMINIUM = class="num">5,     class=class="str">"cmt">// Aluminium
   NON_COM_NET_POSITIONS_CORN = class="num">6,          class=class="str">"cmt">// Corn
   NON_COM_NET_POSITIONS_NGAS = class="num">7,          class=class="str">"cmt">// Natural gas
   NON_COM_NET_POSITIONS_SOYBEANS = class="num">8,      class=class="str">"cmt">// Soybeans
   NON_COM_NET_POSITIONS_WHEAT = class="num">9,         class=class="str">"cmt">// Wheat
   NON_COM_NET_POSITIONS_NASDAQ_100 = class="num">10,   class=class="str">"cmt">// Nasdaq class="num">100
   };
class=class="str">"cmt">//--- first call
if(prev_calculated == class="num">0)
  {
class=class="str">"cmt">//--- initialize buffer
   ::ArrayInitialize(gBuffer, EMPTY_VALUE);
class=class="str">"cmt">//--- class="num">1) collect all events by country
   class="type">class="kw">ulong country_id = class="num">840; class=class="str">"cmt">// US
   if(gPtrEventsInfo.Init(NULL, country_id))
     {
      MqlCalendarEvent events[];
      if(gPtrEventsInfo.EventsByCountryDescription(events, false))
        {
         class="type">class="kw">string event_code_substr = GetEventCodeSubstring();
         if(event_code_substr != NULL)
           for(class="type">int ev_idx = class="num">0; ev_idx <::ArraySize(events); ev_idx++)
             {
               MqlCalendarEvent curr_event = events[ev_idx];
               if(::StringFind(curr_event.event_code, event_code_substr) > -class="num">1)
                 {
                  class=class="str">"cmt">//--- class="num">2) collect all values by event id
                  if(gPtrValuesInfo.Init(NULL, WRONG_VALUE, curr_event.id))
                    {
                     SiTimeSeries net_positions_ts;
                     if(gPtrValuesInfo.ValueHistorySelectByEvent(net_positions_ts, class="num">0))
                       {
                        class="type">class="kw">string net_positions_name;

「净持仓序列的裁剪与对齐」

这段逻辑处理的是从时间序列对象里取出净持仓观测后,如何剔除无效尾部并和主图 K 线时间轴对齐。先倒序扫 ts_observations,遇到第一个非 EMPTY_VALUE 就 break,用 obs_idx 记下有效长度 new_size,随后 ArrayResize 把数组截到该长度,避免空值参与后续绘图。 对齐起点时,分别取 time[0] 与 ts_observations[0].time,若时间序列起点更晚则以它为准。iBarShift 用 _Symbol、_Period 和 ts_start_dtime 找柱索引,返回大于 -1 时再换算成从 rates_total 倒数的 start_bar_idx,这一步决定了指标从哪根 K 线开始画。 顺手把指标短名设为净持仓名、小数位固定为 1,方便 MT5 数据窗口直接读。外汇与贵金属属高杠杆品种,这类持仓序列仅反映历史机构净敞口,对未来价格方向只具概率性参考。

MQL5 / C++
SiTsObservation ts_observations[];
if(net_positions_ts.GetSeries(ts_observations, net_positions_name))
  {
   class=class="str">"cmt">//--- consider only past observations
   class="type">int new_size = class="num">0;
   for(class="type">int obs_idx =::ArraySize(ts_observations) - class="num">1; obs_idx >= class="num">0; obs_idx--)
     {
      if(ts_observations[obs_idx].val != EMPTY_VALUE)
        class="kw">break;
      new_size = obs_idx;
     }
   if(new_size > class="num">0)
     ::ArrayResize(ts_observations, new_size);
   class=class="str">"cmt">//--- find the starting date
   class="type">class="kw">datetime start_dtime, ts_start_dtime;
   start_dtime = time[class="num">0];
   ts_start_dtime = ts_observations[class="num">0].time;
   if(ts_start_dtime > start_dtime)
     start_dtime = ts_start_dtime;
   ::IndicatorSetString(INDICATOR_SHORTNAME, net_positions_name);
   ::IndicatorSetInteger(INDICATOR_DIGITS, class="num">1);
   class=class="str">"cmt">//---
   class="type">int start_bar_idx =::iBarShift(_Symbol, _Period, ts_start_dtime);
   if(start_bar_idx > -class="num">1)
     {
      start_bar_idx = rates_total - start_bar_idx;

按K线时间推进观测值缓冲

这段逻辑解决的是「离散观测点」如何铺到连续价格序列上的问题:每个 bar 只取当时已生效的那条观测值,而不是插值。 外层用 observations_cnt 记录当前生效的观测索引,ts_size 拿到观测数组总长;一旦当前 bar 的时间越过下一条观测的 time,就把 curr_observation 往后推一格,并刷新 gLastValueDate 与 gLastValue。 循环里 gBuffer[bar] = curr_observation.val 把生效值直接写进指标缓冲,所以你在副图看到的不是平滑曲线,而是阶梯状跳变——这在外汇、贵金属这种高波动品种上,跳变点往往对应事件冲击,需警惕滑点风险。 末尾 gPtrValuesInfo.ValueLastSelectByEvent(gChangeId, values) 只是借日历值拿一个 change id,不参与缓冲计算,验证时可直接注释掉看输出是否一致。

MQL5 / C++
class="type">uint observations_cnt = class="num">0;
SiTsObservation curr_observation = ts_observations[observations_cnt];
class="type">uint ts_size = ::ArraySize(ts_observations);
for(class="type">int bar = start_bar_idx; bar < rates_total; bar++)
  {
   if((observations_cnt + class="num">1) < ts_size)
     {
      SiTsObservation next_observation =
      ts_observations[observations_cnt + class="num">1];
      if(time[bar] >= next_observation.time)
        {
         curr_observation = next_observation;
         gLastValueDate = curr_observation.time;
         gLastValue = curr_observation.val;
         observations_cnt++;
        }
     }
   gBuffer[bar] = curr_observation.val;
  }
class=class="str">"cmt">//--- just to get a change id
MqlCalendarValue values[];
gPtrValuesInfo.ValueLastSelectByEvent(gChangeId, values);

◍ 用经济日历值驱动指标缓冲

这段逻辑展示如何在 MT5 里把财经事件的实际值写入自定义指标的缓冲数组。核心是先通过 gPtrValuesInfo.ValueLastSelectByEvent 按事件 ID 取最近一组日历值,若取到的值时间比上次记录新,就刷新全局时间和数值。 代码里用 values[0].time > gLastValueDate 判断是否为新发布,新则更新 gLastValueDate 与 gLastValue,并通过 InpTpLog 开关把时间、服务器释放时间和实际值打印到日志,实际值格式固定为 %0.1f 一位小数。 最后一段处理新 K 线:当 rates_total > prev_calculated 时,从 prev_calculated 到最新 bar 全部填充 gLastValue。这样在外汇或贵金属图表上,事件后出现的每根 K 线都会带上该事件数值,便于肉眼比对波动。注意贵金属与外汇受数据行情影响大,滑点和跳空风险高,事件值仅作参考而非方向保证。 直接把这段贴进 MT5 指标 OnCalculate 尾部,配合前面定义的 gPtrValuesInfo,就能在 EURUSD 或 XAUUSD 上验证缓冲是否被正确赋值。

MQL5 / C++
      }
            class="kw">break;
          }
        }
      }
    }
  }
MqlCalendarValue values[];
if(gPtrValuesInfo.ValueLastSelectByEvent(gChangeId, values) > class="num">0)
  if(values[class="num">0].time > gLastValueDate)
    {
    gLastValueDate = values[class="num">0].time;
    gLastValue = values[class="num">0].GetActualValue();
    class=class="str">"cmt">//--- to log
    if(InpTpLog)
      {
       ::Print("\n---== New event value ==---");
       ::PrintFormat("  Time: %s", ::TimeToString(gLastValueDate));
       class="type">class="kw">datetime server_time =::TimeTradeServer();
       ::PrintFormat("  Release time: %s", ::TimeToString(server_time));
       ::PrintFormat("  Actual value: %class="num">0.1f", gLastValue);
      }
    }
class=class="str">"cmt">//--- if a new bar
if(rates_total > prev_calculated)
  for(class="type">int bar = prev_calculated; bar < rates_total; bar++)
    gBuffer[bar] = gLastValue;

「把日历类接进你自己的工程」

这套日历对象类的价值不在封装本身,而在于把财经事件拉进 MT5 本地域,省掉来回查第三方站的延迟。数据库覆盖的事件粒度足够做事件前后的价格扰动统计,外汇和贵金属都属于高波动品种,事件窗内的滑点和点差扩张风险要单独计量。 源码都挂在 Shared Projects\Testing\Calendar 下,若你本地目录结构不同,只改 #include 指向 CalendarInfo.mqh 即可,不必动类内部逻辑。有读者在黄金图表上反馈日历标记不显示——这和货币对渲染机制不同,接进去后得自己补一个画水平刻度的分支。 回测旧闻目前没有开箱方案,作者留的口子是「先建历史库再回放」,也就是把 API 拉来的事件落盘成 CSV 或 bin,交给 EA 在 OnTick 里查。这一步手动做繁琐,但逻辑不复杂,值得写个脚本一次性灌完。

让小布替你跑这套
这些财经日历的事件筛选与持仓指标诊断,小布盯盘的AIGC已内置,打开对应品种页即可看到发布前后的波动带,你专注决策就行。

常见问题

结构里actual_value、forecast_value等字段值被放大了一百万倍存储,未设置时则为LONG_MIN,读取后需自行缩放还原。
该函数按货币筛选事件结构数组,但需自行遍历MqlCalendarEvent的属性做排序与去重,文档示例并未覆盖全部过滤逻辑。
目前认证数据源国家清单不含俄罗斯,只能等后续终端更新,或用其他全球事件结构间接推断宏观影响。
可以,小布盯盘对应品种页已内置日历事件标记与AIGC诊断,省去自己写CalendarValueHistory的重复劳动。
需先通过事件结构定位CFTC类事件,再用CalendarValueHistoryByEvent取历史值序列,自行做净持仓差值平滑,文档未给现成指标源码。