构建自动运行的 EA(第 06 部分):账户类型(I)

Daniel Jose | 27 三月, 2023

概述

在上一篇文章构建自动运行的 EA(第 05 部分):手工触发器(II)中,我们开发了一个相当简单的 EA,但具有很高的健壮性和可靠性。 它可用于任何资产的交易,包括外汇和股票品种。 但它尚未完成自动化,完全由手工控制。

当前状态下,我们的 EA 已能在任何状况下工作,但尚未准备好自动化。 我们仍然需要在几点上努力。 在我们添加盈亏平衡或尾随停止之前,尚有一些工作要做,因为如果我们太早加入这些机制,我们以后将不得不取消一些事情。 故此,我们将采取稍微不同的路径,首先研究创建一个通用 EA。


C_Manager 类的诞生

C_Manager 类将是 EA 和订单系统之间的隔离层。 与此同时,该类将开始为我们的 EA 推进某种自动化,允许它自动执行某些操作。

现在我们来看看类的构建是如何开始的。 其初始代码如下所示:

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include "C_Orders.mqh"
//+------------------------------------------------------------------+
class C_Manager : private C_Orders
{
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage;
                        bool    IsDayTrade;
                }m_InfosManager;
        public  :
//+------------------------------------------------------------------+
                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade)
                        :C_Orders(magic)
                        {
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                        }
//+------------------------------------------------------------------+
                ~C_Manager() { }
//+------------------------------------------------------------------+
                void CreateOrder(const ENUM_ORDER_TYPE type, const double Price)
                        {
                                C_Orders::CreateOrder(type, Price, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+  
                void ToMarket(const ENUM_ORDER_TYPE type)
                        {
                                C_Orders::ToMarket(type, m_InfosManager.FinanceStop, m_InfosManager.FinanceTake, m_InfosManager.Leverage, m_InfosManager.IsDayTrade);
                        }
//+------------------------------------------------------------------+
};

您在上面的代码中看到的是我们将要构建的基本结构。 请注意,EA 在发送订单时不再需要提供某些信息。 一切管理都在这个 C_Manager 类中进行。 实际上,在构造函数调用中,我们传递所需的所有值,以便创建订单或发送订单开立市价持仓。

但我希望您注意一个事实:C_Manager 类继承自 C_Orders 类,但这个继承是私密的。 为什么? 原因是安全性和更高的可靠性。 当把该类放置在这里作为 “syndicator” 类型时,我们希望它是 EA 和负责发送订单的类之间的唯一通信点。

由于 C_Manager 将控制订单系统的访问,能够发送、关闭或修改订单和持仓,因此我们为 EA 提供了某种访问订单系统的方法。 但这种访问将是受限的。以下是 EA 用于访问订单系统的两个初始函数。 如您所见,它们比 C_Orders 类中的限制要多得多,但它们更安全。

为了理解我们在这里实现的事物级别,我们将上一篇文章中的 EA 代码与当前文章进行比较。 我们只创建了 C_Manager 类。 查看 EA 中存在的两个函数发生了什么。

int OnInit()
{
        manager = new C_Orders(def_MAGIC_NUMBER);
        manager = new C_Manager(def_MAGIC_NUMBER, user03, user02, user01, user04);
        mouse = new C_Mouse(user05, user06, user07, user03, user02, user01);

        return INIT_SUCCEEDED;
}

以前的代码已被删除,并替换为具有大量参数的新代码。 但这仅是一个小细节。 主要的事情(在我看来,这会令一切风险更甚)如下所示:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        uint    BtnStatus;
        double  Price;
        static double mem = 0;
        
        (*mouse).DispatchMessage(id, lparam, dparam, sparam);
        (*mouse).GetStatus(Price, BtnStatus);
        if (TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL))
        {
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY, user03, user02, user01, user04);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL, user03, user02, user01, user04);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_UP))  (*manager).ToMarket(ORDER_TYPE_BUY);
                if (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN))(*manager).ToMarket(ORDER_TYPE_SELL);
        }
        if ((def_SHIFT_Press(BtnStatus) != def_CTRL_Press(BtnStatus)) && def_BtnLeftClick(BtnStatus))
        {
                if (mem == 0) (*manager).CreateOrder((def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), mem = Price, user03, user02, user01, user04);
                if (mem == 0) (*manager).CreateOrder((def_SHIFT_Press(BtnStatus) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL), mem = Price);
        }else mem = 0;
}

