English Русский Español Deutsch 日本語 Português
preview
构建自动运行的 EA(第 11 部分):自动化(III)

构建自动运行的 EA(第 11 部分):自动化(III)

MetaTrader 5交易 | 24 五月 2023, 08:53
1 364 1
Daniel Jose
Daniel Jose

概述

在上一篇文章“构建自动运行的 EA(第 10 部分):自动化(II)”中,我们研究了添加 EA 操作计划控制的方法。 而当整个 EA 系统已经构建为优先考虑自治,但在我们即将获得 100% 自动化 EA 的最后阶段之前,我们需要对代码进行一些小的修改。

在自动化阶段,我们不得以任何方式修改、创建或更改现有代码的任何部分。 我们只需要删除交易者和 EA 之间存在交互的那些点,并添加某种自动触发器。 应避免对旧代码进行任何进一步的修改。 若是您,在实现自动化功能时必须修改旧代码,则意味着代码规划不周,您必须返工所有规划才能令系统具有以下特征。

  • 健壮:系统不应含有破坏代码任何部分完整性的主要错误;
  • 可靠:可靠的系统是能经受许多潜在危险情况影响,但仍可无故障运行的系统;
  • 稳定:在某些时候运行良好的系统,然却无法解释地令平台崩溃,这样的系统是没有意义的;
  • 可扩展:系统应该能平稳增长,且不涉及大量编程;
  • 封装:只有真正需要使用的功能,才有必要在所创建代码之外的地方可见;
  • 迅捷:如果由于准备的代码不足而运行缓慢,那么即使拥有最佳模型也是没有意义的。

其中一些功能涉及面向对象的编程模型。 实际上,此模型是迄今为止最适合创建需要良好安全级别的程序的模型。 有因于此,从本系列开始,您一定已经注意到,所有的编程都集中在类的使用上,这意味着正是应用了面向对象的编程。 我知道这种类型的编程一开始可能看起来令人困惑且难以学习,但相信我,如果您真正付出努力,并学会将代码创建为类,您将受益匪浅。

我这样说是为了向你们展示,作为有抱负的专业程序员,应如何创建您的代码。 您必须习惯使用 3 个文件夹,其中将保存代码。 无论程序多么复杂或简单,您都必须始终分为 3 个步骤工作:

  1. 在第一阶段,即开发文件夹中,我们创建、修改和测试所有代码。 任何新功能或更改都必须仅在此文件夹中的代码中进行。
  2. 构建并测试代码后,必须将其移至第二个文件夹,即工作文件夹。 在此文件夹中,代码也许仍包含一些错误,但您不应在此处修改它。 如果您需要编辑在此文件夹中的代码,请将其移回开发文件夹。 一个细节:如果您编辑代码只是纠正一些发现的缺陷,而不进行任何其它更极端的修改,则可以将此工作文件夹的代码保留在其中,并接受适当的更正。
  3. 最后,在不同情况下反复运行代码若干次,而无需任何新的更改后,将其移至第三个也是最后一个“稳定”文件夹。 此文件夹中存在的代码现在已经证明无缺陷,并且对于为其设计的任务非常有用和高效。 切勿在此文件夹中添加新的代码。

如果您应用这种方式一段时间,您最终会创建一个非常有趣的函数和过程数据库,您就能够非常快速和安全地编程。 这种事情正得到高度赞赏,尤其是在金融市场这样的活动中,没有人会有兴趣使用无法应对市场存在风险的代码。 鉴于我们始终在一个不接受运行时错误的领域工作,并且一切都发生在最坏的场景类型(即实时)中,因此您的代码必须能够随时抵抗意外事件。

综上所述,都是为了达到以下几点,如图例 01 所示:

图例 01

图例 01. 手动控制系统

许多人并不完全明白他们正在进行的编程究竟做什么或创建什么。 这是因为他们中的许多人并不真正理解系统内部发生了什么,且最终认为平台应该提供交易者想要做的事情。 然而,如果您查看图例 01,您就明白该平台并不会专注于提供交易者想要的东西。 取而代之,它应该提供与交易者互动的方式,同时能与交易服务器稳定、快速、高效地互动。 与此同时,平台必须保持运行,并且必须支持与交易者交互的元素。

