构建自动运行的 EA(第 03 部分):新函数

Daniel Jose | 13 三月, 2023

概述

在上一篇文章构建自动运行的 EA(第 02 部分):代码入门中,我们已启动开发了一个将在自动化 EA 中使用的订单系统。 然而,我们只创建了一个必要的函数。

通常,订单系统需要少量额外的东西才能在全自动模式下工作。 此外,有些人更喜欢运行若干个 EA,以不同设置操控同一资产。

对于净持结算系统的账户,不建议这样做。 原因是交易服务器会创建所谓的均摊持仓价格,而这可能会导致一个 EA 试图卖出,而另一个 EA 试图买入的情况。 显而易见,这将导致两个 EA 均失效,在短时间内亏光本金。 对冲账户的状况就并非如此。 在这样的账户上,一个 EA 可以卖出,而另一个 EA 可以买入。 一笔订单不会抵消另一笔订单。

有些人甚至手工交易与 EA 相同的资产 — 但这必须在对冲账户上完成。 如有疑问,切勿运行两个 EA 交易相同资产的(至少不要在同一经纪商),也切勿在自动 EA 运行时手工交易相同的资产。

现在您已经被警告了,我们可以向 EA 添加其它必要的函数,这将涵盖 90% 以上的情况。


为什么我们需要新函数?

通常,当自动 EA 进行交易时,它基本上直接入场和离场,即 EA 很少在订单簿中下单。 但是,在某些情况下,需要在订单簿中下单。 实现这种安置的过程是最困难的之一。 出于这个原因,上一篇文章曾专门讨论这种函数的实现。 但这对于自动 EA 来说还不够。 因为,在大多数情况下,它直接入场和离场。 因此,我们至少需要两个额外函数

只有这两个函数,以及上一篇文章中讨论过的函数,才是自动化 EA 中真正需要的。 现在,我们来看看为什么我们只需要添加这两个函数,仅此而已。 由此,当 EA 开仓时,无论是卖出还是买入,它通常会按市价,以及交易者预先确定的交易量开仓。

当您需要平仓时,您可以采取两种方式进行:第一种是按市价,以相反的方向和相同的交易量进行交易。 但这通常不会导致平仓。 实际上,该方式仅适用于净持结算账户。 在对冲账户上,此操作不会平仓。 在这种情况下,我们需要一个明确的订单来把现有持仓进行平仓。

尽管在对冲账户中以相同的交易量开立相反方向的交易将允许您锁定盈亏,但这实际上并不意味着持仓已被平仓。 锁单既不会带来盈利也不会带来巨亏。 出于这个原因,我们将为 EA 添加平仓的能力,这需要实现第三个函数。 注意:在净持账户上,您可以简单地发送交易量相同、但方向相反的市价订单,持仓即被平仓。

现在我们需要一笔仓位能够改变订单价格。 在某些操作模型中,EA 的工作方式是这样的:它开立一笔市价持仓,并立即发送一个止损挂单。 此订单将加到订单簿中,并一直保留在那里,直到平仓。 在这种情况下,EA 实际上不会发送平仓订单,或任何其它类型的请求。 它简单地管理账簿中的订单,令平仓订单始终处于有效状态。

这在净持结算账户上工作良好,但对于对冲账户,该系统将无法按预期工作:在这种情况下,订单簿中的订单简单地遵照我上面已解释过的关于交易平仓方式的操作执行。 但言归正传。 管理订单簿中订单的相同过程,也可用于移动 OCO(“订单冲抵订单”)订单中的止盈和止损价位

通常,自动 EA 不使用 OCO 订单,它只与市价订单、和订单簿中的订单配套工作。 但在对冲账户的情况下,这种机制可以使用 OCO 订单来完成,而只需设置止损价位。 或者,如果程序员愿意,他们可以简单地入场,让 EA 以某种方式观望市场。 一旦达到某个点位,或价格水平,EA 将发送平仓订单。

我所解释的所有这些,只是为了表明做同样的事情可以有不同的途径 — 在我们的例子中是平仓。 开仓是流程中最简单的部分。 而平仓才是最棘手的部分,因为您必须考虑以下几点:

在创建自动 EA 时,必须考虑和观察所有这些要点。 还有其它几点,例如一些程序员为 EA 增加了操作时间。 至于这一点,我会说实话。 这根本没用。 尽管其它文章展示了如何做到这一点,但我不建议这样做,而此处就是原因。