有些部份已被简单地删除。 反过来,您可以看到新代码要简单得多。但不仅如此。 使用类来执行“管理员”工作可以保证 EA 能准确使用在类初始化时定义的那些参数。 故此,不会有在其中一个调用中放置错误或无效信息的风险。 一切都集中在一个地方,即在 C_Manager 类当中,该类现在充当 EA 和 C_Orders 之间的通信中介。 这显著提高了 EA 代码的安全性和可靠性级别。


NETTING、EXCHANGE 或 HEDGING 账户... 这就是问题所在

尽管许多人忽略或未意识到这一事实,但这里有一个严重的问题。 正因为如此,EA 可能会、也可能不会很好地工作 — 这就是账户类型。 大多数交易者和 MetaTrader 5 平台用户不知道市场上有三种类型的账户。 但对于那些想要开发在全自动模式下运行的智能系统的人来说,这些知识至关重要。

在本系列文章中,我们将讨论两种账户类型:净持结算和对冲。 原因很简单:EA 针对净持结算账户的操作方式与证券账户相同。

即使 EA 具有简单的自动化,例如盈亏平衡或尾随停止激活,它在净持结算账户上与在对冲账户上运行时的操作事实上完全不同。 原因在于交易服务器的操作方式。 在净持结算账户上,交易服务器会随着您增加或减少持仓而创建摊平价格。

而对冲账户在服务器上不会这样做。 它分别处理所有持仓,因此您可以同时拥有同一资产的多头和空头持仓。 这永远不会发生在净持结算账户上。 如果您尝试以相同的手数开立相反的仓位,服务器实际上是平仓。

出于这个原因,我们必须知道 EA 是为净持结算、还是为对冲账户设计的,因为操作原理完全不同。 但这仅适用于自动化 EA,或具有一定自动化级别的 EA。 对于手工 EA,这无关紧要。

因为这个事实,我们若不排除编程或可用性方面的一些困难,就难以创建任何自动化级别。

在此,我们需要稍微进行一点标准化。 换句话说,我们需要确保 EA 能够以标准化的方式在任何账户类型上操作。 确实,这将降低 EA 的能力。 然而,自动 EA 理应具有很大的自由度。 最好的方法是限制 EA,令其行为良好。 如果它偏离了一些,就必须禁止它、或至少受到一些惩罚。

标准化的方式是 EA 在对冲账户上的操作方式与净持结算账户上类似。 我知道这可能看起来令人困惑和复杂,但我们真正想要的是允许 EA 只有一笔持仓和一笔挂单,换言之,它极端有限,并且无法做任何其它事情。

因此,我们将以下代码添加到 C_Manager 类之中:

class C_Manager : private C_Orders
{
        private :
                struct st00
                {
                        double  FinanceStop,
                                FinanceTake;
                        uint    Leverage;
                        bool    IsDayTrade;
                }m_InfosManager;
//---
                struct st01
                {
                        ulong   Ticket;
                        double  SL,
                                TP,
                                PriceOpen,
                                Gap;
                        bool    EnableBreakEven,
                                IsBuy;
                        int     Leverage;
                }m_Position;
                ulong           m_TicketPending;
                bool            m_bAccountHedging;
		double		m_Trigger;


在该结构中,我们创建了处理持仓可能需要的所有内容。它已经具有一些与第一级自动化相关的东西,例如盈亏平衡和尾随停止。 挂单将采用更简单的方式存储,即单号。但是,如果我们将来需要更多数据,我们也能实现它。 现在这就足够了。 我们还有另一个变量能告诉我们所用的是对冲账户亦或是净持结算账户。 它在某些时刻会特别实用。 像往常一样,还添加了另一个变量,其在此阶段不会用到,但我们稍后在创建盈亏平衡和尾随停止触发器时将需要它

这就是我们如何开始令事情正常化。 之后,我们可以对类构造函数进行修改,如下所示:

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger)
                        :C_Orders(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                                switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
                                {
                                        case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
                                                m_bAccountHedging = true;
                                                szInfo = "HEDGING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
                                                szInfo = "NETTING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_EXCHANGE:
                                                szInfo = "EXCHANGE";
                                                break;
                                }
                                Print("Detected Account ", szInfo);
                        }

