DoEasy 库中的其他类(第六十七部分):图表对象类(基础篇)
📘

DoEasy 库中的其他类(第六十七部分):图表对象类(基础篇)

第 1/3 篇

◍ 给 DoEasy 补一个图表对象类

在 DoEasy 库的前序迭代里,图表对象一直散落在各控件逻辑中,没有统一封装。第六十七部分把这类对象抽成独立类,让后续调用更顺手。 原文示例发布于 2021 年 6 月 11 日 14:30,至整理时已累积 1477 次查看,说明这类底层封装对 MT5 自建工具链的读者有稳定需求。 新类主要承接图表上各类图形对象的创建、属性读取与销毁,避免在每个 EA 里重复写 ObjectCreate 那一套。外汇与贵金属品种波动大、点差跳变频繁,用统一类管理对象能降低脚本误删、重名的概率,但任何自动化封装都不消除交易本身的高风险。

「先搭图表对象,再修信号集合的刷新逻辑」

做这套函数库,第一步是把图表封装成一个对象,把所有图表属性都收进去。属性分三类:整数型、实数型、字符串型。之后顺着对象细化,参数集合会变——有的加进来,有的挪到别处,但起点一定是从最简结构开始。 顺带要改的是 MQL5 信号对象类和信号集合类。原来的集合类有个毛病:首次建好全量信号列表后,就算重新更新,也只拉数据库里新出现的信号,老信号的属性纹丝不动。可信号提供者在自己账户上交易,属性明明会变,旧逻辑等于漏了这块。 修正方向很直接——每次更新集合列表,除了补新信号,已存在信号的属性也要同步刷新。这样列表里的数据才跟得上提供者端的真实变动。外汇和贵金属信号跟随本身高风险,属性滞后可能放大跟单偏差,改完建议在 MT5 里用真实信号跑一轮更新验证。

给信号类补上按 ID 重选与刷属性的接口

原信号集合类在构造时就把信号参数写死进对象属性,但 MQL5 信号数据库里的索引会随上下架频繁变动,靠索引长期绑定某信号并不可靠。更稳的做法是在对象里留三个公开方法:按属性刷新已有对象、依据信号 ID 反查库内索引、以及按 ID 选中信号供后续读取。 新增的 IndexBase() 逻辑很直接:按 SignalBaseTotal() 的总数循环,逐个取下一个信号的 ID 与入参比对,命中即返回当前循环变量作为索引,全程未命中则返回 -1。SelectBase() 只是包了一层——若 IndexBase() 不等于 -1 就视为选中成功并返回 true,否则返回 false,调用方据此判断 ID 是否还有效。 集合类 MQLSignalsCollection 的更新也同步改了:列表刷新时不再跳过已存在的对象,而是对它们调用 SetProperties(),把数据库当前选中信号的参数重新灌进各属性。这样即便库内索引漂移,只要 ID 不变,对象数据就能跟着纠正。 下面这段是 CMQLSignal 类里三个新方法的声明与 SetProperties() 实现头几行,注意背景高亮处即为本次扩展入口:

MQL5 / C++
class=class="str">"cmt">//--- Compare CMQLSignal objects by a specified class="kw">property (to sort the list by an MQL5 signal object)
  class="kw">virtual class="type">int       Compare(class="kw">const CObject *node,class="kw">const class="type">int mode=class="num">0) class="kw">const;
class=class="str">"cmt">//--- Compare CMQLSignal objects by all properties(to search for equal MQL5 signal objects)
  class="type">bool              IsEqual(CMQLSignal* compared_obj) class="kw">const;

class=class="str">"cmt">//--- Set signal object properties
  class="type">void              SetProperties(class="type">void);
class=class="str">"cmt">//--- Look for a signal with a specified ID in the database, class="kw">return the signal index
  class="type">int               IndexBase(class="kw">const class="type">long signal_id);
class=class="str">"cmt">//--- Select a signal in the signal database by its ID
  class="type">bool              SelectBase(class="kw">const class="type">long signal_id);

class=class="str">"cmt">//--- Constructors
                  CMQLSignal(){;}
                  CMQLSignal(class="kw">const class="type">long signal_id);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set object properties                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CMQLSignal::SetProperties(class="type">void)
  {
   this.m_long_prop[SIGNAL_MQL5_PROP_SUBSCRIPTION_STATUS]       = (::SignalInfoGetInteger(SIGNAL_INFO_ID)==this.ID());
   this.m_long_prop[SIGNAL_MQL5_PROP_TRADE_MODE]                = ::SignalBaseGetInteger(SIGNAL_BASE_TRADE_MODE);
   this.m_long_prop[SIGNAL_MQL5_PROP_DATE_PUBLISHED]            = ::SignalBaseGetInteger(SIGNAL_BASE_DATE_PUBLISHED);
   this.m_long_prop[SIGNAL_MQL5_PROP_DATE_STARTED]              = ::SignalBaseGetInteger(SIGNAL_BASE_DATE_STARTED);

◍ 信号基础属性的三类抓取

在 MT5 的信号订阅机制里,每个信号的基础元数据被拆成整数、双精度、字符串三种类型,分别用 SignalBaseGetInteger / SignalBaseGetDouble / SignalBaseGetString 拉取。上面这段把 SIGNAL_BASE_* 宏逐一映射到类内部数组,整数类含更新时间、杠杆、点值、评级、订阅数、交易笔数等 6 项。 双精度类覆盖余额、净值、增益、最大回撤、报价、ROI 共 6 项,字符串类则抓作者登录名、券商、服务器、信号名、结算货币 5 项。注意 IndexProp() 只是把枚举转成内部下标,真正取值仍依赖全局 SignalBase 函数。 构造函数 CMQLSignal(long signal_id) 只做一件事:先把 ID 写进 m_long_prop,再调 SetProperties() 一次性填充其余字段。你在 EA 里 new 一个信号对象时,如果 ID 传错,后面所有属性都会是 0 或空串,开 MT5 用 SignalBaseGetInteger(SIGNAL_BASE_SUBSCRIBERS) 打印验证即可。

MQL5 / C++
   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">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Parametric constructor                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
CMQLSignal::CMQLSignal(class="kw">const class="type">long signal_id)
  {
   this.m_long_prop[SIGNAL_MQL5_PROP_ID] = signal_id;
   this.SetProperties();
  }
class=class="str">"cmt">//+------------------------------------------------------------------+

「按 ID 抓取信号库里的那条记录」

在 MT5 信号数据库里按指定 ID 捞记录,核心就是 CMQLSignal::IndexBase 这一层。它先拿 SignalBaseTotal 得到库内信号总数,再逐个索引用 SignalBaseSelect 选中、比对 SIGNAL_BASE_ID,命中就返回当前下标,全程没撞上就丢 WRONG_VALUE(-1)。 SelectBase 只是 IndexBase 的薄封装:返回值不等于 WRONG_VALUE 即为选中成功。实盘里若你维护多信号订阅列表,用这个布尔判断比直接吃下标安全,能避开越界读空指针。 CMQLSignalsCollection::Refresh 负责把库里信号同步成本地对象集合。它遍历总数、逐条 Select,用 id 建临时 CMQLSignal 对象,再靠 m_list 按 SORT_BY_SIGNAL_MQL5_ID 排序后 Search;若列表里已有同 ID 对象(index != WRONG_VALUE),就 delete 临时件复用旧对象,避免重复占用内存。 开 MT5 按 F4 把下面代码塞进你的信号管理类,改 signal_id 参数就能验证:库里若有对应信号,IndexBase 返回的下标应小于 SignalBaseTotal() 且大于等于 0,否则返回 -1 说明该 ID 已失效或从未同步。外汇与贵金属信号跟随自带杠杆风险,信号库数据延迟可能导致订阅偏差,仓位须自担。

MQL5 / C++
class="type">int CMQLSignal::IndexBase(class="kw">const class="type">long signal_id)
  {
   class="type">int total=::SignalBaseTotal();
   for(class="type">int i=class="num">0;i<total;i++)
     {
      if(::SignalBaseSelect(i) && ::SignalBaseGetInteger(SIGNAL_BASE_ID)==signal_id)
        class="kw">return i;
     }
   class="kw">return WRONG_VALUE;
  }
class="type">bool CMQLSignal::SelectBase(class="kw">const class="type">long signal_id)
  {
   class="kw">return(this.IndexBase(signal_id)!=WRONG_VALUE);
  }
class="type">void CMQLSignalsCollection::Refresh(class="kw">const class="type">bool messages=true)
  {
   this.m_signals_base_total=::SignalBaseTotal();
   for(class="type">int i=class="num">0;i<this.m_signals_base_total;i++)
     {
      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;
      this.m_list.Sort(SORT_BY_SIGNAL_MQL5_ID);
      class="type">int index=this.m_list.Search(signal);
      if(index!=WRONG_VALUE)
        {
         class="kw">delete signal;
        }
     }
  }

信号入列后的对象生命周期处理

在遍历信号集合时,若通过 m_list.At(index) 已拿到已有对象的指针,就直接调用 SetProperties() 刷新属性并 continue 跳过后续插入逻辑,避免重复建对象。 当列表里还没有该信号对象时,才尝试用 InsertSort(signal) 按序插入。插入失败立即 delete signal 并 continue,防止悬空指针占用内存——这套写法在 MT5 实盘 EA 里跑一晚可能触发数十次回收,取决于扫描到的信号去重频率。 插入成功且方法入参 messages 为真,则通过 Print 输出新信号提示并调用 signal.PrintShort(true) 打印简报。外汇与贵金属信号扫描受点差和报价抖动影响,误报概率可能偏高,建议先在策略测试器用历史数据验证去重逻辑。 下方枚举片段定义了图表对象相关的消息常量,例如 MSG_CHART_OBJ_EVENT_MOUSE_WHEEL 控制是否向图表上所有 MQL5 程序广播滚轮事件,MSG_CHART_OBJ_EVENT_OBJECT_DELETE 对应图形对象销毁事件的通知开关。

MQL5 / C++
signal=this.m_list.At(index);
  class=class="str">"cmt">//--- if the pointer is received, update all signal properties
  if(signal!=NULL)
      signal.SetProperties();
  class=class="str">"cmt">//--- move on to the next loop iteration
  class="kw">continue;
  }