考虑以下几点:您不知道如何交易,您有一个自动 EA 来做这件事,您设置了一份时间表。 现在您认为可以放松一下了,做点别的事情......大错特错永远不要,我再重复一遍,永远不要离开 EA,即便它是无需监督的自动化 EA。 永远不要。 当它运行时,您或您信任的人应该在它附近待命,并观察它是如何工作的。

任由 EA 在无人看管下运行,意味着打开麻烦之门。 添加调度方法或触发器来启动和结束 EA,是在使用自动 EA 时一个人所做的最愚蠢的事情。 请不要这样做。 如果您希望 EA 为您工作,那么打开它,并在那里观察。 若您需要离开时,把它停掉,然后再去做您需要做的事情。 永远不要让 EA 离开监督,自行交易。千万不要这样做,因为结果可能会极度令人失望

实现所需函数 

由于执行市价单的过程与提交挂单的过程非常相似,我们可以创建一个通用程序 — 如此就可填写所有具有相同类型的字段。 只有特定于操作的字段才会在局部填充。 如此,我们来看看这个常用填充的函数

inline void CommonData(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double Desloc;
                                
                                ZeroMemory(m_TradeRequest);
				m_TradeRequest.magic		= m_Infos.MagicNumber;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.stoplimit        = 0;
                                m_TradeRequest.expiration       = 0;
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                        }

请注意,我们在这个通用函数中的所有内容都存在于我们在上一篇文章中研究的挂单创建函数之中。 但我还是添加了一些以前不存在的额外东西,但如果您正在使用对冲账户,或打算创建一个仅观察已创建订单的 EA,它会非常实用。 这就是所谓的魔幻数字。 通常我不会用此数字,但是如果您要这样做,那么您就已经有了现成的方式来支持它。

那么,就让我们来看看这个负责发送挂单的新函数:

                ulong CreateOrder(const ENUM_ORDER_TYPE type, const double Price, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                double  bid, ask, Desloc;                               
                                
                                Price = AdjustPrice(Price);
                                bid = SymbolInfoDouble(_Symbol, (m_Infos.PlotLast ? SYMBOL_LAST : SYMBOL_BID));
                                ask = (m_Infos.PlotLast ? bid : SymbolInfoDouble(_Symbol, SYMBOL_ASK));
                                CommonData(type, AdjustPrice(Price), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_PENDING;
                                m_TradeRequest.type     = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                    (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));                              
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action           = TRADE_ACTION_PENDING;
                                m_TradeRequest.symbol           = _Symbol;
                                m_TradeRequest.volume           = NormalizeDouble(m_Infos.VolMinimal + (m_Infos.VolStep * (Leverage - 1)), m_Infos.nDigits);
                                m_TradeRequest.type             = (type == ORDER_TYPE_BUY ? (ask >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : 
                                                                                            (bid < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
                                m_TradeRequest.price            = NormalizeDouble(Price, m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceStop, Leverage);
                                m_TradeRequest.sl               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? -1 : 1)), m_Infos.nDigits);
                                Desloc = FinanceToPoints(FinanceTake, Leverage);
                                m_TradeRequest.tp               = NormalizeDouble(Desloc == 0 ? 0 : Price + (Desloc * (type == ORDER_TYPE_BUY ? 1 : -1)), m_Infos.nDigits);
                                m_TradeRequest.type_time        = (IsDayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC);
                                m_TradeRequest.type_filling     = ORDER_FILLING_RETURN;
                                m_TradeRequest.deviation        = 1000;
                                m_TradeRequest.comment          = "Order Generated by Experts Advisor.";
                                
                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

所有划掉的部分都已从代码中删除,因为这些字段均由通用函数来填充。 我们真正需要做的是调整这两个值,订单系统将创建一笔挂单,如上一篇文章所示。

现在我们来看看我们实际编程需要什么,才能创建以市价发送执行请求的订单系统。 所需代码如下所示:

                ulong ToMarket(const ENUM_ORDER_TYPE type, const double FinanceStop, const double FinanceTake, const uint Leverage, const bool IsDayTrade)
                        {
                                CommonData(type, SymbolInfoDouble(_Symbol, (type == ORDER_TYPE_BUY ? SYMBOL_ASK : SYMBOL_BID)), FinanceStop, FinanceTake, Leverage, IsDayTrade);
                                m_TradeRequest.action   = TRADE_ACTION_DEAL;
                                m_TradeRequest.type     = type;

                                return (((type == ORDER_TYPE_BUY) || (type == ORDER_TYPE_SELL)) ? ToServer() : 0);
                        };

看看它是多么容易:与挂单相比,所有您需要更改的只是这两件事。 以这种方式,我们保证服务器将始终接收兼容的数据,因为唯一的变化就是请求类型。

因此,无论是在分析回报方面,还是在调用下挂单,或执行市场交易过程的方式方面,操作几乎是相同的。 对于那些调用该过程的人来说,唯一真正的区别是,当执行市价订单时,您不需要提供价格,因为该类会正确填写数值,而挂单则需要指定价格。 除此之外,所有操作都相同。

现在,我们来看看系统中发生了哪些变化。 由于我们添加了一个魔幻数字值,因此我们需要创建一个接收此值的类。 这应该在类构造函数中完成。 这是构造函数现在的样子:

                C_Orders(const ulong magic = 0)
                        {
                                m_Infos.MagicNumber     = magic;
                                m_Infos.nDigits         = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
                                m_Infos.VolMinimal      = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
                                m_Infos.VolStep         = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
                                m_Infos.PointPerTick    = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
                                m_Infos.ValuePerPoint   = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
                                m_Infos.AdjustToTrade   = m_Infos.PointPerTick / m_Infos.ValuePerPoint;
                                m_Infos.PlotLast        = (SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE) == SYMBOL_CHART_MODE_LAST);
                        };

