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

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

第 1/3 篇

把品种集合事件接进自己的函数库

在 MT5 里做多品种监控时,手动轮询 SymbolTotal() 和 SymbolName() 既耗资源又容易漏掉运行时新上的品种。MetaTrader 5 的事件模型里有一类「品种集合事件」,能在品种列表变动时主动通知程序,不用你定时扫。 要接住这类事件,得在自定义函数库的 CEngine 基类里重写 OnSymbolsChange() 虚函数,并在事件分发层把 CHARTEVENT_SYMBOL_ADD / CHARTEVENT_SYMBOL_REMOVE 转给对应处理器。这样你的 EA 或指标在「某品种被加到市场观察」或「被移除」的瞬间就能跑逻辑,而不是下一根 K 线才反应。 下面这段是从可复用引擎里抽出来的最小骨架,只保留品种集合事件相关部分,你可以直接贴进自己的 include 文件验证。注意:涉及多品种切换与下单的外汇、贵金属策略属高风险,事件触发频繁时可能引发行级重算压力。

MQL5 / C++
class CEngine {
class="kw">public:
   class="kw">virtual class="type">void OnSymbolsChange(class="type">void) {}
};

class=class="str">"cmt">// 在事件分发中
class="type">void CApp::OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) {
   if(id==CHARTEVENT_SYMBOL_ADD || id==CHARTEVENT_SYMBOL_REMOVE)
      m_engine.OnSymbolsChange();
}

◍ 给所有库对象造一个公共基类

在函数库早期做账户对象和账户集合时(对应系列第十二、十三部分),以及后续跟踪账户事件时,出现了一个明确需求:需要一种能把事件直接推给 Engine 对象的新基类。账户事件和第四篇往后讲的交易事件机制不一样——交易事件被写进集合,随时可回查历史;账户事件只活在“此刻”,用 EventChartCustom() 发到图表,再从库基类拿同时刻的事件列表处理。 品种集合也用同一套事件安置方式,差别只在规模:账户事件只盯当前连接账户,品种事件要覆盖集合里每一个品种,可能一个、几个或服务器全量符号。反复给每个新对象手写重复属性,显然该收口了。 决定很直接:建一个从标准库 CObject 派生的库基类,让所有函数库对象继承它。共有属性在基类里一次性设好,派生对象自动携带。更关键的是,基类里放一个事件对象,所有库对象就能把自身状态通过 EventChartCustom() 告知程序——事件结构就是重复发送事件 ID、long、double、string 四类参数。 外汇和贵金属市场高波动、高杠杆,这类事件驱动封装只解决架构耦合,不承诺任何收益。先在 MT5 里建事件对象和基类,再接品种集合事件,账户事件类思路顺带复用;交易事件结构冲突且已成型,保持原样不发改动。

「所有函数库对象的基准对象类」

在 \MQL5\Include\ DoEasy\Objects\ 函数库文件夹中,在 BaseObj.mqh 文件中创建新的类 CBaseObj 。 CObject 标准库基类将用作该类的基对象。 该类很小巧,所以我将在这里展示其完整清单。 然后我们将通过其成员和方法对其进行分析。 为避免为创建一个 对象事件类 新文件,请在同一文件的基类之前编写该类: 我们看一下基准对象事件类 。 在基准对象事件类的私密部分中,声明存储所有事件属性的类成员变量: 事件时间 , 发送事件的所在图表 ID , 事件 ID ,事件的 长整数型 、 双精度型 和 字符串型 数值会传递给控制程序所在的图表。 大多数这些变量的数值都会传递给类构造函数: 在其中会为它们分配数值。 此外,在类初始化清单中,图表 ID 变量 接收 当前图表 ID 。 比较两个对象事件类的方法: 逐个元素的比较两个类的所有字段( 当前那个 和 通过指针传递给方法的那个 )。 如果所有字段都相等,则该方法返回 0 ,由于这些对象存储在 CArrayObj 清单中,因此在标准库指针的 动态列表中搜索同一对象是必需的 ,而其 Search() 方法用来在列表中搜索相似对象: 在对象中需要存在的 Compare() 虚方法。 如果两个对象的所有属性都匹配,则方法 返回 0 。 在类的公开部分中,声明用于设置和返回所有对象属性的方法: 所有这些内容都很清楚,故无需注释。 这是完整的对象事件类。 我们研究所有函数库对象的基准对象类。 在类的受保护部分中,我们声明了处理先前对象时已经遇到的类成员变量: 对象事件列表 m_list_events 用于存储我们上面所研究事件类的对象。 该对象可能一次有几个事件,因此我们需要定义所有事件,并将它们放在列表中。 这将令我们能够从 CEngine 函数库主对象中提取所有事件的列表,并进行处理。 m_tick 即时报价的结构用于获取价格和事件时间。 m_hash_sum 哈希值是定义对象属性发生变化所必需的。 通过比较当前和以前的( m_hash_sum_prev )哈希总和来定义对象属性的变化。 m_digits_currency 是帐户币种的小数位数,对于正确显示某些事件的货币值数据变化是必需的。 当任何函数发生错误时返回的错误代码将被写入 m_global_error 全局错误代码。 此代码将传递给调用程序以便进行进一步处理。 控制程序所在图表 ID m_chart_id 用于指定对象事件发送到的图表。 m_is_event 是必需要有的对象事件标志,用于通知程序发生对象事件,以便及时对该事件做出反应。 m_event_code 对象事件编号存储所有同时发生的事件的标志。 这些标志的存在令我们可以定义同时发生的对象事件的列表。 m_name 是必需要有的对象名称,用于通知程序,并从其接收事件对象的某些文本属性。 例如,对于帐户,这是帐号+客户名称+服务器名称,而对于品种,则为品种名称。 为了将对象保存到文件中, m_folder_name 必需填写存储目标文件的文件夹名:此处,同类型目标文件应存储在同名的子文件夹之中。 子文件夹的基准目录是所有客户机终端的公用文件目录 + 函数库文件夹名称:“DoEasy\\”。 在函数库论述的 第十二部分 中,我们已经讨论了在创建帐户集合时存储文件的方法。 我们已在函数库对象中创建了所有这些属性,并在对象相对应的各种函数库论述部分中对它们进行了讨论。 现在,我们将它们置于一个类中 — 所有函数库对象的基准对象是 CBaseObj ,

