MQL5 酷宝书 财经日历·进阶篇
(2/3)· 21 国经济事件如何被代码读取?从文档函数到 CiCalendarInfo 类,逐层透视日历内核
◍ 用类封装日历属性与事件时间序列
做 MT5 日历类库时,我倾向面向对象一端。下面这个类能直接访问日历属性,并顺手把选定事件的时间序列(TS)造出来,思路类似平台自带的 CAccountInfo、CSymbolInfo。 先打地基:SiTimeSeries 结构用 timevals[] 存时间、datavals[] 存数值,两个数组都放在私密段,意味着序列建好就不能改。Test_TS.mq5 拉取 2016-01-01 到 2021-11-01 的美国非农,用事件报告区间当时间线,在图表画出实际值与预测值两条曲线,序列由 70 个观测值构成。 观测单元用 SiTsObservation 结构表达,SiTimeSeries 里重载了索引运算符返回单个观测。对上面非农序列,取首末观测直接靠索引,日志里能见到对应记录。 CiCalendarInfo 的成员里,m_currency、m_country_id、m_event_id 是向数据库发请求的标准三元组;m_country_description 和 m_event_description 做快速说明;m_countries 是静态数组,因为国家数据是常量,不必每次 new 对象都重抓。 初始化方法吃这三个标准做参数,还能开日志打印。它有一层防重入保护:想从 EUR 改到 USD 必须先调逆初始化,否则返回 false 组合(如 EUR–Germany–nonfarm-payrolls)。 说明类方法基本是标准日历函数的包装。CountryDescription()、EventDescription() 返回初始化时锁定的文字;德国在库里有 61 条事件,全欧洲 276 条。枚举说明方法覆盖 8 个枚举(前 7 个归 MqlCalendarEvent,最后 ENUM_CALENDAR_EVENT_IMPACT 归 MqlCalendarValue),Test_enums_descriptions.mq5 用 MathSrand(77) 固定随机种子选 10 条英国事件,例如 “BoE Housing Equity Withdrawal q/q” 被标为指标/资金/季度/低重要/十亿/正面。 历史访问由 ::CalendarValueHistoryByEvent() 经两个重载透出:一个回 MqlCalendarValue 数组,一个直接转时间序列。状态检查方法只在取值数为 0 且系统错误码 >0 时才报错的。 国家与大陆数据方法里,ENUM_CONTINENT 含 World 到 Antarctica 共 8 项,ScontryContinente 结构内置 197 个国家映射。Test_get_countries.mq5 跑出来:当前日历覆盖 7 大洲 23 个国家(不计 World 常数)。 事件选取方法支持按名和按标志过滤。FilterEvents() 用 long 的 64 位扛下 49 个标准位(enum 只有 32 位不够)。Test_filter_events.mq5 建 EUR 对象后,模块 1 按 “Unemployment” 名筛出 33 条;模块 3 看重要性:27 条中等、6 条低等、0 条未指定。外汇与贵金属受此类数据跳空影响明显,实操前请在策略测试器用历史数据验证冲击。
用结构体把时间序列装进类里
在 MT5 里做价格行为回测,第一步是把「时间—数值」成对数据封装成独立对象,而不是散着用数组硬算。下面这段结构定义了 SiTimeSeries,私有区放了 init 标记、size 计数、timevals[] 和 datavals[] 两个平行数组,以及 name 字符串,对外只暴露必要接口。 SiTimeSeries 的公开方法覆盖了构造、析构、拷贝构造、赋值与相等判断,还重载了 [] 运算符返回 SiTsObservation 单体观察值。Init() 接收时间数组与数值数组的引用完成装载,GetSeries() 有两种重载:一种吐出原始数组,一种直接给观察值对象数组,方便后续统计模块调用。 IsInit() 和 Size() 写成内联,直接返回私有成员,避免每次判空或取长度都走函数调用。实盘或回测里若没先判 IsInit() 就调 [],可能读到空数组导致 EA 在 0 号 Bar 就崩掉。 外汇与贵金属杠杆高、滑点跳空频繁,这类基础结构务必在 OnInit 里跑一次 Init 并打印 Size,确认 K 线数符合预期再进主逻辑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Time series structure | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct SiTimeSeries { class="kw">private: class="type">bool init; class=class="str">"cmt">// is initialized? class="type">uint size; class="type">class="kw">datetime timevals[]; class=class="str">"cmt">// time values class="type">class="kw">double datavals[]; class=class="str">"cmt">// data values class="type">class="kw">string name; class=class="str">"cmt">// ts name class="kw">public: class=class="str">"cmt">//--- constructor class="type">void SiTimeSeries(class="type">void); class=class="str">"cmt">//--- destructor class="type">void ~SiTimeSeries(class="type">void); class=class="str">"cmt">//--- copy consructor class="type">void SiTimeSeries(class="kw">const SiTimeSeries &src_ts); class=class="str">"cmt">//--- assignment class="kw">operator class="type">void class="kw">operator=(class="kw">const SiTimeSeries &src_ts); class=class="str">"cmt">//--- equality class="kw">operator class="type">bool class="kw">operator==(class="kw">const SiTimeSeries &src_ts); class=class="str">"cmt">//--- indexing class="kw">operator SiTsObservation class="kw">operator[](class="kw">const class="type">uint idx) class="kw">const; class=class="str">"cmt">//--- initialization class="type">bool Init(class="type">class="kw">datetime &ts_times[], class="kw">const class="type">class="kw">double &ts_values[], class="kw">const class="type">class="kw">string ts_name); class=class="str">"cmt">//--- get series properties class="type">bool GetSeries(class="type">class="kw">datetime &dst_times[], class="type">class="kw">double &dst_values[], class="type">class="kw">string &dst_name); class="type">bool GetSeries(SiTsObservation &dst_observations[], class="type">class="kw">string &dst_name); class=class="str">"cmt">//--- service class="type">bool IsInit(class="type">void) class="kw">const { class="kw">return init; }; class="type">uint Size(class="type">void) class="kw">const { class="kw">return size; };
「从日历值到时间序列观测的落地写法」
把 NFP 日历事件转成可回测的时间序列,核心是先按索引把实际值与预测值分别塞进两个数组。下面这段循环就是干这个:用 MqlCalendarValue 的 GetActualValue 和 GetForecastValue 分别取数,时间轴用 period 字段对齐。 for(int v_idx=0; v_idx<nfp_values_size; v_idx++) { MqlCalendarValue curr_nfp_val = nfp_values[v_idx]; datetime curr_nfp_time = curr_nfp_val.period; timevals[v_idx] = curr_nfp_time; double curr_nfp_dataval = curr_nfp_val.GetActualValue(); datavals1[v_idx] = curr_nfp_dataval; curr_nfp_dataval = curr_nfp_val.GetForecastValue(); datavals2[v_idx] = curr_nfp_dataval; } 取完数后用结构体 SiTsObservation 框住单条观测(时间+数值),再抓首尾两条打印出来核对。实测在 USDCAD H1 上跑,首条观测落在 2015.12.01、实际值 292,末条在 2021.09.01、实际值 194,跨度近 6 年、样本量足以做事件前后波动统计。 外汇与贵金属受非农这类事件冲击,点差与滑点可能在数秒内放大,回测若忽略执行成本会高估策略概率。打开 MT5 把 nfp_values 换成你关注的央行事件,改 datavals1/2 的取数字段,就能复刻这条时间序列。
for(class="type">int v_idx=class="num">0; v_idx<nfp_values_size; v_idx++) { MqlCalendarValue curr_nfp_val = nfp_values[v_idx]; class="type">class="kw">datetime curr_nfp_time = curr_nfp_val.period; timevals[v_idx] = curr_nfp_time; class="type">class="kw">double curr_nfp_dataval = curr_nfp_val.GetActualValue(); datavals1[v_idx] = curr_nfp_dataval; curr_nfp_dataval = curr_nfp_val.GetForecastValue(); datavals2[v_idx] = curr_nfp_dataval; }
◍ 把财经日历封装成一个可调用类
想在 EA 里自动读非农、利率决议这类事件,先得有个结构把 MT5 内置的日历类型收拢起来。下面这段类声明把国家、事件、数值三套 MqlCalendar 结构做了成员变量缓存,避免每次查询都重新拉全表。 类里用静态数组 m_countries[] 存国家清单,m_is_init 标记是否已完成初始化;对外暴露的 Init() 支持按货币代码、国家 ID、事件 ID 三种方式定位,to_log 参数控制是否在初始化时打印日志。 方法分组很直接:CountryDescription / EventDescription / ValueDescription 负责把单条结构展开成可读文本;EventsByCountryDescription、EventsByCurrencyDescription、EventsBySector 则按不同维度批量取事件。最后 EventTypeDescription 把枚举转成字符串,方便在面板直接显示。 实盘接数据时建议先 Init("USD") 跑通,再逐步换成事件 ID 过滤——外汇与贵金属受事件跳空影响大,这类封装能降低手动查表漏看的概率,但行情仍属高风险。
class="type">class="kw">ulong m_country_id; MqlCalendarCountry m_country_description; class="type">class="kw">ulong m_event_id; MqlCalendarEvent m_event_description; class="kw">static MqlCalendarCountry m_countries[]; class="type">bool m_is_init; class=class="str">"cmt">//--- === Methods === --- class="kw">public: class=class="str">"cmt">//--- constructor/destructor class="type">void CiCalendarInfo(class="type">void); class="type">void ~CiCalendarInfo(class="type">void) {}; class=class="str">"cmt">//--- initialization class="type">bool 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="type">void Deinit(class="type">void); class=class="str">"cmt">//--- Calendar structures descriptions class="type">bool CountryDescription(MqlCalendarCountry &country, class="kw">const class="type">bool to_log = false); class="type">bool EventDescription(MqlCalendarEvent &event, class="kw">const class="type">bool to_log = false); class="type">bool ValueDescription(class="type">class="kw">ulong value_id, MqlCalendarValue &value, class="kw">const class="type">bool to_log = false); class="type">bool EventsByCountryDescription(MqlCalendarEvent &events[], class="kw">const class="type">bool to_log = false); class="type">bool EventsByCurrencyDescription(MqlCalendarEvent &events[], class="kw">const class="type">bool to_log = false); class="type">bool EventsBySector(class="kw">const ENUM_CALENDAR_EVENT_SECTOR event_sector, MqlCalendarEvent &events[], class="kw">const class="type">bool to_log = false); class=class="str">"cmt">//--- Calendar enum descriptions class="type">class="kw">string EventTypeDescription(class="kw">const ENUM_CALENDAR_EVENT_TYPE event_type);
把财经日历枚举翻成可读字符串
在 MT5 的财经日历类里,有一组描述函数专门把内部枚举转成中文或英文文案,方便直接在面板或日志里打印。比如 EventSectorDescription() 接收 ENUM_CALENDAR_EVENT_SECTOR,返回「央行 / 就业 / 通胀」这类板块名;EventImportanceDescription() 把三级重要性枚举转成「低 / 中 / 高」字样。 这些函数没有副作用,纯查表返回 string,调用成本极低。写 EA 时若要把事件推送到微信或小布盯盘告警,先用它们做一层本地化,比在后处理里硬写 switch 更稳。 历史数据抓取走 ValueHistorySelectByEvent(),它有两个重载:一个填 MqlCalendarValue 数组,另一个填 SiTimeSeries 时间序列。datetime_to 默认 0 表示取到当前,datetime_from 必须给,否则拿不到区间。 别把正态当圣经 枚举描述函数只解决「展示」问题,真正驱动交易的是 ValueHistorySelect 拿到的实际值序列;外汇与贵金属受事件冲击跳空概率高,用历史值做统计前先确认样本里剔除了流动性断裂时段。
class="type">class="kw">string EventSectorDescription(class="kw">const ENUM_CALENDAR_EVENT_SECTOR event_sector); class="type">class="kw">string EventFrequencyDescription(class="kw">const ENUM_CALENDAR_EVENT_FREQUENCY event_frequency); class="type">class="kw">string EventTimeModeDescription(class="kw">const ENUM_CALENDAR_EVENT_TIMEMODE event_time_mode); class="type">class="kw">string EventUnitDescription(class="kw">const ENUM_CALENDAR_EVENT_UNIT event_unit); class="type">class="kw">string EventImportanceDescription(class="kw">const ENUM_CALENDAR_EVENT_IMPORTANCE event_importance); class="type">class="kw">string EventMultiplierDescription(class="kw">const ENUM_CALENDAR_EVENT_MULTIPLIER event_multiplier); class="type">class="kw">string ValueImpactDescription(class="kw">const ENUM_CALENDAR_EVENT_IMPACT event_impact); class=class="str">"cmt">//--- history class="type">bool ValueHistorySelectByEvent( MqlCalendarValue &values[], class=class="str">"cmt">// array for value descriptions class="type">class="kw">datetime datetime_from, class=class="str">"cmt">// left border of a time range class="type">class="kw">datetime datetime_to = class="num">0 class=class="str">"cmt">// right border of a time range ) class="kw">const; class="type">bool ValueHistorySelectByEvent( SiTimeSeries &dst_ts, class=class="str">"cmt">// timeseries for value descriptions class="type">class="kw">datetime datetime_from, class=class="str">"cmt">// left border of a time range class="type">class="kw">datetime datetime_to = class="num">0 class=class="str">"cmt">// right border of a time range ) class="kw">const; class="type">bool ValueHistorySelect( MqlCalendarValue &values[], class=class="str">"cmt">// array for value descriptions class="type">class="kw">datetime datetime_from, class=class="str">"cmt">// left border of a time range class="type">class="kw">datetime datetime_to = class="num">0 class=class="str">"cmt">// right border of a time range ) class="kw">const; class="type">bool ValueHistorySelect( SiTimeSeries &dst_ts[], class=class="str">"cmt">// array of timeseries for value descriptions class="type">class="kw">datetime datetime_from, class=class="str">"cmt">// left border of a time range class="type">class="kw">datetime datetime_to = class="num">0 class=class="str">"cmt">// right border of a time range ); class=class="str">"cmt">//--- the calendar database status class="type">int ValueLastSelectByEvent( class="type">class="kw">ulong& change_id, class=class="str">"cmt">// Calendar change ID
「把财经日历接口接进自己的EA」
上面这段是 CiCalendarInfo 类尾部暴露的公共方法群,做事件驱动策略时基本绕不开。ValueLastSelect 接收 change_id 并把对应 MqlCalendarValue 塞进 values[],用来抓某次日历变更后的最新数值;ValueLast 则按时间窗直接拉取,两个重载都带 const 修饰,调用方不能改内部状态。 国家与洲际的筛选用 GetCountries / GetUniqueContinents / GetCountriesByContinent 三件套完成,返回 CArrayString 或 MqlCalendarCountry 结构数组。GetCountryNameById 拿 country_id 换名字,实盘里常用来在日志里标注“美国CPI”而非裸ID。 事件侧 GetEventsByName 支持按名称模糊拉取,FilterEvents 用 ulong filter 位掩码从源数组过滤出子集。Print* 三个方法纯调试用,能把国家、事件、数值结构直接打到Experts日志,省得自己拼字段。 私有段 ValidateProperties / CountryById / EventId 不对外,但决定了前面所有查询是否合法——自己扩这个类时别动这三个,否则可能让整棵日历树失效。外汇和贵金属受数据行情跳空影响大,用这些接口做自动下单务必加滑点保护。
class="type">int ValueLast( class="kw">const class="type">class="kw">ulong from_id, class="kw">const class="type">class="kw">ulong to_id, MqlCalendarValue& values[] class=class="str">"cmt">// array for value descriptions ) class="kw">const; class="type">int ValueLastSelect( class="type">class="kw">ulong& change_id, class=class="str">"cmt">// Calendar change ID MqlCalendarValue& values[] class=class="str">"cmt">// array for value descriptions ) class="kw">const; class=class="str">"cmt">//--- countries and continents class="type">bool GetCountries(CArrayString &countries_arr); class="type">bool GetCountries(MqlCalendarCountry &countries[]); class="type">bool GetUniqueContinents(class="type">class="kw">string &continents[]); class="type">bool GetCountriesByContinent(class="kw">const ENUM_CONTINENT src_continent, CArrayString &countries_arr); class="type">class="kw">string GetCountryNameById(class="kw">const class="type">class="kw">ulong country_id); class=class="str">"cmt">//--- events class="type">bool GetEventsByName(CArrayString &events_arr, class="kw">const class="type">class="kw">string name = NULL); class="type">bool GetEventsByName(MqlCalendarEvent &events[], class="kw">const class="type">class="kw">string name = NULL); class="type">bool FilterEvents(MqlCalendarEvent &filtered_events[], MqlCalendarEvent &src_events[], class="kw">const class="type">class="kw">ulong filter); class=class="str">"cmt">//--- print class="type">void PrintCountryDescription(class="kw">const MqlCalendarCountry &country); class="type">void PrintEventDescription(class="kw">const MqlCalendarEvent &event); class="type">void PrintValueDescription(class="kw">const MqlCalendarValue &value); class=class="str">"cmt">//--- class="kw">private: class="type">bool ValidateProperties(class="type">void); class="type">bool CountryById(class="kw">const class="type">class="kw">ulong country_id); class="type">bool EventId(class="type">void); }; MqlCalendarCountry CiCalendarInfo::m_countries[];