请注意,没有一支箭超越其顶点,这表明这是一个手动交易系统。 另外,请注意,EA 由平台提供服务,而不是绕其它路,交易者不直接与 EA 通信。 尽管也许看起来很奇怪,但实际上交易者通过平台访问 EA,而平台控制 EA 的手段则是向它发送交易者创建或执行的事件。 作为响应,EA 向平台发送请求,必须经由平台发送到交易服务器。 当服务器响应时,它会将这些响应返回给平台,平台再将它们转发给 EA。 EA 在分析和处理服务器的响应后,向平台发送一些信息,以便它可以向交易者示意正在发生的事情,或交易者所提交请求的结果。

许多人没有意识到这样的事情。 如果 EA 中发生任何故障,平台并不会有此问题。 问题出在 EA 上,但经验不足的交易者可能会错误地责怪平台没有做到他们想干的事。

如果您不曾作为程序员参与平台的开发和维护,那么您不应该试图影响它的工作方式。 确保代码响应平台的相应需求。

我们终于遇到一个问题:为什么要创建一个手动 EA,并运营一段时间,然后才将其自动化? 准确的原因是这样 — 我们创造了一种方式来实际测试代码,并准确创建我们需要的东西,不多也不少。

为了正确自动化系统,且在不影响控制点和订单系统哪怕一行代码的情况下,我们需要添加一些功能,以及略微的修改。 因此,在上一篇文章中创建的代码将放置在工作文件夹中,在上一篇文章中创建的代码将放置在稳定文件夹中,本文中提供的代码将移至开发文件夹之中。 通过这种方式,开发过程得到了扩展和演变,而我们有了速度更快的编码。 如果出现一些问题,我们总能够退回两个版本,其操作并无异常。


实现修改

我们实际要做的第一件事是修改计时系统。 此修改显示在下面的代码中:

virtual const bool CtrlTimeIsPassed(void) final
                        {
                                datetime dt;
                                MqlDateTime mdt;
                                
                                TimeToStruct(TimeLocal(), mdt);
                                TimeCurrent(mdt);
                                dt = (mdt.hour * 3600) + (mdt.min * 60);
                                return ((m_InfoCtrl[mdt.day_of_week].Init <= dt) && (m_InfoCtrl[mdt.day_of_week].End >= dt));
                        }

被删除的行已替换为高亮显示的行。 我们为什么要进行这些修改? 原因有二:首先,我们将两个调用替换为一个。 在删除的行中,我们首先有一个调用来获取时间,然后另一个调用来将其转换为结构。 第二个原因是 TimeLocal 实际上返回的是计算机时间,而不是市场观察元素中显示的时间,如图例 02 所示。

图例 02

图例 02. 服务器在上次更新中提供的时间。

如果通过 NTP 服务器(保持最新时间的官方服务器)进行同步,则用计算机时间不是问题。 然而,大多数情况下人们不用此类服务器。 故此,时间控制系统可能会令 EA 更早进入或退出。 为了避免这种不便,有必要进行更改。

进行的这些变化不是为了从根本上修改代码,而是为交易者提供所期望的更大程度的稳定性。 因为如果 EA 比预期更早进入和退出,交易者可能会认为平台或代码中存在错误。 但原因实际上是由于计算机和交易服务器之间缺乏时间同步造成的。 交易服务器很可能使用 NTP 服务器来维持官方时间,而进行操作的计算机可能并未用到此服务器。

以下更改是在订单系统中实现的:

                ulong ToServer(void)
                        {
                                MqlTradeCheckResult     TradeCheck;
                                MqlTradeResult          TradeResult;
                                bool bTmp;
                                
                                ResetLastError();
                                ZeroMemory(TradeCheck);
                                ZeroMemory(TradeResult);
                                bTmp = OrderCheck(m_TradeRequest, TradeCheck);
                                if (_LastError == ERR_SUCCESS) bTmp = OrderSend(m_TradeRequest, TradeResult);
                                if (_LastError != ERR_SUCCESS) MessageBox(StringFormat("Error Number: %d", GetLastError()), "Order System", MB_OK);
                                if (_LastError != ERR_SUCCESS) PrintFormat("Order System - Error Number: %d", _LastError);
                                return (_LastError == ERR_SUCCESS ? TradeResult.order : 0);
                        }

这也是令系统可用于其预期目标的必要修改,以便 EA 可以实现自动化,且不会有太多麻烦。 事实上,删除的代码由高亮显示的代码替换,不是因为代码可以更快或更稳定,而是因为必须处理相应错误的发生。 当我们有一个自动化的 EA 时,某些类型的故障可以被忽略,正如我们在之前的文章中所讨论的那样。