我们来看看上面的代码中发生了什么。 当我们像这里一样声明默认值时,我们在创建类时务须通知它,因此我们令这个构造函数就像它是默认一样(即其中不会接收任何类型的参数)。

但是,如果您要强制类的用户(即程序员),在创建类的阶段告知应使用哪些值,则要删除参数的值。 如此,当编译器尝试生成代码时,它会注意到缺少某些内容,并询问哪些值要用到。 但这仅当类不包含其它构造函数时才有效。 这是一个很多人都难以理解的小细节:为什么有时我们必须指定值,有时不用指定。

如您所见,编程可能非常有趣。 在许多情况下,我们真正在做的是尝试以最少的工作量创建一个解决方案,这样就没有那么多东西需要编码和测试。 但我们还没有完成 C_Orders 类。 它仍然需要一个必需的函数,和一个可选的函数,由于对冲和净持结算账户之间的差异,我们仍要创建这些函数。 我们继续前进:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action   = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order    = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

上述程序极端重要,甚至比我们将看到的下一个程序更重要。 原因是此过程负责操纵价格仓位,无论是挂单情况下的订单簿仓位,还是持仓情况下的限制。 上面的函数非常强大,可以创建或删除订单或仓位限制。 为了理解它是如何工作的,我们来分析一下它的内部代码。 这有助于您明白如何正确使用此函数。

为了令解释更简单、更易于理解,我将把所有内容分解成几个部分,所以请小心不要迷失在解释之中。

我们从了解以下内容开始:当您提交订单时,无论是向订单簿下挂单,还是提交市价单,您都会获得返回值。 如果请求中未发生错误,则此值将为非空。 然而,不应当忽略此值,因为应非常小心地存储订单或仓位创建函数返回的数值,因为它代表订单或仓位的单号。

这个单号作为一种通行证,将为您提供若干种可能性,包括操纵交易服务器上的订单或仓位的能力。 如此,您在以市价发送交易,或尝试下挂单时获得的数值,以及由执行此过程的函数返回的值,本质上(当它不同于零值时)作为 EA 与服务器通信的通行证。 依据取用票证,您就可操纵价格。

每笔订单或仓位都有一个唯一的单号,所以要注意这个数字,不要试图随机创建它。 如果您不知道该数值或丢失了它,有一些途径可以得到它。 不过,这些途径需要 EA 的时间,因此请确保您不会丢失此数字。

我们首先假设我们有一笔订单,我们想删除或修改该订单的限价值(止盈或止损)。 不要将订单与仓位混淆。 当我说“订单(order)”时,我指的是可能和未来的仓位;通常订单在订单簿之中,而当订单被实际成交时即成为仓位。 在这种情况下,您将提供订单单号和新价格值。 请注意,这些是现在的价格值,您将不再指示财务价值(与买卖关联的货币价值)。 我们现在期望的是您在图表上看到的面值。 故此,您不能在此随机操作,否则您的请求将被拒绝。

