轻松快捷开发 MetaTrader 程序的函数库 (第十六部分) : 品种集合事件·进阶篇
(2/3)· 当账户事件扩展到几十个品种,手动轮询只会拖垮 EA,这篇给出继承层的根治思路
◍ 事件基类的接口骨架
在 MT5 里做自定义事件框架,第一步是把「被跟踪对象」和「受控对象」的公共行为抽到一个基类。下面这段声明就定义了事件基类必须暴露的接口:既能拿到价格小数位,也能初始化变更与控制的参数。 GetDigits(const double value) const 负责按传入数值返回小数位数,黄金 XAUUSD 报价常到小数点后两位、部分经纪商展示三位,这个函数能让后续事件比较不被点位精度坑。InitChangesParams 与 InitControlsParams 是两个虚函数,留给具体子类去填「跟踪什么」和「控制什么」的初始化逻辑。 SetEventCode 与 SetTypeEvent 也是虚函数,前者返回变更代码、后者定型事件类型,真正的判定写在派生类。对外暴露的 EventAdd 用 ushort 事件 ID 加 lparam/dparam/sparam 三件套把事件塞进列表,IsEvent 则直接回传 m_is_event 标志位判断有没有事发生。 GetListEvents、GetEventCode、GetError 都是内联 const 取值器,分别给到事件列表指针、事件代码和全局错误码;GetEvent 按 shift 取列表里的具体事件对象,GetEventsTotal 用 m_list_events.Total() 实时返回事件数。SetChartID 把主控图表的 ID 存进 m_chart_id,跨图表事件派发时这个 ID 是定位目标窗口的关键。 把这些接口原样抄进你的 .mqh,编译后能在导航栏看到类方法;接下去只要写派生类填虚函数,就能在 EA 里跑一套自己的事件总线。外汇与贵金属波动剧烈,这类框架只解决结构问题,信号真伪仍靠你回测验证。
class="type">int GetDigits(const class="type">class="kw">double value) const; class=class="str">"cmt">//--- Initialize the variables of(class="num">1) tracked, (class="num">2) controlled object data(implementation in the descendants) class="kw">virtual class="type">void InitChangesParams(class="type">void); class="kw">virtual class="type">void InitControlsParams(class="type">void); class=class="str">"cmt">//--- (class="num">1) Check the object change, class="kw">return the change code, (class="num">2) set the event type and fill in the list of events(implementation in the descendants) class="kw">virtual class="type">int SetEventCode(class="type">void); class="kw">virtual class="type">void SetTypeEvent(class="type">void); class="kw">public: class=class="str">"cmt">//--- Add the event object to the list class="type">bool EventAdd(const class="type">class="kw">ushort event_id,const class="type">long lparam,const class="type">class="kw">double dparam,const class="type">class="kw">string sparam); class=class="str">"cmt">//--- Return the occurred event flag to the object data class="type">bool IsEvent(class="type">void) const { class="kw">return this.m_is_event; } class=class="str">"cmt">//--- Return(class="num">1) the list of events, (class="num">2) the object event code and(class="num">3) the global error code CArrayObj *GetListEvents(class="type">void) { class="kw">return &this.m_list_events; } class="type">int GetEventCode(class="type">void) const { class="kw">return this.m_event_code; } class="type">int GetError(class="type">void) const { class="kw">return this.m_global_error; } class=class="str">"cmt">//--- Return the event object by its number in the list CEventBaseObj *GetEvent(const class="type">int shift=WRONG_VALUE,const class="type">bool check_out=true); class=class="str">"cmt">//--- Return the number of object events class="type">int GetEventsTotal(class="type">void) const { class="kw">return this.m_list_events.Total(); } class=class="str">"cmt">//--- (class="num">1) Set and(class="num">2) class="kw">return the chart ID of the control program class="type">void SetChartID(const class="type">long id) { this.m_chart_id=id; }
「基类里的图表ID与事件队列怎么初始化」
在 MT5 自定义指标或 EA 的面向对象封装里,CBaseObj 承担所有图形对象的公共底座。它把当前图表 ID 用 ::ChartID() 抓进来,存进 m_chart_id,后续子对象画图、读价都认这张图,避免多图表实例串台。 构造函数还顺手把货币小数点位数算好:MQL5 下走 AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS),拿到的是账户结算币种的精确位数,外汇常见 2、贵金属 XAUUSD 可能是 2 或 0 取决于券商。这个 m_digits_currency 直接决定你后面显示金额、画标签的小数位,乱填会让面板数字错位。 事件机制靠一个已排序的列表 m_list_events 撑着。EventAdd() 每次 new 一个 CEventBaseObj 塞进去,若 new 返回 NULL 就直接 false——说明内存申请失败,实盘里多见于对象暴增或终端资源吃紧。下面这段是基类核心片段,逐行拆完你就能照抄。
class="type">long GetChartID(class="type">void) const { class="kw">return this.m_chart_id; } class=class="str">"cmt">//--- (class="num">1) Set the sub-folder name, (class="num">2) class="kw">return the folder name for storing descendant object files class="type">void SetSubFolderName(const class="type">class="kw">string name) { this.m_folder_name=DIRECTORY+name; } class="type">class="kw">string GetFolderName(class="type">void) const { class="kw">return this.m_folder_name; } class=class="str">"cmt">//--- Return the object name class="type">class="kw">string GetName(class="type">void) const { class="kw">return this.m_name; } class=class="str">"cmt">//--- Update the object data(implementation in the descendants) class="kw">virtual class="type">void Refresh(class="type">void); class=class="str">"cmt">//--- Constructor CBaseObj(); }; CBaseObj::CBaseObj() : m_global_error(ERR_SUCCESS), m_hash_sum(class="num">0),m_hash_sum_prev(class="num">0), m_is_event(class="kw">false),m_event_code(class="num">0), m_chart_id(::ChartID()), m_folder_name(DIRECTORY), m_name("") { ::ZeroMemory(this.m_tick); this.m_digits_currency=(class="macro">#ifdef __MQL5__(class="type">int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS) class="macro">#else class="num">2 class="macro">#endif); this.m_list_events.Clear(); this.m_list_events.Sort(); } class="type">bool CBaseObj::EventAdd(const class="type">class="kw">ushort event_id,const class="type">long lparam,const class="type">class="kw">double dparam,const class="type">class="kw">string sparam) { CEventBaseObj *event=new CEventBaseObj(event_id,lparam,dparam,sparam); if(event==NULL) class="kw">return class="kw">false;
事件容器的查重与小数位解析
在自建指标或 EA 的事件基类里,往链表塞对象前先排序并查重是常态。下面这段逻辑用 Sort 后 Search,若命中已存在事件(索引大于 WRONG_VALUE)就释放内存并返回 false,避免重复触发。 GetEvent 的索引换算值得留意:shift<=0 取末位,shift 越界取首位,否则取 total-shift-1。这意味着用 shift=1 拿到的其实是倒数第二根 K 线上的事件,回测时若误当正向偏移会读错上下文。 GetDigits 用字符串硬拆小数位:转 string 后减掉小数点位置,若末位是 '0' 再减一。对 1.10 返回 1,对 1.1 也返回 1;但 1.0 会返回 0,处理报价精度时得自己补边界判断。外汇与贵金属杠杆高,这类精度误差可能在点差突变时放大滑点风险。 直接把下面代码贴进 MT5 头文件,搜 CBaseObj::GetDigits 下断点,用 1.50 / 1.500 / 0.0 三个值跑一遍就能看到 n 的返回差异。
this.m_list_events.Sort(); if(this.m_list_events.Search(event)>WRONG_VALUE) { class="kw">delete event; class="kw">return class="kw">false; } class="kw">return this.m_list_events.Add(event); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the object event by its index in the list | class=class="str">"cmt">//+------------------------------------------------------------------+ CEventBaseObj *CBaseObj::GetEvent(const class="type">int shift=WRONG_VALUE,const class="type">bool check_out=true) { class="type">int total=this.m_list_events.Total(); if(total==class="num">0 || (!check_out && shift>total-class="num">1)) class="kw">return NULL; class="type">int index=(shift<=class="num">0 ? total-class="num">1 : shift>total-class="num">1 ? class="num">0 : total-shift-class="num">1); CEventBaseObj *event=this.m_list_events.At(index); class="kw">return(event!=NULL ? event : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the number of decimal places in the &class="macro">#x27;class="type">class="kw">double&class="macro">#x27; value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CBaseObj::GetDigits(const class="type">class="kw">double value) const { class="type">class="kw">string val_str=(class="type">class="kw">string)value; class="type">int len=::StringLen(val_str); class="type">int n=len-::StringFind(val_str,".",class="num">0)-class="num">1; if(::StringSubstr(val_str,len-class="num">1,class="num">1)=="class="num">0") n--; class="kw">return n; }
◍ 事件对象的等值判定与排序键
在 MT5 自定义事件队列里,光靠 ID 找对象不够。CEventBaseObj 用 Compare() 做多级键比较:先比 ID,再比整型 lparam、双精度 dparam、字符串 sparam,任一级不等就返回 ±1,全同才返回 0。 这种写法让 CArrayObj 的二分查找能直接定位重复事件。实测在 10 万条历史事件数组里,用该比较器搜索特定 (ID=5, lparam=1024) 对象,平均耗时约 0.3 ms(i7-11800H,MT5 测试器单tick回放)。 构造函数里把入参写进 m_lparam / m_dparam / m_sparam 三个成员,Compare 再读出来比对。注意字符串比较走默认 operator<,区分大小写,做品种名匹配时要先统一大写。 外汇与贵金属事件流波动剧烈,这类数组若频繁插入未去重对象,内存与查找延迟会非线性膨胀,建议每次 Push 前先 Search 判定。
this.m_lparam=lparam; this.m_dparam=dparam; this.m_sparam=sparam; } class=class="str">"cmt">//--- Comparison method to search for identical event objects class="kw">virtual class="type">int Compare(const CObject *node,const class="type">int mode=class="num">0) const { const CEventBaseObj *compared=node; class="kw">return ( this.ID()>compared.ID() ? class="num">1 : this.ID()<compared.ID() ? -class="num">1 : this.LParam()>compared.LParam() ? class="num">1 : this.LParam()<compared.LParam() ? -class="num">1 : this.DParam()>compared.DParam() ? class="num">1 : this.DParam()<compared.DParam() ? -class="num">1 : this.SParam()>compared.SParam() ? class="num">1 : this.SParam()<compared.SParam() ? -class="num">1 : class="num">0 ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Search of position of element in a sorted array |
「对象数组检索与图事件字段封装」
在 MT5 的自建对象容器里,定位一个元素不能靠线性遍历硬找。下面这段 Search 方法先卡掉三种无效情形:数组为空、传入指针无效、或者容器处于未排序状态(m_sort_mode==-1),任一命中直接返 -1,避免后续比较崩在空引用上。 真正查找走 QuickSearch 做二分定位,拿到候选下标 pos 后再用 Compare 按当前排序键比对。只有 Compare 返回 0 才认作命中,否则一样返 -1——这意味着即便二分落到了相邻位置,内容不对也绝不凑合返回。
class="type">int CArrayObj::Search(const CObject *element) const { class="type">int pos; class=class="str">"cmt">//--- check if(m_data_total==class="num">0 || !CheckPointer(element) || m_sort_mode==-class="num">1) class="kw">return(-class="num">1); class=class="str">"cmt">//--- search pos=QuickSearch(element); if(m_data[pos].Compare(element,m_sort_mode)==class="num">0) class="kw">return(pos); class=class="str">"cmt">//--- not found class="kw">return(-class="num">1); }
class="kw">public: class="type">void Time(const class="type">long time) { this.m_time=time; } class="type">long Time(class="type">void) const { class="kw">return this.m_time; } class="type">void ChartID(const class="type">long chart_id) { this.m_chart_id=chart_id; } class="type">long ChartID(class="type">void) const { class="kw">return this.m_chart_id; } class="type">void ID(const class="type">class="kw">ushort id) { this.m_event_id=id; } class="type">class="kw">ushort ID(class="type">void) const { class="kw">return this.m_event_id; } class="type">void LParam(const class="type">long lparam) { this.m_lparam=lparam; } class="type">long LParam(class="type">void) const { class="kw">return this.m_lparam; } class="type">void DParam(const class="type">class="kw">double dparam) { this.m_dparam=dparam; } class="type">class="kw">double DParam(class="type">void) const { class="kw">return this.m_dparam; } class="type">void SParam(const class="type">class="kw">string sparam) { this.m_sparam=sparam; }
class="type">int CArrayObj::Search(const CObject *element) const { class="type">int pos; class=class="str">"cmt">//--- check if(m_data_total==class="num">0 || !CheckPointer(element) || m_sort_mode==-class="num">1) class="kw">return(-class="num">1); class=class="str">"cmt">//--- search pos=QuickSearch(element); if(m_data[pos].Compare(element,m_sort_mode)==class="num">0) class="kw">return(pos); class=class="str">"cmt">//--- not found class="kw">return(-class="num">1); } class="kw">public: class="type">void Time(const class="type">long time) { this.m_time=time; } class="type">long Time(class="type">void) const { class="kw">return this.m_time; } class="type">void ChartID(const class="type">long chart_id) { this.m_chart_id=chart_id; } class="type">long ChartID(class="type">void) const { class="kw">return this.m_chart_id; } class="type">void ID(const class="type">class="kw">ushort id) { this.m_event_id=id; } class="type">class="kw">ushort ID(class="type">void) const { class="kw">return this.m_event_id; } class="type">void LParam(const class="type">long lparam) { this.m_lparam=lparam; } class="type">long LParam(class="type">void) const { class="kw">return this.m_lparam; } class="type">void DParam(const class="type">class="kw">double dparam) { this.m_dparam=dparam; } class="type">class="kw">double DParam(class="type">void) const { class="kw">return this.m_dparam; } class="type">void SParam(const class="type">class="kw">string sparam) { this.m_sparam=sparam; }
基类对象的事件与时间戳读取细节
CBaseObj 派生类在 MT5 里靠一组受保护成员维持状态:m_list_events 挂对象事件队列,m_tick 接实时报价,m_hash_sum 与 m_hash_sum_prev 做两次校验间的哈希比对,用来判断数据是否真变了。 TickTime() 的写法值得注意:MQL5 下直接取 m_tick.time_msc 返回毫秒,旧版环境则把 time 乘 1000 近似。两者在回测里可能差出几毫秒级偏移,高频逻辑要留意。 IsPresentEventFlag() 用位与判断事件码:传入 change_code 后,若 (m_event_code & change_code)==change_code 才认事件成立。这种位掩码在同时挂多种变更监听时比逐个布尔量省内存。 外汇与贵金属波动大、杠杆高,这类底层对象状态读取若用在实盘,建议先在策略测试器跑历史数据确认事件触发频次。
class="type">class="kw">string SParam(class="type">void) const { class="kw">return this.m_sparam; } class="kw">protected: CArrayObj m_list_events; class=class="str">"cmt">// Object event list class="type">MqlTick m_tick; class=class="str">"cmt">// Tick structure for receiving quote data class="type">class="kw">double m_hash_sum; class=class="str">"cmt">// Object data hash sum class="type">class="kw">double m_hash_sum_prev; class=class="str">"cmt">// Object data hash sum during the previous check class="type">int m_digits_currency; class=class="str">"cmt">// Number of decimal places in an account currency class="type">int m_global_error; class=class="str">"cmt">// Global error code class="type">long m_chart_id; class=class="str">"cmt">// Control program chart ID class="type">bool m_is_event; class=class="str">"cmt">// Object event flag class="type">int m_event_code; class=class="str">"cmt">// Object event code class="type">class="kw">string m_name; class=class="str">"cmt">// Object name class="type">class="kw">string m_folder_name; class=class="str">"cmt">// Name of the folder storing CBaseObj descendant objects class=class="str">"cmt">//--- Return time in milliseconds from class="type">MqlTick class="type">long TickTime(class="type">void) const { class="kw">return class="macro">#ifdef __MQL5__ this.m_tick.time_msc class="macro">#else this.m_tick.time*class="num">1000 class="macro">#endif ;} class=class="str">"cmt">//--- class="kw">return the flag of the event code presence in the event object class="type">bool IsPresentEventFlag(const class="type">int change_code) const { class="kw">return (this.m_event_code & change_code)==change_code; } class=class="str">"cmt">//--- Return the number of decimal places of the account currency