使用限价订单替代止盈且无需修改 EA 的原始代码

Dmitriy Gizlyk | 10 十二月, 2018

内容

概述

在各种论坛中,用户批评 MetaTrader 5 的市场止盈价位性能。 这样的帖子也可以在本网站的论坛里找到。 用户撰写了许多在执行止盈期间滑点对结算结果的负面影响。 作为备选方案,一些人建议使用限价订单来取代标准止盈。

另一方面,与标准止盈相比,采用限价订单允许交易者实现部分和逐级平仓的算法,因为在限价订单中,您可以指定不同于实际持仓量的交易量。 在本文中,我想提供一种能够实现此类止盈的替代选项。

1. 一般观点

我认为,没有必要争论哪种方案更好 — 内置的止盈或取代它的限价订单。 每个交易者都应该根据他们的策略原则和需求来解决这个问题。 本文仅提供一种可能的解决方案。

在开发限价订单系统之前,我们研究一下在设计算法时应该注意的一些方面。

我们应记住的主要事件是,止盈是指平仓的订单。 这似乎是不言而喻的,但所有人都习惯于由终端和系统来执行这一任务。 由于我们决定替换设定止盈的系统,我们应对其负全部维护责任。

我到底在说什么? 一笔持仓不仅可以通过止盈平仓,而且可以通过止损或交易者自行决定平仓(通常运行一些 EA 以市价平仓)。 这意味着我们的系统应该能跟踪市场中是否存在相应持仓,并在无持仓或出现任何原因时立即删除限价挂单。 否则,与标准止盈激活期间的滑点相比,此方法可能会在不期望的情况下开仓,从而导致更大的损失。

此外,持仓量可以部分平仓,也可以增加(对于净持账户)。 所以,重要的是不仅要跟踪有效持仓,还要跟踪其仓量。 如果持仓量已变化,则应立即修改限价挂单。

另一层面则涉及对冲系统操作。 该系统能够进行持仓的单独结算,并允许单一品种同时开多仓。 这意味着激活限价订单不会将现有持仓平仓。 代之,它会开一笔新仓。 因此,在触发限价订单后,我们需要以相反的仓位进行平仓。

另一个可能的问题是挂单的止盈。 在这种情况下,我们应该确保在处理订单之前不会触发止盈。 第一反应,是可以使用 stop-limit 挂单。 例如,我们可以同时放置 sell stop 挂单和 buy stop 限价挂单。 但是系统不允许我们使用 sell limit 执行类似的操作。 这会引发随后一系列限价止盈的跟踪挂单激活的问题。 反过来,跟踪程序内的挂单激活并放置一笔无止盈挂单,有可能导致开仓失控。 结果就是,价格也许触及止盈并反转。 程序失却控制,不允许平仓,最终造成亏损。

我个人的解决方案是在指定标准止盈时放置挂单。 在开仓后,放置限价挂单替代止盈,并将止盈设置为零。 此选项可确保我们不会失去对事态的控制。 如果程序失去与服务器的连接,则系统将激活订单止盈。 在此情况下,由负值滑点引起的可能亏损低于失控而导致的亏损。

另一个问题是改变先前设定的止盈。 通常,在运用不同的策略时,您必须跟踪并调整持仓的止盈。 我们有两种选择。

  1. 如果我们修改这样的 EA 代码,那么为了在代码中不必搜索所有可能的选项来变更止盈,我们仅需调用我们的类方法来替换调用 OrderSend 函数,在类方法中我们已检查了是否存在先前设置的限价订单,以及它是否对应于新价位。 如有必要,修改先前下达的订单,或者,如果先前下达的限价订单符合新需求,则忽略该命令。
  2. 我们运用采购的 EA,但我们无法访问其代码; 我们的程序不会开仓,只会取代止盈。 在这种情况下,很高概率可能会为我们已设置限价单的持仓设置了止盈。 这意味着我们应加强现有限价订单的相关检查并调整它们,同时将止盈字段设置为零。

此外,我们应该跟踪自当前价格设置挂单的最小距离,以及经纪商设置的冻结交易的距离。 如果前者同样应用在设置系统止盈,则冻结距离可能会拖累,因为在小于其范围内删除或修改用于平仓的限价挂单是不可能的。 不幸的是,这种风险不仅应该在构建系统时考虑,还应该在运用时加以考虑,因为它不依赖于系统的算法。

2. 实现 "持仓 - 限制订单" 链接的原理

正如我之前已经提到的,跟踪持仓的状态并搜索与之匹配的限价止盈是必要的。 我们来看看如何实现这一点。 首先,我们需要判断在什么时间点我们需要进行控制以避免终端过载。

在交易时段开放期间随时都有可能修改持仓。 不过,这种情况不会频繁发生,而在每次逐笔报价时进行检查则会显著增加 EA 要执行的操作。 在此我们可以利用事件。 根据 MQL5 文档,在交易服务器上完成一次交易操作时均会生成一个交易事件。 事件的结局会启动 OnTrade 函数。 因此,该函数能够启动持仓和限价止盈之间的匹配检查。 这样我们就无需在每次逐笔报价时检查匹配,同时也不会错过任何变化。

接下来是标识问题。 初看,一切都很简单。 我们应该简单地检查限价订单和持仓。 然而,我们希望构建一套适用于不同帐户类型和不同策略的通用算法。 另请注意,可以在策略中使用限价订单。 因此,我们应该部署限价止盈。 我提议用注释来标识它们。 由于我们的限价订单用于取代止盈,我们将在订单注释的开头添加 "TP" 来标识它们。 接下来,我们将添加一个阶段编号,以防多阶段持仓被误操作平仓。 这对于净持结算系统来说已经足够了,但是我们不要忘记对冲账户,它能够在同一账户里开立多笔同品种的仓位。 因此,我们应该在限价止盈注释中添加相应的仓位 ID。

3. 创建限价止盈类

我们总结一下上述内容。 我们类其功能可以分为两个逻辑过程:

  1. 更改发送至服务器的交易请求。
  2. 监控和调整持仓,并下达限价挂单。

为了便于使用,我们将算法设计为 CLimitTakeProfit 类,其中的所有函数声明为静态。 这样我们就可以直接使用类方法,而无需在程序代码中声明其实例。

class CLimitTakeProfit : public CObject
  {
private:
   static CSymbolInfo       c_Symbol;
   static CArrayLong        i_TakeProfit; //固定止盈
   static CArrayDouble      d_TakeProfit; //按盈利百分比平仓
   
public:
                     CLimitTakeProfit();
                    ~CLimitTakeProfit();
//---
   static void       Magic(int value)  {  i_Magic=value; }
   static int        Magic(void)       {  return i_Magic;}
//---
   static void       OnlyOneSymbol(bool value)  {  b_OnlyOneSymbol=value;  }
   static bool       OnlyOneSymbol(void)        {  return b_OnlyOneSymbol; }
//---
   static bool       OrderSend(const MqlTradeRequest &request, MqlTradeResult &result);
   static bool       OnTrade(void);
   static bool       AddTakeProfit(uint point, double percent);
   static bool       DeleteTakeProfit(uint point);
   
protected:
   static int        i_Magic;          //用于控制的魔幻数字
   static bool       b_OnlyOneSymbol;  //仅针对一个品种的控制
//---
   static bool       SetTakeProfits(ulong position_ticket, double new_tp=0);
   static bool       SetTakeProfits(string symbol, double new_tp=0);
   static bool       CheckLimitOrder(MqlTradeRequest &request);
   static void       CheckLimitOrder(void);
   static bool       CheckOrderInHistory(ulong position_id, string comment, ENUM_ORDER_TYPE type, double &volume, ulong call_position=0);
   static double     GetLimitOrderPriceByComment(string comment);
  };