现在这点已经变得很清晰了,您可以在订单中放置几乎任何止盈和止损值,但实际上这并不完全正确。 如果您拥有的是买单,止损值不能大于开仓价,止盈值不能小于开仓价。 如果您试图执行此操作,服务器将返回错误。

现在要密切注意这一点:订单中的止盈和止损值必须满足这些准则,但在持仓的情况下,买入仓位的止损值可以高于开盘价,在这种情况下,止损就变成了止盈,即如果止损订单被击发,您实际上已获得了一些利润。 但我们稍后会详细讨论这一点。 至于现在,请记住,对于买入订单,止损应该低于开盘价,而对于卖单则应该高于。 仓位的止损可以在任何地方。

以上仅涉及订单和持仓的止损价位。 但如果您查看上面的函数,您就会发现我们也可以操纵仓位的开仓价,只要它仍然是订单状态。 重要的细节:当您这样做时,您将不得不同时移动止损和止盈。 如果您不这样做,在某些时候,服务器就会拒绝您更改开仓价的请求。

为了理解这部分,我们来创建一个小型 EA 程序,以便检查这些情况。 创建一个新的 EA 文件,然后复制以下代码,并粘贴到打开的文件之中。 之后,编译 EA,并在图表上启动。 然后我们就能看到解释:

#property copyright "Daniel Jose"
#property description "This one is an automatic Expert Advisor"
#property description "for demonstration. To understand how to"
#property description "develop yours in order to use a particular"
#property description "operational, see the articles where there"
#property description "is an explanation of how to proceed."
#property version   "1.03"
#property link      "https://www.mql5.com/pt/articles/11226"
//+------------------------------------------------------------------+
#include <Generic Auto Trader\C_Orders.mqh>
//+------------------------------------------------------------------+
C_Orders *orders;
//+------------------------------------------------------------------+
input int       user01   = 1;           //Lot value
input int       user02   = 100;         //Take Profit
input int       user03   = 75;          //Stop Loss
input bool      user04   = true;        //Day Trade ?
input double    user05   = 84.00;       //Entry price...
//+------------------------------------------------------------------+
int OnInit()
{
        orders = new C_Orders(1234456789);
        
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
        delete orders;
}
//+------------------------------------------------------------------+
void OnTick()
{
}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
#define KEY_UP                  38
#define KEY_DOWN                40
#define KEY_NUM_1               97
#define KEY_NUM_2               98
#define KEY_NUM_3               99
#define KEY_NUM_7               103
#define KEY_NUM_8               104
#define KEY_NUM_9               105

        static ulong sticket = 0;
        int key = (int)lparam;
        
        switch (id)
        {
                case CHARTEVENT_KEYDOWN:
                        switch (key)
                        {
                                case KEY_UP:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        break;
                                case KEY_DOWN:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        break;
                                case KEY_NUM_1:
                                case KEY_NUM_7:
                                        if (sticket > 0) ModifyStop(key == KEY_NUM_7, sticket);
                                        break;
                                case KEY_NUM_2:
                                case KEY_NUM_8:
                                        if (sticket > 0) ModifyPrice(key == KEY_NUM_8, sticket);
                                        break;
                                case KEY_NUM_3:
                                case KEY_NUM_9:
                                        if (sticket > 0) ModifyTake(key == KEY_NUM_9, sticket);
                                        break;
                        }
                        break;
        }
}
//+------------------------------------------------------------------+
void ModifyPrice(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        s = OrderGetDouble(ORDER_SL);
        t = OrderGetDouble(ORDER_TP);
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+
void ModifyTake(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN);
        s = OrderGetDouble(ORDER_SL);
        t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+
void ModifyStop(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN);
        s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        t = OrderGetDouble(ORDER_TP);
        (*orders).ModifyPricePoints(ticket, p, s, t);
}
//+------------------------------------------------------------------+

如果乍一看,这段代码很复杂,请不要担心。 它只是为了显示可能发生的一件事。 您应该了解它,以便将来用它来发挥自己的优势。

代码本身与我们在上一篇文章中看到的代码非常相似,但在此我们可以做更多的事情:我们可以通过修改止盈、止损、和开仓价的数值来操纵订单。 代码中仍然存在的唯一不便之处在于,一旦在图表上下了订单,就不能简单地用放置另一笔订单将其删除。 您可以将订单保留在图表上,没关系,但当此 EA 创建挂单(目前它仅适用于挂单)时,您就能够使用数字键盘(物理键盘右侧的键盘)更改所示订单的价格点。

