这里的最大问题是设计一个兼具两个品质的系统:速度和可靠性。 在某些类型的系统中,这很困难,甚至不可能实现兼顾两者。 如此这般,在许多情况下,我们试图平衡事态。 但由于它涉及资金,我们的血汗钱,我们不想冒险去得到一个不具备这些品质的系统。 必须要牢记的是,我们正在与一个实时操作的系统打交道,这是开发人员遭遇的最困难的场景,因为我们应该始终尝试拥有一个极端快捷的系统:它必须立即对事件做出反应,且当我们尝试改进它时能表现出足够的可靠性,不至于崩溃。 因此,这项任务显然相当困难。
确保以最合适的方式调用和执行函数,避免不必要的调用,尤其是不必要的次数,如此可以达成速度提升。 这将在语言范围内提供尽可能快速的系统。 不过,如果我们想要某方面更快,那么我们必须下沉到机器语言级别,在这种情况下,我们指的是汇编语言。 但这往往是不必要的,我们能用 C 语言得到同样好的结果。
实现所需健壮性的途径之一是尝试尽可能多地重用代码,从而能在不同情况下不断对其进行测试。 但这只是其中一种方式。 另一种方式是使用 OOP(面向对象编程)。 如果每个对象类不直接操作对象类数据(继承除外),就能正确且恰如其分地完成操作,那么它就足以作为一个十分健壮的系统了。 有时,这样做会降低执行速度,但这种降低是如此之微弱,以至于比之类封装提供的指数性增长,可以被忽略不计。 这种封装提供了我们所需的健壮性。
如您所见,达成速度和稳健性双赢并不是那么简单。 但其伟大之处在于,我们不必牺牲太多东西,如同您乍眼一看那样。 我们能够简单地检查系统文档,看看哪些修改可以改进内容。 简单的事实,我们没有试图重新发明轮子,这就已经是一个良好的开端。 但请记住,程序和系统在持续改进。 故此,我们应该始终尝试尽可能多地利用可用的东西,只有在最后的无奈情况下,才去真正重新发明轮子。
之前,有些人发现没有必要在文中介绍所做的更改,或者认为我正在大量更改代码而并没实际移动它,我要解释一下:当我们编写代码时,我们真的无法想象最终代码将如何工作。 我们所拥有的全部只是要实现的目标。 一旦这个目标达成了,我们开始研究如何实现这个目标,并试图加以改进,从而令它们变得更好。
对于商业系统的情况,无论是可执行文件还是库文件,我们都会持续进行修改,并将其作为更新补丁发布。 用户实际上并不需要知道实现目标所涉及的路径,因为它是一个商业系统。 他不知道这些实际上是件好事。 但由于它是一个开放的系统,我不想让您误以为可以立即研发出一个非常高效的系统,所以从一开始就这样的。 以这种方式思考是不妥当的,它甚至是一种侮辱,因为程序员或开发人员对所用的语言无论了解多寡,总有一些东西会随时间推移而改进。
如此,不要把这个系列当作可以在 3 或 4 篇文章中总结的东西,因为如果是这样的话,最好是简单地创建代码,保持我认为最合适的方式,并将其商业化。 这并非我的本意。 我通过观摩其他更有经验的程序员的代码来学习编程,我知道这有什么价值。 了解事物如何随着时间的推移而发展,比简单地套用完成的解决方案,并尝试了解其工作原理要重要得多。
在观摩这些之后,我们继续研发。
2.0. 实现
2.0.1. 新的仓位指标建模
在新代码格式中要留意的第一件事就是函数已改为宏替换。
inline string MountName(ulong ticket, eIndicatorTrade it, eEventType ev, bool isGhost = false) { return StringFormat("%s%c%c%c%llu%c%c%c%s", def_NameObjectsTrade, def_SeparatorInfo, (char)it, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)(isGhost ? ev + 32 : ev), def_SeparatorInfo, (isGhost ? def_IndicatorGhost : def_IndicatorReal)); }
即使编译器在每处引用点都用到了此代码(这要归功于保留字 “inline”),您也不应将其视为理所当然,因为该函数在代码中被多次调用。 我们需要确保它在实际中能尽可能快速地运行,因此我们的新代码将如下所示:
#define macroMountName(ticket, it, ev, Ghost) \ StringFormat("%s%c%llu%c%c%c%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo, \ ticket, def_SeparatorInfo, \ (char)it, def_SeparatorInfo, \ (char)(Ghost ? ev + 32 : ev), def_SeparatorInfo, \ (Ghost ? def_IndicatorGhost : def_IndicatorReal))
请注意,旧版本的宏替换中的数据和此版本中的数据有所不同。 如此更改是有原因的,我们稍后将在文中讨论。
但是由于这个修改,我们还必须对另一个函数的代码略微进行修改。
inline bool GetIndicatorInfos(const string sparam, ulong &ticket, eIndicatorTrade &it, eEventType &ev) { string szRet[]; char szInfo[]; if (StringSplit(sparam, def_SeparatorInfo, szRet) < 2) return false; if (szRet[0] != def_NameObjectsTrade) return false; ticket = (ulong) StringToInteger(szRet[1]); StringToCharArray(szRet[2], szInfo); it = (eIndicatorTrade)szInfo[0]; StringToCharArray(szRet[3], szInfo); ev = (eEventType)szInfo[0]; return true; }
此处的更改仅针对索引,该索引将指明哪个是单号,哪个是指标。 这没什么复杂的。 只需完成一个简单的细节,否则在调用该函数时,我们将得到不一致的数据。
您也许会惊讶:“为什么我们需要这些修改? 系统运行不正常吗?” 是的,它能工作了。 但有些事情我们无法控制。 例如,当 MetaTrader 5 开发人员改进了一些 EA 中未用到的函数时,我们从中并未受益。 规则是避免重新发明轮子,取而代之的是可用资源的再生。 因此,我们应该始终尝试利用语言提供的函数,在我们的例子中是 MQL5,并避免自行创建函数。 这也许看起来很荒谬,但实际上,如果您冷静思考,您会发现平台不时在为某些函数提供改进,如果您恰好用到了这些相同的函数,则无需付出任何额外的努力,即可在程序中获得更好的性能和更高的安全性。
因此,结局证明这是合理的。 然而,上述变更是否有助于 EA 从 MQL5 函数库的任何改进中受益? 这个问题的答案是 否定的。上述变更对于确保对象名称建模正确性是必要的,如此我们就能够有效地利用来自 MQL5 和 MetaTrader 5 开发人员未来可能的改进。 以下是可能有用的项目之一:
inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT)) ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo))); else ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, def_SeparatorInfo, (char)it)); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); m_InfoSelection.bIsMovingSelect = false; ChartRedraw(); }
下面显示的是以前版本的相同代码,供那些不记得它,或以前没有遇到过它的人参考。 代码如下所示:
inline void RemoveIndicator(ulong ticket, eIndicatorTrade it = IT_NULL) { #define macroDestroy(A, B) { \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_GROUND, B)); \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_LINE, B)); \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_CLOSE, B)); \ ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_EDIT, B)); \ if (A != IT_RESULT) ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_MOVE, B)); \ else ObjectDelete(Terminal.Get_ID(), MountName(ticket, A, EV_PROFIT, B)); \ } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT)) { macroDestroy(IT_RESULT, true); macroDestroy(IT_RESULT, false); macroDestroy(IT_PENDING, true); macroDestroy(IT_PENDING, false); macroDestroy(IT_TAKE, true); macroDestroy(IT_TAKE, false); macroDestroy(IT_STOP, true); macroDestroy(IT_STOP, false); } else { macroDestroy(it, true); macroDestroy(it, false); } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); #undef macroDestroy }
看起来好似代码变得更加紧凑。 但不仅如此。 代码缩减是一件显而易见的事,但真相要深刻得多。 旧代码已被新代码所取代,从而能更好地利用平台资源。 但由于以前所用的对象名称模型不允许这种改进,因此我们修改了建模,如此我们便可以期待从 MQL5 函数中受益。 如果该函数因任何原因得到改进,则 EA 亦将从中受益,且无需我们对 EA 结构进行任何更改。 我说的是 ObjectsDeleteAll 函数。 如果我们正确调用它,则 MetaTrader 5 将进行清理。 我们不需要指定太多细节,我们只需指定一个或多个对象的名称,而 MetaTrader 5 会完成剩下的工作。 新代码中高亮显示的位置调用了该函数。 请注意我们是如何进行建模的,以便通知将使用的前缀。 在修改对象名称建模之前,这是不可能的。
我想提请您注意新代码中的一个片段细节,如下面高亮显示。
if ((it == IT_NULL) || (it == IT_PENDING) || (it == IT_RESULT)) ObjectsDeleteAll(Terminal.Get_ID(), StringFormat("%s%c%llu%c", def_NameObjectsTrade, def_SeparatorInfo, ticket, (ticket > 1 ? '*' : def_SeparatorInfo)));
您细想为什么我添加了高亮显示的部分?
这是因为如果系统从数值为 1 开始创建单号,那么一旦放置了一笔挂单,所有对象都将从屏幕上删除。 不是很清楚? 为放置挂单,输入值已设为 1,即,指标 0 实际上其值应为 1,而不是 0,因为 0 只是在 EA 中用于执行其它测试。 因此该初始值为 1。 现在,我们遇到一个问题:假设交易系统创建一个单号 1221766803。 然后,表示此单号的对象将拥有以下数值作为前缀:SMD_OT#1221766803。 当 EA 执行 ObjectsDeleteAll 函数去删除指标 0 时,对象名称将是 SMD_OT#1,这将删除以此值开头的所有对象,包括新创建的系统。 为了解决这个问题,我们将针对名称进行略微的调整,在名称末尾添加一个额外的字符来通知 ObjectsDeleteAll 函数,从而函数就知道我们是删除指标 0,还是另一个指标。
因此,如果要删除指标 0,则该函数将收到值 SMD_OT#1#。 这样就避免了问题。 与此同时,在上面的示例中,该函数将得到名称 SMD_OT#1221766803*。 这看上去很简单,但正因为如此,您可能会感到困惑,为什么 EA 不断删除新下订单的指标对象。
现在我们来谈谈一个奇怪的细节。 在函数的末尾,有一个 ChartRedraw 的调用。 这个有啥用? 难道 MetaTrader 5 自己不会刷新图表吗? 它会做的。 但我们不知道它何时会发生。 还有另一个问题:所有在图表上放置或删除对象的调用都是同步的,即它们在特定时间执行,而这个时间不一定是我们预期的。 然而,我们的订单系统将使用对象来显示或管理订单,我们需要确保对象处于图表之上。 我们不能想当然地认定 MetaTrader 5 已经在图表上放置或删除了对象,因为我们需要确认它,这就是为什么我们要强制平台进行刷新的原因。
因此,当我们调用 ChartRedraw 时,我们强制平台刷新图表上的对象列表,如此这般我们就可以确保某个对象在图表上是否存在。 如果这还不清楚,我们先进入下一个主题。
2.0.2. 对象越少 — 速度越快
以前版本中的初始化函数很繁琐。 它有很多重复的检查,且有些东西是重复的。 除了这些次要问题外,该系统还很少重用的现有的能力。 因此,为了发挥新的建模,我决定减少在初始化期间创建的对象数量。 由此,现在系统看起来像这样:
void Initilize(void) { ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false); ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false); for (int c0 = OrdersTotal(); c0 >= 0; c0--) IndicatorInfosAdd(OrderGetTicket(c0)); for (int c0 = PositionsTotal(); c0 >= 0; c0--) IndicatorInfosAdd(PositionGetTicket(c0)); }
看起来一切都不一样,事实上也是如此。 现在我们正在重用以前没有被充分利用的函数 — 就是往图表上添加指标的函数。 我们来看看这个特殊函数。
inline void IndicatorAdd(ulong ticket) { char ret; if (ticket == def_IndicatorTicket0) ret = -1; else { if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_PENDING, EV_LINE, false), OBJPROP_PRICE) != 0) return; if (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_RESULT, EV_LINE, false), OBJPROP_PRICE) != 0) return; if ((ret = GetInfosTradeServer(ticket)) == 0) return; } switch (ret) { case 1: CreateIndicatorTrade(ticket, IT_RESULT); PositionAxlePrice(ticket, IT_RESULT, m_InfoSelection.pr); break; case -1: CreateIndicatorTrade(ticket, IT_PENDING); PositionAxlePrice(ticket, IT_PENDING, m_InfoSelection.pr); break; } ChartRedraw(); UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy); }
请仔细查看上面的代码。 似乎代码包含不必要的检查。 但它们是出于一个非常简单的原因而存在。 该函数是实际创建挂单或仓位指标的唯一方法。 高亮显示的两行就是检查指标是否存在。 为此,它要检查作为指示线的对象中是否存储了任何数值。 在此,它是对象所处位置的价格值。 如果指示对象位于图表之上,则该值必须为非零。 在所有其它情况下,它将等于零,因为该对象不存在,或者出于任何其它原因,而这无关紧要。 现在搞清楚为什么我们必须强制刷新图表了吗? 如果不这样做,EA 会添加不必要的对象,因此我们不能等待平台在某个未知时间执行此操作。 我们必须确保图表已更新。 否则,当这些检查完成时,它们将报告与对象的当前状态不匹配的内容,从而令系统缺乏可靠性。
尽管这些检查似乎减慢了 EA 速度,但这是一个概念性错误。 当我们执行这样的检查,且不尝试强制平台创建可能已等候在创建队列中的对象时,我们会告诉平台“立即更新”。 然后,当我们需要它时,我们检查对象是否已被创建,如果已被创建的情况下,我们就如需使用它。 这被称为“编程的正确打开方式”。 以这种方式,我们令平台操作更少,并避免不必要的检查对象是否创建等等,如此我们令 EA 更加可靠,因为我们知道哪些数据是自己想要用的。
鉴于检查将显示没有与指定单号匹配的对象,因此会创建该对象。 请注意,在创建指标 0 亦或其它任何指标的过程开头,还会有另一项检查。 这可确保我们没有不必要的未由 MetaTrader 5 支持的对象:我们只有那些在图表上实际用到的对象。 如果我们创建指标 0,则无需进一步测试,因为我们只在非常特殊和特定的条件下创建它。 对象 0 只用于由 Shift 或 CTRL + 鼠标来定位订单。 别担心,我们很快就会看到它是如何工作的。
上面的代码中有一个重要的细节:为什么我们要在调用 Update 函数之前更新图表? 这是毫无意义的。 为了理解这一点,我们来看一眼下面的 UpdateIndicators 函数。
void UpdateIndicators(ulong ticket, double tp, double sl, double vol, bool isBuy) { double pr; bool b0 = false; pr = macroGetLinePrice(ticket, IT_RESULT); pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING)); SetTextValue(ticket, IT_PENDING, vol); if (tp > 0) { if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_TAKE, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0)) CreateIndicatorTrade(ticket, IT_TAKE); PositionAxlePrice(ticket, IT_TAKE, tp); SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp)); } if (sl > 0) { if (b0 = (ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, IT_STOP, EV_LINE, false), OBJPROP_PRICE) == 0 ? true : b0)) CreateIndicatorTrade(ticket, IT_STOP); PositionAxlePrice(ticket, IT_STOP, sl); SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl)); } if (b0) ChartRedraw(); }
此函数基本上是关注指向限价的指标。 现在看一下高亮显示的两行:如果图表未更新,则这些行将不会触发,返回值 0;如果更新,则其余代码将不起作用,导致限价指标将无法在屏幕上正确显示。
但在创建限价指标之前,我们必须进行一些检查,从而了解它们是否真的需要创建,或者只是需要调整它们。 这样做的方式与创建基准对象的方式相同。 即便于此,当创建对象时,我们也会强制更新图表,以便图表始终保持最新状态。
您也许想知道:“为什么有如此多的强制更新,它们真的有必要吗?” — 答案要大声说是的...其原因在于以下函数:
inline double SecureChannelPosition(void) { double Res = 0, sl, profit, bid, ask; ulong ticket; bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID); ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK); for (int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol()) { IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET)); SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN)); sl = PositionGetDouble(POSITION_SL); if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { if (ask < sl) ClosePosition(ticket); }else { if ((bid > sl) && (sl > 0)) ClosePosition(ticket); } Res += profit; } return Res; };
您也许会认为该函数并无什么特别之处。 您确定吗? 错! 这个函数包含一个关键点:我们必须确保对象在图表上,否则所有创建它的代码都会被多次调用,进而会创建一个由 MetaTrader 5 管理的庞大队列,且某些数据可能会丢失或过时。 所有这些都会令系统不稳定,安全性较低,因此不可靠。 高亮显示的就是调用创建对象的函数。 如果我们不去强制 MetaTrader 5 在策略时刻更新图表,那么我们可能会遇到问题,因为上面的函数是由 OnTick 事件调用的,并且在高波动性期间,来自 OnTick 的调用次数非常频繁,这可能会导致队列中积压过多的对象,这肯定不太对头。 因此,强制调用 ChartRedraw 刷新数据,并调用 ObjectGetDouble 进行验证,从而降低了队列中积压太多对象的可能性。
即使不看系统是如何工作的,您也可能会想:“现在也不错,如果意外删除了 TradeLine 对象,EA 会注意到这一点,如果通过 ObjectGetDouble 检查失误,并做出指标,指标将被重新创建”。就是这种想法。 但不建议用户在不知道对象实际作用的情况下删除对象列表中存在的对象,因为如果您删除任何对象(TradeLine 除外),EA 可能不会注意到指标不在了,也不会保留访问它的手段,因为它除了通过其上存在的按钮之外没有其它访问途径。
如果不是紧随其后的函数,并负责维护类中的整个消息流,上面的脚本将是一个真正的噩梦。 然而,它仍然不仅是切入点。 我正在谈论的是 DispatchMessage 函数,我们来看看它。
void DispatchMessage(int id, long lparam, double dparam, string sparam) { ulong ticket; double price; bool bKeyBuy, bKeySell, bEClick; datetime dt; uint mKeys; char cRet; eIndicatorTrade it; eEventType ev; static bool bMounting = false, bIsDT = false; static double valueTp = 0, valueSl = 0, memLocal = 0; switch (id) { case CHARTEVENT_MOUSE_MOVE: Mouse.GetPositionDP(dt, price); mKeys = Mouse.GetButtonStatus(); bEClick = (mKeys & 0x01) == 0x01; //Left mouse click bKeyBuy = (mKeys & 0x04) == 0x04; //SHIFT pressed bKeySell = (mKeys & 0x08) == 0x08; //CTRL pressed if (bKeyBuy != bKeySell) { if (!bMounting) { Mouse.Hide(); bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl); valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol); valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol); m_InfoSelection.it = IT_PENDING; m_InfoSelection.pr = price; } m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp)); m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl); m_InfoSelection.bIsBuy = bKeyBuy; if (!bMounting) { IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0); m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false)); m_InfoSelection.bIsMovingSelect = bMounting = true; } MoveSelection(price); if ((bEClick) && (memLocal == 0)) { RemoveIndicator(def_IndicatorTicket0); CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price, price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT); } }else if (bMounting) { RemoveIndicator(def_IndicatorTicket0); Mouse.Show(); memLocal = 0; bMounting = false; }else if ((!bMounting) && (bKeyBuy == bKeySell)) { if (bEClick) SetPriceSelection(price); else MoveSelection(price); } break; case CHARTEVENT_OBJECT_DELETE: if (GetIndicatorInfos(sparam, ticket, it, ev)) { if (GetInfosTradeServer(ticket) == 0) break; CreateIndicatorTrade(ticket, it); if ((it == IT_PENDING) || (it == IT_RESULT)) PositionAxlePrice(ticket, it, m_InfoSelection.pr); ChartRedraw(); m_TradeLine.SpotLight(); m_InfoSelection.bIsMovingSelect = false; UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy); } break; case CHARTEVENT_CHART_CHANGE: ReDrawAllsIndicator(); break; case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev) { case EV_CLOSE: if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it) { case IT_PENDING: case IT_RESULT: if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket); break; case IT_TAKE: case IT_STOP: m_InfoSelection.ticket = ticket; m_InfoSelection.it = it; m_InfoSelection.bIsMovingSelect = true; SetPriceSelection(0); break; } break; case EV_MOVE: if (m_InfoSelection.bIsMovingSelect) { m_TradeLine.SpotLight(); m_InfoSelection.bIsMovingSelect = false; }else { m_InfoSelection.ticket = ticket; m_InfoSelection.it = it; if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0)) m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false)); } break; } break; } }
这个函数经历了如此多的变更,我不得不把它分解成小部分,分别解释它里面经历了什么。 如果您已拥有编程经验,那么您将不难理解它的作用。 不过,如果您只是一个 MQL5 编程爱好者或新手,那么理解这个函数可能有点困难,所以我将在下一个主题中平静地解释它。
2.0.3. 分解 DispatchMessage 函数
本主题讲解 DispatchMessage 函数中发生的情况。 如果您能通过简单地查看代码来理解它的工作原理,那么本主题就不会为您提供任何新鲜内容。
在局部变量之后,我们首先看到的是静态变量。
static bool bMounting = false, bIsDT = false; static double valueTp = 0, valueSl = 0, memLocal = 0;
在类中可以把它们声明为私密变量,但由于它们仅在代码中该处使用,因此类中的其它函数查看这些变量是没有意义的。 它们应声明为静态,因为若再次调用函数时它们必须记住其值。 如果我们不添加 “static” 关键字,它们将在函数结束后立即丢弃其值。 一旦这步完成,我们将开始处理 MetaTrader 5 与 EA 的交互事件。
第一个事件可以在下面看到:
case CHARTEVENT_MOUSE_MOVE: Mouse.GetPositionDP(dt, price); mKeys = Mouse.GetButtonStatus(); bEClick = (mKeys & 0x01) == 0x01; //Left mouse click bKeyBuy = (mKeys & 0x04) == 0x04; //SHIFT pressed bKeySell = (mKeys & 0x08) == 0x08; //CTRL pressed
在此,我们从鼠标和与鼠标关联的一些键(从键盘)中收集并隔离数据。 一旦我们完成了这一步,就会进入一长段从测试开始的代码。
if (bKeyBuy != bKeySell)
如果您按下 SHIFT 或 CTRL 键,但不是同时按下这两者,如此令 EA 了解您希望在特定价位下单。 若是如此,则进一步检查。
if (!bMounting) { Mouse.Hide(); bIsDT = Chart.GetBaseFinance(m_InfoSelection.vol, valueTp, valueSl); valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_InfoSelection.vol); valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_InfoSelection.vol); m_InfoSelection.it = IT_PENDING; m_InfoSelection.pr = price; }
如果尚未设置指标 0,则此测试将略过。 鼠标将被隐藏,然后捕获 Chart Trade 中的值。 然后,根据交易者于 Chart Trade 上指示的价位,这些值将会被转换为点数。 初始值显示下单处的价位。 该序列每次循环只应出现一次。
下一步是创建止盈和止损价位,并指示我们是否该买入亦或卖出。
m_InfoSelection.tp = m_InfoSelection.pr + (bKeyBuy ? valueTp : (-valueTp)); m_InfoSelection.sl = m_InfoSelection.pr + (bKeyBuy ? (-valueSl) : valueSl); m_InfoSelection.bIsBuy = bKeyBuy;
它们是在循环之外创建的,因为当我们将鼠标移动到不同的价格范围时,我们还必须移动止盈和止损。 但是为什么上面的代码不放在装配测试之内呢? 原因在于,如果您有变化,释放 SHIFT 键并按 CTRL 键,或反之亦然,无需移动鼠标,而屏幕上有指标,则止盈和止损指标的值将交换。 为避免这种情况,该片段必须不参与测试。 但这又迫使我们进行新的装配测试,如下所示:
if (!bMounting) { IndicatorAdd(m_InfoSelection.ticket = def_IndicatorTicket0); m_TradeLine.SpotLight(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_LINE, false)); m_InfoSelection.bIsMovingSelect = bMounting = true; }
为什么我们要进行两个测试? 我们只做一个行吗? 这只是个理想,但是上面代码中高亮显示的函数不允许我们这样做。 我们需要查看 IndicatorAdd 来认清这一事实。 创建指标 0 后,我们将其设置为选中,并示意它已运行并构建。 因此,您可以将其与下一行一起移动。
MoveSelection(price);
然而,即使在按下 SHIFT 或 CTRL 下挂单的相同标准内,我们还有最后一步。
if ((bEClick) && (memLocal == 0)) { RemoveIndicator(def_IndicatorTicket0); CreateOrderPendent(m_InfoSelection.vol, bKeyBuy, memLocal = price, price + m_InfoSelection.tp - m_InfoSelection.pr, price + m_InfoSelection.sl - m_InfoSelection.pr, bIsDT); }
这就是在目标点添加挂单。 这必须满足两个条件。 第一个是点击鼠标左键,第二个是在相同的价格,我们以往没有做过同样的操作。 也就是说,若要以相同的价格下两笔或多笔订单,我们必须在不同的调用过程中才能下新订单,因为这在同一次调用里不可发生。
从图表中删除指标 0 的同时,已经正确填充参数的订单被发送到交易服务器。
现在我们移入下一步...
if (bKeyBuy != bKeySell) { // ... code described so far .... }else if (bMounting) { RemoveIndicator(def_IndicatorTicket0); Mouse.Show(); memLocal = 0; bMounting = false; }
如果设置了指标 0,但由于仅按了 SHIFT 或 CTRL 而不满足条件,则会执行高亮显示的代码,这会从对象列表中删除指标 0,同时重置鼠标,并还原静态变量保持其初始状态。 换句话说,系统将是干净的。
鼠标事件处理内部的下一步也是最后一步如下所示:
if (bKeyBuy != bKeySell) { // ... previously described code ... }else if (bMounting) { // ... previously described code ... }else if ((!bMounting) && (bKeyBuy == bKeySell)) { if (bEClick) SetPriceSelection(price); else MoveSelection(price); }
高亮显示的代码是鼠标消息处理中的最后一个步骤。 如果我们没有为指标 0 区别设置 SHIFT 或 CTRL 键的不同状态,这意味着它们可以同时按下或释放,我们有以下行为:如果我们左键单击,则价格将被发送到指标,如果我们只移动鼠标,则仅用价格来移动指标。 但之后我们遇到一个问题:哪个指标? 不用担心,我们很快就会看到它是哪个指标,但在您想知道的情况下,指标 0 不会使用此选择。 如果您还不理解,请返回到本章节的开头,参阅处理此消息的工作原理。
以下是下一条消息:
case CHARTEVENT_OBJECT_DELETE: if (GetIndicatorInfos(sparam, ticket, it, ev)) { if (GetInfosTradeServer(ticket) == 0) break; CreateIndicatorTrade(ticket, it); if ((it == IT_PENDING) || (it == IT_RESULT)) PositionAxlePrice(ticket, it, m_InfoSelection.pr); ChartRedraw(); m_TradeLine.SpotLight(); m_InfoSelection.bIsMovingSelect = false; UpdateIndicators(ticket, m_InfoSelection.tp, m_InfoSelection.sl, m_InfoSelection.vol, m_InfoSelection.bIsBuy); } break;
记住,我上面曾说过,EA 有一个小型的安全系统来防止不正确的指标删除? 此系统包含在代码中,用于在删除对象时处理有关 MetaTrader 5 发送的事件消息。
当发生这种情况时,MetaTrader 5 会通过 sparam 参数报告已删除对象的名称,检查它是否是指标,如果是,则调查是哪一个。 哪个对象受到影响并不重要。 我们想知道的是哪个指标受到影响,之后我们将检查是否有任何与指标关联的订单或仓位,如果有,则我们要再次创建整个指标。 在极端情况下,如果受影响的指标是基础指标,我们会立即重新定位它,并强制 MetaTrader 5 立即将指标放置在图表上,无论指标是什么。 我们删除选择指示,并下一笔订单来更新指标阈值数据。
下一个要处理的事件非常简单,它只是请求调整屏幕上所有指标的大小,其代码如下所示。
case CHARTEVENT_CHART_CHANGE: ReDrawAllsIndicator(); break;
下面是对象单击事件。
case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev) { //.... } break;
它开始如上所示:MetaTrader 5 告知我们点击了哪个对象,如此 EA 即可检查所要处理的事件类型。 到目前为止,我们有 2 个事件,平仓(CLOSE)和移动(MOVE)。 我们首先考察 CLOSE 事件,它将平仓,并结束在屏幕上的指标。
case EV_CLOSE: if ((cRet = GetInfosTradeServer(ticket)) != 0) switch (it) { case IT_PENDING: case IT_RESULT: if (cRet < 0) RemoveOrderPendent(ticket); else ClosePosition(ticket); break; case IT_TAKE: case IT_STOP: m_InfoSelection.ticket = ticket; m_InfoSelection.it = it; m_InfoSelection.bIsMovingSelect = true; SetPriceSelection(0); break; } break;
CLOSE 事件将执行以下操作:它依据单号在服务器上搜索应该平仓的内容,并检查是否有任何要关闭的信息,因为可能此时服务器已经这样做了,但 EA 尚未知情。 由于我们有东西要平仓,我们要正确地完成它,如此我们还要检查并以正确的方法通知类去平仓,或从图表中删除指标。
如此,我们来到了本主题的最后一步,如下所示。
case EV_MOVE: if (m_InfoSelection.bIsMovingSelect) { m_TradeLine.SpotLight(); m_InfoSelection.bIsMovingSelect = false; }else { m_InfoSelection.ticket = ticket; m_InfoSelection.it = it; if (m_InfoSelection.bIsMovingSelect = (GetInfosTradeServer(ticket) != 0)) m_TradeLine.SpotLight(macroMountName(ticket, it, EV_LINE, false)); } break;