DoEasy 函数库中的其他类(第六十六部分):MQL5.com 信号集合类·综合运用
- 把信号源基础属性一次性抓进内存
- 集合 ID 与对象层的头文件映射
- 按属性筛信号:整数、实数与字符串的三套重载
- 信号筛选与极值定位的底层实现
- 信号列表里挑极值位置的写法
- 在信号列表里捞最小字符串属性
- MQL5 信号对象的集合类
- 信号集合类的私有封装与取数入口
- 信号集合类的检索与订阅接口
- 信号账户同步参数的取值与改写接口
- 信号订阅接口的开关与状态读取
- 信号订阅参数的描述函数与集合构造
- 信号集合的刷新与按ID检索实现
- 按名称抓取信号与日志分级打印
- 信号订阅与退订的底层调用
- 信号订阅与同步参数的代码落点
- 信号账户参数的静默配置入口
- 信号订阅参数的描述方法实现
- 把信号订阅参数打到日志里
- 引擎基类的集合装配
- 从深度缓存里抠买卖盘口量
- 信号集合的取数接口与订阅动作
- 信号源当前配置的接口封装
- 从深度挂单里抠出买卖量
- 从深度快照里抠出买卖盘真实挂量
- 在 OnTick 里跑通信号订阅回测
- 把跟单信号塞进对象数组
- 信号筛选与EA生命周期的收尾逻辑
- 自动挑涨幅最高的免费信号并订阅
- 把信号服务接进引擎时的开关逻辑
- 信号订阅日志里的真实参数坑
- 编译报错先改 Trading.mqh 访问权限
- 交易请求前的止损位合规校验
- 挂单请求对象的私有检索与构造收口
「把信号源基础属性一次性抓进内存」
在 MT5 里做信号源分析时,最忌讳每次要用一个字段就调一次平台 API。上面这段构造函数把信号源的整组基础属性在初始化阶段全部读进对象自身的数组,后面随用随取,不再反复走 SignalBaseGetInteger / GetDouble / GetString。 long 类属性覆盖了起止日期、杠杆、点值、评级、订阅人数与总交易笔数——这些是过滤信号源的第一道筛子。比如 SIGNAL_BASE_SUBSCRIBERS 和 SIGNAL_BASE_TRADES 若长期偏低(例如订阅不足 50、交易少于 100 笔),样本量可能不足以判断策略稳定性。 double 类则拿到了余额、净值、增益、最大回撤、报价与 ROI。注意 SIGNAL_BASE_MAX_DRAWDOWN 是双精度浮点,实盘中外汇与贵金属杠杆放大下,回撤超过 30% 已属高风险区间,需结合杠杆字段一起看。 string 类存的是作者登录名、经纪商、服务器、信号名与账户币种。跨经纪商比对时,SERVER 与 CURRENCY 不一致会导致点值换算偏差,这一步不抓全,后面统计全是脏数据。 末尾的宏 COLLECTION_HISTORY_ID 定义为 0x777A,是历史采集列表的内部标识,后续做本地缓存或回放时靠它区分信号源快照批次。
this.m_long_prop[SIGNAL_MQL5_PROP_DATE_STARTED] = ::SignalBaseGetInteger(SIGNAL_BASE_DATE_STARTED); this.m_long_prop[SIGNAL_MQL5_PROP_DATE_UPDATED] = ::SignalBaseGetInteger(SIGNAL_BASE_DATE_UPDATED); this.m_long_prop[SIGNAL_MQL5_PROP_LEVERAGE] = ::SignalBaseGetInteger(SIGNAL_BASE_LEVERAGE); this.m_long_prop[SIGNAL_MQL5_PROP_PIPS] = ::SignalBaseGetInteger(SIGNAL_BASE_PIPS); this.m_long_prop[SIGNAL_MQL5_PROP_RATING] = ::SignalBaseGetInteger(SIGNAL_BASE_RATING); this.m_long_prop[SIGNAL_MQL5_PROP_SUBSCRIBERS] = ::SignalBaseGetInteger(SIGNAL_BASE_SUBSCRIBERS); this.m_long_prop[SIGNAL_MQL5_PROP_TRADES] = ::SignalBaseGetInteger(SIGNAL_BASE_TRADES); this.m_double_prop[this.IndexProp(SIGNAL_MQL5_PROP_BALANCE)] = ::SignalBaseGetDouble(SIGNAL_BASE_BALANCE); this.m_double_prop[this.IndexProp(SIGNAL_MQL5_PROP_EQUITY)] = ::SignalBaseGetDouble(SIGNAL_BASE_EQUITY); this.m_double_prop[this.IndexProp(SIGNAL_MQL5_PROP_GAIN)] = ::SignalBaseGetDouble(SIGNAL_BASE_GAIN); this.m_double_prop[this.IndexProp(SIGNAL_MQL5_PROP_MAX_DRAWDOWN)] = ::SignalBaseGetDouble(SIGNAL_BASE_MAX_DRAWDOWN); this.m_double_prop[this.IndexProp(SIGNAL_MQL5_PROP_PRICE)] = ::SignalBaseGetDouble(SIGNAL_BASE_PRICE); this.m_double_prop[this.IndexProp(SIGNAL_MQL5_PROP_ROI)] = ::SignalBaseGetDouble(SIGNAL_BASE_ROI); this.m_string_prop[this.IndexProp(SIGNAL_MQL5_PROP_AUTHOR_LOGIN)] = ::SignalBaseGetString(SIGNAL_BASE_AUTHOR_LOGIN); this.m_string_prop[this.IndexProp(SIGNAL_MQL5_PROP_BROKER)] = ::SignalBaseGetString(SIGNAL_BASE_BROKER); this.m_string_prop[this.IndexProp(SIGNAL_MQL5_PROP_BROKER_SERVER)]= ::SignalBaseGetString(SIGNAL_BASE_BROKER_SERVER); this.m_string_prop[this.IndexProp(SIGNAL_MQL5_PROP_NAME)] = ::SignalBaseGetString(SIGNAL_BASE_NAME); this.m_string_prop[this.IndexProp(SIGNAL_MQL5_PROP_CURRENCY)] = ::SignalBaseGetString(SIGNAL_BASE_CURRENCY); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//--- Collection list IDs class="macro">#define COLLECTION_HISTORY_ID(0x777A) class=class="str">"cmt">// Historical collection list ID
集合 ID 与对象层的头文件映射
在搭建 MT5 多对象管理框架时,先用一组宏把各类集合的标识锁死,能避免运行时 ID 冲突。下面这段把市场、事件、账户、品种、时序、指标缓冲、指标、指标数据、Tick 序列、深度报单簿(DOM)以及 MQL5 信号分别映射到 0x777B 至 0x7785 的十六进制区间,相邻 ID 仅差 1,方便后续用循环遍历。 高亮的 0x7785 对应 MQL5 信号集合,配套引入 ..\Objects\MQLSignalBase\MQLSignal.mqh;外汇与贵金属信号订阅涉及杠杆与滑点风险,回测与实盘表现可能偏离。 代码后半段通过 #include 把订单、事件、账户、品种、挂单请求、序列、指标、Tick、簿记等对象类挂进来,形成统一的数据层。开 MT5 把这段贴进自定义库头文件,编译后可在调试器看到 11 个集合 ID 常量。
class="macro">#define COLLECTION_MARKET_ID(0x777B) class=class="str">"cmt">// Market collection list ID class="macro">#define COLLECTION_EVENTS_ID(0x777C) class=class="str">"cmt">// Event collection list ID class="macro">#define COLLECTION_ACCOUNT_ID(0x777D) class=class="str">"cmt">// Account collection list ID class="macro">#define COLLECTION_SYMBOLS_ID(0x777E) class=class="str">"cmt">// Symbol collection list ID class="macro">#define COLLECTION_SERIES_ID(0x777F) class=class="str">"cmt">// Timeseries collection list ID class="macro">#define COLLECTION_BUFFERS_ID(0x7780) class=class="str">"cmt">// Indicator buffer collection list ID class="macro">#define COLLECTION_INDICATORS_ID(0x7781) class=class="str">"cmt">// Indicator collection list ID class="macro">#define COLLECTION_INDICATORS_DATA_ID(0x7782) class=class="str">"cmt">// Indicator data collection list ID class="macro">#define COLLECTION_TICKSERIES_ID(0x7783) class=class="str">"cmt">// Tick series collection list ID class="macro">#define COLLECTION_MBOOKSERIES_ID(0x7784) class=class="str">"cmt">// DOM series collection list ID class="macro">#define COLLECTION_MQL5_SIGNALS_ID(0x7785) class=class="str">"cmt">// MQL5 signals collection list ID class=class="str">"cmt">//--- Data parameters for file operations class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include "..\Objects\Orders\Order.mqh" class="macro">#include "..\Objects\Events\Event.mqh" class="macro">#include "..\Objects\Accounts\Account.mqh" class="macro">#include "..\Objects\Symbols\Symbol.mqh" class="macro">#include "..\Objects\PendRequest\PendRequest.mqh" class="macro">#include "..\Objects\Series\SeriesDE.mqh" class="macro">#include "..\Objects\Indicators\Buffer.mqh" class="macro">#include "..\Objects\Indicators\IndicatorDE.mqh" class="macro">#include "..\Objects\Indicators\DataInd.mqh" class="macro">#include "..\Objects\Ticks\DataTick.mqh" class="macro">#include "..\Objects\Book\MarketBookOrd.mqh" class="macro">#include "..\Objects\MQLSignalBase\MQLSignal.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Methods of working with MQL5 signal data |
◍ 按属性筛信号:整数、实数与字符串的三套重载
在 MT5 里做信号聚合时,常需要从一批 CMQLSignal 对象里挑出满足某属性条件的子集。CSelect 类给了三组同名静态方法 ByMQLSignalProperty,分别吃 integer、double、string 三种属性枚举,配合 ENUM_COMPARER_TYPE 决定大于、小于还是等于。 整数版的实现很直接:先判空 list_source,建一个不自动释放元素的 CArrayObj(FreeMode(false) 避免误删原信号),再遍历源列表,用 SupportProperty 跳过不支持该属性的对象,取属性值后交给 CompareValues 做比较,命中就 Add 进结果集。 实数版与字符串版逻辑完全同构,只是 value 参数类型换成 double 和 string,属性枚举换成 ENUM_SIGNAL_MQL5_PROP_DOUBLE / STRING。这样设计让调用方不用关心底层比较细节,一行就能拿到过滤后的信号指针数组。 另有 FindMQLSignalMax / FindMQLSignalMin 的三种重载,返回的是信号在列表中的下标而非对象本身——做排序或择优展示时,先拿下标再 At() 取对象,比每次拷贝对象更省。外汇与贵金属信号受杠杆与流动性影响,筛选结果仅代表历史快照,实盘信号可能快速失效,须以 MT5 终端实时数据为准。
class="kw">static CArrayObj *ByMQLSignalProperty(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode); class="kw">static CArrayObj *ByMQLSignalProperty(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode); class="kw">static CArrayObj *ByMQLSignalProperty(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode); class="kw">static class="type">int FindMQLSignalMax(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property); class="kw">static class="type">int FindMQLSignalMax(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_DOUBLE class="kw">property); class="kw">static class="type">int FindMQLSignalMax(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property); class="kw">static class="type">int FindMQLSignalMin(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property); class="kw">static class="type">int FindMQLSignalMin(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_DOUBLE class="kw">property); class="kw">static class="type">int FindMQLSignalMin(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property); CArrayObj *CSelect::ByMQLSignalProperty(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); ListStorage.Add(list); class="type">int total=list_source.Total(); for(class="type">int i=class="num">0; i<total; i++) { CMQLSignal *obj=list_source.At(i); if(!obj.SupportProperty(class="kw">property)) class="kw">continue; class="type">long obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } class="kw">return list; } CArrayObj *CSelect::ByMQLSignalProperty(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false);
「信号筛选与极值定位的底层实现」
在自建信号选择器里,按属性过滤 MQL5 信号是高频动作。上面这段代码给了两个 ByMQLSignalProperty 重载:一个吃整数/实数属性,一个吃字符串属性,逻辑完全对称——先建一个不自动释放元素的 CArrayObj,再遍历源列表,只把支持该属性且比较命中的信号塞进去。
遍历时用了 obj.SupportProperty(property) 做前置拦截,不支持就 continue,避免对不支持的属性调用 GetProperty 直接崩。比较本身交给 CompareValues 统一处理,所以等于把『大于/小于/等于/包含』这类规则从筛选函数里解耦了。
极值查找走的是 FindMQLSignalMax 重载。注意它从 i=1 起步,首元素默认当基准(index=0),后面逐个用 CompareValues(...,MORE) pk,赢了的才更新下标。空列表或空指针都返回 WRONG_VALUE,调用方得自己判。
实盘里如果你用这类封装去挑『当前魔法号收益最高的一笔信号』,记得 Total() 返回 0 时函数不给下标而给 -1 类的错误常量,直接拿去 At() 会空指针——外汇和贵金属波动大,信号列表为空是常事,别省这个判空。
CArrayObj *CSelect::ByMQLSignalProperty(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode) { if(list_ class="kw">return NULL; CArrayObj *list=new CArrayObj(); if(list==NULL) class="kw">return NULL; list.FreeMode(false); ListStorage.Add(list); for(class="type">int i=class="num">0; i<list_source.Total(); i++) { CMQLSignal *obj=list_source.At(i); if(!obj.SupportProperty(class="kw">property)) class="kw">continue; class="type">class="kw">string obj_prop=obj.GetProperty(class="kw">property); if(CompareValues(obj_prop,value,mode)) list.Add(obj); } class="kw">return list; } class="type">int CSelect::FindMQLSignalMax(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CMQLSignal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CMQLSignal *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">long obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; }
信号列表里挑极值位置的写法
在 MT5 自建信号筛选类里,经常要从一堆 CMQLSignal 对象中找出某项属性最大或最小的那个下标。下面这组方法按属性类型分了重载:字符串、整数、双精度各一套,逻辑骨架完全一致,只是比较方向(MORE / LESS)和取值类型不同。 以字符串属性取最大值为例,函数先判空与总数,total 为 0 时直接回 WRONG_VALUE;随后从下标 1 开始遍历,把当前对象属性与已记录 max_obj 的属性丢给 CompareValues 比大小,胜出就更新 index。整数与双精度版本仅把 string 换成 long / double,并把 MORE 换成 LESS 即可实现取最小。 实盘接这套代码时,注意 list_source 传入前必须非空且 Total()>0,否则第 4 行 max_obj=list_source.At(index) 在空表上会拿到 NULL 再取属性直接崩。外汇与贵金属信号波动大、滑点频繁,这类极值筛选只反映历史快照,后续信号可能快速反转,属高风险操作。
class="type">class="kw">double obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the MQL5 signal index in the list | class=class="str">"cmt">//| with the maximum class="type">class="kw">string class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindMQLSignalMax(CArrayObj *list_source,ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property) { if(list_ class="kw">return WRONG_VALUE; class="type">int index=class="num">0; CMQLSignal *max_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CMQLSignal *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); max_obj=list_source.At(index); class="type">class="kw">string obj2_prop=max_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,MORE)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the MQL5 signal index in the list | class=class="str">"cmt">//| with the minimum integer class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindMQLSignalMin(CArrayObj* list_source,ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property) { class="type">int index=class="num">0; CMQLSignal *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CMQLSignal *obj=list_source.At(i); class="type">long obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">long obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the MQL5 signal index in the list | class=class="str">"cmt">//| with the minimum real class="kw">property value | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSelect::FindMQLSignalMin(CArrayObj* list_source,ENUM_SIGNAL_MQL5_PROP_DOUBLE class="kw">property) { class="type">int index=class="num">0; CMQLSignal *min_obj=NULL; class="type">int total=list_source.Total(); if(total== class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CMQLSignal *obj=list_source.At(i); class="type">class="kw">double obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">double obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 在信号列表里捞最小字符串属性
做 MQL5 信号筛选时,经常要从一堆 CMQLSignal 对象里找出某个字符串属性值最小的那一条。下面这段函数就是干这个活的:丢进一个对象数组和要比较的属性枚举,吐出最小项在数组里的下标。 逻辑不复杂但有个坑:下标从 0 起步,循环却从 i=1 开始比,相当于默认第 0 个就是临时最小,后面逐个用 CompareValues 按 LESS 规则覆盖 index。若数组为空直接返 WRONG_VALUE,调用方得自己判。 外汇与贵金属信号受平台数据延迟影响,字符串比较结果可能和网页端展示不一致,实盘前建议在 MT5 策略测试器里用真实信号集跑一遍验证下标指向。
class="type">int CSelect::FindMQLSignalMin(CArrayObj* list_source,ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property) { class="type">int index=class="num">0; CMQLSignal *min_obj=NULL; class="type">int total=list_source.Total(); if(total==class="num">0) class="kw">return WRONG_VALUE; for(class="type">int i=class="num">1; i<total; i++) { CMQLSignal *obj=list_source.At(i); class="type">class="kw">string obj1_prop=obj.GetProperty(class="kw">property); min_obj=list_source.At(index); class="type">class="kw">string obj2_prop=min_obj.GetProperty(class="kw">property); if(CompareValues(obj1_prop,obj2_prop,LESS)) index=i; } class="kw">return index; }
「MQL5 信号对象的集合类」
在 \MQL5\Include\DoEasy\ Collections\ 函数库文件夹之下,于 MQLSignalsCollection.mqh 文件里创建新类 CMQLSignalsCollection。 在类文件里, 包含其操作所需的所有类文件 : 该类应从所有函数库对象的基准对象中派生 : 我们来看看类主体,并分析一下它包含的方法: 在类的私密部分提供存储 MQL5 信号对象的列表对象,以及辅助变量和方法。 在类的公开部分,提供 操控对象集合列表的标准方法 ,以及 两种依据 ID 和名称选择信号并订阅的方法 。 类的公开部分还提供了 操控处于订阅激活状态的当前信号的方法 。 我们来看看一些方法的实现。 在类的构造方法里 , 清除集合列表 , 为其设置已排序列表标志 , 为列表设置 MQL5 信号对象集合的 ID , 写下 MQL5.com 信号数据库中的信号总数 ,并 调用集合创建方法 。 由于我们不打算自动更新信号列表,也不会托管给函数库,故列表更新方法就足够了。 读取数据库中出现的所有信号,并在方法中将其发送集合列表。 如果用户希望从 MQL5.com 信号数据库中得到更新的信号列表,则从集合中接收任何数据之前,他们必须自己调用 Refresh() 更新方法。 不过,无论如何我们的集合创建方法都会提供与典型函数库一套集合方法的兼容性。 该方法本身将简单地清除列表,并调用集合更新方法。 从集合创建方法第一次调用 Refresh() 方法之后,集合列表就已被填充完毕,可以处理了。 如果为了搜索可能的新信号应该更新集合列表,只需在访问集合列表之前调用 Refresh() 方法。 集合创建方法: 此处, 我们清除信号集合列表 ,并 用来自 MQL5.com 信号数据库的信号填充 。 如果集合列表中的信号数量超过零 (列表已满), 显示集合列表创建成功的消息,并返回 true 。 否则,返回 false 。 更新集合列表的方法: 该方法的逻辑在代码注释中已有详述。 简言之:该方法接收标志,指示检测到新信号之后是否需要通知。 由于该方法不会清除集合列表,因此只能将新发现的信号添加到其中。 如果消息标志已设置,则日志里会显示有关新检测到信号,并成功添加到列表中的消息。 目前,这是最简单的方法,务须提供更新现有信号的参数 — 您能够在您的程序中自行更新它们,或依据其 ID 访问信号对象,并为其属性设置新值。 稍后,我将添加按时间参数自动更新现有信号。 如果 MQL5.com 信号集合类是刚需,我将实现发送有关新信号的事件,并更改跟踪信号的参数。 该方法依据信号 ID 返回指向 MQL5 信号对象的指针: 依据信号 ID 获取 MQL5 信号对象列表 , 并 从得到的列表中返回单一对象,或 NULL 。 该方法依据信号名称返回指向 MQL5 信号对象的指针: 依据信号名称获取 MQL5 信号对象列表 ,并 从得到的列表中返回单一对象,或者返回 NULL 。 该方法将完整的集合列表反馈到日志: 标题会首先显示 。 然后, 循环遍历集合列表 , 获取下一个 MQL 信号对象 ,并 显示其完整描述 。 该方法返回显示在日志里的集合列表简要: 根据所传递的标志,该方法在日志中显示各种消息和列表。 标题会排第一。 如果列表标志已设置 ,则日志会显示来自集合信号的简述。 会考虑 付费信号 和 免费信号 的标志。 取决于它们的状态,日志显示所有信号,或仅显示付费信号,或仅显示免费信号。 如果不显示列表说明 ,则标题后面是集合列表中免费和付费信号的总数。 该方法 订阅信
信号集合类的私有封装与取数入口
在 MT5 信号订阅工具的底层,常会见到一个 CMQLSignalsCollection 类,它继承自 CBaseObj,专门管理一批 MQL5 信号对象。私有段里用 CListObj m_list 挂住所有信号实例,另用 m_signals_base_total 记录信号库中的总数,这两个成员不直接暴露,避免外部误改。 私有方法里能看到几个关键开关:Subscribe() 按 signal_id 订阅;CurrentSetConfirmationsDisableFlag() 控制是否跳过同步确认弹窗;CurrentSetSLTPCopyFlag() 决定跟单时是否复制止损止盈;CurrentSetSubscriptionEnabledFlag() 则管是否允许按订阅关系拷贝信号。实盘接这类封装时,这几个 flag 的默认值最好在初始化阶段显式设一次,否则可能沿用上次的终端配置。 公开段提供了取数入口:GetObject() 返回自身指针,GetList() 有两种重载——无参版直接回传内部列表指针,带参版按整数属性(ENUM_SIGNAL_MQL5_PROP_INTEGER)配合比较模式做筛选,底层调的是 CSelect::ByMQLSignalProperty()。这意味着你写 EA 时,可以用 GetList( SIGNAL_MQL5_SUBSCRIPTIONS, 1, EQUAL ) 之类的方式,只捞出自已已订阅的信号,省掉手动遍历。外汇与贵金属信号跟单杠杆高,筛选后仍需人工核对滑点与点差环境。
class="macro">#class="kw">property copyright "Copyright class="num">2021, MetaQuotes Software Corp." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#include "ListObj.mqh" class="macro">#include "..\Services\Select.mqh" class="macro">#include "..\Objects\MQLSignalBase\MQLSignal.mqh" class CMQLSignalsCollection : class="kw">public CBaseObj { class="kw">private: CListObj m_list; class=class="str">"cmt">// List of MQL5 signal objects class="type">int m_signals_base_total;class=class="str">"cmt">// Number of signals in the MQL5 signal database class="type">bool Subscribe(const class="type">long signal_id); class="type">bool CurrentSetConfirmationsDisableFlag(const class="type">bool flag); class="type">bool CurrentSetSLTPCopyFlag(const class="type">bool flag); class="type">bool CurrentSetSubscriptionEnabledFlag(const class="type">bool flag); class="kw">public: CMQLSignalsCollection *GetObject(class="type">void) { class="kw">return &this; } CArrayObj *GetList(class="type">void) { class="kw">return &this.m_list; } CArrayObj *GetList(ENUM_SIGNAL_MQL5_PROP_INTEGER class="kw">property,class="type">long value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByMQLSignalProperty(this.GetList(),class="kw">property,value,mode); }
◍ 信号集合类的检索与订阅接口
在 MQL5 里把信号当成对象集合来管理时,核心是一个 CMQLSignalsCollection 类。它提供按属性过滤的 GetList 重载,也提供按 ID、名称、索引三种方式取单个 CMQLSignal 指针,方便在 EA 里动态切换观察目标。 GetList 的两个重载分别接收 ENUM_SIGNAL_MQL5_PROP_DOUBLE 和 ENUM_SIGNAL_MQL5_PROP_STRING 类型的属性,配合 ENUM_COMPARER_TYPE 比较模式(默认 EQUAL)即可筛出满足条件的信号对象数组。实际写策略时,可以用它快速挑出「盈利因子大于某值」或「名称包含某词」的信号做横向比对。 DataTotal 直接返回内部 m_list.Total(),也就是当前集合里的信号对象数量;GetMQLSignal(const int index) 则用 m_list.At(index) 按位取指针。注意索引越界会返回 NULL,调用前先用 DataTotal 判长度。 订阅动作由 SubscribeByID 和 SubscribeByName 完成,二者均返回 bool 表示成败。ProgramIsAllowed 内部读 MQLInfoInteger(MQL_SIGNALS_ALLOWED),在信号服务被终端禁用的环境下会直接返 false,写自动订阅逻辑前务必先过这道检查。外汇与贵金属信号订阅涉及跟单执行,滑点及断连风险偏高,任何订阅都应先在模拟环境验证。
CArrayObj *GetList(ENUM_SIGNAL_MQL5_PROP_DOUBLE class="kw">property,class="type">class="kw">double value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByMQLSignalProperty(this.GetList(),class="kw">property,value,mode); } CArrayObj *GetList(ENUM_SIGNAL_MQL5_PROP_STRING class="kw">property,class="type">class="kw">string value,ENUM_COMPARER_TYPE mode=EQUAL) { class="kw">return CSelect::ByMQLSignalProperty(this.GetList(),class="kw">property,value,mode); } class=class="str">"cmt">//--- Return the number of MQL5 signal objects in the list class="type">int DataTotal(class="type">void) const { class="kw">return this.m_list.Total(); } class=class="str">"cmt">//--- Return the pointer to the MQL5 signal object(class="num">1) by ID, (class="num">2) by name and(class="num">3) by index in the list CMQLSignal *GetMQLSignal(const class="type">long id); CMQLSignal *GetMQLSignal(const class="type">class="kw">string name); CMQLSignal *GetMQLSignal(const class="type">int index) { class="kw">return this.m_list.At(index); } class=class="str">"cmt">//--- Create the collection list of MQL5 signal objects class="type">bool CreateCollection(class="type">void); class=class="str">"cmt">//--- Update the collection list of MQL5 signal objects class="type">void Refresh(const class="type">bool messages=true); class=class="str">"cmt">//--- Display(class="num">1) the complete and(class="num">2) class="type">class="kw">short collection description in the journal class="type">void Print(class="type">void); class="type">void PrintShort(const class="type">bool list=false,const class="type">bool paid=true,const class="type">bool free=true); class=class="str">"cmt">//--- Constructor CMQLSignalsCollection(); class=class="str">"cmt">//--- Subscribe to a signal by(class="num">1) ID and(class="num">2) signal name class="type">bool SubscribeByID(const class="type">long signal_id); class="type">bool SubscribeByName(const class="type">class="kw">string signal_name); class=class="str">"cmt">//--- Return the flag allowing working with the signal service class="type">bool ProgramIsAllowed(class="type">void) { class="kw">return (class="type">bool)::MQLInfoInteger(MQL_SIGNALS_ALLOWED); } class=class="str">"cmt">//--- Unsubscribe from the current signal class="type">bool CurrentUnsubscribe(class="type">void);
「信号账户同步参数的取值与改写接口」
在 MT5 信号订阅机制里,CurrentSet* 系列函数负责在本地覆盖信号源的下单约束,而 Current* 系列则是把这些约束读出来核对。两者配合,才能在跟单 EA 里动态限制滑点、权益占用和手数比例,避免信号源参数与本地风控脱节。 下面这段接口声明直接暴露了可调控的字段:权益上限(EquityLimit)、市价单滑点(Slippage)、入金占比上限(DepositPercent)都以 double/int 传入,改写后即时影响后续同步。 读值函数全部走 SignalInfoGetDouble / SignalInfoGetInteger,宏常量如 SIGNAL_INFO_SLIPPAGE 对应后台信号属性。实盘跟单前,建议先打印 CurrentSlippage() 与 CurrentVolumePercent(),若返回滑点 > 10 点、 volume percent = 0,说明信号端禁用了比例复制,本地需手动接管手数。 外汇与贵金属跟单属于高风险操作,信号历史表现不代表未来,参数误配可能导致超额回撤。
class="type">bool CurrentSetEquityLimit(const class="type">class="kw">double value); class=class="str">"cmt">//--- Set the market order slippage used when synchronizing positions and copying deals class="type">bool CurrentSetSlippage(const class="type">class="kw">double value); class=class="str">"cmt">//--- Set deposit limitations(in %) class="type">bool CurrentSetDepositPercent(const class="type">int value); class=class="str">"cmt">//--- Return the percentage for converting deal volume class="type">class="kw">double CurrentEquityLimit(class="type">void) { class="kw">return ::SignalInfoGetDouble(SIGNAL_INFO_EQUITY_LIMIT); } class=class="str">"cmt">//--- Return the market order slippage used when synchronizing positions and copying deals class="type">class="kw">double CurrentSlippage(class="type">void) { class="kw">return ::SignalInfoGetDouble(SIGNAL_INFO_SLIPPAGE); } class=class="str">"cmt">//--- Return the flag allowing synchronization without confirmation dialog class="type">bool CurrentConfirmationsDisableFlag(class="type">void) { class="kw">return (class="type">bool)::SignalInfoGetInteger(SIGNAL_INFO_CONFIRMATIONS_DISABLED); } class=class="str">"cmt">//--- Return the flag of copying Stop Loss and Take Profit class="type">bool CurrentSLTPCopyFlag(class="type">void) { class="kw">return (class="type">bool)::SignalInfoGetInteger(SIGNAL_INFO_COPY_SLTP); } class=class="str">"cmt">//--- Return deposit limitations(in %) class="type">int CurrentDepositPercent(class="type">void) { class="kw">return (class="type">int)::SignalInfoGetInteger(SIGNAL_INFO_DEPOSIT_PERCENT); } class=class="str">"cmt">//--- Return the flag allowing the copying of signals by subscription class="type">bool CurrentSubscriptionEnabledFlag(class="type">void) { class="kw">return (class="type">bool)::SignalInfoGetInteger(SIGNAL_INFO_SUBSCRIPTION_ENABLED); } class=class="str">"cmt">//--- Return the limitation by funds for a signal class="type">class="kw">double CurrentVolumePercent(class="type">void) { class="kw">return ::SignalInfoGetDouble(SIGNAL_INFO_VOLUME_PERCENT); } class=class="str">"cmt">//--- Return the signal ID class="type">long CurrentID(class="type">void) { class="kw">return ::SignalInfoGetInteger(SIGNAL_INFO_ID); }
信号订阅接口的开关与状态读取
在 MT5 的 Signals 服务封装类里,有一组以 Current 开头的方法专门控制当前信号账户的同步行为。它们不直接操作交易,而是改写客户端配置,调用后需重启信号订阅才能生效。 读取类方法靠 SignalInfoGetInteger 与 SignalInfoGetString 拿底层状态:CurrentTermsAgreeFlag 返回是否勾选过服务条款协议,CurrentName 返回信号源名称字符串。这两个是只读探针,常用于 EA 启动前做合规校验。 开关类方法成对出现,ON/OFF 只是对同一个 Flag 设置器传 true/false。例如 CurrentSetConfirmationsDisableON 关闭同步确认弹窗,CurrentSetSLTPCopyOFF 禁止复制止损止盈,CurrentSetSubscriptionEnableOFF 暂停按订阅跟单。外汇与贵金属信号跟单存在滑点放大和断连风险,关闭确认框后误操作概率可能上升。 下面这段源码展示了接口形态,逐行看逻辑比看文档快: // 返回是否同意信号服务条款 bool CurrentTermsAgreeFlag(void) { return (bool)::SignalInfoGetInteger(SIGNAL_INFO_TERMS_AGREE); } // 返回信号名称 string CurrentName(void) { return ::SignalInfoGetString(SIGNAL_INFO_NAME); } // 开启无确认框同步 bool CurrentSetConfirmationsDisableON(void) { return this.CurrentSetConfirmationsDisableFlag(true); } // 关闭无确认框同步 bool CurrentSetConfirmationsDisableOFF(void) { return this.CurrentSetConfirmationsDisableFlag(false); } // 开启复制 SL/TP bool CurrentSetSLTPCopyON(void) { return this.CurrentSetSLTPCopyFlag(true); } // 关闭复制 SL/TP bool CurrentSetSLTPCopyOFF(void) { return this.CurrentSetSLTPCopyFlag(false); } // 开启订阅跟单 bool CurrentSetSubscriptionEnableON(void) { return this.CurrentSetSubscriptionEnabledFlag(true); } // 关闭订阅跟单 bool CurrentSetSubscriptionEnableOFF(void) { return this.CurrentSetSubscriptionEnabledFlag(false); } 在 MT5 导航器里挂一个测试 EA,依次调用 CurrentSetSLTPCopyOFF 与 CurrentSubscriptionEnableON,能在信号日志里看到配置位翻转,验证这些方法确实改了运行态。
class=class="str">"cmt">//--- Return the flag of agreeing to the terms of use of the Signals service class="type">bool CurrentTermsAgreeFlag(class="type">void) { class="kw">return (class="type">bool)::SignalInfoGetInteger(SIGNAL_INFO_TERMS_AGREE); } class=class="str">"cmt">//--- Return the signal name class="type">class="kw">string CurrentName(class="type">void) { class="kw">return ::SignalInfoGetString(SIGNAL_INFO_NAME); } class=class="str">"cmt">//--- Enable synchronization without the confirmation dialog class="type">bool CurrentSetConfirmationsDisableON(class="type">void) { class="kw">return this.CurrentSetConfirmationsDisableFlag(true); } class=class="str">"cmt">//--- Disable synchronization without the confirmation dialog class="type">bool CurrentSetConfirmationsDisableOFF(class="type">void){ class="kw">return this.CurrentSetConfirmationsDisableFlag(false); } class=class="str">"cmt">//--- Enable copying Stop Loss and Take Profit class="type">bool CurrentSetSLTPCopyON(class="type">void) { class="kw">return this.CurrentSetSLTPCopyFlag(true); } class=class="str">"cmt">//--- Disable copying Stop Loss and Take Profit class="type">bool CurrentSetSLTPCopyOFF(class="type">void) { class="kw">return this.CurrentSetSLTPCopyFlag(false); } class=class="str">"cmt">//--- Enable copying deals by subscription class="type">bool CurrentSetSubscriptionEnableON(class="type">void) { class="kw">return this.CurrentSetSubscriptionEnabledFlag(true); } class=class="str">"cmt">//--- Disable copying deals by subscription class="type">bool CurrentSetSubscriptionEnableOFF(class="type">void) { class="kw">return this.CurrentSetSubscriptionEnabledFlag(false); } class=class="str">"cmt">//--- Return the description of enabling working with signals for the launched program class="type">class="kw">string ProgramIsAllowedDescription(class="type">void); class=class="str">"cmt">//--- Return the percentage description for converting the deal volume class="type">class="kw">string CurrentEquityLimitDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the market order slippage used when synchronizing positions and copying deals
◍ 信号订阅参数的描述函数与集合构造
在封装信号订阅管理类时,一组 Current*Description 方法负责把各项限制条件转成可读字符串,方便在日志里排查订阅为何未生效。比如 CurrentSlippageDescription 对应滑点限制说明,CurrentVolumePercentDescription 返回资金占比限制描述,CurrentSLTPCopyFlagDescription 说明是否复制止损止盈,二者缺一则可能在跟单时只开仓不挂防护。 构造函数 CMQLSignalsCollection() 里先清空链表、排序并设定类型为 COLLECTION_MQL5_SIGNALS_ID,随后用 SignalBaseTotal() 取基础信号总数赋给 m_signals_base_total,最后调 CreateCollection() 填充。CreateCollection 内部再次 Clear 后以 Refresh(false) 拉取,若 m_list.Total()>0 就打印「集合创建成功」——这个 >0 的判断是验证信号是否真正加载进来的硬指标。 外汇与贵金属信号跟单存在复制延迟和滑点放大风险,实盘前应在策略测试器或模拟盘用 Print 输出 CurrentSubscriptionParameters 全量参数,确认订阅开关、保证金占比与 SLTP 标志符合预期再上真仓。
class="type">class="kw">string CurrentSlippageDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the limitation by funds for a signal class="type">class="kw">string CurrentVolumePercentDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the flag allowing synchronization without confirmation dialog class="type">class="kw">string CurrentConfirmationsDisableFlagDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the flag of copying Stop Loss and Take Profit class="type">class="kw">string CurrentSLTPCopyFlagDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the deposit limitations(in %) class="type">class="kw">string CurrentDepositPercentDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the flag allowing the copying of signals by subscription class="type">class="kw">string CurrentSubscriptionEnabledFlagDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the signal ID class="type">class="kw">string CurrentIDDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the flag of agreeing to the terms of use of the Signals service class="type">class="kw">string CurrentTermsAgreeFlagDescription(class="type">void); class=class="str">"cmt">//--- Return the description of the signal name class="type">class="kw">string CurrentNameDescription(class="type">void); class=class="str">"cmt">//--- Display the parameters of signal copying settings in the journal class="type">void CurrentSubscriptionParameters(class="type">void); class=class="str">"cmt">//--- }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Constructor | class=class="str">"cmt">//+------------------------------------------------------------------+ CMQLSignalsCollection::CMQLSignalsCollection() { this.m_list.Clear(); this.m_list.Sort(); this.m_list.Type(COLLECTION_MQL5_SIGNALS_ID); this.m_signals_base_total=::SignalBaseTotal(); this.CreateCollection(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Create the collection list of MQL5 signal objects | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMQLSignalsCollection::CreateCollection(class="type">void) { this.m_list.Clear(); this.Refresh(false); if(m_list.Total()>class="num">0) { ::Print(CMessage::Text(MSG_MQLSIG_COLLECTION_TEXT_MQL5_SIGNAL_COLLECTION)," ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_CREATED_OK));
「信号集合的刷新与按ID检索实现」
在 MT5 自研信号管理类里,Refresh 方法负责把信号数据库里的全部条目同步进内存集合。它先通过 SignalBaseTotal 拿到库内信号总数,再逐个用 SignalBaseSelect 按索引定位,避免手动维护列表导致的漏更或重复。
循环内每取到一个信号,就用 SignalBaseGetInteger(SIGNAL_BASE_ID) 读出唯一 ID 并 new 一个 CMQLSignal 对象;若列表已按 ID 排序且 Search 命中,说明重复,直接 delete 跳过。这一去重逻辑能保证集合里同一个 MQL5 信号只保留一份实例。
插入阶段调用 InsertSort 做有序写入,失败同样释放对象。只有成功加入且 messages=true 时,才用 Print 输出“新信号”提示并调用 signal.PrintShort(true) 打印摘要,方便在“小布盯盘”面板里实时看到新增信号源。
按 ID 取对象则由 GetMQLSignal 完成:它调用 GetList(SIGNAL_MQL5_PROP_ID, id, EQUAL) 拿到过滤后的子列表,命中则返回 list.At(0),否则返回 NULL。外汇与贵金属信号波动剧烈、杠杆风险高,这类集合在 EA 里只应作为参考,实盘前务必在策略测试器用真实点差回测。
class="type">void CMQLSignalsCollection::Refresh(const class="type">bool messages=true) { this.m_signals_base_total=::SignalBaseTotal(); class=class="str">"cmt">//--- loop through all signals in the signal database for(class="type">int i=class="num">0;i<this.m_signals_base_total;i++) { class=class="str">"cmt">//--- Select a signal from the signal database by the loop index if(!::SignalBaseSelect(i)) class="kw">continue; class=class="str">"cmt">//--- Get the current signal ID and class=class="str">"cmt">//--- create a new MQL5 signal object based on it class="type">long id=::SignalBaseGetInteger(SIGNAL_BASE_ID); CMQLSignal *signal=new CMQLSignal(id); if(signal==NULL) class="kw">continue; class=class="str">"cmt">//--- Set the sorting flag for the list by signal ID and, class=class="str">"cmt">//--- if such a signal is already present in the collection list, class=class="str">"cmt">//--- remove the created object and go to the next loop iteration m_list.Sort(SORT_BY_SIGNAL_MQL5_ID); if(this.m_list.Search(signal)!=WRONG_VALUE) { class="kw">delete signal; class="kw">continue; } class=class="str">"cmt">//--- If failed to add a new signal object to the collection list, class=class="str">"cmt">//--- remove the created object and go to the next loop iteration if(!this.m_list.InsertSort(signal)) { class="kw">delete signal; class="kw">continue; } class=class="str">"cmt">//--- If an MQL5 signal object is successfully added to the collection class=class="str">"cmt">//--- and the new object message flag is set in the parameters passed to the method, class=class="str">"cmt">//--- display a message about a newly found signal else if(messages) { ::Print(DFUN,CMessage::Text(MSG_MQLSIG_COLLECTION_TEXT_SIGNALS_NEW),":"); signal.PrintShort(true); } } } CMQLSignal *CMQLSignalsCollection::GetMQLSignal(const class="type">long id) { CArrayObj *list=GetList(SIGNAL_MQL5_PROP_ID,id,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); }
按名称抓取信号与日志分级打印
在信号集合类里,按名称取对象是高频操作。GetMQLSignal 先调用 GetList 以 SIGNAL_MQL5_PROP_NAME 做等于匹配,命中后直接取列表第 0 个元素,未命中返回 NULL,调用方需自行判空。 Print 方法负责完整描述输出:遍历 m_list 全部成员,对每一个非空 CMQLSignal 指针调用其 Print(),集合多大就输出多少行,适合调试时全量核对。 PrintShort 提供更可控的短描述输出。它用 list 参数决定模式:为 true 时按 paid/free 开关逐条过滤(Price()>0 且关闭付费、或 Price()==0 且关闭免费则跳过);为 false 时先按价格排序,再用 GetList 以价格 0 等值匹配抽免费信号数,num_free 即为免费信号条数,调用方可以此做占比统计。 外汇与贵金属信号订阅涉及杠杆与跟单风险,免费信号不代表低回撤,实盘前应在 MT5 策略测试器或用 PrintShort 抽样核对历史净值曲线。
class=class="str">"cmt">//| Return the pointer to an MQL5 signal object by a name | class=class="str">"cmt">//+------------------------------------------------------------------+ CMQLSignal *CMQLSignalsCollection::GetMQLSignal(const class="type">class="kw">string name) { CArrayObj *list=GetList(SIGNAL_MQL5_PROP_NAME,name,EQUAL); class="kw">return(list!=NULL ? list.At(class="num">0) : NULL); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display complete collection description to the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMQLSignalsCollection::Print(class="type">void) { ::Print(CMessage::Text(MSG_MQLSIG_COLLECTION_TEXT_MQL5_SIGNAL_COLLECTION),":"); for(class="type">int i=class="num">0;i<this.m_list.Total();i++) { CMQLSignal *signal=this.m_list.At(i); if(signal==NULL) class="kw">continue; signal.Print(); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Display the class="type">class="kw">short collection description in the journal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CMQLSignalsCollection::PrintShort(const class="type">bool list=false,const class="type">bool paid=true,const class="type">bool free=true) { class=class="str">"cmt">//--- Display the header in the journal ::Print(CMessage::Text(MSG_MQLSIG_COLLECTION_TEXT_MQL5_SIGNAL_COLLECTION),":"); class=class="str">"cmt">//--- If the list is full, display class="type">class="kw">short descriptions of all signals in the collection class=class="str">"cmt">//--- according to the flags indicating the necessity to display paid and free signals if(list) for(class="type">int i=class="num">0;i<this.m_list.Total();i++) { CMQLSignal *signal=this.m_list.At(i); if(signal==NULL || (signal.Price()>class="num">0 && !paid) || (signal.Price()==class="num">0 && !free)) class="kw">continue; signal.PrintShort(true); } class=class="str">"cmt">//--- If not the signal list else { class=class="str">"cmt">//--- Sort the list by signal price this.m_list.Sort(SORT_BY_SIGNAL_MQL5_PRICE); class=class="str">"cmt">//--- Get the list of free signals and their number CArrayObj *list_free=this.GetList(SIGNAL_MQL5_PROP_PRICE,class="num">0,EQUAL); class="type">int num_free=(list_free==NULL ? class="num">0 : list_free.Total()); class=class="str">"cmt">//--- Sort the list by signal price this.m_list.Sort(SORT_BY_SIGNAL_MQL5_PRICE);
◍ 信号订阅与退订的底层调用
在 MQL5 里做信号集合管理,订阅动作最终落到 SignalSubscribe(signal_id) 这个系统函数。封装层 CMQLSignalsCollection::Subscribe 先检查程序是否被允许操作信号,若 ProgramIsAllowed() 返回 false 就直接打日志并返回 false,不会碰网络请求。
订阅前用 ResetLastError() 清掉旧错误码,调用失败则通过 GetLastError() 取具体原因并写日志,成功则在终端打印已订阅信号的 ID 与名称。这里没有收益承诺,只是把『允许性校验 → 清错 → 调系统 API → 报错或回显』这条链路坐实。
退订走 CurrentUnsubscribe(),逻辑相似但多一步:先记下调出 CurrentID() 和 CurrentName(),若 ID 为 0 说明本就没订阅,直接返回 true 避免无效解订。外汇与贵金属信号跟单存在滑点、断连与策略失效等高风险,订阅前应在 MT5 信号选项卡核对历史权益曲线。
下面这段是原文里订阅与退订两个方法的主体,可直接拷进 MT5 的包含类里验证编译:
class="type">bool CMQLSignalsCollection::Subscribe(const class="type">long signal_id) { if(!this.ProgramIsAllowed()) { ::Print(DFUN,CMessage::Text(MSG_SIGNAL_INFO_ERR_SIGNAL_NOT_ALLOWED)); ::Print(DFUN,CMessage::Text(MSG_SIGNAL_INFO_TEXT_CHECK_SETTINGS)); class="kw">return false; } ::ResetLastError(); if(!::SignalSubscribe(signal_id)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } ::Print(CMessage::Text(MSG_SIGNAL_INFO_TEXT_SIGNAL_SUBSCRIBED)," ID ",(class="type">class="kw">string)this.CurrentID()," \"",CurrentName(),"\""); class="kw">return true; } class="type">bool CMQLSignalsCollection::CurrentUnsubscribe(class="type">void) { if(!this.ProgramIsAllowed()) { ::Print(DFUN,CMessage::Text(MSG_SIGNAL_INFO_ERR_SIGNAL_NOT_ALLOWED)); ::Print(DFUN,CMessage::Text(MSG_SIGNAL_INFO_TEXT_CHECK_SETTINGS)); class="kw">return false; } ::ResetLastError(); class="type">long id=this.CurrentID(); class="type">class="kw">string name=this.CurrentName(); if(id==class="num">0) class="kw">return true; if(!::SignalUnsubscribe()) { CMessage::ToLog(DFUN,::GetLastError(),true);
「信号订阅与同步参数的代码落点」
这段 MT5 信号管理类的实现,把「按 ID / 按名称订阅」和「同步时风控参数写入」拆成了独立方法。先看订阅部分:无论走 SubscribeByID 还是 SubscribeByName,都先通过 GetMQLSignal 拿到信号对象指针,拿不到就直接 Print 报错并返回 false,拿到后才转调 this.Subscribe(signal.ID())。 也就是说,名称订阅本质还是落回 ID 订阅,外部调用层可以不用关心信号在服务器里的数字编号,只要名字对得上就能跟单。 风控参数两块值得盯:CurrentSetEquityLimit 写的是 SIGNAL_INFO_EQUITY_LIMIT,控制本金占用比例;CurrentSetSlippage 写的是 SIGNAL_INFO_SLIPPAGE,决定跟单市价单允许的滑点。两个方法都先 ResetLastError,再调 SignalInfoSetDouble,失败就记日志返 false。 外汇与贵金属信号跟单自带高杠杆与滑点放大风险,这两个值设歪了,复制过来的仓位可能比信号源更激进。开 MT5 把下面代码贴进你的信号类,改 name 或 value 就能现场验证订阅与限额是否生效。
::Print(CMessage::Text(MSG_SIGNAL_INFO_TEXT_SIGNAL_UNSUBSCRIBED)," ID ",(class="type">class="kw">string)id," \"",name,"\""); class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Subscribe to a signal by ID | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMQLSignalsCollection::SubscribeByID(const class="type">long signal_id) { CMQLSignal *signal=GetMQLSignal(signal_id); if(signal==NULL) { ::Print(DFUN,CMessage::Text(MSG_MQLSIG_COLLECTION_ERR_FAILED_GET_SIGNAL),": ",signal_id); class="kw">return false; } class="kw">return this.Subscribe(signal.ID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Subscribe to a signal by signal name | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMQLSignalsCollection::SubscribeByName(const class="type">class="kw">string signal_name) { CMQLSignal *signal=GetMQLSignal(signal_name); if(signal==NULL) { ::Print(DFUN,CMessage::Text(MSG_MQLSIG_COLLECTION_ERR_FAILED_GET_SIGNAL),": \"",signal_name,"\""); class="kw">return false; } class="kw">return this.Subscribe(signal.ID()); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Set the percentage for converting a deal volume | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMQLSignalsCollection::CurrentSetEquityLimit(const class="type">class="kw">double value) { ::ResetLastError(); if(!::SignalInfoSetDouble(SIGNAL_INFO_EQUITY_LIMIT,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Define the slippage used to set | class=class="str">"cmt">//| market orders when synchronizing positions and copying deals | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool CMQLSignalsCollection::CurrentSetSlippage(const class="type">class="kw">double value) { ::ResetLastError(); if(!::SignalInfoSetDouble(SIGNAL_INFO_SLIPPAGE,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class=class="str">"cmt">//+------------------------------------------------------------------+
信号账户参数的静默配置入口
在 MT5 信号订阅机制的底层封装里,有一组 CurrentSet* 方法专门负责改写当前程序的信号配置标志,调用方无需弹出确认对话框即可生效。 下面四个函数都走同一套路:先 ResetLastError 清错误码,再调 SignalInfoSetInteger 写入对应宏,失败就记日志并返回 false,成功返回 true。 CurrentSetConfirmationsDisableFlag 控制是否跳过同步确认框;CurrentSetSLTPCopyFlag 决定跟单时是否复制信号源的止损止盈;CurrentSetDepositPercent 接收 int 类型的入参,设置跟单占用保证金上限百分比;CurrentSetSubscriptionEnabledFlag 则开关「允许按订阅跟信号」的总闸。 ProgramIsAllowedDescription 不走写入逻辑,它拼一段人类可读文本,用 ProgramIsAllowed() 的布尔结果映射成「是 / 否」字样,方便在日志里一眼看出当前 EA 是否被放行做信号操作。外汇与贵金属信号跟单存在滑点、断线及策略失效等高风险,参数改动后应在策略测试器或模拟账户先验证。 让小布替你跑这套 把 CurrentSetDepositPercent(10) 丢进 EA 初始化里,再故意传个超范围值(如 200),看日志是否打出 false,能快速确认你的终端信号 API 权限没被 broker 阉割。
class="type">bool CMQLSignalsCollection::CurrentSetConfirmationsDisableFlag(const class="type">bool flag) { ::ResetLastError(); if(!::SignalInfoSetInteger(SIGNAL_INFO_CONFIRMATIONS_DISABLED,flag)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class="type">bool CMQLSignalsCollection::CurrentSetSLTPCopyFlag(const class="type">bool flag) { ::ResetLastError(); if(!::SignalInfoSetInteger(SIGNAL_INFO_COPY_SLTP,flag)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class="type">bool CMQLSignalsCollection::CurrentSetDepositPercent(const class="type">int value) { ::ResetLastError(); if(!::SignalInfoSetInteger(SIGNAL_INFO_DEPOSIT_PERCENT,value)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class="type">bool CMQLSignalsCollection::CurrentSetSubscriptionEnabledFlag(const class="type">bool flag) { ::ResetLastError(); if(!::SignalInfoSetInteger(SIGNAL_INFO_SUBSCRIPTION_ENABLED,flag)) { CMessage::ToLog(DFUN,::GetLastError(),true); class="kw">return false; } class="kw">return true; } class="type">class="kw">string CMQLSignalsCollection::ProgramIsAllowedDescription(class="type">void) { class="kw">return ( CMessage::Text(MSG_SIGNAL_INFO_SIGNALS_PERMISSION)+": "+ (this.ProgramIsAllowed() ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO)) ); }
◍ 信号订阅参数的描述方法实现
在 MT5 信号订阅模块里,CMQLSignalsCollection 类提供了一组 Current*Description 方法,把内部数值和开关状态拼成可读字符串,供界面或日志直接输出。 看 CurrentEquityLimitDescription,它取当前净值限制比例,用 DoubleToString(val,2) 保留两位小数再拼上 '%'。也就是说如果你在跟单设置里限制了 30.00% 净值,调用后得到的文本就是 'Equity limit: 30.00%' 这类格式。 CurrentSlippageDescription 的拼法略有不同:滑点不是直接出百分比,而是 '点差 * N.NN' 的写法,N.NN 同样保留两位小数,这提醒你滑点限额是相对点差的倍数而非绝对点数。 开关类如 CurrentConfirmationsDisableFlagDescription、CurrentSLTPCopyFlagDescription、CurrentSubscriptionEnabledFlagDescription 都走三元判断,把布尔值映射成 'Yes/No' 文本;而 CurrentDepositPercentDescription 直接把整型存款限制转成 string 加 '%',没有走 DoubleToString,若后台返回非整会丢精度。 把这些描述方法接进你自己的信号监控 EA,能在每笔跟单前打印一条完整配置摘要,方便排查为什么某次没跟单——可能是 ConfirmationsDisabled 为 No 弹了确认框,或 DepositPercent 已触顶。外汇与贵金属信号跟单本身属高风险,参数误读可能放大回撤。
class="type">class="kw">string CMQLSignalsCollection::CurrentEquityLimitDescription(class="type">void) { class="kw">return CMessage::Text(MSG_SIGNAL_INFO_EQUITY_LIMIT)+": "+::DoubleToString(this.CurrentEquityLimit(),class="num">2)+"%"; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the slippage used to set | class=class="str">"cmt">//| market orders when synchronizing positions and copying deals | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CMQLSignalsCollection::CurrentSlippageDescription(class="type">void) { class="kw">return CMessage::Text(MSG_SIGNAL_INFO_SLIPPAGE)+": "+CMessage::Text(MSG_LIB_TEXT_BAR_SPREAD)+" * "+::DoubleToString(this.CurrentSlippage(),class="num">2); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the limitation by funds for a signal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CMQLSignalsCollection::CurrentVolumePercentDescription(class="type">void) { class="kw">return CMessage::Text(MSG_SIGNAL_INFO_VOLUME_PERCENT)+": "+::DoubleToString(this.CurrentVolumePercent(),class="num">2)+"%"; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the flag enabling synchronization | class=class="str">"cmt">//| without confirmation dialog | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CMQLSignalsCollection::CurrentConfirmationsDisableFlagDescription(class="type">void) { class="kw">return ( CMessage::Text(MSG_SIGNAL_INFO_CONFIRMATIONS_DISABLED)+": "+ (this.CurrentConfirmationsDisableFlag() ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO)) ); } class=class="str">"cmt">//+----------------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the flag of copying Stop Loss and Take Profit | class=class="str">"cmt">//+----------------------------------------------------------------------------+ class="type">class="kw">string CMQLSignalsCollection::CurrentSLTPCopyFlagDescription(class="type">void) { class="kw">return ( CMessage::Text(MSG_SIGNAL_INFO_COPY_SLTP)+": "+ (this.CurrentSLTPCopyFlag() ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO)) ); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the deposit limitations(in %) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CMQLSignalsCollection::CurrentDepositPercentDescription(class="type">void) { class="kw">return CMessage::Text(MSG_SIGNAL_INFO_DEPOSIT_PERCENT)+": "+(class="type">class="kw">string)this.CurrentDepositPercent()+"%"; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the description of the flag enabling | class=class="str">"cmt">//| deal copying by subscription | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string CMQLSignalsCollection::CurrentSubscriptionEnabledFlagDescription(class="type">void) { class="kw">return ( CMessage::Text(MSG_SIGNAL_INFO_SUBSCRIPTION_ENABLED)+": "+ (this.CurrentSubscriptionEnabledFlag() ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO)) ); } class=class="str">"cmt">//+------------------------------------------------------------------+
「把信号订阅参数打到日志里」
做信号跟单类 EA 时,最怕运行时搞不清当前到底加载了哪条信号、跟单比例和滑点怎么设的。MQL5 里 CMQLSignalsCollection 这个类提供了一组 Description 方法,把关键字段拼成可读字符串,再统一用 Print 吐到专家日志。
下面这段是核心输出函数 CurrentSubscriptionParameters,它先打分隔条,再逐行输出是否允许程序化交易、是否同意信号服务条款、订阅开关、确认弹窗禁用标志、SL/TP 拷贝标志、滑点、权益上限、入金百分比、手数百分比、信号 ID 与名称,最后空一行。你在 MT5 按 F4 编译挂上 EA,开专家日志就能看到这套参数清单,便于排查「跟单没生效」类问题。
几个 Description 方法的写法也值得抄:比如 CurrentIDDescription 里用三元运算符判断 CurrentID()>0,非正就回 MSG_LIB_PROP_EMPTY 占位文本;CurrentTermsAgreeFlagDescription 把布尔标志转成「是/否」文案。这种防御式拼串能避免日志出现空指针式的空白。
外汇与贵金属信号跟单存在杠杆放大与滑点漂移的高风险,日志里的百分比参数仅反映设置值,不预示任何跟单收益。
class="type">class="kw">string CMQLSignalsCollection::CurrentIDDescription(class="type">void) { class="kw">return CMessage::Text(MSG_SIGNAL_INFO_ID)+": "+(this.CurrentID()>class="num">0 ? (class="type">class="kw">string)this.CurrentID() : CMessage::Text(MSG_LIB_PROP_EMPTY)); } class="type">class="kw">string CMQLSignalsCollection::CurrentTermsAgreeFlagDescription(class="type">void) { class="kw">return ( CMessage::Text(MSG_SIGNAL_INFO_TERMS_AGREE)+": "+ (this.CurrentTermsAgreeFlag() ? CMessage::Text(MSG_LIB_TEXT_YES) : CMessage::Text(MSG_LIB_TEXT_NO)) ); } class="type">class="kw">string CMQLSignalsCollection::CurrentNameDescription(class="type">void) { class="kw">return CMessage::Text(MSG_SIGNAL_INFO_NAME)+": "+(this.CurrentName()!="" ? this.CurrentName() : CMessage::Text(MSG_LIB_PROP_EMPTY)); } class="type">void CMQLSignalsCollection::CurrentSubscriptionParameters(class="type">void) { ::Print("============= ",CMessage::Text(MSG_SIGNAL_INFO_PARAMETERS)," ============="); ::Print(this.ProgramIsAllowedDescription()); ::Print(this.CurrentTermsAgreeFlagDescription()); ::Print(this.CurrentSubscriptionEnabledFlagDescription()); ::Print(this.CurrentConfirmationsDisableFlagDescription()); ::Print(this.CurrentSLTPCopyFlagDescription()); ::Print(this.CurrentSlippageDescription()); ::Print(this.CurrentEquityLimitDescription()); ::Print(this.CurrentDepositPercentDescription()); ::Print(this.CurrentVolumePercentDescription()); ::Print(this.CurrentIDDescription()); ::Print(this.CurrentNameDescription()); ::Print(""); }
引擎基类的集合装配
一个完整的 MT5 交易引擎通常不是把所有逻辑写在一个文件里,而是用基类把各类数据集合拆开管理。下面这段声明展示了 CEngine 类在私有区挂了 12 个集合对象,覆盖历史、市场、事件、账户、品种、时序、缓冲、指标、 tick、深度盘口与信号服务。
其中 m_signals_mql5 这一行(背景高亮)专门对接 MQL5.com 信号服务的信号集合,意味着引擎有能力把跟单信号纳入统一调度,而不是只盯本地策略。外汇与贵金属市场波动剧烈,接入外部信号前务必在策略测试器里验证延迟与滑点表现。
代码里的 #include 路径也值得注意:Services 下放计时器,Collections 下按数据类型分了 11 个头文件。这种结构让你可以单独改 HistoryCollection.mqh 而不动行情相关代码,降低出 bug 概率。开 MT5 新建 EA 时,照这个目录层级搭框架能省掉大量重复劳动。
class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Include files | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#include "Services\TimerCounter.mqh" class="macro">#include "Collections\HistoryCollection.mqh" class="macro">#include "Collections\MarketCollection.mqh" class="macro">#include "Collections\EventsCollection.mqh" class="macro">#include "Collections\AccountsCollection.mqh" class="macro">#include "Collections\SymbolsCollection.mqh" class="macro">#include "Collections\ResourceCollection.mqh" class="macro">#include "Collections\TimeSeriesCollection.mqh" class="macro">#include "Collections\BuffersCollection.mqh" class="macro">#include "Collections\IndicatorsCollection.mqh" class="macro">#include "Collections\TickSeriesCollection.mqh" class="macro">#include "Collections\BookSeriesCollection.mqh" class="macro">#include "Collections\MQLSignalsCollection.mqh" class="macro">#include "TradingControl.mqh" class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Library basis class | class=class="str">"cmt">//+------------------------------------------------------------------+ class CEngine { class="kw">private: CHistoryCollection m_history; class=class="str">"cmt">// Collection of historical orders and deals CMarketCollection m_market; class=class="str">"cmt">// Collection of market orders and deals CEventsCollection m_events; class=class="str">"cmt">// Event collection CAccountsCollection m_accounts; class=class="str">"cmt">// Account collection CSymbolsCollection m_symbols; class=class="str">"cmt">// Symbol collection CTimeSeriesCollection m_time_series; class=class="str">"cmt">// Timeseries collection CBuffersCollection m_buffers; class=class="str">"cmt">// Collection of indicator buffers CIndicatorsCollection m_indicators; class=class="str">"cmt">// Indicator collection CTickSeriesCollection m_tick_series; class=class="str">"cmt">// Collection of tick series CMBookSeriesCollection m_book_series; class=class="str">"cmt">// Collection of DOM series CMQLSignalsCollection m_signals_mql5; class=class="str">"cmt">// Collection of MQL5.com Signals service signals CResourceCollection m_resource; class=class="str">"cmt">// Resource list
◍ 从深度缓存里抠买卖盘口量
盘口快照对象在 MQL5 里通常以类成员方式挂到主逻辑上:交易控制、暂停控制、定时器计数器列表各占一个引用,盘口序列则单独由 m_book_series 托管。 刷新指定品种的盘口序列只需一行:把 symbol 和毫秒时间丢进 Refresh,后续所有读取都基于这次更新后的快照。注意 MT5 的 DOM 推送频率依赖券商,黄金 XAUUSD 在流动性好的时段可能每 50~200 毫秒推一次,稀烂时段可能几秒才动。 读取接口分两条路:按索引取历史快照,或按毫秒时间戳取精确一刻。下面这组声明直接暴露了买卖盘口量的四种取法。
CTradingControl m_trading; class=class="str">"cmt">// Trading management object CPause m_pause; class=class="str">"cmt">// Pause object CArrayObj m_list_counters; class=class="str">"cmt">// List of timer counters class=class="str">"cmt">//--- Update the DOM series of a specified symbol class="type">void MBookSeriesRefresh(const class="type">class="kw">string symbol,const class="type">long time_msc) { this.m_book_series.Refresh(symbol,time_msc); } class=class="str">"cmt">//--- Return(class="num">1) the DOM series of a specified symbol, the DOM(class="num">2) by index and(class="num">3) by time in milliseconds CMBookSeries *GetMBookSeries(const class="type">class="kw">string symbol) { class="kw">return this.m_book_series.GetMBookseries(symbol); } CMBookSnapshot *GetMBook(const class="type">class="kw">string symbol,const class="type">int index) { class="kw">return this.m_book_series.GetMBook(symbol,index); } CMBookSnapshot *GetMBook(const class="type">class="kw">string symbol,const class="type">long time_msc) { class="kw">return this.m_book_series.GetMBook(symbol,time_msc);} class=class="str">"cmt">//--- Return the volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and index class="type">long MBookVolumeBuy(const class="type">class="kw">string symbol,const class="type">int index); class="type">long MBookVolumeSell(const class="type">class="kw">string symbol,const class="type">int index); class=class="str">"cmt">//--- Return the increased precision volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and index class="type">class="kw">double MBookVolumeBuyReal(const class="type">class="kw">string symbol,const class="type">int index); class="type">class="kw">double MBookVolumeSellReal(const class="type">class="kw">string symbol,const class="type">int index); class=class="str">"cmt">//--- Return the volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and time in milliseconds class="type">long MBookVolumeBuy(const class="type">class="kw">string symbol,const class="type">long time_msc); class="type">long MBookVolumeSell(const class="type">class="kw">string symbol,const class="type">long time_msc); class=class="str">"cmt">//--- Return the increased precision volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and time in milliseconds class="type">class="kw">double MBookVolumeBuyReal(const class="type">class="kw">string symbol,const class="type">long time_msc); class="type">class="kw">double MBookVolumeSellReal(const class="type">class="kw">string symbol,const class="type">long time_msc);
CTradingControl m_trading; class=class="str">"cmt">// Trading management object CPause m_pause; class=class="str">"cmt">// Pause object CArrayObj m_list_counters; class=class="str">"cmt">// List of timer counters class=class="str">"cmt">//--- Update the DOM series of a specified symbol class="type">void MBookSeriesRefresh(const class="type">class="kw">string symbol,const class="type">long time_msc) { this.m_book_series.Refresh(symbol,time_msc); } class=class="str">"cmt">//--- Return(class="num">1) the DOM series of a specified symbol, the DOM(class="num">2) by index and(class="num">3) by time in milliseconds CMBookSeries *GetMBookSeries(const class="type">class="kw">string symbol) { class="kw">return this.m_book_series.GetMBookseries(symbol); } CMBookSnapshot *GetMBook(const class="type">class="kw">string symbol,const class="type">int index) { class="kw">return this.m_book_series.GetMBook(symbol,index); } CMBookSnapshot *GetMBook(const class="type">class="kw">string symbol,const class="type">long time_msc) { class="kw">return this.m_book_series.GetMBook(symbol,time_msc);} class=class="str">"cmt">//--- Return the volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and index class="type">long MBookVolumeBuy(const class="type">class="kw">string symbol,const class="type">int index); class="type">long MBookVolumeSell(const class="type">class="kw">string symbol,const class="type">int index); class=class="str">"cmt">//--- Return the increased precision volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and index class="type">class="kw">double MBookVolumeBuyReal(const class="type">class="kw">string symbol,const class="type">int index); class="type">class="kw">double MBookVolumeSellReal(const class="type">class="kw">string symbol,const class="type">int index); class=class="str">"cmt">//--- Return the volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and time in milliseconds class="type">long MBookVolumeBuy(const class="type">class="kw">string symbol,const class="type">long time_msc); class="type">long MBookVolumeSell(const class="type">class="kw">string symbol,const class="type">long time_msc); class=class="str">"cmt">//--- Return the increased precision volume of a(class="num">1) buy and(class="num">2) sell DOM specified by symbol and time in milliseconds class="type">class="kw">double MBookVolumeBuyReal(const class="type">class="kw">string symbol,const class="type">long time_msc); class="type">class="kw">double MBookVolumeSellReal(const class="type">class="kw">string symbol,const class="type">long time_msc);
「信号集合的取数接口与订阅动作」
在自定义封装类里,对外暴露信号集合的入口通常就靠两个 getter:一个回传 CMQLSignalsCollection 指针本身,另一个直接吐出内部的 CArrayObj 信号列表。前者适合要继续调集合级方法,后者方便你拿去遍历做筛选。
付费与免费信号的拆分用的是同一个 GetList 重载,区别只在比较符。传入 SIGNAL_MQL5_PROP_PRICE 配 MORE 拿到的就是价格大于 0 的付费信号;配 EQUAL 且基准值 0,筛出来的是免费信号。
建集合和刷数据是两个独立动作:CreateCollection() 负责首次拉取并构建,Refresh() 只做增量更新,不重建结构。实盘跑 EA 时建议分开调用,避免每次都全量请求拖慢 tick 处理。
订阅支持按 ID 或名称双通道,退订则只退「当前」那一个。要确认自己挂在哪条信号上,用 CurrentID() 取长整型 ID 即可,返 0 一般代表没订阅。外汇与贵金属信号跟随存在滑点跟丢的高风险,订阅前应在策略测试器里先验证历史同步表现。
class=class="str">"cmt">//--- Return(class="num">1) the collection of mql5.com Signals service signals and(class="num">2) the list of signals from the mql5.com Signals service signal collection CMQLSignalsCollection *GetSignalsMQL5Collection(class="type">void) { class="kw">return &this.m_signals_mql5; } CArrayObj *GetListSignalsMQL5(class="type">void) { class="kw">return this.m_signals_mql5.GetList(); } class=class="str">"cmt">//--- Return the list of(class="num">1) paid and(class="num">2) free signals CArrayObj *GetListSignalsMQL5Paid(class="type">void) { class="kw">return this.m_signals_mql5.GetList(SIGNAL_MQL5_PROP_PRICE,class="num">0,MORE); } CArrayObj *GetListSignalsMQL5Free(class="type">void) { class="kw">return this.m_signals_mql5.GetList(SIGNAL_MQL5_PROP_PRICE,class="num">0,EQUAL);} class=class="str">"cmt">//--- (class="num">1) Create and(class="num">2) update the collection of mql5.com Signals service signals class="type">bool SignalsMQL5Create(class="type">void) { class="kw">return this.m_signals_mql5.CreateCollection(); } class="type">void SignalsMQL5Refresh(class="type">void) { this.m_signals_mql5.Refresh(); } class=class="str">"cmt">//--- Subscribe to a signal by(class="num">1) ID and(class="num">2) signal name class="type">bool SignalsMQL5Subscribe(const class="type">long signal_id) { class="kw">return this.m_signals_mql5.SubscribeByID(signal_id);} class="type">bool SignalsMQL5Subscribe(const class="type">class="kw">string signal_name) { class="kw">return this.m_signals_mql5.SubscribeByName(signal_name);} class=class="str">"cmt">//--- Unsubscribe from the current signal class="type">bool SignalsMQL5Unsubscribe(class="type">void) { class="kw">return this.m_signals_mql5.CurrentUnsubscribe(); } class=class="str">"cmt">//--- Return(class="num">1) ID and(class="num">2) the name of the current signal subscription is performed to class="type">long SignalsMQL5CurrentID(class="type">void) { class="kw">return this.m_signals_mql5.CurrentID(); }
信号源当前配置的接口封装
在跟单类 EA 里,对当前选中信号源的参数调整通常走一套 wrapper 方法,把底层 CSignalsMQL5 实例的成员函数再包一层,方便在主逻辑里统一调用。下面这段代码就列出了从读取名称到开关订阅、复制 SL/TP 的全部当前项 setter。 值得注意的是,EquityLimit 与 DepositPercent 这两个接口分别接收 double 与 int 类型,前者是按净值比例换算手数的上限(例如 0.1 代表 10%),后者是入金百分比限制(int 型,如 50 即 50%)。外汇与贵金属跟单自带高杠杆风险,盲目把 DepositPercent 拉到 100 可能让账户净值回撤超出承受范围。 其中 ConfirmationsDisable 系列用于绕过同步确认弹窗,实盘自动化时若关掉对话框,需自己确保信号源可信,否则错单会直接成交。SignalsMQL5Print() 则把完整配置、简版说明和复制参数打印到日志,调试时跑一次就能核对所有当前值。
class="type">class="kw">string SignalsMQL5CurrentName(class="type">void) { class="kw">return this.m_signals_mql5.CurrentName(); } class=class="str">"cmt">//--- Set the percentage for converting deal volume class="type">bool SignalsMQL5CurrentSetEquityLimit(const class="type">class="kw">double value) { class="kw">return this.m_signals_mql5.CurrentSetEquityLimit(value); } class=class="str">"cmt">//--- Set the market order slippage used when synchronizing positions and copying deals class="type">bool SignalsMQL5CurrentSetSlippage(const class="type">class="kw">double value) { class="kw">return this.m_signals_mql5.CurrentSetSlippage(value); } class=class="str">"cmt">//--- Set deposit limitations(in %) class="type">bool SignalsMQL5CurrentSetDepositPercent(const class="type">int value) { class="kw">return this.m_signals_mql5.CurrentSetDepositPercent(value); } class=class="str">"cmt">//--- Enable synchronization without the confirmation dialog class="type">bool SignalsMQL5CurrentSetConfirmationsDisableON(class="type">void) { class="kw">return this.m_signals_mql5.CurrentSetConfirmationsDisableON();} class=class="str">"cmt">//--- Disable synchronization without the confirmation dialog class="type">bool SignalsMQL5CurrentSetConfirmationsDisableOFF(class="type">void) { class="kw">return this.m_signals_mql5.CurrentSetConfirmationsDisableOFF();} class=class="str">"cmt">//--- Enable copying Stop Loss and Take Profit class="type">bool SignalsMQL5CurrentSetSLTPCopyON(class="type">void) { class="kw">return this.m_signals_mql5.CurrentSetSLTPCopyON(); } class=class="str">"cmt">//--- Disable copying Stop Loss and Take Profit class="type">bool SignalsMQL5CurrentSetSLTPCopyOFF(class="type">void) { class="kw">return this.m_signals_mql5.CurrentSetSLTPCopyOFF(); } class=class="str">"cmt">//--- Enable copying deals by subscription class="type">bool SignalsMQL5CurrentSetSubscriptionEnableON(class="type">void) { class="kw">return this.m_signals_mql5.CurrentSetSubscriptionEnableON(); } class=class="str">"cmt">//--- Disable copying deals by subscription class="type">bool SignalsMQL5CurrentSetSubscriptionEnableOFF(class="type">void) { class="kw">return this.m_signals_mql5.CurrentSetSubscriptionEnableOFF();} class=class="str">"cmt">//--- Display(class="num">1) the complete, (class="num">2) class="type">class="kw">short collection description in the journal and(class="num">3) parameters of the signal copying settings class="type">void SignalsMQL5Print(class="type">void) { m_signals_mql5.Print(); }
◍ 从深度挂单里抠出买卖量
做价格行为的人常看 DOM(深度挂单),但 MT5 标准接口拿到的买/卖量精度有限。下面这组 CEngine 方法把指定品种、指定索引的快照买量、卖量直接吐出来,空指针时返回 0,调用前不用自己判空。 MBookVolumeBuy 与 MBookVolumeSell 返回 long 类型整数手数;MBookVolumeBuyReal / MBookVolumeSellReal 则返回 double,对应扩展精度下的真实成交量。若你的券商提供扩展 DOM 精度,后两个值可能比前两个更细,价差品种上常差出 1~2 个小数位。
…
class="type">void SignalsMQL5PrintShort(const class="type">bool list=false,const class="type">bool paid=true,const class="type">bool free=true) { m_signals_mql5.PrintShort(list,paid,free); } class="type">void SignalsMQL5CurrentSubscriptionParameters(class="type">void) { this.m_signals_mql5.CurrentSubscriptionParameters();} class=class="str">"cmt">//--- Return(class="num">1) the buffer collection and(class="num">2) the buffer list from the collection class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the buy volume of a DOM | class=class="str">"cmt">//| specified by symbol and index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long CEngine::MBookVolumeBuy(const class="type">class="kw">string symbol,const class="type">int index) { CMBookSnapshot *mbook=this.GetMBook(symbol,index); class="kw">return(mbook!=NULL ? mbook.VolumeBuy() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the sell volume of a DOM | class=class="str">"cmt">//| specified by symbol and index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long CEngine::MBookVolumeSell(const class="type">class="kw">string symbol,const class="type">int index) { CMBookSnapshot *mbook=this.GetMBook(symbol,index); class="kw">return(mbook!=NULL ? mbook.VolumeSell() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the extended accuracy buy volume | class=class="str">"cmt">//| of a DOM specified by symbol and index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CEngine::MBookVolumeBuyReal(const class="type">class="kw">string symbol,const class="type">int index) { CMBookSnapshot *mbook=this.GetMBook(symbol,index); class="kw">return(mbook!=NULL ? mbook.VolumeBuyReal() : class="num">0); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Return the extended accuracy sell volume | class=class="str">"cmt">//| of a DOM specified by symbol and index | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CEngine::MBookVolumeSellReal(const class="type">class="kw">string symbol,const class="type">int index) { CMBookSnapshot *mbook=this.GetMBook(symbol,index); class="kw">return(mbook!=NULL ? mbook.VolumeSellReal() : class="num">0); }
「从深度快照里抠出买卖盘真实挂量」
做盘口分析时,光看最新一笔成交不够,得把某毫秒快照里的买卖挂单量拽出来。下面这组 CEngine 方法就是干这个的:给符号和时间戳,返回对应 DOM 快照的买/卖量,取不到就回 0。 买量用 long 型 MBookVolumeBuy 取整数手数,卖量对称用 MBookVolumeSell。若经纪商开了扩展精度(比如部分贵金属品种报价精确到 0.001 手),就得换 MBookVolumeBuyReal / MBookVolumeSellReal,返回 double,避免取整把薄盘信号吃掉。 四个函数都先调 this.GetMBook(symbol,time_msc) 拿指针,空指针直接返 0——这意味着你传的时间戳若不在缓存窗口,量必然为 0,不是真没单。实盘中外汇/贵金属盘口瞬变,高频快照易失效,用前先确认 GetMBook 缓存深度,否则统计出的买卖失衡可能倾向失真。
class="type">long CEngine::MBookVolumeBuy(const class="type">class="kw">string symbol,const class="type">long time_msc) { CMBookSnapshot *mbook=this.GetMBook(symbol,time_msc); class="kw">return(mbook!=NULL ? mbook.VolumeBuy() : class="num">0); } class="type">long CEngine::MBookVolumeSell(const class="type">class="kw">string symbol,const class="type">long time_msc) { CMBookSnapshot *mbook=this.GetMBook(symbol,time_msc); class="kw">return(mbook!=NULL ? mbook.VolumeSell() : class="num">0); } class="type">class="kw">double CEngine::MBookVolumeBuyReal(const class="type">class="kw">string symbol,const class="type">long time_msc) { CMBookSnapshot *mbook=this.GetMBook(symbol,time_msc); class="kw">return(mbook!=NULL ? mbook.VolumeBuyReal() : class="num">0); } class="type">class="kw">double CEngine::MBookVolumeSellReal(const class="type">class="kw">string symbol,const class="type">long time_msc) { CMBookSnapshot *mbook=this.GetMBook(symbol,time_msc); class="kw">return(mbook!=NULL ? mbook.VolumeSellReal() : class="num">0); }
在 OnTick 里跑通信号订阅回测
把上一篇文章里的 EA 放到 \MQL5\Experts\TestDoEasy\Part66\ 目录,命名 TestDoEasyPart66.mq5,就能开始验证信号集合的创建流程。测试逻辑很直接:拉取信号库完整列表、筛出免费信号、挑盈利潜力最高的订阅,成功后再显示信号参数与跟单设置,下一次 tick 直接取消订阅。 之前所有信号操控都写在 OnInit() 里,这次挪到 OnTick() 执行,所以 OnInit() 里多余的测试代码要删掉,新模块按小节开头的测试条件重写并附了详细注释。 EA 输入里有一组操控信号服务的开关,编译后挂到图表上,记得在「通用」选项卡勾选“允许修改信号设置”,否则 EA 根本没有权限动信号。启动后日志先报集合创建成功,再打印部分免费信号列表与最大涨幅百分比,随后输出订阅参数,下一次报价便提示取消订阅成功。 下面这段是 EA 输入变量声明,逐行看:InpMagic=123 是订单魔术码;InpLots=0.1 固定手数;InpStopLoss 与 InpTakeProfit 都是 150 点;InpDistance 系列三个 50 点分别对应挂单、StopLimit 及待激活距离;InpBarsDelayPReq=5 表示当前周期延后 5 根 K 线激活;InpSlippage=5 点滑点容忍;InpSpreadMultiplier=1 按点差倍数调止损;InpTotalAttempts=5 次交易尝试;InpWithdrawal=10 仅测试仪提款;最后两个 InpButtShiftX/Y 是按钮像素偏移。外汇与贵金属杠杆高,信号跟单实测前务必在策略测试器先跑一遍。
class=class="str">"cmt">//--- input variables input class="type">class="kw">ushort InpMagic = class="num">123; class=class="str">"cmt">// Magic number input class="type">class="kw">double InpLots = class="num">0.1; class=class="str">"cmt">// Lots input class="type">uint InpStopLoss = class="num">150; class=class="str">"cmt">// StopLoss in points input class="type">uint InpTakeProfit = class="num">150; class=class="str">"cmt">// TakeProfit in points input class="type">uint InpDistance = class="num">50; class=class="str">"cmt">// Pending orders distance(points) input class="type">uint InpDistanceSL = class="num">50; class=class="str">"cmt">// StopLimit orders distance(points) input class="type">uint InpDistancePReq = class="num">50; class=class="str">"cmt">// Distance for Pending Request&class="macro">#x27;s activate(points) input class="type">uint InpBarsDelayPReq = class="num">5; class=class="str">"cmt">// Bars delay for Pending Request&class="macro">#x27;s activate(current timeframe) input class="type">uint InpSlippage = class="num">5; class=class="str">"cmt">// Slippage in points input class="type">uint InpSpreadMultiplier = class="num">1; class=class="str">"cmt">// Spread multiplier for adjusting stop-orders by StopLevel input class="type">uchar InpTotalAttempts = class="num">5; class=class="str">"cmt">// Number of trading attempts sinput class="type">class="kw">double InpWithdrawal = class="num">10; class=class="str">"cmt">// Withdrawal funds(in tester) sinput class="type">uint InpButtShiftX = class="num">0; class=class="str">"cmt">// Buttons X shift sinput class="type">uint InpButtShiftY = class="num">10; class=class="str">"cmt">// Buttons Y shift
◍ 把跟单信号塞进对象数组
这段输入参数定义了 EA 可调的全局开关:追踪止损默认 50 点、步长 20 点,修改止损/止盈的触发阈值分别是 20 点与 60 点;品种覆盖 EURUSD、AUDUSD 等 11 个货币对,周期从 M1 到 MN1 共 9 档。 把信号服务开关 InpUseMqlSignals 设为 INPUT_YES 后,程序会在初始化时拉取信号库。下面这段代码演示了如何把信号库里的全部信号实例化为 CMQLSignal 对象并压入 CArrayObj 容器。 调用 SignalBaseTotal 拿到数据库信号总数,再逐个 SignalBaseSelect 选中,用 SignalBaseGetInteger(SIGNAL_BASE_ID) 取 ID 构造对象。外汇与贵金属杠杆高,跟单信号仅作概率参考,实盘前务必在 MT5 策略测试器用历史数据验证。
sinput ENUM_INPUT_YES_NO InpUseMqlSignals = INPUT_YES; class=class="str">"cmt">// Use signal service CArrayObj *list=new CArrayObj(); if(list!=NULL) { class=class="str">"cmt">//--- request the total number of signals in the signal database class="type">int total=SignalBaseTotal(); class=class="str">"cmt">//--- loop through all signals for(class="type">int i=class="num">0;i<total;i++) { class=class="str">"cmt">//--- select a signal for further operation if(!SignalBaseSelect(i)) class="kw">continue; class="type">long id=SignalBaseGetInteger(SIGNAL_BASE_ID); CMQLSignal *signal=new CMQLSignal(id); if(signal==NULL) class="kw">continue; if(!list.Add(signal)) {
「信号筛选与EA生命周期的收尾逻辑」
这段片段承接前面的信号抓取,核心在做一件事:把列表中「盈利且有人订阅」的信号挑出来打印到日志。过滤条件是 signal.Price()>0(亏损或零浮盈)或 Subscribers()==0 的直接跳过,因此实际输出的都是当前浮亏为零以上、且订阅数非零的免费信号。 第一个命中的信号用 signal.Print() 全量展开,后续同条件信号只走 PrintShort() 简版——避免日志被刷屏,也方便在 MT5 终端快速扫一眼哪些信号值得跟。 EA 的卸载函数 OnDeinit 里用 ObjectsDeleteAll(0,prefix) 按前缀清图形对象,并调 engine.OnDeinit() 释放库资源;OnTick 中若处于策略测试器,会手动触发 OnTimer、按钮控制和事件处理,弥补测试环境无真实定时器的缺口。 若 trailing_on 为真,每个 tick 都会跑 TrailingPositions 与 TrailingOrders 两套移动止损。外汇与贵金属杠杆高,回测里这套 trailing 逻辑可能降低回撤,但实盘滑点下保护效果倾向打折,开 MT5 用自有品种验证参数前别直接信。
class="kw">delete signal; class="kw">continue; } } class=class="str">"cmt">//--- display all profitable free signals with a non-zero number of subscribers Print(""); class="kw">static class="type">bool done=false; for(class="type">int i=class="num">0;i<list.Total();i++) { CMQLSignal *signal=list.At(i); if(signal==NULL) class="kw">continue; if(signal.Price()>class="num">0 || signal.Subscribers()==class="num">0) class="kw">continue; class=class="str">"cmt">//--- The very first suitable signal is fully displayed in the journal if(!done) { signal.Print(); done=true; } class=class="str">"cmt">//--- Short descriptions are displayed for the rest else signal.PrintShort(); } class="kw">delete list; } class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- Remove EA graphical objects by an object name prefix ObjectsDeleteAll(class="num">0,prefix); Comment(""); class=class="str">"cmt">//--- Deinitialize library engine.OnDeinit(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- Handle the NewTick event in the library engine.OnTick(rates_data); class=class="str">"cmt">//--- If working in the tester if(MQLInfoInteger(MQL_TESTER)) { engine.OnTimer(rates_data); class=class="str">"cmt">// Working in the timer PressButtonsControl(); class=class="str">"cmt">// Button pressing control engine.EventsHandling(); class=class="str">"cmt">// Working with events } class=class="str">"cmt">//--- If the trailing flag is set if(trailing_on) { TrailingPositions(); class=class="str">"cmt">// Trailing positions TrailingOrders(); class=class="str">"cmt">// Trailing pending orders }
自动挑涨幅最高的免费信号并订阅
EA 在首次运行且允许使用 MQL5 信号时,会扫描数据库里的免费信号,挑出历史涨幅百分比最高的一条去订阅。逻辑上用静态变量 done 锁住,只在 OnInit 阶段跑一次,避免每 tick 重复订阅。 订阅成功后紧接着设参数:复制开平仓、同步不弹确认框、连信号的 SL/TP 一起抄,滑点给 2 点,仓位换算上限 50%,入金占用上限 70%。这些数字直接决定跟单激进程度,外汇和贵金属杠杆高,跟单爆仓概率会随仓位上限抬升而放大。 如果当前已有订阅(ID>0),代码会先退订再走后面的品种 tick 序列创建。下面把这段核心逻辑原样贴出,并逐行拆给你看。 静态 done 标记保证只搜一次;InpUseMqlSignals 是 EA 输入开关。GetListSignalsMQL5Free 拿免费信号数组,FindMQLSignalMax 按 GAIN 属性找最大涨幅下标。订阅后那串 SignalsMQL5CurrentSet* 就是上面说的跟单参数落点,最后 Unsubscribe 处理已有订阅的清理。
class=class="str">"cmt">//--- Search for available signals in the database and check the ability to subscribe to a signal by its name class="kw">static class="type">bool done=false; class=class="str">"cmt">//--- If the first launch and working with signals is enabled in EA custom settings if(InpUseMqlSignals && !done) { class=class="str">"cmt">//--- Display the list of all free signals in the journal Print(""); engine.GetSignalsMQL5Collection().PrintShort(true,false,true); class=class="str">"cmt">//--- Get the list of free signals CArrayObj *list=engine.GetListSignalsMQL5Free(); class=class="str">"cmt">//--- If the list is obtained if(list!=NULL) { class=class="str">"cmt">//--- Find a signal with the maximum growth in % in the list class="type">int index_max_gain=CSelect::FindMQLSignalMax(list,SIGNAL_MQL5_PROP_GAIN); CMQLSignal *signal_max_gain=list.At(index_max_gain); class=class="str">"cmt">//--- If the signal is found if(signal_max_gain!=NULL) { class=class="str">"cmt">//--- Display the full signal description in the journal signal_max_gain.Print(); class=class="str">"cmt">//--- If managed to subscribe to a signal if(engine.SignalsMQL5Subscribe(signal_max_gain.ID())) { class=class="str">"cmt">//--- Set subscription parameters class=class="str">"cmt">//--- Enable copying deals by subscription engine.SignalsMQL5CurrentSetSubscriptionEnableON(); class=class="str">"cmt">//--- Set synchronization without the confirmation dialog engine.SignalsMQL5CurrentSetConfirmationsDisableOFF(); class=class="str">"cmt">//--- Set copying Stop Loss and Take Profit engine.SignalsMQL5CurrentSetSLTPCopyON(); class=class="str">"cmt">//--- Set the market order slippage used when synchronizing positions and copying deals engine.SignalsMQL5CurrentSetSlippage(class="num">2); class=class="str">"cmt">//--- Set the percentage for converting deal volume engine.SignalsMQL5CurrentSetEquityLimit(class="num">50); class=class="str">"cmt">//--- Set deposit limitations(in %) engine.SignalsMQL5CurrentSetDepositPercent(class="num">70); class=class="str">"cmt">//--- Display subscription parameters in the journal engine.SignalsMQL5CurrentSubscriptionParameters(); } } } done=true; class="kw">return; } class=class="str">"cmt">//--- If a signal subscription is active, unsubscribe if(engine.SignalsMQL5CurrentID()>class="num">0) { engine.SignalsMQL5Unsubscribe(); }
◍ 把信号服务接进引擎时的开关逻辑
在 MT5 里把第三方信号源挂进自建分析引擎,第一步是批量建好 tick 序列和深度图(DOM)序列,并立刻把集合描述打到日志里核对。 engine.TickSeriesCreateAll(); engine.GetTickSeriesCollection().Print(); engine.GetMBookSeriesCollection().Print(); 上面三行跑完,日志应能看到已建序列的数量与品种范围,否则后续信号对齐会错位。 信号集合的创建受输入开关 InpUseMqlSignals 控制:只有显式开启且 engine.SignalsMQL5Create() 返回成功,才打开订阅跟单;否则直接关闭订阅开关,避免静默跟单。 从一次实际拉取看,信号池里免费信号 195 个、付费信号 805 个。样本里 'VantageFX Sunphone Dragon' 增长 537.89%、回撤 39.06%、订阅 21 人;'Prospector Scalper EA' 增长 334.76%、回撤 43.93%、订阅 215 人。高增长往往伴随四成上下回撤,外汇与贵金属跟单属高风险,信号历史表现不预示未来。 开 MT5 把这段代码塞进 OnInit,观察日志里 SignalsMQL5PrintShort 输出的 Growth/Drawdown,先筛回撤低于自身承受阈值的再考虑订阅。
engine.TickSeriesCreateAll(); class=class="str">"cmt">//--- Check created tick series - display descriptions of all created tick series in the journal engine.GetTickSeriesCollection().Print(); class=class="str">"cmt">//--- Check created DOM series - display descriptions of all created DOM series in the journal engine.GetMBookSeriesCollection().Print(); class=class="str">"cmt">//--- Create the collection of mql5.com Signals service signals class=class="str">"cmt">//--- If working with signals is enabled and the signal collection is created if(InpUseMqlSignals && engine.SignalsMQL5Create()) { class=class="str">"cmt">//--- Enable copying deals by subscription engine.SignalsMQL5CurrentSetSubscriptionEnableON(); class=class="str">"cmt">//--- Check created MQL5 signal objects of the Signals service - display the class="type">class="kw">short collection description in the journal engine.SignalsMQL5PrintShort(); } class=class="str">"cmt">//--- If working with signals is not enabled or failed to create the signal collection, class=class="str">"cmt">//--- disable copying deals by subscription else engine.SignalsMQL5CurrentSetSubscriptionEnableOFF();
「信号订阅日志里的真实参数坑」
上面这段是某次信号订阅操作留下的原始日志,订阅的是 ID 784584 的 "Tradewai" 信号,发布于 2020.07.02,最新统计更新到 2021.03.07。信号账户在 MetaQuotes-Demo 服务器上跑,杠杆 33 倍,期间成交 1825 笔,订阅人数仅 6 人,评级排第 872 位。 账面数据看着夸张:账户余额 12061.98 USD,权益 12590.32 USD,增长 1115.93%,信号 ROI 1169.19%,但最大回撤 70.62%,且交易结果以 pip 计为 -19248988——这种量级通常是多单对冲或微型手数堆叠出来的统计假象,不能直接当收益看。外汇与贵金属信号跟单本身属高风险,这类极端回撤意味着爆仓概率不低。 复制参数里有两个值值得手动改:成交量转换百分比设了 50.00%,存款限制 70%,信号权益限制 7.00%,同步滑点允许到点差 2 倍。如果你在 MT5 里跟单,至少把滑点收紧到 Spread*1.0 以内,并把存款限制降到 30% 以下,否则一次同步就可能吃掉账户近三成。 日志末尾显示已取消订阅,说明这套参数实盘跑下来并不适合长持。开 MT5 终端翻「信号」标签里的历史订阅记录,对照上面字段挨个核对,比看任何收益率截图都实在。
Publication date: class="num">2020.07.class="num">02 class="num">16:class="num">29 Monitoring start date: class="num">2020.07.class="num">02 class="num">16:class="num">29 Date of the latest update of the trading statistics: class="num">2021.03.class="num">07 class="num">15:class="num">11 ID: class="num">784584 Trading account leverage: class="num">33 Trading result in pips: -class="num">19248988 Position in the Rating of Signals: class="num">872 Number of subscribers: class="num">6 Number of trades: class="num">1825 Status of account subscription to a signal: No ------ Account balance: class="num">12061.98 Account equity: class="num">12590.32 Account growth in %: class="num">1115.93 Maximum drawdown: class="num">70.62 Signal subscription price: class="num">0.00 Signal ROI(Return on Investment) in %: class="num">1169.19 ------ Author login: "tradewai.com" Broker(company) name: "MetaQuotes Software Corp." Broker server: "MetaQuotes-Demo" Name: "Tradewai" Account currency: "USD" ============= End of parameter list(Signal from the MQL5.com Signal service) ============= Subscribed to signal ID class="num">784584 "Tradewai" ============= Signal copying parameters ============= Allow using signals for program: Yes Agree to the terms of use of the Signals service: Yes Enable copying deals by subscription: Yes Enable synchronization without confirmation dialog: No Copying Stop Loss and Take Profit: Yes Market order slippage when synchronizing positions and copying deals: Spread * class="num">2.00 Percentage for converting deal volume: class="num">50.00% Limit by deposit: class="num">70% Limitation on signal equity: class="num">7.00% Signal ID: class="num">784584 Signal name: Tradewai Unsubscribed from the signal ID class="num">784584 "Tradewai"
编译报错先改 Trading.mqh 访问权限
有读者在评论区反馈,更新 MT5 终端后旧版函数库直接编译失败,根因是 Trading.mqh 里几个本该给派生类用的方法被放进了 private 区,新编译器不再放过这种写法。作者确认这是自己疏忽、旧编译器漏检,新版检测到了。 具体修法:打开 Trading.mqh,把第 84–89 行的方法从 private 移到 protected;第 155–181 行做类似迁移。外汇与贵金属交易本就高杠杆高风险,用到这类自定义信号操控库前,先在策略测试器跑通编译再谈实盘。 下面这段是头文件里权限区划分的节选,注意 protected 和 private 的着色行——编译报错往往就出在 private 区里写了派生类要调的模板函数。 别把编译器静默当没问题 旧版 MT5 终端对访问权限检查宽松,很多隐藏写法能混过去;终端一更新就可能集体暴露。养成习惯:每次升级后重新编译你依赖的所有 include 文件。
class=class="str">"cmt">//--- 按操作类型返回顺序方向 ENUM_ORDER_TYPE DirectionByActionType(const ENUM_ACTION_TYPE action) const; class=class="str">"cmt">//--- 将交易对象设置为所需的声音 class="type">void SetSoundByMode(const ENUM_MODE_SET_SOUND mode,const ENUM_ORDER_TYPE action,const class="type">class="kw">string sound,CTradeObj *trade_obj); class="kw">protected: class=class="str">"cmt">//--- 设置交易请求价格 class="kw">template <class="kw">typename PR,class="kw">typename SL,class="kw">typename TP,class="kw">typename PL> class="type">bool SetPrices(const ENUM_ORDER_TYPE action,const PR price,const SL sl,const TP tp,const PL limit,const class="type">class="kw">string source_method,CSymbol *symbol_obj); class="kw">private: class=class="str">"cmt">//--- Returns the flag for checking permissibility by the distance of(class="num">1) StopLoss, (class="num">2) TakeProfit, (class="num">3) the order setting price from StopLevel price class="type">bool CheckStopLossByStopLevel(const ENUM_ORDER_TYPE order_type,const class="type">class="kw">double price,const class="type">class="kw">double sl,const CSymbol *symbol_obj);
◍ 交易请求前的止损位合规校验
在 MT5 自建交易封装类时,开仓或挂单前必须确认止损、止盈与当前价的距离不小于券商的 StopLevel。下面这组公开方法就是干这个的:CheckTakeProfitByStopLevel 按订单类型、开仓价、tp 价和品种对象判断止盈是否合规;CheckPriceByStopLevel 则校验挂单价格本身,limit 参数默认 0 表示只按最小止损距离卡。 错误返回怎么处理由 ResultProccessingMethod(uint result_code) 决定,它把 MT5 交易返回值映射成预定义的纠错策略枚举。RequestErrorsCorrecting 更进一步,直接拿着 MqlTradeRequest 引用去改单:传入订单类型、点差倍数、品种与交易对象,函数内部可能自动放宽 deviation 或重算 sl/tp 后再返工。 protected 区里的 OpenPosition 和 PlaceOrder 都用了模板参数 SL、TP、PR、PL,意味着止损止盈和挂单触发价可以是 double 也能是价格层级对象。实际写 EA 时,如果你传的 tp 比 StopLevel 还小,CheckTakeProfitByStopLevel 会直接返回 false,订单不会发出——开 MT5 跑一遍 EU 品种的回测,把 tp 设成 1 点往往就能看到拦截生效。外汇与贵金属杠杆高,这类校验失败若被忽略,可能触发即时滑点亏损。
class="type">bool CheckTakeProfitByStopLevel(const ENUM_ORDER_TYPE order_type,const class="type">class="kw">double price,const class="type">class="kw">double tp,const CSymbol *symbol_obj); class="type">bool CheckPriceByStopLevel(const ENUM_ORDER_TYPE order_type,const class="type">class="kw">double price,const CSymbol *symbol_obj,const class="type">class="kw">double limit=class="num">0); class=class="str">"cmt">//--- 返回错误处理方法 ENUM_ERROR_CODE_PROCESSING_METHOD ResultProccessingMethod(const class="type">uint result_code); class=class="str">"cmt">//--- 纠错 ENUM_ERROR_CODE_PROCESSING_METHOD RequestErrorsCorrecting(class="type">MqlTradeRequest &request,const ENUM_ORDER_TYPE order_type,const class="type">uint spread_multiplier,CSymbol *symbol_obj,CTradeObj *trade_obj); class="kw">protected: class=class="str">"cmt">//--- (class="num">1) 开仓,(class="num">2) 设置挂单 class="kw">template<class="kw">typename SL,class="kw">typename TP> class="type">bool OpenPosition(const class="type">ENUM_POSITION_TYPE type, const class="type">class="kw">double volume, const class="type">class="kw">string symbol, const class="type">ulong magic=ULONG_MAX, const SL sl=class="num">0, const TP tp=class="num">0, const class="type">class="kw">string comment=NULL, const class="type">ulong deviation=ULONG_MAX, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE); class="kw">template<class="kw">typename PR,class="kw">typename PL,class="kw">typename SL,class="kw">typename TP> class="type">bool PlaceOrder( const ENUM_ORDER_TYPE order_type, const class="type">class="kw">double volume, const class="type">class="kw">string symbol, const PR price,
「挂单请求对象的私有检索与构造收口」
这段类声明的末尾给出了挂单请求容器的三个私有定位函数,以及一组公开构造参数默认值。GetIndexPendingRequestByID 按 1 字节标识符回查挂单在列表里的下标,GetIndexPendingRequestByOrder 用订单票号定位,GetIndexPendingRequestByPosition 则用持仓票号定位,三者都把查找逻辑封在 private 段,外部只能走公开接口。 构造参数里 price_limit、sl、tp 全默认 0,magic 给到 ULONG_MAX,expiration 为 0,type_time 与 type_filling 初值都是 WRONG_VALUE——意味着不显式传参时,时间类型和成交类型处于未定义态,实盘发单前必须覆盖。 在 MT5 里把这段抄进你自己的 CTrade 封装类,编译后试传一个 WRONG_VALUE 的 type_filling 去下单,终端会直接拒单并报参数错误;外汇与贵金属杠杆品种波动剧烈,这类默认值陷阱可能让你漏单或错单,调参时逐一显式赋值更稳。
const PL price_limit=class="num">0, const SL sl=class="num">0, const TP tp=class="num">0, const class="type">ulong magic=ULONG_MAX, const class="type">class="kw">string comment=NULL, const class="type">class="kw">datetime expiration=class="num">0, const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE); class="kw">private: class=class="str">"cmt">//--- 按(class="num">1)标识符返回查询对象在列表中的索引、 class=class="str">"cmt">//----(class="num">2)订单票,(class="num">3)请求位置票 class="type">int GetIndexPendingRequestByID(const class="type">uchar id); class="type">int GetIndexPendingRequestByOrder(const class="type">ulong ticket); class="type">int GetIndexPendingRequestByPosition(const class="type">ulong ticket); class="kw">public: