DoEasy 库中的其他类(第六十七部分):图表对象类(基础篇)
◍ 给 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() 实现头几行,注意背景高亮处即为本次扩展入口:
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) 打印验证即可。
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 已失效或从未同步。外汇与贵金属信号跟随自带杠杆风险,信号库数据延迟可能导致订阅偏差,仓位须自担。
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 对应图形对象销毁事件的通知开关。
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