轻松快捷开发 MetaTrader 程序的函数库 (第十六部分) : 品种集合事件·进阶篇
📡

轻松快捷开发 MetaTrader 程序的函数库 (第十六部分) : 品种集合事件·进阶篇

(2/3)· 当账户事件扩展到几十个品种,手动轮询只会拖垮 EA,这篇给出继承层的根治思路

案例拆解新手友好 第 2/3 篇
很多人给 EA 加品种监控时,习惯在 OnTick 里逐个 SymbolInfo 轮询,品种一多延迟立刻抬头。其实对象自己把状态变化推出来,比程序反复拉取更省事,本篇就是把这个机制落到函数库基类里。

◍ 事件基类的接口骨架

在 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 里跑一套自己的事件总线。外汇与贵金属波动剧烈,这类框架只解决结构问题,信号真伪仍靠你回测验证。

MQL5 / C++
  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——说明内存申请失败,实盘里多见于对象暴增或终端资源吃紧。下面这段是基类核心片段,逐行拆完你就能照抄。

MQL5 / C++
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 的返回差异。

MQL5 / C++
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 判定。

MQL5 / C++
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——这意味着即便二分落到了相邻位置,内容不对也绝不凑合返回。

MQL5 / C++
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);
  }
逐行拆解:第 1 行定义 const 方法,保证不改动容器;第 4~6 行三条件任一为真即退出,CheckPointer 是 MT5 内置空指针守卫;第 8 行 QuickSearch 依赖 m_sort_mode 有效,故前面必须拦掉 -1;第 9~10 行用对象自身 Compare 做最终确认,0 表示等价;第 12 行统一未命中出口。 另一头,图表事件对象把时间、图表 ID、事件 ID、以及 lparam / dparam / sparam 三组参数都用内联存取器包了起来。Time() 和 ChartID() 返回 long,ID() 返回 ushort,DParam() 走 double 通道——如果你在 EA 里捕获 OnChartEvent,用这套封装可以把临时 event 参数直接塞进对象数组排队处理,而不是在回调里写一堆全局变量。
MQL5 / C++
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;        }
外汇与贵金属行情受杠杆与跳空影响,这类事件驱动结构若用于实时挂单辅助,需先在模拟盘验证回调频率;图表事件在快速 tick 下可能丢触发,概率上不保证每次鼠标动作都被捕获。

MQL5 / C++
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 才认事件成立。这种位掩码在同时挂多种变更监听时比逐个布尔量省内存。 外汇与贵金属波动大、杠杆高,这类底层对象状态读取若用在实盘,建议先在策略测试器跑历史数据确认事件触发频次。

MQL5 / C++
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
把事件分发交给小布盯盘
这些品种与账户事件的诊断逻辑小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时状态推送,你只需专注策略决策而非底层轮询。

常见问题

账户事件只针对当前连接账户,实时发生即发送;品种事件要覆盖集合里每一个品种,数量可变,需在基类统一挂载事件对象后由各自实例上报。
只在初始化阶段多一次继承结构搭建,运行期每个对象自动携带共有属性,反而减少重复赋值,整体开销倾向更低。
通常把事件 ID 放 lparam,对象指针或索引放 lparam 高位,double 放 dparam,品种名放 sparam,具体布局看基类结构定义。
可以,小布盯盘的品种页已封装好事件接收与展示,不用自己写 EventChartCustom 转发,接好数据源就能看多品种状态流。
事件驱动模式下只有发生变化的品种才会推送,空闲品种零开销,相比轮询方案在大规模集合下概率上更稳。