这是由事件处理程序完成的。 请注意,每次的关键是应对一件事,例如,增加止损或降低订单价格。 在工具箱的交易选项卡中观看执行此操作的结果。 您会学到很多东西。

如果您不确定是否要在平台中启动此代码(它绝对无害,但您仍然应该小心),请观看下面的视频,该视频演示了代码的实际作用。



以上代码的演示

您可以看到,在移动止损和止盈时,我们得到正确的移动,但当我们移动开仓价时,止损和止盈保持不变。 为什么会这样? 原因是对于交易服务器,您实际上是在移动订单,其可能是另一笔开仓交易的停止单。

请记住,在开始我就提到平仓交易的一种方式是在订单簿中下订单,并平滑移动。 这正是现阶段应该发生的事情。 也就是说,对于服务器,要移动的价格只是其提供的价格信息。 它并未将 OCO 订单视为一个整体。 OCO 订单显示为不同的价格点。 一旦达到其中一个限价,服务器将发送一个事件,该事件将删除持仓的价格。 止盈和止损这两个订单均将不复存在,因为相关单号将被系统删除。

在这种情况下,我们需要做的是令止损和止盈与订单价格一起移动。 为了实现这一点,我们对上面的代码进行以下修改:

void ModifyPrice(bool IsUp, const ulong ticket)
{
        double p, s, t;
        
        if (!OrderSelect(ticket)) return;
        p = OrderGetDouble(ORDER_PRICE_OPEN) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

现在,一旦开单价移动,止盈和止损价格也会随之移动,同时始终与开单点保持相同的距离。

它工作方式类似于针对仓位,但我们不能移动仓位开仓价。 我们将使用相同的函数来移动止盈和止损,仅有很小差异:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
// ... Code to move orders...
                                }else if (PositionSelectByTicket(ticket))
                                {
                                        m_TradeRequest.action   = TRADE_ACTION_SLTP;
                                        m_TradeRequest.position = ticket;
                                        m_TradeRequest.tp       = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                        m_TradeRequest.sl       = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

利用上面的代码,我们实现了盈亏平衡,或尾随停止。 我们唯一需要做的就是检查触发器开始移动的阈值 — 这是盈亏平衡值。 一旦触发激发,我们取固定仓位的开仓价作为止损价,然后我们修改仓位止损价位,结果是盈亏平衡。

尾随止损的工作方式完全相同,只是在这种情况下,当新价格距旧止损价格大于阈值时触发,止损价位才会随之移动。 当这种情况发生时,我们取新值用于止损,并调用上面的函数。 这太简单了。

稍后我将研究盈亏平衡和尾随止损触发器,届时我将展示如何为自动工作的 EA 开发这些触发器。 如果您是一位爱好者,您也许已经产生了一些关于这些级别的想法。 如果是这种情况 — 恭喜! 您走上了正确的道路上。

现在,我们回到价格更改程序,因为有些事情我还没有提到。 重要的是要知道它是如何、以及为什么存在的。 为了更容易解释,我们关注以下代码:

                bool ModifyPricePoints(const ulong ticket, const double Price, const double PriceStop, const double PriceTake)
                        {
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.symbol   = _Symbol;
                                if (OrderSelect(ticket))
                                {
                                        m_TradeRequest.action = (Price > 0 ? TRADE_ACTION_MODIFY : TRADE_ACTION_REMOVE);
                                        m_TradeRequest.order  = ticket;
                                        if (Price > 0)
                                        {
                                                m_TradeRequest.price      = NormalizeDouble(AdjustPrice(Price), m_Infos.nDigits);
                                                m_TradeRequest.sl         = NormalizeDouble(AdjustPrice(PriceStop), m_Infos.nDigits);
                                                m_TradeRequest.tp         = NormalizeDouble(AdjustPrice(PriceTake), m_Infos.nDigits);
                                                m_TradeRequest.type_time  = (ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) ;
                                                m_TradeRequest.expiration = 0;
                                        }
                                }else if (PositionSelectByTicket(ticket))
                                {
// Code for working with positions ...
                                }else return false;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

在某些情况下,我们需要从订单簿中删除订单,即取消或关闭它。 并且存在一种危险是,在执行过程中,您的一些作为会导致 EA 生成等于零的价格。 相信我,它发生了,而且很常见,特别是如果您所用的是自动 EA。 然后 EA 发送订单,如此修改订单价格,但由于错误,被发送价格为零。

在这些情况下,交易服务器会拒绝订单,但 EA 仍然呆立无知,几乎疯狂地坚持以零值作为开单价。 如果您不对此进行干预,它可能会进入循环,这在真实账户上是极端不幸的。 为了避免 EA 停滞在那里,坚持采用服务器不接受的东西,我引入以下思路:如果 EA 发送的开单价等于零,则订单必须由服务器平仓。 这正是此处代码的作用

它通知交易服务器必须关闭订单,并从订单簿中删除订单。 发生这种情况时,订单将不复存在,您会收到通知。 但我在这里还没有包含代码,因为还有其它同等有用的用途,而不仅仅是阻止 EA 盲目坚持某些东西。 它可用于简单地从订单簿中删除订单。

但我们还没有完成这篇文章。 还有最后一个过程,虽然它是可选的,但在某些情况下它可能很实用。 故此,既然我在这里打开了订单系统如何工作的黑匣子,我们再来看一个过程,如下所示:

                bool ClosePosition(const ulong ticket, const uint partial = 0)
                        {
                                double v1 = partial * m_Infos.VolMinimal, Vol;
                                bool IsBuy;
                                
                                if (!PositionSelectByTicket(ticket)) return false;
                                IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
                                Vol = PositionGetDouble(POSITION_VOLUME);
                                ZeroMemory(m_TradeRequest);
                                m_TradeRequest.action    = TRADE_ACTION_DEAL;
                                m_TradeRequest.type      = (IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);
                                m_TradeRequest.price     = SymbolInfoDouble(_Symbol, (IsBuy ? SYMBOL_BID : SYMBOL_ASK));
                                m_TradeRequest.position  = ticket;
                                m_TradeRequest.symbol    = _Symbol;
                                m_TradeRequest.volume    = ((v1 == 0) || (v1 > Vol) ? Vol : v1);
                                m_TradeRequest.deviation = 1000;
                                ToServer();
                                
                                return (_LastError == ERR_SUCCESS);
                        };

上面的这个程序让很多人做着远大的梦想,想象着某事,满眼冒星星。 在查看此过程时,您肯定认为它是用于平仓。 为什么有人会做白日梦呢?

冷静下来,我亲爱的读者。 您仍然不明白,当您看到过程的名称时,您有一个先入为主的观念。 但我们更深入一点,分析代码,就明白为什么许多人梦想远大。 这个过程里有一些计算,但为什么呢? 这样做是为了允许部分关闭。 我们来了解一下这是如何实现的。 假设您有一笔交易量为 300 的持仓。 然后,如果最小允许交易量为 100,则可以按 100、200 或 300 退出。

但为此,您必须通知一个数值,默认情况下为零,即告诉函数,该笔持仓将被完全平仓,但这只在您将其保留为默认值时才会发生。 但有一个细节:您未保留默认。 您指定了一个交易量数值,即要平仓的手数。 如果最小手数为 100,而您有 300,则表示交易量为 3 倍。 若要部分关闭,可以指定 1 或 2。 如果您指定 0、3 或大于 3 的数值,则持仓将被彻底平仓。

然而,有一些替代方案。 例如,在 B3(Bolsa do Brasil)的情况下,公司股票以100 手交易,但亦有允许分数的市场,您可以从 1 手开始交易。 在这种情况下,如果您让 EA 以分数模式运行,则同一示例中的 300 值可以从 1 变为 299,即使如此,仓位也不会完全平仓,留下一个未平仓残余。

我希望现在您明白了。 具体过程取决于市场和交易者的利益。 如果您在对冲账户上操作,您肯定需要上述函数。 没有它,仓位将简单地积累,需 EA 花费时间和资源来分析可能已经平仓的内容。

为了结束本文,并涵盖有关订单系统的所有问题,我们来看看 EA 代码理应如何,能消除以下限制:一旦创建订单,就无法下其它订单,也无法操纵数据。 为了修复这些问题,我们必须对 EA 代码进行一些修改,但这样做,您已经能够感到开心,并做出更多的事情。 也许这会令您更感振奋,即用相对简单的代码、更少的编程知识,就能完成。

为了部分修正 EA,我们必须修改负责处理图表事件的代码。 新代码如下所示:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
#define KEY_UP                  38
#define KEY_DOWN                40
#define KEY_NUM_1               97
#define KEY_NUM_2               98
#define KEY_NUM_3               99
#define KEY_NUM_7               103
#define KEY_NUM_8               104
#define KEY_NUM_9               105

        static ulong sticket = 0;
        ulong ul0;
        int key = (int)lparam;
        
        switch (id)
        {
                case CHARTEVENT_KEYDOWN:
                        switch (key)
                        {
                                case KEY_UP:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        ul0 = (*orders).CreateOrder(ORDER_TYPE_BUY, user05, user03, user02, user01, user04);
                                        sticket = (ul0 > 0 ? ul0 : sticket);
                                        break;
                                case KEY_DOWN:
                                        if (sticket == 0)
                                                sticket = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        ul0 = (*orders).CreateOrder(ORDER_TYPE_SELL, user05, user03, user02, user01, user04);
                                        sticket = (ul0 > 0 ? ul0 : sticket);
                                        break;
                                case KEY_NUM_1:
                                case KEY_NUM_7:
                                        if (sticket > 0) ModifyStop(key == KEY_NUM_7, sticket);
                                        break;
                                case KEY_NUM_2:
                                case KEY_NUM_8:
                                        if (sticket > 0) ModifyPrice(key == KEY_NUM_8, sticket);
                                        break;
                                case KEY_NUM_3:
                                case KEY_NUM_9:
                                        if (sticket > 0) ModifyTake(key == KEY_NUM_9, sticket);
                                        break;
                        }
                        break;
        }
}

我们删除了划掉的部分,并添加了一个新变量,该变量将接收订单类返回的单号数值如果该值不为零,则新单号将保存在静态变量之中,该变量将存储该值,直到写入新值来取代它。 好了,这已经可以让您管理更多的东西,如果订单意外执行,并且您在开立新订单时没有覆盖该值,您仍然可以管理订单限价。

现在,作为一个额外的任务,为了测试您是否真的学会了如何使用订单系统(在下一篇文章之前),尝试用订单系统开立一笔市价仓位,并利用单号值来管理持仓的限价。无需阅读进一步的解释即可执行此操作。 这将有助于您了解是否能够跟上解释。

现在我们来看看代码改变价格。 开立的代码是相同的。 我们可以跳过订单转换为仓位的情况。 我们研究止盈:

void ModifyTake(bool IsUp, const ulong ticket)
{
        double p = 0, s, t;
        
        if (!OrderSelect(ticket)) return;
        if (OrderSelect(ticket))
        {
                p = OrderGetDouble(ORDER_PRICE_OPEN);
                s = OrderGetDouble(ORDER_SL);
                t = OrderGetDouble(ORDER_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        }else if (PositionSelectByTicket(ticket))
        {
                s = PositionGetDouble(POSITION_SL);
                t = PositionGetDouble(POSITION_TP) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
        }else return;
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

划掉的代码不再存在,因此我们可以使用新代码来管理持仓的止盈值。 这同样适用于如下所示的止损代码:

void ModifyStop(bool IsUp, const ulong ticket)
{
        double p = 0, s, t;
        
        if (!OrderSelect(ticket)) return;
        if (OrderSelect(ticket))
        {
                p = OrderGetDouble(ORDER_PRICE_OPEN);
                s = OrderGetDouble(ORDER_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
                t = OrderGetDouble(ORDER_TP);
        }else if (PositionSelectByTicket(ticket))
        {
                s = PositionGetDouble(POSITION_SL) + (SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) * (IsUp ? 1 : -1));
                t = PositionGetDouble(POSITION_TP);
        }else return;
        (*orders).ModifyPricePoints(ticket, p, s, t);
}

将此 EA 用于学习工具,尤其是在模拟账户上 — 充分利用它;全面探讨前三篇文章中的内容,因为此刻我认为订单系统已完成。 在下一篇文章中,我将展示如何初始化 EA,以便捕获与此初始化相关的一些信息、问题和可能的解决方案。 但这些问题不是订单系统的一部分 — 我们已经实现了令 EA 自动化真正需要的一切。


结束语

尽管我们在前三篇文章中见识了一些,但我们距离构建一个完整的自动化 EA 还有很长的路要走。 许多人经常忽略、或不明白这里提供的细节,这是危险的。 我们刚刚开始谈论自动化系统,以及在没有相应知识的情况下使用它们会遇到的危险。

附件中提供了所有先前所研究的代码。 研究它,并了解事物是如何运作的。 希望在下一篇文章中再遇见您。