Magic, OnlyOneSymbol, AddTakeProfit 和 DeleteTakeProfit 方法是用于配置类操作的方法。 Magic — 用于跟踪持仓的魔幻数字(对冲账户)。 如为 -1, 该类适用于所有仓位。 OnlyOneSymbol 指示该类仅针对 EA 启动时的图表品种工作。 AddTakeProfit 和 DeteleTakeProfit 方法用于添加和删除固定止盈价位,并指示要平仓的交易量占初始仓量的百分比。

用户可以根据需要应用这些方法,但它们都是可选的。 默认情况下,该方法适用于未设置固定止盈的所有魔幻数字和品种。 仅设置限价订单来替代指定持仓的止盈。

3.1. 修改发送交易订单

OrderSend 方法会监视 EA 发送的订单。 方法调用的名称和形式类似于向 MQL5 发送订单的标准函数。 用我们的方法替换标准函数,简化了将算法嵌入到先前编写的 EA 代码中。

我们已经阐述了用挂单替换止盈的问题。 为此,我们只能在此模块中替换市场中订单的止盈。 但请记住,由服务器接受的订单并不一定意味着它将被执行。 此外,在发送订单后,我们会收到订单编号,但不会收到仓位 ID。 所以,我们将在监控模块中取代止盈。 在此,我们只跟踪先前已设定止盈变化的时刻。

在方法代码的开头,检查所发送请求是否对应于算法操作的过滤器设置。 另外,我们应该检查交易的类型。 它应该对应于持仓的止损价位修改请求。 另外,不要忘记检查请求中是否存在止盈。 如果请求难以满足其中至少一个需求,则会立即将其发送到服务器。

检查需求后,请求将传递给 SetTakeProfit 方法,在该方法中会放置限价订单。 请注意,该类具有两种使用持仓单号和品种的方法。 如果请求不包含持仓单号,则第二个更适用于净持帐户。 如果方法成功,则将请求中的止盈字段设置为零。

由于该请求可能会改变止盈和止损,因此请检查该持仓中的止损和止盈是否恰当。 如有必要,则向服务器发送请求并退出该函数。 完整的方法代码显示如下。

bool CLimitTakeProfit::OrderSend(MqlTradeRequest &request,MqlTradeResult &result)
  {
   if((b_OnlyOneSymbol && request.symbol!=_Symbol) ||
      (i_Magic>=0 && request.magic!=i_Magic) || !(request.action==TRADE_ACTION_SLTP && request.tp>0))
      return(::OrderSend(request,result));
//---
   if(((request.position>0 && SetTakeProfits(request.position,request.tp)) ||
       (request.position<=0 && SetTakeProfits(request.symbol,request.tp))) && request.tp>0)
      request.tp=0;
   if((request.position>0 && PositionSelectByTicket(request.position)) ||
      (request.position<=0 && PositionSelect(request.symbol)))
     {
      if(PositionGetDouble(POSITION_SL)!=request.sl || PositionGetDouble(POSITION_TP)!=request.tp)
         return(::OrderSend(request,result)); 
     }
//---
   return true;
  }

现在,我们详细分析一下 SetTakeProfit 方法。 在方法的开头,检查指定的持仓是否存在,以及我们是否要处理该持仓品种。 接下来,更新持仓品种的数据。 之后,计算限价订单的最接近许可价格。 如果出现任何错误,则使用 "false" 结果退出方法。