我将为那些没有编程经验的人一点一点地展示代码。 我希望我不会对此感到太无聊,因为我希望每个人都能够理解我们正在这里做什么。 以下就是解释。 这些行通知编译器我们希望在构造函数代码执行开始之前初始化这些变量。当创建一个变量时,编译器通常会为其分配零值。

在这些行中,我们告诉编译器,当我们创建变量值时它应是什么值。 在此刻,我们重置结构的所有内容。 以这种方式,我们就能用更少的代码来获得更快的结果。 在此,我们注意到我们正在操控对冲账户的事实。 如果在某些时候需要此信息,我们有一个变量来说明这一点。 我们已在此处通知终端找到了哪种帐户类型。这样做是为了指示类型,以防用户事先并不知晓。

但在我们查看这些过程之前,思考以下事项:如果 EA 发现多笔持仓(对冲账户),或多笔挂单该怎么办? 那会发生什么呢? 在这种情况下,我们将收到一个错误,因为 EA 将无法处理多于一笔的持仓和订单。 为了解决这个问题,我们在代码中创建以下枚举:

class C_Manager : private C_Orders
{
        enum eErrUser {ERR_Unknown};
        private :

// ... The rest of the code...

};

在此,我们将使用枚举,因为向其添加新的错误代码更容易。为此,我们只需要指定一个新名称,编译器就会为代码生成一个值,同时不会因为疏忽而产生数值重叠的风险。 请注意,枚举位于私密代码部分之前,因此它将是公开的。 但是要在类的外部访问它,我们需要使用一个小细节来通知编译器哪个枚举是正确的。 当我们想要使用与特定类相关的枚举时,这特别有用。 现在我们来看一下该过程,如何加载可能留在图表上的内容,以及 EA 在开始工作之前必须恢复的内容。 第一个如下:

inline void LoadOrderValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = OrdersTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = OrderGetTicket(c0)) == 0) continue;
                                        if (OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
                                        if (OrderGetInteger(ORDER_MAGIC) != GetMagicNumber()) continue;
                                        if (m_TicketPending > 0) SetUserError(ERR_Unknown); else m_TicketPending = value;
                                }
                        }

我们来看看这段代码是如何工作的,以及为什么它看起来如此不寻常。 在此,我们使用循环来读取订单簿中的所有挂单。 如果有订单存在,OrdersTotal 函数将返回一个大于零的数值。 索引始终从零开始。 它来自 C/C++。 但我们有两个令循环结束的条件:首先,c0 变量的值小于零,其次 - _LastError 是来自 ERR_SUCESS 的不同形式 ,这表明 EA 中发生了一些故障。

因此,我们进入循环,并捕获由变量 c0 指示的索引处第一笔订单OrderGetTicket 将返回单号值或零。 如果它为零,我们将返回循环,但现在我们从变量 c0 中减一。

由于 OrderGetTicket 加载订单值,且系统不会区分它们,因此我们需要过滤所有内容,如此 EA 就只得到我们的特定订单。 因此,我们将用到的第一个过滤器是资产名称;为此,我们将按资产的排列顺序与正在运行的 EA 进行比较。如果它们不同,订单将被忽略,如果匹配,我们将其返回。

下一个过滤器是魔幻数字,因为订单簿也许有手工下达的订单,或由其它 EA 下达的订单。 我们可以根据每个 EA 所独有的魔幻数字来核实订单是否由我们的 EA 所下达。 如果魔幻数字与 EA 所用的不同,则应忽略该笔订单。 然后我们回到开头寻找新订单。

现在我们来到了十字路口。如果以前 EA 下过订单,但出于某种原因被从图表中删除(稍后我们将看到原因是什么),那么 EA 重启后它也会被记忆,即变量会指示挂单的单号具有非零值。 然后,如果又遇到第二笔订单,它将被视为出错。 该函数将使用枚举来显示已发生错误。

在此,我用公共值 ERR_Unknown,但您可以创建一个值来指定错误,该错误将显示在 _LastError 值当中。 SetUserError 函数负责在 _LastError 变量中设置错误值。但如果一切正常,并且包含订单单号的变量设置为零,则经过所有过滤之后找到的订单值将保存在 m_TicketPending 变量中,以供进一步使用。 这就是已完成过程需要我们解释的地方。 我们来研究下一段负责搜索任何持仓的模块。 其代码如下所示:

inline void LoadPositionValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = PositionsTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = PositionGetTicket(c0)) == 0) continue;
                                        if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
                                        if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) continue;
                                        if (m_Position.Ticket > 0) SetUserError(ERR_Unknown); else
					{
						m_Position.Ticket = value;
						SetInfoPositions();
					}
                                }
                        }

我所说的关于前面代码的所有内容也适用于此段代码。 唯一的区别是以前我们操纵订单,现在我们操纵持仓。 但逻辑是一样的,只是直到下一次调用:SetInfoPositions,它必须存储、更正和处理最新的持仓数据。 为此,我们将使用以下代码:

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);
                                m_Position.EnableBreakEven = (m_Position.IsBuy ? (v1 < v2) : (v1 > v2));
                                m_Position.Gap = FinanceToPoints(m_Trigger, m_Position.Leverage);

                                return m_Position.Leverage - tmp;
                        }

在处理最新位置数据时,此段代码尤其有趣。 但要当心:在调用它之前,您必须调用以下之一更新持仓数据:PositionGetTicketPositionSelectPositionGetSymbolPositionSelectByTicket。 通常,在此我们根据需要初始化或配置所有内容。 我们必须单独放置此代码,因为我们将在必要时从其它地方调用它来更新持仓数据。

基本上就是这样,但现在我们需要对类的构造函数进行新的修改,以便 EA 可以完全正确初始化。 我们所需做的全部就是添加上面所显的调用。 然后最终的构造函数代码将如下所示:

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger)
                        :C_Orders(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                                switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
                                {
                                        case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
                                                m_bAccountHedging = true;
                                                szInfo = "HEDGING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
                                                szInfo = "NETTING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_EXCHANGE:
                                                szInfo = "EXCHANGE";
                                                break;
                                }
                                Print("Detected Account ", szInfo);
                                LoadPositionValid();
                                LoadOrderValid();
                                if (_LastError == ERR_SUCCESS)
                                {
                                        szInfo = "Successful upload...";
                                        szInfo += StringFormat("%s", (m_Position.Ticket > 0 ? "\nTicket Position: " + (string)m_Position.Ticket : ""));
                                        szInfo += StringFormat("%s", (m_TicketPending > 0 ? "\nTicket Order: " + (string)m_TicketPending : ""));
                                        Print(szInfo);
                                }
                        }

这两行将令 EA 加载持仓和挂单。 现在请注意:构造函数不能返回任何数值的事实,因为这是一个错误。 我们需要某种方式来告诉其余代码构造函数中出了问题。

每个编程系统都提供、或能让我们创造这样的手段。 但 MQL5 提供了一种非常实用的方式,即为此使用 _LastError 变量。 如果在初始化期间一切正常,我们就会在终端中看到相关消息如果系统找到了任何持仓,我们还将看到一条消息,指示 EA 将观察哪笔持仓的单号如果找到了订单,我们还将看到一条消息,告知我们 EA 找到的挂单单号

_LastError 值将用作检查 EA 是否在某个时候离线的一种方式。 如果您在错误列表中添加了更多类型的消息类型,如此便可更精确地指示实际发生的情况,这也许会很有趣。


对冲账户上自动 EA 遇到的问题

尽管一切看起来都美丽且美妙,特别是对于那些开始学习编程的人来说,我们仍在继续开发,从而在自动化 EA 中实现更高等级的稳健性。 当在对冲账户上操作时,我们的系统仍然存在一个潜在问题。 甚至在我们继续处理负责允许 EA 向服务器发送订单或请求的代码之前,我们就遇到过它。 问题在于,与对冲账户不同,在净持账户中,服务器会在入场新的市价订单或执行挂单后,依据变更后的持仓重新创建平均价格,而对冲账户没有那么多控制权,它极其容易,且均来自服务器。

对冲账户的问题在于我们可以有一笔持仓,如果挂单执行,它不会直接改变现有持仓。 可能发生的情况,以及实际上将要发生的是,当挂单被执行时,将开立一笔新持仓。 这笔新的持仓可以锁定价格,令我们既没有利润也没有损失。 但它也可以提高我们的整体仓位。 这将在订单执行后立即发生。

