从头开始开发智能交易系统(第 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
<<<< 指标 0 初始化线程 请注意,并不是真正由类来创建在图表上显示的订单。 它只会尝试这样做。 如果一切顺利,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