bool CLimitTakeProfit::SetTakeProfits(ulong position_ticket, double new_tp=0)
  {
   if(!PositionSelectByTicket(position_ticket) || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
      return false;
   if(!c_Symbol.Name(PositionGetString(POSITION_SYMBOL)) || !c_Symbol.Select() || !c_Symbol.Refresh() || !c_Symbol.RefreshRates())
      return false;
//---
   double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());

之后,准备结构模板以便发送下达限价挂单的交易请求。 计算该持仓应设置或指定的止盈价位,所设的固定止盈不应超过计算出的距离。

   MqlTradeRequest tp_request={0};
   MqlTradeResult tp_result={0};
   tp_request.action =  TRADE_ACTION_PENDING;
   tp_request.magic  =  PositionGetInteger(POSITION_MAGIC);
   tp_request.type_filling =  ORDER_FILLING_RETURN;
   tp_request.position=position_ticket;
   tp_request.symbol=c_Symbol.Name();
   int total=i_TakeProfit.Total();
   double tp_price=(new_tp>0 ? new_tp : PositionGetDouble(POSITION_TP));
   if(tp_price<=0)
      tp_price=GetLimitOrderPriceByComment("TPP_"+IntegerToString(position_ticket));
   double open_price=PositionGetDouble(POSITION_PRICE_OPEN);
   int tp_int=(tp_price>0 ? (int)NormalizeDouble(MathAbs(open_price-tp_price)/c_Symbol.Point(),0) : INT_MAX);
   double position_volume=PositionGetDouble(POSITION_VOLUME);
   double closed=0;
   double closed_perc=0;
   double fix_closed_per=0;

接下来,安排循环检查和固定止盈。 首先,设置订单注释(编码原理如上所述)。 然后确保在持仓或请求中指定的止盈不超过固定值。 如果超过,则转到下一个止盈。 此外,请确保先前设置的限价订单的交易量与持仓量不会溢出。 如果限价订单与持仓量溢出,则退出循环。

   for(int i=0;i<total;i++)
     {
      tp_request.comment="TP"+IntegerToString(i)+"_"+IntegerToString(position_ticket);
      if(i_TakeProfit.At(i)<tp_int && d_TakeProfit.At(i)>0)
        {
         if(closed>=position_volume || fix_closed_perc>=100)
            break;

下一步是填写交易请求结构中缺失的元素。 为此,计算新限价订单的交易量并指定订单类型和开仓价。

//---
         double lot=position_volume*MathMin(d_TakeProfit.At(i),100-closed)/(100-fix_closed_perc);
         lot=MathMin(position_volume-closed,lot);
         lot=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((lot-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
         lot=NormalizeDouble(lot,2);
         tp_request.volume=lot;
         switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
           {
            case POSITION_TYPE_BUY:
              tp_request.type=ORDER_TYPE_SELL_LIMIT;
              tp_request.price=c_Symbol.NormalizePrice(open_price+i_TakeProfit.At(i)*c_Symbol.Point());
              break;
            case POSITION_TYPE_SELL:
              tp_request.type=ORDER_TYPE_BUY_STOP;
              tp_request.price=c_Symbol.NormalizePrice(open_price-i_TakeProfit.At(i)*c_Symbol.Point());
              break;
           }

填写交易请求后,检查之前是否设置了具有相同参数的限价挂单。 为此,将已填充的请求结构传递给 CheckLimitOrder 方法(方法的算法将在下面研究)。 如果之前未设置订单,则将设定的订单交易量添加到该持仓的已设定交易量总和之中。 这对于确保持仓量和所放置的限价订单交易量彼此对应是必要的。

         if(CheckLimitOrder(tp_request))
           {
            if(tp_request.volume>=0)
              {
               closed+=tp_request.volume;
               closed_perc=closed/position_volume*100;
              }
            else
              {
               fix_closed_per-=tp_request.volume/(position_volume-tp_request.volume)*100;
              }
            continue;
           }

如果订单尚未下达,则参考经纪商关于当前价格的要求调整其价格,并向服务器发送请求。 如果请求成功发送,我们会将订单的交易量添加到先前为该持仓设置的交易量之和。

         switch(tp_request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              tp_request.price=MathMin(tp_request.price,max_buy_limit);
              break;
            case  ORDER_TYPE_SELL_LIMIT:
              tp_request.price=MathMax(tp_request.price,min_sell_limit);
              break;
           }
         if(::OrderSend(tp_request,tp_result))
           {
            closed+=tp_result.volume;
            closed_perc=closed/position_volume*100;
            ZeroMemory(tp_result);
           }
        }
     }

完成循环后,使用相同的算法放置限价订单,来弥补指定价格的修订请求(或持仓)中缺失的交易量。 如果交易量小于允许的最小交易量,则以 'false' 结果退出该函数。

   if(tp_price>0 && position_volume>closed)
     {
      tp_request.price=tp_price;
      tp_request.comment="TPP_"+IntegerToString(position_ticket);
      tp_request.volume=position_volume-closed;
      if(tp_request.volume<c_Symbol.LotsMin())
         return false;
      tp_request.volume=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
      tp_request.volume=NormalizeDouble(tp_request.volume,2);
//---
      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           tp_request.type=ORDER_TYPE_SELL_LIMIT;
           break;
         case POSITION_TYPE_SELL:
           tp_request.type=ORDER_TYPE_BUY_LIMIT;
           break;
        }
      if(CheckLimitOrder(tp_request) && tp_request.volume>=0)
        {
         closed+=tp_request.volume;
         closed_perc=closed/position_volume*100;
        }
      else
        {
         switch(tp_request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              tp_request.price=MathMin(tp_request.price,max_buy_limit);
              break;
            case  ORDER_TYPE_SELL_LIMIT:
              tp_request.price=MathMax(tp_request.price,min_sell_limit);
              break;
           }
         if(tp_request.volume<=0)
           {
            tp_request.volume=position_volume-closed;
            tp_request.volume=c_Symbol.LotsMin()+MathMax(0,NormalizeDouble((tp_request.volume-c_Symbol.LotsMin())/c_Symbol.LotsStep(),0)*c_Symbol.LotsStep());
            tp_request.volume=NormalizeDouble(tp_request.volume,2);
           }
         if(::OrderSend(tp_request,tp_result))
           {
            closed+=tp_result.volume;
            closed_perc=closed/position_volume*100;
            ZeroMemory(tp_result);
           }
        }
     }      

在方法完成时,检查放置的限价订单交易量是否涵盖持仓量。 如果是,将持仓的止盈设置为零并退出该函数。

   if(closed>=position_volume && PositionGetDouble(POSITION_TP)>0)
     {
      ZeroMemory(tp_request);
      ZeroMemory(tp_result);
      tp_request.action=TRADE_ACTION_SLTP;
      tp_request.position=position_ticket;
      tp_request.symbol=c_Symbol.Name();
      tp_request.sl=PositionGetDouble(POSITION_SL);
      tp_request.tp=0;
      tp_request.magic=PositionGetInteger(POSITION_MAGIC);
      if(!OrderSend(tp_request,tp_result))
         return false;
     }
   return true;
  }

我们来看看 CheckLimitOrder 方法的算法,以便全景完整。 在功能上,该方法检查已准备完毕的交易请求是否存在先前已放置的限制订单。 如果已设置订单,则该方法返回 "true",且不会设置新订单。

在方法开始时,判断放置限价订单的最接近的可能价位。 如果有必要修改先前所下订单,我们将需要它们。

bool CLimitTakeProfit::CheckLimitOrder(MqlTradeRequest &request)
  {
   double min_sell_limit=c_Symbol.NormalizePrice(c_Symbol.Ask()+c_Symbol.StopsLevel()*c_Symbol.Point());
   double max_buy_limit=c_Symbol.NormalizePrice(c_Symbol.Bid()-c_Symbol.StopsLevel()*c_Symbol.Point());

下一步是安排循环来迭代所有持仓。 由其注释来标识必要的订单。

   for(int i=0;i<total;i++)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
      if(OrderGetString(ORDER_COMMENT)!=request.comment)
         continue;

搜索包含必要注释的订单时,检查其交易量和订单类型。 如果其中任一个参数不匹配,则删除现有的挂单,并以 'false' 结果退出该函数。 如果订单删除错误,现有订单的交易量将显示在请求的交易量字段中。

      if(OrderGetDouble(ORDER_VOLUME_INITIAL) != request.volume || OrderGetInteger(ORDER_TYPE)!=request.type)
        {
         MqlTradeRequest del_request={0};
         MqlTradeResult del_result={0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
            return false;
         request.volume=OrderGetDouble(ORDER_VOLUME_INITIAL);
        }

在下一阶段,检查已检测到的订单的开仓价和参数中指定的价格。 如有必要,修改当前订单并以 "true" 结果退出方法。

      if(MathAbs(OrderGetDouble(ORDER_PRICE_OPEN)-request.price)>=c_Symbol.Point())
        {
         MqlTradeRequest mod_request={0};
         MqlTradeResult mod_result={0};
         mod_request.action=TRADE_ACTION_MODIFY;
         mod_request.price=request.price;
         mod_request.magic=request.magic;
         mod_request.symbol=request.symbol;
         switch(request.type)
           {
            case ORDER_TYPE_BUY_LIMIT:
              if(mod_request.price>max_buy_limit)
                 return true;
              break;
            case ORDER_TYPE_SELL_LIMIT:
              if(mod_request.price<min_sell_limit)
                 return true;
              break;
           }
         bool mod=::OrderSend(mod_request,mod_result);
        }
      return true;
     }

不过,我们不应忘记,可能存在限价订单已经与交易量匹配的情况。 因此,如果在已放置挂单中未找到必要的订单,则检查当前仓位的订单历史记录。 此功能在我们最后调用的 CheckOrderInHistory 方法中实现。

   if(!PositionSelectByTicket(request.position))
      return true;
//---
   return CheckOrderInHistory(PositionGetInteger(POSITION_IDENTIFIER),request.comment, request.type, request.volume);
  }

根据帐户类型,我们有两个激活限价订单的选项:

  1. 在某个位置直接激活(净持账户)。
  2. 限价订单反向开仓,仓位互平(对冲账户)。

在搜索这种可能性时,请注意此类订单也许与此持仓无关,因此我们将执行成交搜索,并从其一得到单号。

bool CLimitTakeProfit::CheckOrderInHistory(ulong position_id, string comment, ENUM_ORDER_TYPE type, double &volume, ulong call_position=0)
  {
   if(!HistorySelectByPosition(position_id))
      return true;
   int total=HistoryDealsTotal();
   bool hedging=(AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
//---
   for(int i=0;i<total;i++)
     {
      ulong ticket=HistoryDealGetTicket((uint)i);
      ticket=HistoryDealGetInteger(ticket,DEAL_ORDER);
      if(!HistoryOrderSelect(ticket))
         continue;
      if(ticket<=0)
         continue;

对于对冲账户,我们应首先检查订单是否与另一笔持仓相关。 如果检测到来自其它持仓的订单,则依据该持仓搜索具有必要注释的订单。 为此,执行 CheckOrderInHistory 函数的递归调用。 为避免死循环,请在调用方法之前检查方法是否自此持仓调用。 如果检测到订单,则以 "true" 结果退出方法。 否则,重新加载持仓的历史记录并继续下一笔成交。

      if(hedging && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=position_id && HistoryOrderGetInteger(ticket,ORDER_POSITION_ID)!=call_position)
        {
         if(CheckOrderInHistory(HistoryOrderGetInteger(ticket,ORDER_POSITION_ID),comment,type,volume))
            return true;
         if(!HistorySelectByPosition(position_id))
            continue;
        }

检查当前持仓订单的注释和订单类型。 如果检测到订单,将其交易量的负数值写入请求并退出方法。

      if(HistoryOrderGetString(ticket,ORDER_COMMENT)!=comment)
         continue;
      if(HistoryOrderGetInteger(ticket,ORDER_TYPE)!=type)
         continue;
//---
      volume=-OrderGetDouble(ORDER_VOLUME_INITIAL);
      return true;
     }
   return false;
  }

附件中提供了所有方法和函数的完整代码。

3.2. 处理交易操作

监控和调整现有持仓和开立限价单是我们算法的第二个模块。

在账户上进行交易会产生交易事件,从而触发执行 OnTrade 函数。 在类中添加相应的方法来处理交易。

方法的算法一开始会准备一些工作:获取帐户上的持仓数量并检查订单类型。

bool CLimitTakeProfit::OnTrade(void)
  {
   int total=PositionsTotal();
   bool result=true;
   bool hedhing=AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING;

接下来,安排循环来迭代持仓。 在循环开始时,检查持仓是否对应于品种和魔幻数字的分类条件(对于对冲账户)。

   for(int i=0;i<total;i++)
     {
      ulong ticket=PositionGetTicket((uint)i);
      if(ticket<=0 || (b_OnlyOneSymbol && PositionGetString(POSITION_SYMBOL)!=_Symbol))
         continue;
//---
     if(i_Magic>0)
        {
         if(hedhing && PositionGetInteger(POSITION_MAGIC)!=i_Magic)
            continue;
        }

对于对冲账户,检查持仓的限价止盈是否经过处理。 如果是,则由操作执行平仓。 在成功平仓后,继续转至下一笔持仓。

      if(hedhing)
        {
         string comment=PositionGetString(POSITION_COMMENT);
         if(StringFind(comment,"TP")==0)
           {
            int start=StringFind(comment,"_");
            if(start>0)
              {
               long ticket_by=StringToInteger(StringSubstr(comment,start+1));
               long type=PositionGetInteger(POSITION_TYPE);
               if(ticket_by>0 && PositionSelectByTicket(ticket_by) && type!=PositionGetInteger(POSITION_TYPE))
                 {
                  MqlTradeRequest   request  ={0};
                  MqlTradeResult    trade_result   ={0};
                  request.action=TRADE_ACTION_CLOSE_BY;
                  request.position=ticket;
                  request.position_by=ticket_by;
                  if(::OrderSend(request,trade_result))
                     continue;
                 }
              }
           }
        }

在循环结束时,调用 SetTakeProfits 方法检查并设置该持仓的限价订单。 方法的算法如上所述。

      result=(SetTakeProfits(PositionGetInteger(POSITION_TICKET)) && result);
     }

在持仓检查的循环完成后,确保有效限价单与持仓相对应,并在必要时,删除平仓后的剩余限价单。 为此,调用 CheckLimitOrder 方法。 在此情况下,与上述函数相比,无需参数的情况下即可调用该函数。 发生这种情况是因为我们调用了一个完全不同的方法,而由于 函数重载 属性,可以应用类似的名称。

   CheckLimitOrder();
//---
   return result;
  }

方法的算法基于迭代所有已放置的订单。 所以使用注释是必然的选择。

void CLimitTakeProfit::CheckLimitOrder(void)
  {
   int total=OrdersTotal();
   bool res=false;
//---
   for(int i=0;(i<total && !res);i++)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
      string comment=OrderGetString(ORDER_COMMENT);
      if(StringFind(comment,"TP")!=0)
         continue;
      int pos=StringFind(comment,"_",0);
      if(pos<0)
         continue;

检测到限价止盈后,从注释中检索反向持仓的 ID。 使用 ID 访问指定的持仓。 如果不存在此笔持仓,则删除该订单。

      long pos_ticker=StringToInteger(StringSubstr(comment,pos+1));
      if(!PositionSelectByTicket(pos_ticker))
        {
         MqlTradeRequest del_request={0};
         MqlTradeResult del_result={0};
         del_request.action=TRADE_ACTION_REMOVE;
         del_request.order=ticket;
         if(::OrderSend(del_request,del_result))
           {
            i--;
            total--;
           }
         continue;
        }

如果您设法访问该持仓,请检查订单类型是否与持仓类型相对应。 对于在交易期间可能出现持仓反转的净持账户,必须进行检查。 如果检测到不匹配,则删除订单并继续检查下一笔订单。

      switch((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE))
        {
         case POSITION_TYPE_BUY:
           if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_LIMIT)
              continue;
           break;
         case POSITION_TYPE_SELL:
           if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_LIMIT)
              continue;
           break;
        }
      MqlTradeRequest del_request={0};
      MqlTradeResult del_result={0};
      del_request.action=TRADE_ACTION_REMOVE;
      del_request.order=ticket;
      if(::OrderSend(del_request,del_result))
        {
         i--;
         total--;
        }
     }
