轻松快捷开发 MetaTrader 程序的函数库(第十七部分):函数库对象之间的交互(基础篇)
📘

轻松快捷开发 MetaTrader 程序的函数库(第十七部分):函数库对象之间的交互(基础篇)

第 1/3 篇

库对象怎么互相调用

MQL5 函数库做到第十七部分时,核心难点从「单对象能跑」变成「对象之间怎么说话」。如果 Order、Symbol、Account 这些类各自为政,上层 EA 每写一行都要手动接线,维护成本会指数级上升。 解决思路是在库里统一一套交互约定:对象不直接改彼此私有成员,而是通过基类暴露的接口方法传递消息。比如行情对象收到新 tick,只负责通知订阅者,具体下单逻辑交给交易对象处理。 原文给出的示例发布于 2019-11-18,在 MetaTrader 5 环境实测,读者可直接在 MT5 里新建库工程对照验证:把交互层抽出来后,EA 主文件代码量通常能砍掉三成以上,且改一个类不易波及全局。外汇与贵金属品种波动剧烈,用此类库做回测时务必先在模拟盘跑通交互逻辑,实盘存在滑点与断线导致对象状态不一致的高风险。

「给基准对象装上可控属性触发器」

在已建立函数库所有对象的基准对象之后,从它继承的衍生类天然带事件能力。现在进一步把‘可控属性’下沉到基准层:每个对象可以按变化幅度、属性值级别等外部条件,标记哪些字段需要被监控。 典型用法是盯品种对象的浮点属性。比如想限制点差和价位再下单,只需在代码里设一个可控点差阈值,并把某价位设为触发线;当实时点差低于阈值、报价触线时,品种对象会向外抛事件,程序据此执行开仓。外汇与贵金属杠杆高、滑点跳空频繁,这类自动触发必须配合熔断与人工复核,否则可能在大波动中连续误触发。 原来跟踪事件要靠事件标志枚举,每种对象得维护一长串可能事件类型,限制多且笨重。改成属性挂钩后,事件种类直接等于对象属性数量(整型+实型)。不关心的字段用 LONG_MAX 初始化,刷新时直接跳过,不参与事件扫描。 对象都收在集合里,计时器里调一次集合的 Refresh(),集合再挨个调成员的 Refresh()。基准对象的 Refresh() 若顺手比对前后值,就能给每个库对象生成极轻的事件模型,事件列表统一汇总到 CEngine 主对象。这样一来,任何集合、任何对象的属性越线,基于该库的程序都能实时感知并改参数。

◍ 把三个事件ID压进一个long参数

函数库把品种、账户等对象的属性变化统一成基准事件后,面临一个现实问题:MQL5 的自定义图表事件 EventChartCustom() 只给了一个 long 型 lparam 来携带信息,而准确识别一次属性变化至少需要事件ID、事件原因、集合类ID三个整数,外加对象名称字符串。 解决办法是把 long(8字节)拆成四个 ushort(各2字节)的容器:字节0-1放毫秒时间,字节2-3放事件原因,字节4-5放类ID,字节6-7留作以后扩展。事件ID本身作为 custom_event_id 参数单独传给 EventChartCustom(),名称字符串仍走原 string 参数。这样一次调用就能在程序端还原出「哪个类的哪个属性、因何原因、在何时」发生了变化。 在基类计时器里,用二维数组分别存整数型和实数型属性的当前值、变化值、受控值和标志,比用结构更省事——因为属性类型可由索引推断(double 型永远排在 long 型之后),不必为每个对象预留重叠字段。首次启动先把当前状态复制为「先前状态」,避免误触发事件;之后每 tick 比对,发现差异就生成基准事件进列表,再经衍生类 Refresh() 转成库事件发出。 MQL4 的 ArrayResize() 有个坑:返回的是各维度大小总和而非第一维大小,所以封装数组大小时要对 MQL4 单独切分返回第二维数值,MQL5 则直接返回正确值。 下面这段是把 ushort 按索引塞进 long 容器的底层方法,索引0对应字节0-1,依次类推,超3报错:

MQL5 / C++
class="type">class="kw">ushort PackUShortToLong(class="type">class="kw">ushort val, class="type">int index)
  {
   if(index>class="num">3)
     {
      Print("错误索引 ",index);
      class="kw">return class="num">0;
     }
   class="kw">return (class="type">class="kw">ushort)(val << (class="num">16*index));
  }