这个细节存在于对冲账户当中,迫使我们采取另一种措施。 我们可以防止 EA 在已有持仓,或订单簿中已有挂单的情况下向市场发送订单。 这可以根据我展示的代码轻松完成。 但问题是,在初始化期间,EA 可能会在对冲账户上找到持仓和挂单。 正如我上面解释的那样,这对净持结算账户来说不是问题。

在这种情况下,EA 应该怎么做? 您还记得,控制 EA 的 C_Manager 类不允许它有两笔持仓、或两笔挂单。 那在这种情况下,我们需要删除一笔挂单、或平掉一笔持仓。 以一种或另一种方式,必须做点什么,因为我们在自动化 EA 中不能允许这种情况。 我再次强调这一点,自动 EA 绝不能同时处理多笔持仓,或同时处理多笔挂单。 手工 EA 中的情况有所不同。

故此,您必须决定应采取哪种措施:平仓还是移除挂单? 如果您想平仓,那 C_Orders 类已经为此提供了过程。 但如果您需要删除挂单,我们在 C_Orders 类中尚无任何相应的过程。 那么,我们需要实现一种方法来做到这一点。 我们从这一点开始,令系统能够删除挂单。 为此,我们将向系统添加新代码:

class C_Orders : protected C_Terminal
{
        protected:
//+------------------------------------------------------------------+
inline const ulong GetMagicNumber(void) const { return m_MagicNumber; }
//+------------------------------------------------------------------+
                void RemoveOrderPendent(const ulong ticket)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action   = TRADE_ACTION_REMOVE;
                                m_TradeRequest.order    = ticket;
                                ToServer();
                        };

// ... The rest of the class code 

}

注意代码中的一些细节。 首先,它位于受保护的代码部分,即,我们即使尝试直接在 EA 中使用 C_Orders 类,由于我之前已经解释过的原因,我们也无法访问此段代码。 第二件事则是它用于删除挂单,故不可平仓或修改挂单。

因此,此代码已在 C_Orders 类中实现。 我们可以返回 C_Manager,并实现一个系统,若对冲账户上已有持仓或有挂单时,防止自动 EA 的运行。 但如果您希望它平仓并保持挂单,则只需对代码进行修改,即可获得所需的行为。 唯一不能发生的是,在对冲账户上运行自动 EA 时,既有持仓,亦有挂单。 这是不允许的。

重点:如果是对冲账户,您可以运行多个 EA 交易同一资产。 如果发生这种情况,一个 EA 有持仓,而另一个 EA 有挂单,但事实上两个 EA 之间不会以任何方式影响彼此的操作。 在这种情况下,它们各自独立。 因此,我们可能会在同一资产上有多笔持仓、或多笔挂单。 这种情况对于单个 EA 是不可能的。 此外,这仅涉及自动 EA。 我会反复强调这一点,因为这对于理解和记忆非常重要。

您也许已经注意到,在构造函数代码中,我们首先捕获一笔持仓,然后才捕获订单。 如有必要,这样可以更轻松地删除订单。 然而,如果您想平仓并保持订单,只需在构造函数中反转它,从而首先捕获订单,然后捕获持仓。 然后,如果需要,该笔持仓将被平仓。 我们来看看在我们所研究的情况下,我们如何做到这一点。 捕获持仓,然后在必要时删除发现的所有挂单。 其代码如下所示:

inline void LoadOrderValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = OrdersTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = OrderGetTicket(c0)) == 0) continue;
                                        if (OrderGetString(ORDER_SYMBOL) != _Symbol) continue;
                                        if (OrderGetInteger(ORDER_MAGIC) != GetMagicNumber()) continue;
                                        if ((m_bAccountHedging) && (m_Position.Ticket > 0))
                                        {
                                                RemoveOrderPendent(value);
                                                continue;
                                        }
                                        if (m_TicketPending > 0) SetUserError(ERR_Unknown); else m_TicketPending = value;
                                }
                        }

我们需要进行的修改是添加高亮显示的代码。 请注意删除挂单是多么容易,但在此我们需要做的就是删除订单。 如果我们有一笔持仓,且账户类型是对冲账户,那么会出现挂单被删除的情况。 但如果我们是一个净持账户或没有持仓,那么这段代码就不会执行,这将令 EA 顺利运行。

不过,由于您可能想要平仓并保留挂单,那我们看看在这种情况下代码应该是什么样子。 您无需更改挂单加载代码 — 使用上面所示的代码。 但您需要进行一些修改,其中第一个是将以下代码添加到加载持仓的过程之中:

inline void LoadPositionValid(void)
                        {
                                ulong value;
                                
                                for (int c0 = PositionsTotal() - 1; (c0 >= 0) && (_LastError == ERR_SUCCESS); c0--)
                                {
                                        if ((value = PositionGetTicket(c0)) == 0) continue;
                                        if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
                                        if (PositionGetInteger(POSITION_MAGIC) != GetMagicNumber()) continue;
                                        if ((m_bAccountHedging) && (m_TicketPending > 0))
                                        {
                                                ClosePosition(value);
                                                continue;
                                        }
                                        if (m_Position.Ticket > 0) SetUserError(ERR_Unknown); else
					{
						m_Position.Ticket = value;
						SetInfoPositions();
					}
                                }
                        }

通过添加高亮显示的代码,您就能够在保持挂单的同时平仓。 但这里还有一处细节:为了保持挂单,并在对冲账户上平仓,我们需要修改构造函数代码中的一处,如下所示:

                C_Manager(const ulong magic, double FinanceStop, double FinanceTake, uint Leverage, bool IsDayTrade, double Trigger)
                        :C_Orders(magic),
                        m_bAccountHedging(false),
                        m_TicketPending(0),
                        m_Trigger(Trigger)
                        {
                                string szInfo;
                                
                                ResetLastError();
                                ZeroMemory(m_Position);
                                m_InfosManager.FinanceStop = FinanceStop;
                                m_InfosManager.FinanceTake = FinanceTake;
                                m_InfosManager.Leverage    = Leverage;
                                m_InfosManager.IsDayTrade  = IsDayTrade;
                                switch ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE))
                                {
                                        case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING:
                                                m_bAccountHedging = true;
                                                szInfo = "HEDGING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_RETAIL_NETTING:
                                                szInfo = "NETTING";
                                                break;
                                        case ACCOUNT_MARGIN_MODE_EXCHANGE:
                                                szInfo = "EXCHANGE";
                                                break;
                                }
                                Print("Detected Account ", szInfo);
                                LoadOrderValid();
                                LoadPositionValid();
                                if (_LastError == ERR_SUCCESS)
                                {
                                        szInfo = "Successful upload...";
                                        szInfo += StringFormat("%s", (m_Position.Ticket > 0 ? "\nTicket Position: " + (string)m_Position.Ticket + "\n" : ""));
                                        szInfo += StringFormat("%s", (m_TicketPending > 0 ? "\nTicket Order: " + (string)m_TicketPending : ""));
                                        Print(szInfo);
                                }
                        }

您也许没注意到任何变化。 但如果将其与上一节末尾的代码进行比较,您会发现高亮显示的部分是不同的。在这种情况下,持仓将被平仓,而在以前的版本中,我们则是删除订单。 这是编程的优雅之处。 有时,一个简单的细节就会改变一切。 于此我们只是改变了代码执行的顺序,但结果却完全不同。

从理论上讲,到目前为止研究的代码没有任何问题,并且可以完美运行。 但这只是理论上。 也许结果是交易服务器报告错误,不是因为发送的请求有问题,而是由于可能发生的某种交互。 _LastError 变量将包含一个指示某种故障的数值。

有些故障是可以允许的,因为它们并不紧迫,而其它故障则不能忽视。 如果您理解这种差异,并接受这个想法,您可以在某些代码部分添加 ResetLastError 调用,从而防止 EA 因存在某种错误而被踢出图表,这很可能不是由 EA 引起的,而是由 EA 和交易服务器之间的不正确交互引起的。

在这个早期阶段,我不会展示您可以在何处添加这些调用。 我这样做是为了让您不会在任何时候不分青红皂白地进行这些调用,或忽略 _LastError 变量中包含的值。


结束语

在本文中,我介绍了最基本的知识,向您展示自动化 EA 应该始终考虑到如何安全、稳定、和强壮的方式。 编写能够自动运行的 EA,并非经验不足的人可以毫无阻碍就能完成的任务,这是一项极其困难的任务,需要程序员非常小心。

在下一篇文章中,我们将研究自动化 EA 需要实现的更多事情。 我们将研究如何安全地将其放置在图表上。 我们必须始终以应有的谨慎和正确的措施行事,以免对我们来之不易的创造发明造成任何损害。