English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)

从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)

MetaTrader 5示例 | 2 十一月 2022, 13:15
1 425 0
Daniel Jose
Daniel Jose

概述

在上一篇文章提供系统健壮性 (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

init_ea  <<<< 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

附加的文件 |
数据科学与机器学习(第 06 部分):梯度下降 数据科学与机器学习(第 06 部分):梯度下降
梯度下降在训练神经网络和许多机器学习算法中起着重要作用。 它是一种快速而智能的算法,尽管它的工作令人印象深刻,但它仍然被许多数据科学家误解,我们来看看有关它的全部。
DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象 DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象
在本文中,我将继续创建 WinForms 对象列表的基准对象,以及两个新对象:ListBox 和 ButtonListBox。
神经网络变得轻松(第二十二部分):递归模型的无监督学习 神经网络变得轻松(第二十二部分):递归模型的无监督学习
我们继续研究无监督学习算法。 这次我建议我们讨论自动编码器应用于递归模型训练时的特性。
从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I) 从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I)
在本文中,我们将令系统更加可靠,来确保健壮和安全的使用。 实现所需健壮性的途径之一是尝试尽可能多地重用代码,从而能在不同情况下不断对其进行测试。 但这只是其中一种方式。 另一个是采用 OOP。