问题是删除的行会始终启动一个消息框,通知有关错误,但在某些情况下,代码可以正确应对错误,故不需要该消息框。 在这种情况下,我们可以简单地在终端中打印一条消息,以便交易者可以采取相应的行动。

请记住,100% 自动 EA 不能等待交易者做出决定。 尽管这可能需要一会儿时间,但如果不报告发生了什么样的问题,您就不能做事。 一旦再次修改代码,则是为了提高敏捷性。 没有重大更改,故无需将系统置于更密集的测试阶段,其原本是排查可能由修改引起的故障。

但与上面所做的这些修改不同,现在我们添加的其它修改需要深入测试,因为其将影响系统的工作方式。


为自动化铺平道路

现在即将进行的修改将令我们能够有效地创建一个 100% 自动化系统。 如果没有这些修改,我们将在下一篇文章中被束缚双手,在那里我将展示如何将已测试过的 EA(我希望您正在执行所有必需的测试,从而了解一切实际工作原理)转变为自动化 EA。 为了实现必要的修改,我们需要删除,或最好说,修改某些内容,并添加其它内容。 我们从修改开始。 下面的代码描述了将要修改的内容:

//+------------------------------------------------------------------+
#include "C_ControlOfTime.mqh"
//+------------------------------------------------------------------+
#define def_MAX_LEVERAGE                10
#define def_ORDER_FINISH                false
//+------------------------------------------------------------------+
class C_Manager : public C_ControlOfTime

这两个定义将不复存在。 取而代之的是出现 2 个新变量,交易者无法修改这些变量,但可以由您(程序员)定义。 为什么要做出这样的改变? 原因在于当我们进行这些修改时,原本的定义将被变量替换,我们将在速度方面有所损失。 即使仅是几个机器周期,实际上也会有很小的性能损失,因为访问常量值比访问变量要快得多。 但作为回报,我们将在类重用中获得收益,您将在下一篇文章中更好地理解这一点。 相信我,易用性和便携性方面的差异弥补了性能的些微损失。 故此,上述两行已替换为以下内容:

class C_Manager : public C_ControlOfTime
{
        enum eErrUser {ERR_Unknown, ERR_Excommunicate};
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage,
                                MaxLeverage;
                        bool    IsDayTrade,
                                IsOrderFinish;                                          
                }m_InfosManager;

当编写代码时要小心:作为程序员,您不应在初始化这两个变量的位置之外更改这两个变量的值。 非常小心不要这样做。 初始化它们的位置恰好在类构造函数中,如以下代码所示:

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger, const bool IsOrderFinish, const uint MaxLeverage)
                        :C_ControlOfTime(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.IsOrderFinish    = IsOrderFinish;
                                m_InfosManager.MaxLeverage      = MaxLeverage;
                                m_InfosManager.FinanceStop      = FinanceStop;
                                m_InfosManager.FinanceTake      = FinanceTake;
                                m_InfosManager.Leverage         = Leverage;
                                m_InfosManager.IsDayTrade       = IsDayTrade;

构造函数现在将收到两个初始化变量的新参数。 之后,我们将在定义被实例化的位置进行修改。 这些更改将在以下几处上进行:

inline int SetInfoPositions(void)
                        {
                                double v1, v2;
                                int tmp = m_Position.Leverage;
                                
                                m_Position.Leverage = (int)(PositionGetDouble(POSITION_VOLUME) / GetTerminalInfos().VolMinimal);
                                m_Position.IsBuy = ((ENUM_POSITION_TYPE) PositionGetInteger(POSITION_TYPE)) == POSITION_TYPE_BUY;
                                m_Position.TP = PositionGetDouble(POSITION_TP);
                                v1 = m_Position.SL = PositionGetDouble(POSITION_SL);
                                v2 = m_Position.PriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
                                if (def_ORDER_FINISH) if (m_TicketPending > 0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                m_Position.EnableBreakEven = (def_ORDER_FINISH ? m_TicketPending == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                if (m_InfosManager.IsOrderFinish) if (m_TicketPending > 0) if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                m_Position.EnableBreakEven = (m_InfosManager.IsOrderFinish ? m_TicketPending == 0 : m_Position.EnableBreakEven) || (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);

                                return m_Position.Leverage - tmp;
                        }

inline void TriggerBreakeven(void)
                        {
                                double price;
                                
                                if (PositionSelectByTicket(m_Position.Ticket))
                                        if (PositionGetDouble(POSITION_PROFIT) >= m_Trigger)
                                        {
                                                price = m_Position.PriceOpen + (GetTerminalInfos().PointPerTick * (m_Position.IsBuy ? 1 : -1));
                                                if (def_ORDER_FINISH)
                                                if (m_InfosManager.IsOrderFinish)
                                                {
                                                        if (m_TicketPending > 0) m_Position.EnableBreakEven = !ModifyPricePoints(m_TicketPending, price, 0, 0);
                                                }else m_Position.EnableBreakEven = !ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
                                        }
                        }

                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }

                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= def_MAX_LEVERAGE) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                tmp = C_Orders::ToMarket(type, (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceStop), (def_ORDER_FINISH ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                        {
// ... The rest of the code ...

                void UpdatePosition(const ulong ticket)
                        {
                                int ret;
                                double price;
                                
                                if ((ticket == 0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == 0)) return;
                                if (PositionSelectByTicket(m_Position.Ticket))
                                {
                                        ret = SetInfoPositions();
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -1 : 1));
                                                if (m_TicketPending > 0) if (OrderSelect(m_TicketPending))
                                                {
                                                        price = OrderGetDouble(ORDER_PRICE_OPEN);
                                                        C_Orders::RemoveOrderPendent(m_TicketPending);
                                                }
                                                if (m_Position.Ticket > 0)      m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, 0, 0, m_Position.Leverage, m_InfosManager.IsDayTrade);
                                        }
                                        m_StaticLeverage += (ret > 0 ? ret : 0);
                                }else
                                {
                                        ZeroMemory(m_Position);
                                        if ((def_ORDER_FINISH) && (m_TicketPending > 0))
                                        if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0))
                                        {
                                                RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                        }
                                }
                                ResetLastError();
                        }

