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

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

MetaTrader 5交易 | 24 十月 2022, 10:05
1 074 0
Daniel Jose
Daniel Jose

概述

有些事情并不那么简单,尽管有些人也许会想当然。 订单系统便是其中之一。 您甚至可以创建一个更适度的系统,为您提供完美的服务,就像我们在从头开始开发智能交易系统的文章中所做的那样,其中我们创建了一个基本系统,它对于大多人都有用,而对某些人来说还不够。 因此,当一切都开始变化,这一时刻就到来了 — 它就是本系列第一部分当中有关新订单系统的诞生。 这可以在从头开始开发智能交易系统(第 18 部分)一文中看到。 这就是我们开发一个系统的起始之处,该系统可以由 EA 管控,同时由 MetaTrader 5 提供支持。 该系统的思路是在图表上的订单没有限制。 起初,这个系统似乎相当冒险,我必须承认一个事实,即创建一个系统,其中的对象不是由 EA 维护,而是由 MetaTrader 5 来维护,这在我看来是毫无意义且低效的。

然而,该系统正处于开发中,在文章从头开始开发智能交易系统(第 23 部分)中,我们开发了一个幻影系统令订单、持仓或破位(止盈和止损)的管理更灵活。 开发过程非常有趣,但遇到一个问题。 如果您查看用到的和可见的对象数量,与 MetaTrader 5 支持的对象数量相比,您肯定会感到惊讶,因为受支持的对象数量总是更高。

在很多情况下,该问题并不那么严重,您甚至可以忍受片刻。 但有两个问题令该系统在市场高波动期间不是很稳定。 在某些状况下,它们会强制用户做出不正确的行动。 这是因为当交易者添加挂单时,系统将其发送到服务器,服务器有时需要耗费比平时更多的时间来响应。 系统在某些时刻指示有订单,而在其它时刻则表明没有订单。 且当针对持仓执行完毕时(请参阅订单和仓位之间差异的文档),这会变得更加棘手,因为不知道服务器能否按预期执行命令。

有几种方式可以解决此问题。 其中一些很简单,而有一些较复杂。 无论如何,我们必须信任 EA,否则我们在任何境况下都不应该使用它。


1.0. 计划

这里的最大问题是设计一个兼具两个品质的系统:速度可靠性。 在某些类型的系统中,这很困难,甚至不可能实现兼顾两者。 如此这般,在许多情况下,我们试图平衡事态。 但由于它涉及资金,我们的血汗钱,我们不想冒险去得到一个不具备这些品质的系统。 必须要牢记的是,我们正在与一个实时操作的系统打交道,这是开发人员遭遇的最困难的场景,因为我们应该始终尝试拥有一个极端快捷的系统:它必须立即对事件做出反应,且当我们尝试改进它时能表现出足够的可靠性,不至于崩溃。 因此,这项任务显然相当困难。

确保以最合适的方式调用和执行函数,避免不必要的调用,尤其是不必要的次数,如此可以达成速度提升。 这将在语言范围内提供尽可能快速的系统。 不过,如果我们想要某方面更快,那么我们必须下沉到机器语言级别,在这种情况下,我们指的是汇编语言。 但这往往是不必要的,我们能用 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;


MOVE 这个事件能确定做到这一点的 — 它选择要移动的指标。 因此,它只选择,但移动本身是在鼠标移动事件期间执行的。 请记住,在主题开始时,我曾说过,在某个条件下我们不用处理指标 0,即使如此,某些东西仍然会移动。 此刻,某些内容会在移动事件中得到指示。 我们在此检查有无任何已选择的东西需要移动。 如果有的话,则所选指标将禁止选择,且不会接收鼠标移动事件,也不会再选择新指标。 在这种情况下,新指标接收自鼠标的数据将存储在一个结构当中,且该指标将收到变化,示意它已被选中。 这种变化可从线条的粗细看出。


2.0.4. 新的鼠标对象类

除了我们上面介绍的改进之外,我们还有其它值得一提的改进。

大多数交易者不需要在 EA 中实现的基于鼠标的指标系统,但另外一些人也许就需要,并希望该系统能完美运行。 但交易者也许会错误地删除一些构成鼠标指标的对象,而这可定会导致其崩溃。 幸运的是,我们可以依靠 EVENT 系统来避免这种情况。 一旦检测到对象删除事件就发送到 EA,该对象所属的类就能再次重新创建该对象,从而为系统提供稳定性。 但是,最好保持关键点列表尽可能小,根据需要创建它们,然后在不再需要它们时删除它们。 这就是我们到目前为止一直在做的事情,但 Mouse 类错过了。

我们从创建一些定义开始,来替换创建常量名称的系统。

