
一张图表上的多个指标(第 06 部分):将 MetaTrader 5 转变为 RAD 系统(II)
概述
在我的上一篇文章中,我向您展示了如何利用 MetaTrader 5 对象创建图表交易,从而将平台转变为 RAD 系统。 该系统运行良好,可以肯定的是,许多读者也许已经考虑过创建一个函数库,令其能够在拟议的系统中扩展功能。 有基于此,就有可能开发一款更直观的智能交易系统,其界面更友好、更易于使用。
这个创意非常好,它促使我逐步向您展示如何开始添加功能。 在此,我将实现两个新的主要功能(我们在按照需要和想要的方式实现其它功能时,应把这些信息作为基础)。 唯一的限制是我们的创造力,因为元素本身能以广泛的方式运用。
计划
我们的 IDE 变更如下图所示:
正如您所看到的,设计本身有一些小的变化。 添加了两个新区域:一个将接收资产名称;另一个将接收当天的累计值。 好吧,这些东西对于我们来讲都是非刚需,它们不会影响我们的决定。 但无论如何,它们都很有趣。 我将展示往 IDE 中添加功能的最简单和正确的方法。 如此,在新界面中打开对象列表。 它如下显现:
这两个被圈起来的对象没有与之关联的事件,这意味着它们在 IDE 中不起作用。 所有其它对象均已某些事件正确关联,当这些事件在 EA 中发生时,MetaTrader 5 可以强制它们正确执行。 这就是说,我们可以根据自己的意愿修改 IDE 界面,但如果功能尚未实现,MetaTrader 5 则只在图表上显示对象。 我们需要那个 “EDIT 00” 对象来接收我们正在交易的资产名称,该名称应会显示在对象的中心。 “EDIT 01” 对象会接收某个时段内的累计值。 我们将采用日线时段来了解我们在日线中是盈利还是亏损。 如果该值为负值,则会以一种颜色显示;如果为正值,将采用另一种颜色。
这两个值显然不能由用户更改,因此您可以将其属性保留为只读,如下图所示。
但是,请记住,无法指定信息的显示方式,也就是说,我们无法对齐文本,使其显示在对象的中心。 如若需要,可以用代码完成此操作,因为有一个属性可以将文本设置为居中对齐。 参见 "对象属性" 了解更多详情。 请注意 表格里的 ENUM_ALIGN_MODE — 在它包含的对象上您可以使用对齐文本。
因此,无论我们要实现什么样的修改,我们需要做的第一件事就是准备一个计划:定义新特性、它们的表示形式、以及用户与它们交互的方式。 这允许 MetaTrader 5 通过自己的界面尽可能多地选择正确的对象,并配置它。 结果就是,我们将有一个现成的 IDE,我们只需要通过 MQL5 代码对其进行调整,这样 IDE 最终将 100% 正常工作。 如此,我们来继续修改。
修改
为了防止代码成为真正的弗兰肯斯坦式悲剧(Frankenstein,科学怪人),我们必须尽可能地自我组织,检查哪些功能已经存在,哪些实际需求尚将实现。 在许多情况下,只需对现有代码进行略微修改,得到新代码,并对其进行测试就足够了。 这段新代码会在我们即将实现的代码中重用,唯一要测试的就是为了创建全新功能,由我们后来添加的小控制函数。 优秀的程序员总是这样做:他们尝试通过向现有代码添加控制点,以某种方式重用现有代码。
修改 1. 添加资产名称
为了实现这一部分,我们不需要进行实质性的更改,但这些更改应该在正确的地方实现。 首先,我们为枚举添加一个新值,源代码如下所示:
enum eObjectsIDE {eRESULT, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};
以下是新代码;高亮显示的部分是已添加的部分。 请注意,我不会将新值添加到枚举的开头或结尾。 这样做是为了避免与其它已经存在,并正在运行的代码部分发生冲突。
enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP};
如果您在枚举的开头或结尾添加新值,则必须查找并更改所有引用这些极值的位置。 在许多情况下,可能由于健忘而导致遗漏,这将引发难以预料的错误。 您可能会认为这些错误看起来是由于新添加的内容引起的,而实际上是由于遗忘。 因此,我们应在极值之间添加修改。
之后,我们需要立即向系统添加一条消息,否则会出现运行时错误。 因此,在源代码中添加以下行。
static const string C_Chart_IDE::szMsgIDE[] = { "MSG_RESULT", "MSG_NAME_SYMBOL", "MSG_BUY_MARKET", "MSG_SELL_MARKET", "MSG_DAY_TRADE", "MSG_CLOSE_POSITION", "MSG_LEVERAGE_VALUE", "MSG_TAKE_VALUE", "MSG_STOP_VALUE" };
正如您所看到的,我们把它加在同一个地方,从而保持了顺序。 但目前常量可加在任何点位,这不会有任何区别,因为它只用于检查哪个对象会接收消息。 出于结构化目的,我们在加入时把它作为第二条消息。
现在,我们回到 MetaTrader 5,并进行如下更改:
现在,MetaTrader 5 已经将 IDE 中的对象识别为接收消息的对象,剩下的只是创建发送消息的过程。 消息中的文本只应添加一次,只要 MetaTrader 5 把 IDE 放在图表上,它就可以被发送。 这可由简单地将所需代码添加到对象类的 Create 函数末尾来实现。 但为了不让代码变成满面疤痕的弗兰肯斯坦,我们将在 DispatchMessage 函数内添加新代码。 原始函数如下所示:
void DispatchMessage(int iMsg, string szArg, double dValue = 0.0) { if (m_CountObject < eEDIT_STOP) return; switch (iMsg) { case CHARTEVENT_CHART_CHANGE: if (szArg == szMsgIDE[eRESULT]) { ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen)); ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2)); } break; case CHARTEVENT_OBJECT_CLICK: // ... The rest of the code... } }
以下是完成相关修改后的代码:
void DispatchMessage(int iMsg, string szArg, double dValue = 0.0) { if (m_CountObject < eEDIT_STOP) return; switch (iMsg) { case CHARTEVENT_CHART_CHANGE: if (szArg == szMsgIDE[eRESULT]) { ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen)); ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2)); }else if (szArg == szMsgIDE[eLABEL_SYMBOL]) { ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol()); ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER); } break; case CHARTEVENT_OBJECT_CLICK: // ... The rest of the code } }
创建 send 函数后,我们可以选择发送此消息的点。 最好的位置实际上是在对象类的 Create 函数的末尾,因此最终的代码如下所示:
bool Create(int nSub) { m_CountObject = 0; if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) return false; FileReadInteger(m_fp, SHORT_VALUE); for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = ""; m_SubWindow = nSub; m_szLine = ""; while (m_szLine != "</chart>") { if (!FileReadLine()) return false; if (m_szLine == "<object>") { if (!FileReadLine()) return false; if (m_szLine == "type") { if (m_szValue == "102") if (!LoopCreating(OBJ_LABEL)) return false; if (m_szValue == "103") if (!LoopCreating(OBJ_BUTTON)) return false; if (m_szValue == "106") if (!LoopCreating(OBJ_BITMAP_LABEL)) return false; if (m_szValue == "107") if (!LoopCreating(OBJ_EDIT)) return false; if (m_szValue == "110") if (!LoopCreating(OBJ_RECTANGLE_LABEL)) return false; } } } FileClose(m_fp); DispatchMessage(CHARTEVENT_CHART_CHANGE, szMsgIDE[eLABEL_SYMBOL]); return true; }
添加的实际内容以绿色高亮显示。 请注意,在几乎不做任何更改的情况下,我们就已经有了一个 100% 实现的消息流,我们可以移步到需要实现的下一条消息。
修改2. 添加当天的累计值(覆盖点)
同样,我们遵循与添加资产名称时相同的逻辑,因此新代码如下所示:
enum eObjectsIDE {eRESULT, eLABEL_SYMBOL, eROOF_DIARY, eBTN_BUY, eBTN_SELL, eCHECK_DAYTRADE, eBTN_CANCEL, eEDIT_LEVERAGE, eEDIT_TAKE, eEDIT_STOP}; // ... Rest of the code static const string C_Chart_IDE::szMsgIDE[] = { "MSG_RESULT", "MSG_NAME_SYMBOL", "MSG_ROOF_DIARY", "MSG_BUY_MARKET", "MSG_SELL_MARKET", "MSG_DAY_TRADE", "MSG_CLOSE_POSITION", "MSG_LEVERAGE_VALUE", "MSG_TAKE_VALUE", "MSG_STOP_VALUE" };
之后,我们用一条新消息更改 IDE:
我们的新 IDE 已经准备就绪。 现在,我们将实现代码,来创建包含当天累计值的消息。 我们应该首先决定在哪个类中实现这个功能。 许多人可能会在 C_Chart_IDE 类中创建此函数,但出于组织原因,最好将其与处理订单的函数放在一起。 因此,代码是在 C_OrderView 类中实现的。 其代码示意如下:
double UpdateRoof(void) { ulong ticket; int max; string szSymbol = Terminal.GetSymbol(); double Accumulated = 0; HistorySelect(macroGetDate(TimeLocal()), TimeLocal()); max = HistoryDealsTotal(); for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0) if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol) Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT); return Accumulated; }
现在代码已经实现,我们需要将消息添加到系统中。 为了让操盘手的生活更轻松,我已经添加了代码来报告已完成的结果。 此为其代码:
void DispatchMessage(int iMsg, string szArg, double dValue = 0.0) { static double AccumulatedRoof = 0.0; bool b0; double d0; if (m_CountObject < eEDIT_STOP) return; switch (iMsg) { case CHARTEVENT_CHART_CHANGE: if ((b0 = (szArg == szMsgIDE[eRESULT])) || (szArg == szMsgIDE[eROOF_DIARY])) { if (b0) { ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_BGCOLOR, (dValue < 0 ? clrLightCoral : clrLightGreen)); ObjectSetString(Terminal.Get_ID(), m_ArrObject[eRESULT].szName, OBJPROP_TEXT, DoubleToString(dValue, 2)); }else { AccumulatedRoof = dValue; dValue = 0; } d0 = AccumulatedRoof + dValue; ObjectSetString(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_TEXT, DoubleToString(MathAbs(d0), 2)); ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eROOF_DIARY].szName, OBJPROP_BGCOLOR, (d0 >= 0 ? clrForestGreen : clrFireBrick)); }else if (szArg == szMsgIDE[eLABEL_SYMBOL]) { ObjectSetString(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_TEXT, Terminal.GetSymbol()); ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eLABEL_SYMBOL].szName, OBJPROP_ALIGN, ALIGN_CENTER); } break; case CHARTEVENT_OBJECT_CLICK: // .... The rest of the code.... } }
高亮显示的部件支持如上所述的系统。 如果不按照这种方式来实现,我们必须向系统发送两条消息,才能正确更新信息。 但若按代码所用的实现方式,我们用一条消息就可以跟踪持仓结果和当天的结果。
EA 中的另一个修改涉及 OnTrade 函数。 看起来像这样:
void OnTrade() { SubWin.DispatchMessage(CHARTEVENT_CHART_CHANGE, C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY], NanoEA.UpdateRoof()); NanoEA.UpdatePosition(); }
虽然此系统可以工作,但您需要小心 OnTrade 函数的执行时间,它与 OnTick 一起工作时可能会降低 EA 的性能。 若 OnTick 中包含代码的情况,这样做不是很好,所以优化至关重要。 其实用 OnTrade 更容易,而当持仓发生变化时,实际上会调用该函数。 知道了这一点,我们就有两种选择。 第一个是修改 UpdateRoof,从而限制其执行时间。 另一种选择是修改 OnTrade 函数本身。 出于实际原因,我们将修改 Update Roof 函数,因此,当我们有一笔持仓时,至少能稍微改善一点执行时间。 新函数如下:
double UpdateRoof(void) { ulong ticket; string szSymbol = Terminal.GetSymbol(); int max; static int memMax = 0; static double Accumulated = 0; HistorySelect(macroGetDate(TimeLocal()), TimeLocal()); max = HistoryDealsTotal(); if (memMax == max) return Accumulated; else memMax = max; for (int c0 = 0; c0 < max; c0++) if ((ticket = HistoryDealGetTicket(c0)) > 0) if (HistoryDealGetString(ticket, DEAL_SYMBOL) == szSymbol) Accumulated += HistoryDealGetDouble(ticket, DEAL_PROFIT); return Accumulated; }
高亮显示的行示意在原始函数里添加的代码。 尽管看起来它们没有多大区别,但它们确实有很大差异。 我们看看为什么。 第一次引用代码时,如果在指定时段的历史订单记录中没有数值,则 memMax 静态变量和 Accumulated 值都将清零。 测试将反映这一点,例程将返回,但如果有任何数据,将会对其进行测试,并且 memMax 和 Accumulated 都将反映新的条件。 事实上,这些变量都是静态的,这意味着它们的值在调用之间均会保持。 因此,当持仓价值因资产的自然移动而改变时,MetaTrader 5 生成一个事件,并触发调用 OnTrade 函数。 此刻,我们有一个新的 UpdateRoof 函数调用,如果持仓尚未平仓,函数立即返回到控制点,这将加快返回过程。
结束语
在本文中,我们看到了如何向 RAD 系统添加新功能,逐步创建一个函数库,令系统非常适合创建 IDE 界面,在构建交互和控制界面时更简单,错误更少。 从现在起,唯一的真正限制就是您的创造力,因为此处我们只研究如何利用 MQL5,但您可以将相同的思路集成到外部函数库当中,从而极大扩展了创建 IDE 的可能性。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10301


