
从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)
概述
在上一篇文章提供系统健壮性 (I)中,我们已经看到了如何更改 EA 的某些部分,从而令系统更加可靠和健壮。
这只是针对我们将要在本文中所做之事的介绍。 请忘记您所知道的、计划的、或希望的一切。 这里最困难的事情莫过于能将事物分离。 自本系列开始以来,EA 几乎一直在持续发展:我们在增加、修改、甚至删除一些东西。 而这一次,我们将把我们一直在做的事情推向极致。
与看似的景象对比,有一个大疑惑:设计优良的 EA 内部能否不含任何类型的指标。 它只观察并确保遵守指示的订单位置。 完美的 EA 本质上只是一个向导,针对价格的所作所为提供真实的洞察力。 它不查看指标,而只查看图表上的仓位或订单。
您也许会认为我在胡言乱语,不知道自己在说啥。 但您有没有想过为什么 MetaTrader 5 要为不同的事务提供不同的类? 为什么平台将指标、服务、脚本、和智能系统分门别类,而不是混杂在一块呢? 这个嘛...
这就是重点。 如果事情被分开来,那么正是因为它们最好分开来处理。
指标用处较广泛,无论它是什么。 如果指标的设计经过深思熟虑,那就太好了,如此可避免损害整体性能 — 我的意思是不要损害 MetaTrader 5 平台,但非其它指标。 因为它们在不同的线程上运行,所以它们可以非常有效地并行执行任务。
服务则以不同的方式提供帮助。 例如,在本系列的文章 访问 Web 上的数据(II) 和 访问 Web 上的数据(III)中,我们利用服务以非常有趣的方式访问数据。 事实上,我们可以直接在 EA 中执行此操作,但正如我已经在其它文章中解释的那样,这并非最合适的途径。
脚本则以一种非常独特的方式帮助我们,因为它们只能存在一段时间,完成一些非常具体的事情,然后就从图表中消失。 或者它们也能一直驻留,直到我们更改某些图表设置,例如时间帧。
这稍微限制了它的可塑性,但这是我们必须接受的一部分。 智能交易系统,或 EA,则相反,是特定的操控交易系统。 虽然我们可以在 EA 中添加不属于交易系统的函数和代码,但这种做法在高性能或高可靠性系统中不是很合适。 原因是所有不属于交易系统的东西都不应该出现在 EA 当中:所有东西应该放在正确的位置,并正确处理。
因此,若要提高可靠性,首先要做的就是从代码中坚决地删除不属于交易系统的所有内容,并将这些东西替换为指标或类似的东西。 EA 代码中唯一保留的是负责管理、分析和处理订单或仓位的部分。 所有其它东西都将被删除。
所以,我们开始吧。
2.0. 实现
2.0.1. 删除 EA 背景虽然这不会损害 EA,或导致任何问题,但有些人有时希望他们的屏幕是空白的,屏幕上只显示某些项目。 因此,我们将从 EA 中删除这部分,并将其转换为指标。 它非常容易实现。 我们不会触及任何类,但要创建以下代码:
#property copyright "Daniel Jose" #property indicator_chart_window #property indicator_plots 0 //+------------------------------------------------------------------+ #include <NanoEA-SIMD\Auxiliar\C_Wallpaper.mqh> //+------------------------------------------------------------------+ input string user10 = "Wallpaper_01"; //Used BitMap input char user11 = 60; //Transparency (from 0 to 100) input C_WallPaper::eTypeImage user12 = C_WallPaper::IMAGEM; //Background image type //+------------------------------------------------------------------+ C_Terminal Terminal; C_WallPaper WallPaper; //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "WallPaper"); Terminal.Init(); WallPaper.Init(user10, user12, user11); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return rates_total; } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); WallPaper.Resize(); break; } ChartRedraw(); } //+------------------------------------------------------------------+
如您所见,一切都非常自然,且易于理解。 我们只是简单地从 EA 中删除了代码,并将其转换为可以添加到图表中的指标。 而任何变化,无论是背景、透明度,甚至是从图表中删除它,都不会对 EA 操作产生影响。
现在我们将开始删除真正导致 EA 性能下降的内容。 这些就是不时,或每次价格变动都会运作的事情,因此有时会导致 EA 变慢,从而阻碍它完成真正的工作 — 观察图表上的订单或仓位发生了什么。
2.0.2. 把价格对应的成交量转换为一个指标
尽管看起来不似这样,但价格对应的交易量系统需要时间,这对 EA 来说通常至关重要。 我指的高波动时刻是,价格剧烈波动,但却没有太多的方向性情况下。 正是在这些时候,EA 需要每个可用的机器周期来完成其任务。 错过一个好时机会令人沮丧,因为一些指标决定接管该项工作。 因此,我们将其从 EA 中删除,并通过创建以下代码将其转换为真实的指标:
#property copyright "Daniel Jose" #property indicator_chart_window #property indicator_plots 0 //+------------------------------------------------------------------+ #include <NanoEA-SIMD\Tape Reading\C_VolumeAtPrice.mqh> //+------------------------------------------------------------------+ input color user0 = clrBlack; //Bar color input char user1 = 20; //Transparency (from 0 to 100 ) input color user2 = clrForestGreen; //Buying input color user3 = clrFireBrick; //Selling //+------------------------------------------------------------------+ C_Terminal Terminal; C_VolumeAtPrice VolumeAtPrice; //+------------------------------------------------------------------+ int OnInit() { Terminal.Init(); VolumeAtPrice.Init(user2, user3, user0, user1); EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { return rates_total; } //+------------------------------------------------------------------+ void OnTimer() { VolumeAtPrice.Update(); } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { VolumeAtPrice.DispatchMessage(id, sparam); ChartRedraw(); } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+
这是最简单的部分。 我们从 EA 中删除了代码,并将其放入指标之中。 如果您想将代码放回 EA 当中,您只需复制指标代码,并将其放回 EA 中即可。
所以,我们先从简单的事情开始。 但现在事情会变得更加复杂 — 我们将从 EA 中删除 Times & Trade。
2.0.3. 把 Times & Trade 转换到一个指标
如果我们的目标是创建可以在 EA 和指标中都能用的代码,这并不那么简单。 作为在子窗口中操作的指标,将其转换为指标似乎很容易。 而正因为它是在子窗口中操作,其实这并不容易。 主要问题是,如果我们像前面的情况一样完成所有事情,那么我们将在指标窗口中得到以下结果:
不建议将此类内容放在指标窗口中,因为如果用户想从屏幕中删除指标,这会让用户感到困惑。 因此,应该以不同的方式完成这件事情。 在这条路径的末端,也许看起来很困惑,但实际上是一组简单的指令和一些剪辑,我们将在指标窗口中得到以下结果。
这正是用户所期望的 — 而不是上图中看到的混乱。
以下是 Times & Trade 指标的完整代码:
#property copyright "Daniel Jose" #property version "1.00" #property indicator_separate_window #property indicator_plots 0 //+------------------------------------------------------------------+ #include <NanoEA-SIMD\Tape Reading\C_TimesAndTrade.mqh> //+------------------------------------------------------------------+ C_Terminal Terminal; C_TimesAndTrade TimesAndTrade; //+------------------------------------------------------------------+ input int user1 = 2; //Scale //+------------------------------------------------------------------+ bool isConnecting = false; int SubWin; //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "Times & Trade"); SubWin = ChartWindowFind(); Terminal.Init(); TimesAndTrade.Init(user1); EventSetTimer(1); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const int begin, const double &price[]) { if (isConnecting) TimesAndTrade.Update(); return rates_total; } //+------------------------------------------------------------------+ void OnTimer() { if (TimesAndTrade.Connect()) { isConnecting = true; EventKillTimer(); } } //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); TimesAndTrade.Resize(); break; } } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+
该代码似乎与 EA 中所用的代码相似,除了高亮显示的行在 EA 代码中不存在。 那有什么收获呢? 还是一无所获? 实际上,有一些收获:代码不完全相同,其中存在差异,它并不在指标或 EA 代码里,而是在类代码之中。 但在研究差别之前,我们先思考以下几点:我们如何告诉编译器要编译什么,以及不要编译什么? 也许,在编程时,您根本不担心这一点 — 也许,您只管简单地创建代码,如果您不喜欢任何东西,直接删掉就好了。
经验丰富的程序员有一条规则:只在肯定不起作用时才会删除某些内容,否则即使它们实际上没有被编译,也要保留片段。 但是,当我们希望编写的函数始终工作时,如何在线性代码中做到这一点呢? 此处的问题是:您知道如何告诉编译器要编译什么,和不要编译什么吗? 如果答案是“否”,那就无妨。 当我开始时,我个人也不知道该怎么做。 但它有很大帮助。 故此,我们来找出如何做到这一点。
某些语言具有编译指令,根据作者的不同,这些指令也可能称为预处理器。 但思路是一致的:告诉编译器要编译什么,以及如何进行编译。 有一种非常特殊类型的指令可用来有意隔离代码,以便我们可以测试特定的东西。 这些就是条件编译指令。 如果使用得当,它们允许我们在编译相同的代码时走不同的途径。 这恰恰是 Times & Trade 示例中所做到的。 我们可选择谁来负责生成条件编译:EA 或指标。 定义该参数后,创建 #define 指令,然后使用条件指令 #ifdef #else #endif 通知编译器如何去编译代码。
这可能难于理解,那么我们就看看它是如何操作的。
在 EA 代码中,定义并添加下面高亮显示的行:
#define def_INTEGRATION_WITH_EA //+------------------------------------------------------------------+ #include <NanoEA-SIMD\Trade\Control\C_IndicatorTradeView.mqh> #ifdef def_INTEGRATION_WITH_EA #include <NanoEA-SIMD\Auxiliar\C_Wallpaper.mqh> #include <NanoEA-SIMD\Tape Reading\C_VolumeAtPrice.mqh> #include <NanoEA-SIMD\Tape Reading\C_TimesAndTrade.mqh> #endif //+------------------------------------------------------------------+
发生以下情况:如果您想编译 EA 时包含 MQH 文件中的类,则在智能系统中保留预定义的 #ifdefine def_INTEGRATION_WITH_EA 指令。 这样就会令 EA 包含我们需要的所有类,原本它们是插入在指标当中。 如果您要删除指标,无需删除代码,而只需简单地注释掉预定义语句即可。 这可简单地通过将声明指令的行转换成注释行来完成。 如此编译器就无视该指令,并认为不存在;且由于它不存在,每次碰到条件指令 #ifdef def_INTEGRATION_WITH_EA 时,都会完全忽略它,而它和上面示例中 #endif 部分之间的代码也不会被编译。
这是我们在 C_TimesAndTrade 类中所要实现的思路。 以下是新类的样子。 我只展示一点来引起您的注意力:
#property copyright "Daniel Jose" //+------------------------------------------------------------------+ #include <NanoEA-SIMD\Auxiliar\C_Canvas.mqh> #ifdef def_INTEGRATION_WITH_EA #include <NanoEA-SIMD\Auxiliar\C_FnSubWin.mqh> class C_TimesAndTrade : private C_FnSubWin #else class C_TimesAndTrade #endif { //+------------------------------------------------------------------+ #define def_SizeBuff 2048 #define macro_Limits(A) (A & 0xFF) #define def_MaxInfos 257 #define def_ObjectName "TimesAndTrade" //+------------------------------------------------------------------+ private : string m_szCustomSymbol; // ... The rest of the class code.... }
对于未用过编译指令的人来说,代码可能看起来很奇怪。 def_INTEGRATION_WITH_EA 指令是在 EA 中声明。 然后发生以下情况。 当编译器依此文件生成目标代码时,它将假定以下关系:如果正在编译的文件是 EA,并且含有声明的指令,则编译器将生成的目标代码会包含介于条件指令 #ifdef def_INTEGRATION_WITH_EA 和 #else 之间的部分。 通常在这种情况下,我们要用 #else 指令。 如果编译另一个文件的情况,例如,未定义指令 def_INTEGRATION_WITH_EA 的指标,则将编译指令 #else 和 #endif 之间的所有内容。 这就是它如何操作的。
当编译 EA 或指标时,查看 C_TimesAndTrade 类的整个代码,从而了解这些测试和常规操作中的每个部分。 因此,MQL5 编译器将完成所有设置,从而节省我们维护两个不同文件所需的时间和精力。
2.0.4. 令 EA 更敏捷
如前所述,EA 应当仅与订单系统协同操作。 迄今为止,它所具备的功能,现在已能演变为指标。 这样做的原因非常个人化,这与 EA 所做事情涉及的计算有关。 但是这个计算系统已经被修改,并转移至另一种方法。 有因于此,我注意到由 EA 接管订单处理,订单系统受到一些事情的损害。 问题最严重之处是 OnTick 事件:
void OnTick() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, 0, TradeView.SecureChannelPosition(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eRESULT]); #ifdef def_INTEGRATION_WITH_EA TimesAndTrade.Update(); #endif }
该事件现可受控于条件指令,如此,那些在高波动期间不做交易的人,若需要的话,就能拥有一个包含所有原始指标的 EA。 但在您认为这是一个好主意之前,我要提醒您 Times & Trade 更新功能是如何工作的。
inline void Update(void) { MqlTick Tick[]; MqlRates Rates[def_SizeBuff]; int i0, p1, p2 = 0; int iflag; long lg1; static int nSwap = 0; static long lTime = 0; if (m_ConnectionStatus < 3) return; if ((i0 = CopyTicks(Terminal.GetFullSymbol(), Tick, COPY_TICKS_ALL, m_MemTickTime, def_SizeBuff)) > 0) { // ... The rest of the code... } }
上面的代码是 C_TimesAndTrade 类中存在的更新函数的一部分。 问题出在高亮显示的部分。 每次它在执行时,都会向服务器发送一个请求,并返回自某个时间点以来完成的所有交易单号,顺便说一下,这并不是问题所在。 问题在于,该调用有时会与另两个事件重合。
第一个也是最明显的事件是发生大量交易占用,这会导致 OnTick 函数的调用次数大增。 除了必须运行 C_TimesAndTrade 类中存在的上述代码外,此函数还将应对另一个问题:调用 C_IndicatorTradeView 类中存在的 SecureChannelPosition 函数。 故此,这是另一个小问题,但这还不是全部。 我曾一次次说过,尽管波动性很低,但我们会遇到两个事件的重合,第一个就是该事件。
第二个是在 OnTime 事件中,该事件已被更新,如下所示:
#ifdef def_INTEGRATION_WITH_EA void OnTimer() { VolumeAtPrice.Update(); TimesAndTrade.Connect(); } #endif
如果您打算按照设计的方式使用 EA,还考虑让它接收更多的代码,那么由于重合事件,它有时可能会出现问题。 当这种情况发生时,EA 就会停顿(即使一秒钟)转而去做与订单系统无关的事情。
与 C_TimesAndTrade 中出现的函数不同,此函数存在于 C_VolumeAtPrice 类中,且在管理订单时确实会损害 EA 性能。 发生这种情况的原因如下:
inline virtual void Update(void) { MqlTick Tick[]; int i1, p1; if (macroCheckUsing == false) return; if ((i1 = CopyTicksRange(Terminal.GetSymbol(), Tick, COPY_TICKS_TRADE, m_Infos.memTimeTick)) > 0) { if (m_Infos.CountInfos == 0) { macroSetInteger(OBJPROP_TIME, m_Infos.StartTime = macroRemoveSec(Tick[0].time)); m_Infos.FirstPrice = Tick[0].last; } for (p1 = 0; (p1 < i1) && (Tick[p1].time_msc == m_Infos.memTimeTick); p1++); for (int c0 = p1; c0 < i1; c0++) SetMatrix(Tick[c0]); if (p1 == i1) return; m_Infos.memTimeTick = Tick[i1 - 1].time_msc; m_Infos.CurrentTime = macroRemoveSec(Tick[i1 - 1].time); Redraw(); }; };
原因在于高亮显示的部分,但其中最糟糕的是重绘。 它极大地损害了 EA 性能,因为每次所接受跳价的交易量高于指定阈值时,整个价格对应的交易量都会从屏幕上删除、重新计算、并在原位重绘。 它会每 1 秒左右发生一次。 这样就很可能与其它事情重合,这就是为什么要把所有指标都从 EA 里删除的原因。 虽然我保留了它们,这是为了让您可以直接在 EA 中使用它们;但由于前面解释的原因,我仍然不建议这样做。
这些修改是必要的。 但还有另一个,更具象征意义,需要去完成。 这次的修改涉及 OnTradeTransaction 事件。 使用此事件是令系统尽可能灵活的一次尝试。 许多编写程序化订单执行 EA 的人都会用到 OnTrade 事件,他们检查哪些订单是否在服务器上,或者哪些持仓仍未平仓。 我并未指责他们做错了。 只在于它不是很有效,因为服务器会通知我们发生了什么。 但 OnTrade 事件的最大问题在于,事实上我们不得不持续检查无关紧要的事情。 如果我们要用 OnTradeTransaction 事件,我们将拥有一个至少在走势分析方面更有效的系统。 但这不是此处的宗旨。 每个人都会采用最适合自己准则的方法。
当开发此 EA 时,我决定不采用任何存储结构,因此可操控的订单或仓位数量不受限制。 但这一事实令状况变得如此复杂,以至于需要 OnTrade 事件的替代方案,这可在 OnTradeTransaction 事件中发现。
这个事件十分难于实现,这可能就是为什么它不被许多人采用的原因。 但我别无选择。 它要么能行,要么不行,否则事情会很复杂。 但在之前的版本中,针对该事件的代码非常低效,您可以在下面看到它:
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { #define def_IsBuy(A) ((A == ORDER_TYPE_BUY_LIMIT) || (A == ORDER_TYPE_BUY_STOP) || (A == ORDER_TYPE_BUY_STOP_LIMIT) || (A == ORDER_TYPE_BUY)) ulong ticket; if (trans.symbol == Terminal.GetSymbol()) switch (trans.type) { case TRADE_TRANSACTION_DEAL_ADD: case TRADE_TRANSACTION_ORDER_ADD: ticket = trans.order; ticket = (ticket == 0 ? trans.position : ticket); TradeView.IndicatorInfosAdd(ticket); TradeView.UpdateInfosIndicators(0, ticket, trans.price, trans.price_tp, trans.price_sl, trans.volume, (trans.position > 0 ? trans.deal_type == DEAL_TYPE_BUY : def_IsBuy(trans.order_type))); break; case TRADE_TRANSACTION_ORDER_DELETE: if (trans.order != trans.position) TradeView.RemoveIndicator(trans.order); else TradeView.UpdateInfosIndicators(0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); if (!PositionSelectByTicket(trans.position)) TradeView.RemoveIndicator(trans.position); break; case TRADE_TRANSACTION_ORDER_UPDATE: TradeView.UpdateInfosIndicators(0, trans.order, trans.price, trans.price_tp, trans.price_sl, trans.volume, def_IsBuy(trans.order_type)); break; case TRADE_TRANSACTION_POSITION: TradeView.UpdateInfosIndicators(0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); break; } #undef def_IsBuy }
虽然上面的代码能用,但可以说有点可怕。 上述代码生成的无用调用次数是疯狂的。 如果上述代码无法修复,则 EA 在稳定性和可靠性方面没有什么可提升的余地。
正因为如此,我在一个模拟账户上做了一些事情,尝试在消息中找到一种范式,而这实际上非常困难。 遗憾地是我没有找到范式,但我确实找到了避免产生疯狂无用调用的措施,如此可令代码更稳定、更可靠,同时足够灵活,能够随时在市场上进行交易。 当然,还有一些漏洞需要修复,但代码非常优秀:
void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { #define def_IsBuy(A) ((A == ORDER_TYPE_BUY_LIMIT) || (A == ORDER_TYPE_BUY_STOP) || (A == ORDER_TYPE_BUY_STOP_LIMIT) || (A == ORDER_TYPE_BUY)) if (trans.type == TRADE_TRANSACTION_HISTORY_ADD) if (trans.symbol == Terminal.GetSymbol()) TradeView.RemoveIndicator(trans.position); if (trans.type == TRADE_TRANSACTION_REQUEST) if ((request.symbol == Terminal.GetSymbol()) && (result.retcode == TRADE_RETCODE_DONE)) switch (request.action) { case TRADE_ACTION_PENDING: TradeView.IndicatorAdd(request.order); break; case TRADE_ACTION_SLTP: TradeView.UpdateIndicators(request.position, request.tp, request.sl, request.volume, def_IsBuy(request.type)); break; case TRADE_ACTION_DEAL: TradeView.RemoveIndicator(request.position); break; case TRADE_ACTION_REMOVE: TradeView.RemoveIndicator(request.order); break; case TRADE_ACTION_MODIFY: TradeView.UpdateIndicators(request.order, request.tp, request.sl, request.volume, def_IsBuy(request.type)); break; } #undef def_IsBuy }
不要试图马上就弄清楚正在发生的事,只需享受这个功能的美好。 这几乎是完美的生活。 我这样说并非因为这是由我完成的,而是因为它所具备的健壮性和敏捷性。
尽管也许看起来很复杂,但此代码中只有两处检查。 下面高亮显示出它们,以便更好地解释正在发生的事情。
if (trans.type == TRADE_TRANSACTION_HISTORY_ADD) if (trans.symbol == Terminal.GetSymbol()) TradeView.RemoveIndicator(trans.position); if (trans.type == TRADE_TRANSACTION_REQUEST) if ((request.symbol == Terminal.GetSymbol()) && (result.retcode == TRADE_RETCODE_DONE)) switch (request.action) { //... inner code ... }
绿色高亮显示的行在每次发生交易时进行检查,查看其资产是否与 EA 观测的资产相同。 如果是的话,C_IndicatorTradeView 类将接受一条命令,从图表中删除指标。 这可能发生在两种情况下:当订单转为持仓时,和持仓被平仓时。 请注意,我只采用净持结算模式,不用对冲模式。 因此,无论发生什么情况,该指标都将从图表中删除。
有人可能会问:如果平仓,一切都好;但如果订单转为持仓 — 对我有用吗? 否。 但问题不是在错误内部解决的,而是在 C_IndicatorTradeView 类当中。 我们将在本文的下一章节里研究它。
另一方面,红色那行,匪夷所思地减少了转发给 C_IndicatorTradeView 类的无用消息数量。 这是通过检查服务器返回的请求响应来做到的,如此我们需要获得确认,EA 正在跟踪的资产与提出请求的资产名称相同。 只有这样,新一轮的调用才会发送到 C_IndicatorTradeView 类。
这就是我能说到的,关于这个系统的全部内容。 但故事还没有结束。 我们还有很多工作要做,从现在开始,我们只关注 C_IndicatorTradeView 类。 我们现在将从所需进行的一些修改开始。
2.0.5. 降低 C_IndicatorTradeView 创建的对象数量
在文章从头开始开发智能交易系统(第 23 部分)中,我介绍了一个相当抽象,但非常有趣的订单或破位价的平移概念。 这个概念是利用幻影或阴影。 它们定义并在图表上显示交易服务器看到的内容,并在实际移动发生之前一直使用这些数值。 这个模型有一个小问题:它添加的对象由 MetaTrader 5 管控,但所添加对象在多数情况下根本不需要,因此 MetaTrader 5 得到的对象列表常常充斥着无用或很罕用的东西。
但我们不希望 EA 持续创建对象,或在列表中保留不必要的对象,因为这会降低 EA 的性能。 由于我们交由 MetaTrader 5 来管理订单,我们应该剔除干扰整个系统的无用对象。
虽说有一个非常简单的解决方案。 但其实它并不简单。 我们将针对 C_IndicatorTradeView 类进行更多修改,从而对其进行改进。 我们将在屏幕上保留幻影,以及我们将采用一种非常神奇,且很少见的方法。
它会很好玩,很有趣。
首先,我们将修改选择结构。 它现在将如下所示:
struct st00 { eIndicatorTrade it; bool bIsBuy, bIsDayTrade; ulong ticket; double vol, pr, tp, sl; }m_Selection;
我不会明确地告诉您有哪些变化 — 您应该自行理解。 但这些修改在某些时刻简化了代码逻辑。
因此,我们的幻影指标现在将有自己的索引:
#define def_IndicatorGhost 2
因此,建模的名称也发生了变化:
#define macroMountName(ticket, it, ev) StringFormat("%s%c%llu%c%c%c%c", def_NameObjectsTrade, def_SeparatorInfo,\ ticket, def_SeparatorInfo, \ (char)it, def_SeparatorInfo, \ (char)(ticket <= def_IndicatorGhost ? ev + 32 : ev))
这似乎是小事一桩,但很快就会有更多改变。 我们来继续。
现在价格位置宏替换始终是直接的,不再有重复,所以我们的代码现在如下所示:
#define macroSetLinePrice(ticket, it, price) ObjectSetDouble(Terminal.Get_ID(), macroMountName(ticket, it, EV_LINE), OBJPROP_PRICE, price) #define macroGetLinePrice(ticket, it) ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, it, EV_LINE), OBJPROP_PRICE)
这些变化迫使我们创建了另外两个函数,现在我展示一个,稍后会是另一个。 首先是替换创建指标本身的函数。 它从字面上就能清楚表明一个指标与另一个指标的实际差别。 这可以从下面看出:
#define macroCreateIndicator(A, B, C, D) { \ m_TradeLine.Create(ticket, sz0 = macroMountName(ticket, A, EV_LINE), C); \ m_BackGround.Create(ticket, sz0 = macroMountName(ticket, A, EV_GROUND), B); \ m_BackGround.Size(sz0, (A == IT_RESULT ? 84 : 92), (A == IT_RESULT ? 34 : 22)); \ m_EditInfo1.Create(ticket, sz0 = macroMountName(ticket, A, EV_EDIT), D, 0.0); \ m_EditInfo1.Size(sz0, 60, 14); \ if (A != IT_RESULT) { \ m_BtnMove.Create(ticket, sz0 = macroMountName(ticket, A, EV_MOVE), "Wingdings", "u", 17, C); \ m_BtnMove.Size(sz0, 21, 23); \ }else { \ m_EditInfo2.Create(ticket, sz0 = macroMountName(ticket, A, EV_PROFIT), clrNONE, 0.0); \ m_EditInfo2.Size(sz0, 60, 14); } \ } void CreateIndicator(ulong ticket, eIndicatorTrade it) { string sz0; switch (it) { case IT_TAKE : macroCreateIndicator(it, clrForestGreen, clrDarkGreen, clrNONE); break; case IT_STOP : macroCreateIndicator(it, clrFireBrick, clrMaroon, clrNONE); break; case IT_PENDING : macroCreateIndicator(it, clrCornflowerBlue, clrDarkGoldenrod, def_ColorVolumeEdit); break; case IT_RESULT : macroCreateIndicator(it, clrDarkBlue, clrDarkBlue, def_ColorVolumeResult); break; } m_BtnClose.Create(ticket, macroMountName(ticket, it, EV_CLOSE), def_BtnClose); } #undef macroCreateIndicator
您也许已经注意到,我喜欢在代码中使用预处理指令。 我几乎每次都这样做。 不过,如您所见,现在很容易就能区分指标。 如果您打算为指标指定颜色,请修改此代码。 由于它们几乎都雷同,因此若利用宏替换,我们可令它们的操作都相同,并具有相同的元素。 这是终极的代码重用。
还有另一个函数的名称与此函数非常相似。 但它所做事情有一些不同,我将在最后详细讨论它。
IndicatorAdd 函数已被修改 — 我们删除了一些片段。
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); UpdateIndicators(ticket, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy); }
其中一段被移除代码将替换为高亮显示的代码。 这是否意味着挂单和 0 指标将不需再创建? 它们仍然会在另一个地方被创建。 因此,轮到另一个函数。
如此这里是 — 创建挂单指标和指标 0 的函数。 UpdateIndicators 代码如下:
#define macroUpdate(A, B) { if (B > 0) { \ if (b0 = (macroGetLinePrice(ticket, A) == 0 ? true : b0)) CreateIndicator(ticket, A); \ PositionAxlePrice(ticket, A, B); \ SetTextValue(ticket, A, vol, (isBuy ? B - pr : pr - B)); \ } else RemoveIndicator(ticket, A); } void UpdateIndicators(ulong ticket, double tp, double sl, double vol, bool isBuy) { double pr; bool b0 = false; if (ticket == def_IndicatorGhost) pr = m_Selection.pr; else { pr = macroGetLinePrice(ticket, IT_RESULT); if ((pr == 0) && (macroGetLinePrice(ticket, IT_PENDING) == 0)) { CreateIndicator(ticket, IT_PENDING); PositionAxlePrice(ticket, IT_PENDING, m_Selection.pr); ChartRedraw(); } pr = (pr > 0 ? pr : macroGetLinePrice(ticket, IT_PENDING)); SetTextValue(ticket, IT_PENDING, vol); } if (m_Selection.tp > 0) macroUpdate(IT_TAKE, tp); if (m_Selection.sl > 0) macroUpdate(IT_STOP, sl); if (b0) ChartRedraw(); } #undef macroUpdate
该函数有一个非常有趣的检查,在代码中高亮显示。 它将有助于创建幻影指标,因此 IndicatorAdd 函数将不再能够创建挂单指标和指标 0。 但仅仅执行此检查还不足以创建幻影指标。
DispatchMessage 函数现在包括一些细节,这些都是小修补,但它们令我们的生活更轻松。 我将展示已修改的部分:
void DispatchMessage(int id, long lparam, double dparam, string sparam) { // ... Code .... switch (id) { case CHARTEVENT_MOUSE_MOVE: // ... Code .... }else if ((!bMounting) && (bKeyBuy == bKeySell) && (m_Selection.ticket > def_IndicatorGhost)) { if (bEClick) SetPriceSelection(price); else MoveSelection(price); } break; // ... Code ... case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev) { case EV_CLOSE: // ... Code ... break; case EV_MOVE: CreateGhostIndicator(ticket, it); break; } break; } }
CHARTEVENT_MOUSE_MOVE 有部分修改。 代码将检查我们是否正在操控幻影。 如果是幻影,则片段被阻止。 但如果不是,则是可以移动的(前提是指标本身可以移动)。
一旦我们单击指标的新位置,幻影及其所有组件都被从对象列表中删除。 我认为这应该很清楚了。 现在注意高亮显示的位置 — 它调用的是 CreateGhostndicator 函数。 我们将在下一章节中讨论此代码。
2.0.6. CreateGhostIndicator 如何操作
CreateGhostIndicator 似乎是一个奇怪的函数。 我们看看下面的代码:
CreateGhostIndicator
#define macroSwapName(A, B) ObjectSetString(Terminal.Get_ID(), macroMountName(ticket, A, B), OBJPROP_NAME, macroMountName(def_IndicatorGhost, A, B)); void CreateGhostIndicator(ulong ticket, eIndicatorTrade it) { if (GetInfosTradeServer(m_Selection.ticket = ticket) != 0) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); macroSwapName(it, EV_LINE); macroSwapName(it, EV_GROUND); macroSwapName(it, EV_MOVE); macroSwapName(it, EV_EDIT); macroSwapName(it, EV_CLOSE); m_TradeLine.SetColor(macroMountName(def_IndicatorGhost, it, EV_LINE), def_IndicatorGhostColor); m_BackGround.SetColor(macroMountName(def_IndicatorGhost, it, EV_GROUND), def_IndicatorGhostColor); m_BtnMove.SetColor(macroMountName(def_IndicatorGhost, it, EV_MOVE), def_IndicatorGhostColor); ObjectDelete(Terminal.Get_ID(), macroMountName(def_IndicatorGhost, it, EV_CLOSE)); m_TradeLine.SpotLight(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); m_Selection.it = it; }else m_Selection.ticket = 0; } #undef macroSwapName
非常有趣的是,此函数中没有创建任何东西。 然而,如果编译 EA 并执行,它将创建幻影,并显示在服务器上的订单状态。 若要理解这一点,请观看以下视频。 这是系统在现实中如何操作的演示。
这就是幻影。 您实际上不会看到它们正在被创建,阅读代码时,没有任何地方能找到说:“这里... 我发现...幻影指标是在这一点创建的...” 事实是,它们早已经在图表上,但不会在任何地方显形,而在我们开始操纵订单或仓位之前 — 只在此刻它们才会变得可见。 这怎么可能?
为了理解这一点,我们来研究 EA 执行线程。
EA 初始化后,我们看到以下执行线程:
线程 1
<<<< System initialization thread
橙色区域是 EA 的一部分,绿色区域是 C_IndicatorTradeView 类的一部分。 查看在创建指标并在屏幕上显示之前会发生什么。 黑色箭头是挂单和持仓的常用路径;蓝色箭头是持仓路径;紫色箭头显示创建挂单指标的路径。 当然,函数内部有一些东西以一种或另一种方式引导线程,但这里的图表旨在展示所有操作的常规要领。
以前的规划流程图仅在系统启动期间运用一次。 现在,每次当我们在图表上放置挂单时,我们都会有两个不同的执行线程:第一个负责创建指标 0,并尝试在图表上放置订单。 如下图所示:
线程 2

请注意,并不是真正由类来创建在图表上显示的订单。 它只会尝试这样做。 如果一切顺利,SetPriceSelect 函数将成功执行,并将创建一个新线程,该线程将在图表上显示订单。 因此,我们将得到以下线程。 它实际上会将订单放在交易服务器报告的价位处,因此为了等待订单实际到达我们最初指定的位置是没有意义的。 如果波动性导致服务器执行订单时与我们指示的点位不同,则 EA 会纠正此问题,并将订单显示在正确的位置。 因此,您只需分析条件是否适合您的交易模型。
线程 3
<<< 挂单放置线程
这只是负责在图表上放置订单的部分。 这里我说的是完整订单,也就是说,它将有一个入场点、止盈和止损。 但是,如果其中一个限价订单(无论是止盈还是止损)从已下订单中删除,线程将会怎样? 这些线程不会对此做出响应。 实际上,那个线程将与此处的线程完全不同,但元素几乎相同。 我们看看下面的示例,如果您单击按钮关闭其中一个限价订单,流程会是什么样子。
这也许看起来很奇怪。
线程 4
<<< 删除一笔订单或中止单
我们有 2 个线程,一个紧接着另一个。 标有紫色箭头的那个将首先被执行。 一旦它被执行,OnTradeTransaction 事件将捕获来自服务器的响应,并触发系统从屏幕中删除指标。 删除中止单与平仓或删除订单之间只有一个区别:在这些情况下,不会执行 SetPriceSelect 函数,但 OnTradeTransaction 事件流则依旧保留。
这一切都很精彩,但它仍然没有回答幻影如何出现的问题。
为了理解幻影是如何创建的,我们需要知道执行线程是如何发生的:EA 如何下挂单,或指标 0 的创建在实际中是如何发生的。 此流程如上图所示。 如果您理解了该执行线程,您会更容易理解幻影。
我们最后来看看幻影是如何被创建的。 再来回顾函数 CreateGhostIndicator。 它不会创建任何东西,而只是操纵一些数据。 为什么? 因为如果我们尝试创建一个对象,它将覆盖在现有对象上,并在它们之上渲染。 因此,所需的对象将被掩盖。 此问题有两种解决方案。 第一个是创建一个比之所有其它更低级的集合。 它将在任何其它对象呈现之前创建。 但是这个解决方案有一个问题。 我们将有很多无用的对象。 但是我们正在修改整个代码,以便避免这种情况。 第二种解决方案是创建一个幻影,然后删除我们正在操作的指针,然后再次创建它。 这些解决方案都不是很实用,甚至它们的代价都非常昂贵。
在研究文档时,我发现了一些信息,并引起我注意:ObjectSetString 函数允许您操控一些乍一看没有意义的对象属性 — OBJPROP_NAME。 我很好奇为什么允许这样做。 这没有意义。 如果对象已经被创建,那么更改其名称有什么意义呢?
关键就在这里。 当我们重命名一个对象时,旧对象将不复存在,并获得新名称。 重命名后,该对象取代原始对象,因此 EA 可以毫无问题地再次创建原始对象,并且幻影可以显现和消失,而不会对图形产生副作用,也不会留下残影。 唯一需要删除的对象是指标关闭按钮。 这是在这一行代码中完成的:
ObjectDelete(Terminal.Get_ID(), macroMountName(def_IndicatorGhost, it, EV_CLOSE));
这里有一个次要细节。 查看 ObjectSetString 函数的文档,我们看到有关其操作的警告:
重命名图形对象时,会同时生成两个事件。 这些事件可以在 EA 或指标中使用 OnChartEvent() 函数进行处理:
- 删除旧名称对象的事件
- 创建新名称对象的事件
这一点很重要,因为我们不希望重命名后的对象,在我们还没有准备好之前就显示出来。 因此,我们在更名前、后再各加一件事:
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); // ... Secure code... ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
代码中的任何内容都不会触发对象创建和删除事件。 我们现在有了幻影在何处出现的完整代码,我们将做出正确的行为。
也许,目前还不清楚代码如何通过简单地重命名指标来实际创建幻影。 在此我把它留给您。 为了帮助您,我将展示幻影执行线程的模样。 这个如下图所示:
线程 5
<<<< 幻影线程
请注意,这是线程 2 近乎完美的克隆,因此您已经可以享受到如何创建和销毁幻影的乐趣,但无需实际编写任何创建幻影的代码。
结束语
作为本文的一名作者,我发现这篇文章非常有趣,甚至令人兴奋。 好吧,我们不得不修改很多 EA 代码。 但这一切都是为了更好。 仍然需要采取少许事情和步骤来令其更加可靠。 但是,已经实现的修改将令整个系统受益匪浅。 我想强调的是,一个设计优良的程序通常会经历此处已实现的某些步骤:研究文档、分析执行线程、对系统进行基准测试、从而查看它是否在关键时刻过载,最重要的是,保持冷静,以免将代码变成真正的怪物。 避免把我们的代码变成弗兰肯斯坦(Frankenstein — 补丁怪)的副本非常重要,因为这不会令代码更好,只会让未来的改进,尤其是矫正更加困难。
温馨拥抱所有关注本系列的人。 希望在下一篇文章中再次见到您,因为我们依旧还没有完成,还有很多事情要做。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10606
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