inline void TriggerTrailingStop(void)
                        {
                                double price, v1;
                                
                                if ((m_Position.Ticket == 0) || (def_ORDER_FINISH ? m_TicketPending == 0 : m_Position.SL == 0)) return;
                                if ((m_Position.Ticket == 0) || (m_InfosManager.IsOrderFinish ? m_TicketPending == 0 : m_Position.SL == 0)) return;
                                if (m_Position.EnableBreakEven) TriggerBreakeven(); else
                                {
                                        price = SymbolInfoDouble(_Symbol, (GetTerminalInfos().ChartMode == SYMBOL_CHART_MODE_LAST ? SYMBOL_LAST : (m_Position.IsBuy ? SYMBOL_ASK : SYMBOL_BID)));
                                        v1 = m_Position.SL;
                                        if (def_ORDER_FINISH)
                                        if (m_InfosManager.IsOrderFinish)
                                                if (OrderSelect(m_TicketPending)) v1 = OrderGetDouble(ORDER_PRICE_OPEN);
                                        if (v1 > 0) if (MathAbs(price - v1) >= (m_Position.Gap * 2)) 
                                        {
                                                price = v1 + (m_Position.Gap * (m_Position.IsBuy ? 1 : -1));
                                                if (def_ORDER_FINISH)
                                                if (m_InfosManager.IsOrderFinish)
                                                        ModifyPricePoints(m_TicketPending, price, 0, 0);
                                                else    ModifyPricePoints(m_Position.Ticket, m_Position.PriceOpen, price, m_Position.TP);
                                        }
                                }
                        }

绝对所有移除的部分都已被高亮显示的片段替换。 通过这种方式,我们不仅提升了急需的类重用,而且还在可用性方面对其进行了改进。 尽管在本文中对此并不十分清晰,但在下一篇文章中,您将看到如何完成此操作。

除了已经进行的修改之外,我们还需要添加一些内容,以便自动化系统能最大程度地访问订单发送系统,从而提高类的重用率。 为此,我们首先添加以下函数:

inline void ClosePosition(void)
	{
		if (m_Position.Ticket > 0)
		{
			C_Orders::ClosePosition(m_Position.Ticket);
			ZeroMemory(m_Position.Ticket);
		}                               
	}

这个函数在某些操作模型中是必需的,所以我们需要把它包含在 C_Manager 类代码当中。 不过,在我们添加此函数后,编译器在尝试编译代码时会生成几个警告。 参见如下图例 03.

图例 03