//---
   return;
  }

在附件中查找所有类方法的完整代码。

4. 将类集成到 EA

类的操作完成后,我们来看看如何将它集成到已开发的 EA 中。

您可能还记得,我们类的所有方法都是静态的,这意味着我们可以在不声明类实例的情况下调用它们。 最初选择这种方法是为了简化将类集成到已开发 EA 中的工作。 实际上,这是将类集成到 EA 中的第一步。

接下来,创建 LimitOrderSend 函数,其调用参数类似于 OrderSend 函数。 它位于类代码下面,它唯一的功能是调用 CLimitTakeProfit::OrderSend 方法。 接下来,使用 #define 指令 将原始 OrderSend 函数替换为自定义函数。 应用该方法允许我们同时将代码嵌入到发送交易请求的所有 EA 函数中,这样我们就不必浪费时间在整个 EA 代码中搜索这些命令。

bool LimitOrderSend(const MqlTradeRequest &request, MqlTradeResult &result)
 { return CLimitTakeProfit::OrderSend(request,result); } 
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
#define OrderSend(request,result)      LimitOrderSend(request,result)

由于许多 EA 没有 OnTrade 函数,我们可能会将其包含在类文件中。 但是如果您的 EA 已具有此函数,则需要删除或注释掉下面的代码,并将 CLimitTakeProfit::OnTrade 方法调用添加到 EA 的函数实体中。