class=class="str">"cmt">//--- No such object in the collection list yet
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);
  }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- CChartObj
  MSG_CHART_OBJ_ID,                                                                     class=class="str">"cmt">// Chart ID
  MSG_CHART_OBJ_SHOW,                                                                   class=class="str">"cmt">// Draw price chart attributes
  MSG_CHART_OBJ_IS_OBJECT,                                                              class=class="str">"cmt">// Chart object
  MSG_CHART_OBJ_BRING_TO_TOP,                                                           class=class="str">"cmt">// Chart above all others
  MSG_CHART_OBJ_CONTEXT_MENU,                                                           class=class="str">"cmt">// Access the context menu using the right click
  MSG_CHART_OBJ_CROSSHAIR_TOOL,                                                         class=class="str">"cmt">// Access the Crosshair tool using the middle click
  MSG_CHART_OBJ_MOUSE_SCROLL,                                                           class=class="str">"cmt">// Scroll the chart horizontally using the left mouse button
  MSG_CHART_OBJ_EVENT_MOUSE_WHEEL,                                                      class=class="str">"cmt">// Send messages about mouse wheel events to all MQL5 programs on a chart
  MSG_CHART_OBJ_EVENT_MOUSE_MOVE,                                                       class=class="str">"cmt">// Send messages about mouse button click and movement events to all MQL5 programs on a chart
  MSG_CHART_OBJ_EVENT_OBJECT_CREATE,                                                    class=class="str">"cmt">// Send messages about the graphical object creation event to all MQL5 programs on a chart
  MSG_CHART_OBJ_EVENT_OBJECT_DELETE                                                    class=class="str">"cmt">// Send messages about the graphical object destruction event to all MQL5 programs on a chart

常见问题

先搭对象基础框架,包含对象ID、所属图表ID、对象类型与基础坐标属性,再补构造与释放逻辑即可起步。
很可能。应先完成图表对象类的基础实例,再在信号集合里调用其接口做重选与属性刷新,避免空引用。
可以。把库结构和信号刷新逻辑贴给小布,它能对照基础篇要点指出缺失的按ID重选或生命周期处理接口。
入列后需明确持有与释放时机,用重选接口刷新属性,退出作用域前调用释放避免内存残留。
指状态、数值与关联对象三类属性;漏抓关联对象会导致后续按ID重选时定位失败、信号失效。