图例 03. 编译警告

与可以忽略的编译器警告(尽管强烈建议不要这样做)不同,图例 03 中的警告可能对程序造成潜在危害,并可能导致生成的代码无法正常工作。

理想情况下,当您注意到编译器已生成此类警告时,应尝试纠正生成这些警告的故障。 有时它很容易解决,有时它却有点复杂。 这也许是一种更改类型,其中部分数据在转换过程中丢失。 无论走那条路,查看编译器生成此类警告的原因始终很重要,即使代码已被编译完毕也应如此。

编译器警告的存在表明代码中某些内容运行不顺利,因为编译器难以理解您正在编程的内容。 如果编译器不能理解它,就不会生成 100% 可靠的代码。

一些编程平台允许您关闭编译器警告,但我个人不建议这样做。 如果您想拥有 100% 可靠的代码,最好启用所有警告。 随着时间的推移,您将意识到,保留标准平台设置是确保代码更可靠的最佳方式。

我们有两种选择来解决上述警告。 第一种是将当前引用 C_Orders 类中存在的 ClosePosition 函数调用替换为 C_Manager 类中添加的新函数。 这是最好的选择,因为我们将检查 C_Manager 中存在的调用。 第二个选项是告诉编译器调用将引用 C_Orders 类。

但是我将修改代码,以便调用新创建的函数。 故此,生成警告的问题点将得到解决,编译器能明白我们正在尝试做什么。

                ~C_Manager()
                        {
                                if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
                                {
                                        if (m_TicketPending > 0) RemoveOrderPendent(m_TicketPending);
                                        if (m_Position.Ticket > 0) ClosePosition(m_Position.Ticket);
                                        ClosePosition();
                                        Print("EA was kicked off the chart for making a serious mistake.");
                                }
                        }

最简单的方法是在析构函数中解决此问题,但有一个棘手的部分,如下所示:

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                if (PositionSelectByTicket(m_Position.Ticket)) ClosePosition(m_Position.Ticket);
                                                ClosePosition();
                                                if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                                                ZeroMemory(m_Position.Ticket);
                                                m_TicketPending = 0;
                                                ResetLastError();
                                        }else   SetUserError(ERR_Unknown);
                                }else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket);
                                m_TicketPending = 0;
                                if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
                                CheckToleranceLevel();
                        }

我删除了一些代码行,并加入了平仓的调用。但是,如果挂单因任何原因变为持仓,我们必须如同原始代码中那样删除该笔持仓。 但该笔持仓尚未被 C_Manager 类所捕获。 在这种情况下,我们告诉编译器调用应引用 C_Orders 类,如高亮显示的代码所示

以下是我们需要进行的另一处修改:

inline void EraseTicketPending(const ulong ticket)
                        {
                                if ((m_TicketPending == ticket) && (m_TicketPending > 0))
                                {
                                        if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); 
                                        else RemoveOrderPendent(m_TicketPending);
                                        m_TicketPending = 0;
                                }
                                ResetLastError();
                                m_TicketPending = (ticket == m_TicketPending ? 0 : m_TicketPending);
                        }

划掉的代码,即原始代码,已由稍微复杂的代码所取代;但它赋予我们更强的能力删除挂单,或者,若它已成为持仓,则删除它。 以前,我们只是响应 MetaTrader 5 平台通知我们的事件,导致为挂单指示的单号值设置为零,如此就可发送新的挂单。 但现在我们要做的远不止这些,因为我们需要在 100% 自动化系统中使用此类功能。

通过实现这些小的更改,我们获得了系统范围的奖励,这将增加代码重用和测试。


最后阶段之前的最终改进