把图表事件封进一个结构体

在 MT5 里捕获 OnChartEvent 时,原生参数散落四处,直接处理容易漏掉上下文。下面这段代码把一次图表事件的关键字段收拢到一个类里,后续回放或调试都省事。 私有成员先落袋:m_chart_id 用 long 存图表句柄,m_event_id 用 ushort 标事件类型,m_lparam/m_dparam/m_sparam 分别对应事件的长整型、双精度、字符串载荷。外汇与贵金属图表的高频交互事件里,这三类参数覆盖了鼠标、键盘与自定义事件的大部分场景。 公开区是一对对的 setter/getter,例如 Time(const long) 写时间、Time(void) const 读时间;ChartID()ID()LParam() 等同理。注意 getter 都带 const 后缀,意味着只读不改对象状态,编译器会帮你挡住误写。 别把事件对象当一次性变量。把这个类实例塞进数组,就能把最近 200 次图表交互全留痕,复盘黄金突破时的手动画线动作可能比看日志更快。

MQL5 / C++
class="type">long                m_chart_id;
class="type">class="kw">ushort              m_event_id;
class="type">long                m_lparam;
class="type">class="kw">double              m_dparam;
class="type">class="kw">string              m_sparam;
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">class="kw">string           SParam(class="type">void)                         const { class="kw">return this.m_sparam;      }
class="kw">public:
class=class="str">"cmt">//--- Constructor

◍ 事件基类怎么在图表上下文里做唯一比对

在 MT5 自定义事件体系里,CEventBaseObj 的构造函数先把 ChartID() 抓进 m_chart_id,再把外部传进来的 event_id、lparam、dparam、sparam 四个字段分别落到成员变量。这样每个事件对象天生就绑死了自己所属的图表,多图表 EA 里不会串号。 Compare 方法决定了事件对象在 CObject 容器里怎么排重和查找。它先按 ID 比,ID 相同再顺次比 LParam、DParam、SParam,任一层分出大小就返回 1 或 -1,全相等才返回 0。这种多级比较让两个事件对象只有在四个字段完全一致时才被判为「相同」。 下面这段是原始构造与比较实现,逐行拆解见注释:构造函数初始化列表里 m_chart_id(::ChartID()) 取当前图表句柄;this.m_event_id=event_id 等四行做字段赋值;Compare 里 const CEventBaseObj *compared=node 把基类指针向下转换;返回的三元嵌套先比 ID() 再比 LParam()、DParam()、SParam(),全同返回 0。开 MT5 建个继承该基类的事件类,往 CArrayObj 里塞重复参数对象,就能验证 Compare 的排重逻辑。

MQL5 / C++
CEventBaseObj(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) : m_chart_id(::ChartID())
            {
              this.m_event_id=event_id;
              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">//| Base object class for all library objects                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBaseObj : class="kw">public CObject
  {
class="kw">private:
class="kw">protected:

「基类里那些被忽略的成员与取值函数」

在图形对象基类里,有一组私有成员承担了事件、报价与时间的中转。m_list_events 用 CArrayObj 存对象事件列表,m_tick 直接接 MqlTick 报价结构,m_hash_sum 与 m_hash_sum_prev 分别存本次与上次检查的对象数据哈希和,用来判断状态是否真变了。 m_digits_currency 记账户货币的小数位,m_global_error 存全局错误码,m_chart_id 是主控 EA 所在图表 ID。m_is_event 是事件触发标志,m_event_code 是具体事件代码,m_name 和 m_folder_name 分别管对象名和派生类对象的存储目录。 TickTime() 在 MQL5 下返回 m_tick.time_msc(毫秒级),MQL4 下用 time*1000 凑毫秒,跨版本写指标时这处能省掉条件编译的麻烦。IsPresentEventFlag() 用位与判断某个 change_code 是否在 m_event_code 里置位,比逐个比较更省事。 DigitsCurrency() 只是把 m_digits_currency 抛出来,但在动态算仓位或点值显示时,少一次 AccountInfoInteger 调用就意味着少一处可能返回错误值的依赖。开 MT5 把这段塞进你的 CBaseObj 派生类,编译后挂 EURUSD 观察 m_tick.time_msc 与本地时间的偏差,通常能在毫秒级内对齐。

MQL5 / C++
  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
  class="type">int                DigitsCurrency(class="type">void)                                                    const { class="kw">return this.m_digits_currency; }
class=class="str">"cmt">//--- Returns the number of decimal places in the &class="macro">#x27;class="type">class="kw">double&class="macro">#x27; value

常见问题

先抽象一个所有库对象共用的基准对象类,把图表ID、子窗口号、事件类型等放进结构体,再让具体事件类继承它做唯一比对。
能把散落的图表ID、窗口号、鼠标坐标等一次性打包,传递和比对都只处理一个实例,避免漏字段导致的误触发。
可以,小布能按你给的基类结构在对应品种页加载事件监听,把触发的集合事件和你的比对规则跑出来看是否漏报。
多是基类成员没在子类构造时初始化,或图表上下文已切换,调用前先确认对象还绑在原来的图表ID上。
在基类里存图表ID和子窗口号,重写比对时同时校验这两项,只有全相等才认作同上下文事件。