把 ushort 塞进 long 与事件描述的反向解耦

把 16 位的 ushort 按字节索引塞进 64 位的 long 容器,核心是先做索引越界检查,再用 UshortToByte() 把值位移到位,最后按位或(OR)写进目标 long。这套封装和上一节的逻辑同构,区别只在于写入位置由调用方按字节序指定,返回时原容器已被就地修改。 基准对象类对衍生类一无所知,所以事件描述方法必须靠外部传入六个要素:被检测的对象属性、事件原因(增值/减值/穿越指定价位)、事件源集合 ID、发生变化的属性数值、衍生类里的属性文本描述、以及该属性数字形式的小数位数。少了任何一项,调用程序拿到的字符串都会缺失上下文。 ENUM_BASE_EVENT_REASON 里列了五种原因:INC、DEC、MORE_THEN、LESS_THEN、EQUALS,对应属性值的增减和与控制位的比较。注意 MARKET_WATCH_EVENT_SYMBOL_SORT 之后用 #define 定义了 SYMBOL_EVENTS_NEXT_CODE 为排序事件码 +1,将来加新集合时只需顺延这个宏,事件描述方法不用改结构。 别把正态当圣经:这种按位打包在跨平台传输时大概率没问题,但 MT5 的 long 字节序若换架构仍要实测,开 MT5 写个脚本打印位移后的十六进制就能验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Possible options of selecting by time                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_SELECT_BY_TIME
  {
   SELECT_BY_TIME_OPEN,                                                        class=class="str">"cmt">// By open time(in milliseconds)
   SELECT_BY_TIME_CLOSE,                                                       class=class="str">"cmt">// By close time(in milliseconds)
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Possible event reasons of the object library base object          |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_BASE_EVENT_REASON
  {
   BASE_EVENT_REASON_INC,                                                      class=class="str">"cmt">// Increase in the object class="kw">property value
   BASE_EVENT_REASON_DEC,                                                      class=class="str">"cmt">// Decrease in the object class="kw">property value
   BASE_EVENT_REASON_MORE_THEN,                                                class=class="str">"cmt">// Object class="kw">property value exceeds the control value
   BASE_EVENT_REASON_LESS_THEN,                                                class=class="str">"cmt">// Object class="kw">property value is less than the control value
   BASE_EVENT_REASON_EQUALS                                                    class=class="str">"cmt">// Object class="kw">property value is equal to the control value
   };
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Data for working with symbols                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| List of possible symbol events in the Market Watch window         |
class=class="str">"cmt">//+------------------------------------------------------------------+
enum ENUM_MW_EVENT
  {
   MARKET_WATCH_EVENT_NO_EVENT = ACCOUNT_EVENTS_NEXT_CODE,   class=class="str">"cmt">// No event
   MARKET_WATCH_EVENT_SYMBOL_ADD,                            class=class="str">"cmt">// Adding a symbol to the Market Watch window
   MARKET_WATCH_EVENT_SYMBOL_DEL,                            class=class="str">"cmt">// Removing a symbol from the Market Watch window
   MARKET_WATCH_EVENT_SYMBOL_SORT,                           class=class="str">"cmt">// Sorting symbols in the Market Watch window
   };
class="macro">#define SYMBOL_EVENTS_NEXT_CODE(MARKET_WATCH_EVENT_SYMBOL_SORT+class="num">1)   class=class="str">"cmt">// The code of the next event after the last symbol event code

「盯盘事件枚举里藏着的品种异动信号」

在 MT5 里写自定义盯盘工具,第一步是把「品种层面可能发生什么」列清楚。下面这段枚举把市场报价窗口和交易会话里的关键状态变化都定义了出来,比单纯读 tick 更省 CPU。 枚举首项 SYMBOL_EVENT_NO_EVENT 从 ACCOUNT_EVENTS_NEXT_CODE 接着编号,说明品种事件和账户事件共用一套回调序号空间,写 OnTradeTransaction 时别把两类 code 搞混。 MW_ADD / MW_DEL / MW_SORT 三个值对应报价窗口的增删与重排——如果你在小布里做了自选品种热度监控,这三个事件可以直接触发重算。TRADE_DISABLE 到 TRADE_FULL 那一组则反映经纪商对该品种的下单限制切换,黄金或外汇直盘在流动性干涸时可能瞬间切到 CLOSEONLY,属于高风险时段的硬信号。 SESSION_DEALS_INC/DEC 与 BUY/SELL_ORDERS_INC/DEC 共 8 个值,监控的是「当前会话」中成交数与买卖挂单总量的越阈变化;阈值由你自己在事件订阅时指定,不是写死在枚举里的。VOLUME_INC/DEC 看最后一笔成交的量变,VOLUME_HIGH_DAY_INC 看日内最大成交量的增量突破——这两类对剥头皮策略找放量启动点倾向有用。 把这段直接贴进 EA 头文件,配合 SymbolInfoInteger 做事件过滤,开 MT5 用 EURUSD 测一轮就能验证哪些事件在你的券商环境里真的会触发。

MQL5 / C++
enum ENUM_SYMBOL_EVENT
  {
   SYMBOL_EVENT_NO_EVENT = ACCOUNT_EVENTS_NEXT_CODE,      class=class="str">"cmt">// No event
   SYMBOL_EVENT_MW_ADD,                                   class=class="str">"cmt">// Adding a symbol to the Market Watch window
   SYMBOL_EVENT_MW_DEL,                                   class=class="str">"cmt">// Removing a symbol from the Market Watch window
   SYMBOL_EVENT_MW_SORT,                                  class=class="str">"cmt">// Sorting symbols in the Market Watch window
   SYMBOL_EVENT_TRADE_DISABLE,                            class=class="str">"cmt">// Disable order execution
   SYMBOL_EVENT_TRADE_LONGONLY,                           class=class="str">"cmt">// Allow buy only
   SYMBOL_EVENT_TRADE_SHORTONLY,                          class=class="str">"cmt">// Allow sell only
   SYMBOL_EVENT_TRADE_CLOSEONLY,                          class=class="str">"cmt">// Enable close only
   SYMBOL_EVENT_TRADE_FULL,                               class=class="str">"cmt">// No trading limitations
   SYMBOL_EVENT_SESSION_DEALS_INC,                        class=class="str">"cmt">// The increase in the number of deals in the current session exceeds the specified value
   SYMBOL_EVENT_SESSION_DEALS_DEC,                        class=class="str">"cmt">// The decrease in the number of deals in the current session exceeds the specified value
   SYMBOL_EVENT_SESSION_BUY_ORDERS_INC,                   class=class="str">"cmt">// The increase in the total number of buy orders currently exceeds the specified value
   SYMBOL_EVENT_SESSION_BUY_ORDERS_DEC,                   class=class="str">"cmt">// The decrease in the total number of buy orders currently exceeds the specified value
   SYMBOL_EVENT_SESSION_SELL_ORDERS_INC,                  class=class="str">"cmt">// The increase in the total number of sell orders currently exceeds the specified value
   SYMBOL_EVENT_SESSION_SELL_ORDERS_DEC,                  class=class="str">"cmt">// The decrease in the total number of sell orders currently exceeds the specified value
   SYMBOL_EVENT_VOLUME_INC,                               class=class="str">"cmt">// Volume increase in the last deal exceeds the specified value
   SYMBOL_EVENT_VOLUME_DEC,                               class=class="str">"cmt">// Volume decrease in the last deal exceeds the specified value
   SYMBOL_EVENT_VOLUME_HIGH_DAY_INC,                      class=class="str">"cmt">// The increase in the maximum volume per day exceeds the specified value

◍ 把品种事件枚举吃进策略里

MT5 的 SYMBOL_EVENT 系列枚举,把品种层面的异动拆成了可订阅的细粒度信号。上面这组常量覆盖了成交量、点差、止损位、冻结位以及 Bid/Ask 极值的变化方向,每一个都对应「超过指定阈值」才触发的条件。 比如 SYMBOL_EVENT_VOLUME_HIGH_DAY_DEC 表示当日最大成交量相对参考值的降幅突破设定,SYMBOL_EVENT_SPREAD_INC 是点差扩张超过指定变动,SYMBOL_EVENT_FREEZELEVEL_DEC 则是冻结水平下移越过阈值。把这些事件挂到 OnTradeTransaction 或 SymbolInfoInteger 轮询里,就能在贵金属跳空、外汇流动性抽干之前拿到第一手提醒。 实际验证时,在 MT5 终端打开 EURUSD 或 XAUUSD 的规格窗口,对照 SYMBOL_EVENT_BID_LAST_LOW_DEC 与 SYMBOL_EVENT_ASK_HIGH_INC 去观察亚盘开盘瞬间的极值刷新,大概率能复现文档里描述的「日最低 Bid 降幅超阈」与「日最高 Ask 增幅超阈」两种情形。外汇与贵金属杠杆高、滑点猛,这类事件只作预警,不构成方向判断。

MQL5 / C++
  SYMBOL_EVENT_VOLUME_HIGH_DAY_DEC,                                                                     class=class="str">"cmt">// The decrease in the maximum volume per day exceeds the specified value
  SYMBOL_EVENT_VOLUME_LOW_DAY_INC,                                                                       class=class="str">"cmt">// The increase in the minimum volume per day exceeds the specified value
  SYMBOL_EVENT_VOLUME_LOW_DAY_DEC,                                                                       class=class="str">"cmt">// The decrease in the minimum volume per day exceeds the specified value
  SYMBOL_EVENT_SPREAD_INC,                                                                               class=class="str">"cmt">// The increase in a spread exceeds the specified change
  SYMBOL_EVENT_SPREAD_DEC,                                                                               class=class="str">"cmt">// The decrease in a spread exceeds the specified change
  SYMBOL_EVENT_STOPLEVEL_INC,                                                                            class=class="str">"cmt">// The increase of a Stop order level exceeds the specified value
  SYMBOL_EVENT_STOPLEVEL_DEC,                                                                            class=class="str">"cmt">// The decrease of a Stop order level exceeds the specified value
  SYMBOL_EVENT_FREEZELEVEL_INC,                                                                          class=class="str">"cmt">// The increase in the freeze level exceeds the specified value
  SYMBOL_EVENT_FREEZELEVEL_DEC,                                                                          class=class="str">"cmt">// The decrease in the freeze level exceeds the specified value
  SYMBOL_EVENT_BID_LAST_INC,                                                                             class=class="str">"cmt">// The increase in the Bid or Last price exceeds the specified value
  SYMBOL_EVENT_BID_LAST_DEC,                                                                             class=class="str">"cmt">// The decrease in the Bid or Last price exceeds the specified value
  SYMBOL_EVENT_BID_LAST_HIGH_INC,                                                                        class=class="str">"cmt">// The increase in the maximum Bid or Last price per day exceeds the specified value
  SYMBOL_EVENT_BID_LAST_HIGH_DEC,                                                                        class=class="str">"cmt">// The decrease in the maximum Bid or Last price per day exceeds the specified value relative to the specified price
  SYMBOL_EVENT_BID_LAST_LOW_INC,                                                                         class=class="str">"cmt">// The increase in the minimum Bid or Last price per day exceeds the specified value relative to the specified price
  SYMBOL_EVENT_BID_LAST_LOW_DEC,                                                                         class=class="str">"cmt">// The decrease in the minimum Bid or Last price per day exceeds the specified value
  SYMBOL_EVENT_ASK_INC,                                                                                  class=class="str">"cmt">// The increase in the Ask price exceeds the specified value
  SYMBOL_EVENT_ASK_DEC,                                                                                  class=class="str">"cmt">// The decrease in the Ask price exceeds the specified value
  SYMBOL_EVENT_ASK_HIGH_INC,                                                                             class=class="str">"cmt">// The increase in the maximum Ask price per day exceeds the specified value

常见问题

给基准对象装可控属性触发器,让对象通过事件ID而非直接引用通信,降低耦合。
用位运算把多个ushort事件标识拼进long,接收端再移位拆解,省去多个参数传递。
小布可把品种事件枚举吃进策略页,自动标记异动并推送,你只管看结论。
用掩码和移位从long里取出对应ushort,再查枚举映射还原文字,互不干扰。
把枚举当过滤器喂给策略,只在命中异动事件时触发动作,减少无效扫描。