在最后阶段之前,我们可以实现更多改进,从而提高代码重用率。 第一处显示在以下代码中:

                void UpdatePosition(const ulong ticket)
                        {
                                int ret;
                                double price;
                                
                                if ((ticket == 0) || (ticket != m_Position.Ticket) || (m_Position.Ticket == 0)) return;
                                if (PositionSelectByTicket(m_Position.Ticket))
                                {
                                        ret = SetInfoPositions();
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                price = m_Position.PriceOpen + (FinanceToPoints(m_InfosManager.FinanceStop, m_Position.Leverage) * (m_Position.IsBuy ? -1 : 1));
                                                if (m_TicketPending > 0) if (OrderSelect(m_TicketPending))
                                                {
                                                        price = OrderGetDouble(ORDER_PRICE_OPEN);
                                                        C_Orders::RemoveOrderPendent(m_TicketPending);
                                                        EraseTicketPending(m_TicketPending);
                                                }
                                                if (m_Position.Ticket > 0)      m_TicketPending = C_Orders::CreateOrder(m_Position.IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY, price, 0, 0, m_Position.Leverage, m_InfosManager.IsDayTrade);
                                        }
                                        m_StaticLeverage += (ret > 0 ? ret : 0);
                                }else
                                {
                                        ZeroMemory(m_Position);
                                        if ((m_InfosManager.IsOrderFinish) && (m_TicketPending > 0))
                                        {
                                                RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                        }
                                        if (m_InfosManager.IsOrderFinish) EraseTicketPending(m_TicketPending);
                                }
                                ResetLastError();
                        }

可以从这些改进中受益的另一处是以下代码:

                ~C_Manager()
                        {
                                if (_LastError == (ERR_USER_ERROR_FIRST + ERR_Excommunicate))
                                {
                                        if (m_TicketPending > 0) RemoveOrderPendent(m_TicketPending);
                                        EraseTicketPending(m_TicketPending);
                                        ClosePosition();
                                        Print("EA was kicked off the chart for making a serious mistake.");
                                }
                        }

最后一处,这也受益于代码重用:

                void PendingToPosition(void)
                        {
                                ResetLastError();
                                if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                {
                                        if (m_InfosManager.IsOrderFinish)
                                        {
                                                ClosePosition();
                                                EraseTicketPending(m_TicketPending);
                                                if (PositionSelectByTicket(m_TicketPending)) C_Orders::ClosePosition(m_TicketPending); else RemoveOrderPendent(m_TicketPending);
                                                m_TicketPending = 0;
                                                ResetLastError();
                                        }else   SetUserError(ERR_Unknown);
                                }else m_Position.Ticket = (m_Position.Ticket == 0 ? m_TicketPending : m_Position.Ticket);
                                m_TicketPending = 0;
                                if (_LastError != ERR_SUCCESS) UpdatePosition(m_Position.Ticket);
                                CheckToleranceLevel();
                        }

为了完成改进和修改的问题,有一个小细节。 例如,EA 的最大确定交易量可能是最小交易量的 10 倍。 但如果交易者在配置交易量时,在执行第三次操作时指示 3 倍杠杆值,则 EA 将非常接近允许的最大交易量。 那么,如果您发送第四个请求,它就突破了允许的最大交易量。

这似乎是一个小缺陷,许多人可能认为它没有什么危险。 从某种意义上说,我同意这个想法,因为第五笔订单永远不会被接受。 但编程时定义的交易量是 10 倍。 故此,当第四笔订单被接受时,EA 已执行的交易量 12 倍,超过最大配置交易量的 2 倍。 这是因为交易者配置了 3 倍的杠杆,但如果他指示了 9 倍的杠杆呢? 在这种情况下,交易者希望 EA 只执行一笔交易。

因此,想象一下用户看到 EA 开立第二笔交易时的惊讶,它超过了最大交易量的 8 倍。 看到这种情况的人甚至可能心脏病发作。

如您所见,尽管这是一个潜在风险不大的漏洞,但它仍然不应该忽视,特别是对于自动化 EA。 这对于手动 EA 来说没有问题,因为交易者会加以检查,从而确保不会以相同杠杆等级创建另一次入场。 无论如何,我们应该为 EA 提供某种模块来应对这种情况。 这应该在下一篇文章之前实现。 这样我以后就不必担心这类事情。

为了修复这个问题,我们将添加几行代码,如下所示:

//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {                               
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
//+------------------------------------------------------------------+

绿色显示的代码避免了上述事件。 但请仔细查看高亮显示的代码。 您有没有注意到它完全一样? 我们可以在这里做两件事:创建一个宏替换来放置所有代码,或者创建一个函数来替换或汇集所有通用代码。

我决定创建一个函数来保持一切简单明了,因为文章的许多读者才刚刚开始他们的编程之旅,可能没有太多的经验或知识。 我们将这个新函数放在代码的私密部分,而 EA 代码的其余部分不需要知道它的存在。 这是新函数:

inline bool IsPossible(const bool IsPending)
	{
		if (!CtrlTimeIsPassed()) return false;
		if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return false;
		if ((IsPending) && (m_TicketPending > 0)) return false;
		if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
		{
			Print("Request denied, as it would violate the maximum volume allowed for the EA.");
			return false;
		}
                                
		return true;
	}