#define def_MousePrefixName "MOUSE "
#define def_NameObjectLineH def_MousePrefixName + "H"
#define def_NameObjectLineV def_MousePrefixName + "TMPV"
#define def_NameObjectLineT def_MousePrefixName + "TMPT"
#define def_NameObjectBitMp def_MousePrefixName + "TMPB"
#define def_NameObjectText  def_MousePrefixName + "TMPI"


之后,新的初始化函数如下所示:

void Init(color c1, color c2, color c3)
{
        m_Infos.cor01 = c1;
        m_Infos.cor02 = c2;
        m_Infos.cor03 = c3;
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_MOUSE_MOVE, true);
        ChartSetInteger(Terminal.Get_ID(), CHART_CROSSHAIR_TOOL, false);
        Show();
}


请注意,它比以前版本简单得多。 此处,我们由调用显示鼠标系统。 调用在前面代码中的高亮显示位置执行。 它调用代码在价格轴上创建实际的指示系统。

inline void Show(void)
{
        if (ObjectGetDouble(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_PRICE) == 0)
        {
                ObjectCreate(Terminal.Get_ID(), def_NameObjectLineH, OBJ_HLINE, 0, 0, 0);
                ObjectSetString(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_TOOLTIP, "\n");
                ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_BACK, false);
        }
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, m_Infos.cor01);
}

这段代码非常有趣:它检查鼠标指针对象是否存在于价格系当中。 如果检查成功,则意味着图表上有一条线,或与鼠标相关的内容,如此我们要做的就是调整水平线的颜色。 我们为什么要执行该检查? 若要理解这一点,请查看负责隐藏或删除与鼠标连接对象的函数。 请参阅下面的函数:

inline void Hide(void)
{
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false);
        ObjectsDeleteAll(Terminal.Get_ID(), def_MousePrefixName + "T");
        ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineH, OBJPROP_COLOR, clrNONE);
}


这种是一种有趣的操作方式。 所有与鼠标连接,并匹配指定名称的对象都将从 MetaTrader 5 图表中删除,如此这般可令对象列表始终很小。 然而,水平线不会被删除,仅仅是改变它的颜色。 因此,显示鼠标的函数需在创建对象之前执行检查,因为它实际上并未从对象列表中排除,而只是隐藏了。 但所有其它对象将从对象列表中删除。 但在调研期间,我们应如何使用其它这些对象呢? 由于调研是短暂的时刻,我们只是想找出一些细节,因此把那些我们只会用到 1-2 次的对象保留在列表中是没有意义的。 最好是创建它们,马上进行调研,然后就把它们从列表中删除,这样我们才能得到一个更可靠的系统。

这也许看起来很愚蠢,但我们所示的订单系统是基于对象的,列表中的对象越多,当我们想要访问其中某个对象时,MetaTrader 5 搜索列表的工作量就越多。 因此,我们不会在图表或对象列表中留下额外的对象,让我们保持系统尽可能轻巧。

现在,请注意 DispatchMessage 函数,其启动方式如下:

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        int     w = 0;
        uint    key;
        static int b1 = 0;
        static double memPrice = 0;


紧接着,我们拥有的代码将开始处理第一个事件。

switch (id)
{
        case CHARTEVENT_MOUSE_MOVE:
                Position.X = (int)lparam;
                Position.Y = (int)dparam;
                ChartXYToTimePrice(Terminal.Get_ID(), Position.X, Position.Y, w, Position.dt, Position.price);
                ObjectMove(Terminal.Get_ID(), def_NameObjectLineH, 0, 0, Position.price = Terminal.AdjustPrice(Position.price));
                if (b1 > 0) ObjectMove(Terminal.Get_ID(), def_NameObjectLineV, 0, Position.dt, 0);
                key = (uint) sparam;
                if ((key & 0x10) == 0x10)    //Middle button....
                {
                        CreateObjectsIntern();
                        b1 = 1;
                }


当我们按下鼠标中键时,我们会生成一次调用。 但现在情况并非如此。 然后我们将看到这个函数的作用。 请注意,我们试图移动一个不存在的对象,因为它不在 MetaTrader 5 所支持的对象列表当中。 仅当按下鼠标中键时,才会发生此调用。 请注意 b1 变量,该变量控制交易者在调研时生成的所涉集合内的点位。

一旦用户单击鼠标左键并完成第一步,我们将运行以下代码:

if (((key & 0x01) == 0x01) && (b1 == 1))
{
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, false);
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 0, Position.dt, memPrice = Position.price);
        b1 = 2;
}

它将定位趋势线,并在更改 b1 变量值后将调用下一步。 在此处,我们可以移步到下一个片段。