void OnTrade()
  {
   CLimitTakeProfit::OnTrade();
  }

接下来,我们必须使用 #include 指令 添加对类文件的引用,以便将类集成到 EA 中。 请记住,在调用其它函数库和 EA 代码之前应定位该类。 下面是从终端标准发行包中将类添加到 MACD Sample.mq5 EA 的示例。

//+------------------------------------------------------------------+
//|                                          MACD Sample LimitTP.mq5 |
//|                             版权所有 2009-2017, MetaQuotes 软件公司 |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright   "版权所有 2009-2017, MetaQuotes 软件公司"
#property link        "http://www.mql5.com"
#property version     "5.50"
#property description "确保智能系统正常工作非常重要"
#property description "图表和用户不应造成任何输入设置错误"
#property description "我们示例中的变量 (Lots, TakeProfit, TrailingStop),"
#property description "我们检查图表中超过 2*trend_period 根柱线上的 TakeProfit"

#define MACD_MAGIC 1234502
//---
#include <Trade\LimitTakeProfit.mqh>
//---
#include <Trade\Trade.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Trade\PositionInfo.mqh>
#include <Trade\AccountInfo.mqh>
//---

您可以将部分平仓添加到 OnInit 函数代码中。 我们的 EA 已做好出发准备。

在实盘账户上使用它之前,不要忘记测试 EA。

//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                  |
//+------------------------------------------------------------------+
int OnInit(void)
  {
//--- 创建所有必要的对象
   if(!ExtExpert.Init())
      return(INIT_FAILED);
   CLimitTakeProfit::AddTakeProfit(100,50);
//--- 成功
   return(INIT_SUCCEEDED);
  }

EA 操作

完整的 EA 代码可以在附件中找到。

结束语

本文提供了通过限价订单替换持仓止盈的机制。 我们尝试尽可能简化地将方法与任何现有的 EA 代码相集成。 我希望,这篇文章对您提供有益帮助,您能够评估这两种方法的所有优、缺点。

本文中使用的程序

#
名称
类型
说明
1 LimitTakeProfit.mqh 类库 以限价订单替换订单止盈
2 MACD Sample.mq5 智能交易系统 原版 MetaTrader 5 示例
3 MACD Sample LimitTP.mq5 智能交易系统 将类集成到 MetaTrader 5 示例中所用的 EA