我们来理清这里发生了什么。 现在,按发送订单顺序,所有代码行均汇集在上面的代码当中。 不过,发送挂单和市价单之间几乎没有区别,这种差异是由这个参与点判定的。 故此,我们应该检查它是挂单还是市价单。 为了区分这两种类型的订单,我们使用一个参数,允许您将所有代码合并为一如果无法发送订单,则返回 false如果可以发送订单,则返回 true

新的函数代码如下所示:

//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                if (!IsPossible(true)) return;
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_TicketPending > 0) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                m_TicketPending = C_Orders::CreateOrder(type, Price, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                ulong tmp;
                                
                                if (!IsPossible(false)) return;
                                if (!CtrlTimeIsPassed()) return;
                                if ((m_StaticLeverage >= m_InfosManager.MaxLeverage) || (m_bAccountHedging && (m_Position.Ticket > 0))) return;
                                if (m_StaticLeverage + m_InfosManager.Leverage > m_InfosManager.MaxLeverage)
                                {
                                        Print("Request denied, as it would violate the maximum volume allowed for the EA.");
                                        return;
                                }
                                tmp = C_Orders::ToMarket(type, (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceStop), (m_InfosManager.IsOrderFinish ? 0 : m_InfosManager.FinanceTake), m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                                m_Position.Ticket = (m_bAccountHedging ? tmp : (m_Position.Ticket > 0 ? m_Position.Ticket : tmp));
                        }
//+------------------------------------------------------------------+

调用现在取决于每个特定情况,而所有划掉的行都已从代码中删除,因为它们不再有存在的意义。 这就是我们如何通过剔除冗余组件来开发安全、可靠、稳定和健壮的程序:通过分析可以更改和改进的内容,以及尽可能多地测试和重用代码。

当您看到完成的代码时,您往往会觉得它就是以这种方式诞生的。 然而,需要若干个步骤才能达到最终形式。 这些步骤包括持续测试和实验,以最大程度地减少故障和潜在缺陷。 这个过程是曲折渐进的,需要奉献精神和毅力才能令代码达到所需的品质。


章末结束语

尽管到目前为止已经谈论并展示了所有内容,但仍然存在我们必须堵上的漏洞,因为它对 100% 自动系统非常有害。 我知道很多人应该渴望看到 EA 自动工作。 但相信我,您不会想要包含违规或操作失败的 100% 自动化 EA。 一些不太严重的缺陷甚至允许在手动系统中通过,但对于 100% 自动化系统,这是不允许的。

鉴于这个主题很难解释,我们将在下一篇文章中谈论它。 文后附上当前状态的代码。 在阅读下一篇文章之前,我在这里留给您一个小挑战。 您知道 EA 中仍有一部分弱点,这会妨碍它安全地自动化。 您能发现它吗? 提示:它在于 C_Manager 类分析 EA 工作的方式。


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

附加的文件 |
最近评论 | 前往讨论 (1)
crt6789
crt6789 | 28 5月 2023 在 14:43
版主什么时候更新完成全部,受益匪浅🫡
重温默里(Murrey)系统 重温默里(Murrey)系统
图形价格分析系统在交易者中当之无愧地广受欢迎。 在本文中,我将讲述完整的默里(Murrey)系统,包括其著名的级别,以及其它一些评估当前价格位置,并据其做出交易决策的实用技术。
种群优化算法:入侵杂草优化(IWO) 种群优化算法:入侵杂草优化(IWO)
在各种条件下杂草的惊人生存能力已演化成强大优化算法的思路。 IWO 是以前审阅过的算法中最好的算法之一。
构建自动运行的 EA(第 12 部分):自动化(IV) 构建自动运行的 EA(第 12 部分):自动化(IV)
如果您认为自动化系统很简单,那么您可能并未完全理解创建它们需要什么。 在本文中,我们将谈谈杀死大量智能系统的问题。 不分青红皂白地触发订单是解决这个问题的可能方法。
神经网络实验(第 3 部分):实际应用 神经网络实验(第 3 部分):实际应用
在本系列文章中,我会采用实验和非标准方法来开发一个可盈利的交易系统,并检查神经网络是否对交易者有任何帮助。 若在交易中运用神经网络,MetaTrader 5 则可作为近乎自给自足的工具。