if (((key & 0x01) == 0x01) && (b1 == 2))
{
        ObjectMove(Terminal.Get_ID(), def_NameObjectLineT, 1, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectLineT, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_COLOR, (memPrice > Position.price ? m_Infos.cor03 : m_Infos.cor02));
        ObjectMove(Terminal.Get_ID(), def_NameObjectBitMp, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectBitMp, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
        ObjectSetString(Terminal.Get_ID(), def_NameObjectText, OBJPROP_TEXT, StringFormat("%.2f ", Position.price - memPrice));
        ObjectMove(Terminal.Get_ID(), def_NameObjectText, 0, Position.dt, Position.price);
        ObjectSetInteger(Terminal.Get_ID(), def_NameObjectText, OBJPROP_ANCHOR, (memPrice > Position.price ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER));
}


上面的这个片段实际上是在屏幕上显示所调研的内容。 当调研结束时,此片段中的所有对象都将不复存在,它们都在此例程中创建和销毁。 虽然这样做似乎不是很有效率,但我没有发现在研习阶段花费的处理时间有任何减少或增加。 事实上,我确实注意到订单系统略有改进,只是非常微妙,而实际上处于可比较评估的误差范围内。 由此,我不能说这些变化带来了处理方面的实际改进。

但请注意,该调研将在按下鼠标左键时执行;一旦我们松开它,就会执行下一个片段。

if (((key & 0x01) != 0x01) && (b1 == 2))
{
        b1 = 0;
        ChartSetInteger(Terminal.Get_ID(), CHART_MOUSE_SCROLL, true);
        Hide();
        Show();
}
Position.ButtonsStatus = (b1 == 0 ? key : 0);


在此,我们从对象列表中删除为调研而创建的所有对象。 我们在屏幕上再次显示鼠标线。 高亮显示的代码是一个好主意,因为它可以防止 EA 中的任何函数或子例程在我们捕获鼠标按钮时误读数值。 如果正在进行任何调研,则 EA 应忽略按钮状态。 为此目的,我们使用高亮显示的行。 这并不是一个完美的解决方案,但总比没有强。

我们没有考虑为运行调研而创建对象的代码。 但由于这是一个相当简单的函数,我不会在本文中特意关注它。


结束语

尽管这些变化也许看起来微不足道,但它们都会对系统本身产生重大影响。 有一件事要记住:我们的订单系统基于屏幕上的图形对象,因此 EA 处理的对象越多,当我们请求特定对象时,其性能就越低效。 致使情况进一步复杂化的是,系统是实时运行的,即我们的 EA 系统越快捷,其性能就越好。 因此,EA 需要做的事情越少越好。 理想情况下,它应该只能与订单系统一起工作,且我们应将其它一切都提升到另一个层次,而 MetaTrader 5 理应关心它。 当然,我们将逐步这样做,因为我们将不得不进行许多小的修改,但都不会太复杂。 在接下来的几篇专门介绍提高 EA 可靠性的文章中会完成这些。

我可以肯定地说一件事:将来,EA 将只负责订单系统。 在下一篇文章中,我们将赋予 EA 一个非常有趣的最终外观:我们将进一步减少 EA 操作期间列表中存在的对象数量,因为订单系统是一个大型对象生成器,并将看到如何以这种方式改进系统,令在 MetaTrader 5 上创建它时的负载最小化。

有鉴于此,我不会在本文里附带任何修改,因为代码本身仍将不断发生变化。 但不必担心,等待下一篇文章是很值得的。 这样的修改将显著提高我们的 EA 交易的整体性能。 那么,期待在本系列的下一篇文章中再相见。


本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/10593

DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象 DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象
在本文中,我将继续创建 WinForms 对象列表的基准对象,以及两个新对象:ListBox 和 ButtonListBox。
神经网络变得轻松(第二十一部分):变分自动编码器(VAE) 神经网络变得轻松(第二十一部分):变分自动编码器(VAE)
在上一篇文章中,我们已熟悉了自动编码器算法。 像其它任何算法一样,它也有其优点和缺点。 在其原始实现中,自动编码器会尽可能多地将对象与训练样本分开。 这次我们将讨论如何应对它的一些缺点。
从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II) 从头开始开发智能交易系统(第 25 部分):提供系统健壮性(II)
在本文中,我们将朝着 EA 的性能迈出最后一步。 为此,请做好长时间阅读的准备。 为了令我们的智能交易系统可靠,我们首先从代码中删除不属于交易系统的所有内容。
学习如何基于标准偏差设计交易系统 学习如何基于标准偏差设计交易系统
此为我们该系列中的一篇新文章,介绍如何利用 MetaTrader 5 交易平台中最受欢迎的技术指标来设计交易系统。 在这篇新文章中,我们将学习如何运用标准偏差指标设计交易系统。