修改仓位的止损和/或止盈水平

MQL 程序可以改变未平仓仓位的保护性 Stop LossTake Profit 价格水平。TRADE_ACTION_SLTP 元素(在 ENUM_TRADE_REQUEST_ACTIONS 枚举中)就是用于这个目的,也就是在填充 MqlTradeRequest 结构体时,应在 action 字段写入 TRADE_ACTION_SLTP。

这是唯一的必填字段。是否需要填写其他字段是由账户操作模式 ENUM_ACCOUNT_MARGIN_MODE 决定的。在对冲账户上,应填写 symbol 字段,但可以省略仓位订单号。相反,在对冲账户上,必须注明 position 仓位订单号,但可以省略交易品种。这是由于不同类型账户的仓位标识方式的特殊性。在净额结算期间,每个交易品种只能有一个仓位。

为了统一代码,建议填写这两个字段(如果有相关信息)。

保护价格水平在 sltp 字段中设置。可以只测试其中一个字段。要删除保护水平,请为其赋零值。

下表总结了根据计数模式填写字段的要求。必填字段标有星号,可选字段标有加号。

字段

净额结算

对冲

action

*

*

symbol

*

+

position

+

*

sl

+

+

tp

+

+

为了执行修改保护水平的操作,我们在 MqlTradeRequestSync 结构体中引入了 adjust 方法的几个重载。

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool adjust(const ulong posconst double stop = 0const double take = 0);
   bool adjust(const string name, const double stop = 0const double take = 0);
   bool adjust(const double stop = 0const double take = 0);
   ...
};

正如我们上面看到的,根据环境的不同,修改只能通过订单号或仓位交易品种来完成。在前两个原型中考虑了这些选项。

此外,由于该结构体可能已经用于先前的请求,它可能已经填充了 positionsymbols 字段。然后可以用最后一个原型调用该方法。

我们还没有展示这三个方法的实现,因为很明显它必须有一个包含发送请求的公共主体。这一部分被设计成一个私有的辅助方法 _adjust,带有一整套选项。此处其代码用一些不影响工作逻辑的缩写给出。

private:
   bool _adjust(const ulong posconst string name,
      const double stop = 0const double take = 0)
   {
      action = TRADE_ACTION_SLTP;
      position = pos;
      type = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE);
      if(!setSymbol(name)) return false;
      if(!setSLTP(stoptake)) return false;
      ZeroMemory(result);
      return OrderSend(thisresult);
   }

我们按照上面的规则填充该结构的所有字段,调用前面说明的 setSymbolsetSLTP 方法,然后向服务器发送请求。结果为成功 (true) 或错误 (false) 状态。

每个重载的 adjust 方法分别为请求准备源参数。在有仓位订单号的情况下是这样做的。

public:
   bool adjust(const ulong posconst double stop = 0const double take = 0)
   {
      if(!PositionSelectByTicket(pos))
      {
         Print("No position: P=" + (string)pos);
         return false;
      }
      return _adjust(posPositionGetString(POSITION_SYMBOL), stoptake);
   }

此处,使用内置的 PositionSelectByTicket 函数,我们可以检查在终端的交易环境中是否存在仓位及其选择,这是后续读取其特性(在本例中为交易品种 (PositionGetString(POSITION_SYMBOL)))所必需的。那么通用的变体就叫 adjust

按交易品种名称修改仓位时(仅适用于净额结算账户),可以使用另一个选项 adjust

   bool adjust(const string name, const double stop = 0const double take = 0)
   {
      if(!PositionSelect(name))
      {
         Print("No position: " + s);
         return false;
      }
      
      return _adjust(PositionGetInteger(POSITION_TICKET), name, stoptake);
   }

此处,仓位选择是使用内置的 PositionSelect 函数完成的,订单号是从其特性 (PositionGetInteger(POSITION_TICKET)) 中获得的。

所有这些功能将在关于 使用仓位仓位特性的章节中详细讨论。

具有最少参数集的 adjust 方法版本,即仅具有 stoptake 水平,如下所示。

   bool adjust(const double stop = 0const double take = 0)
   {
      if(position != 0)
      {
         if(!PositionSelectByTicket(position))
         {
            Print("No position with ticket P=" + (string)position);
            return false;
         }
         const string s = PositionGetString(POSITION_SYMBOL);
         if(symbol != NULL && symbol != s)
         {
            Print("Position symbol is adjusted from " + symbol + " to " + s);
         }
         symbol = s;
      }
      else if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)
         != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
         && StringLen(symbol) > 0)
      {
         if(!PositionSelect(symbol))
         {
            Print("Can't select position for " + symbol);
            return false;
         }
         position = PositionGetInteger(POSITION_TICKET);
      }
      else
      {
         Print("Neither position ticket nor symbol was provided");
         return false;
      }
      return _adjust(positionsymbolstoptake);
   }

此代码确保在各种模式下正确填充 positionsymbols 字段,或者出错时提前退出并在日志中记录错误消息。最后,调用私有版本的 _adjust,其通过 OrderSend 发送请求。

buy/sell 方法类似,adjust 方法集会“异步”工作:在它们完成时,只有请求发送状态是已知的,但是无法确认水平修改是否成功。我们知道,对于证券交易所而言,Take Profit 水平可以作为限价订单转发。因此,在 MqlTradeResultSync 结构体中,我们应提供“同步”等待,直到更改生效。

作为 MqlTradeResultSync::wait 方法形成的一般等待机制已经准备就绪,并已用于等待开仓。wait 方法可接收一个指针作为第一个参数,该指针指向另一个具有预定义原型 condition 的方法,以便在循环中进行轮询,直到满足所需的条件或发生超时。在这种情况下,这种 condition 兼容的方法应对该仓位的止损水平进行应用检查。

我们添加这样一个名为 adjusted 的新方法。

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool adjusted(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE || retcode != TRADE_RETCODE_PLACED)
      {
         return false;
      }
   
      if(!wait(checkSLTPmsc))
      {
         Print("SL/TP modification timeout: P=" + (string)position);
         return false;
      }
      
      return true;
   }

当然,首先我们应检查 retcode 字段中的状态。如果有标准状态,我们将继续检查水平本身,将辅助方法 checkSLTP 传递给 wait

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   static bool checkSLTP(MqlTradeResultSync &ref)
   {
      if(PositionSelectByTicket(ref.position))
      {
         return TU::Equal(PositionGetDouble(POSITION_SL), /*.?.*/)
            && TU::Equal(PositionGetDouble(POSITION_TP), /*.?.*/);
      }
      else
      {
         Print("PositionSelectByTicket failed: P=" + (string)ref.position);
      }
      return false;
   }

此代码可确保在终端的交易环境中使用 PositionSelectByTicket 通过订单号选择仓位,并读取仓位特性 POSITION_SL 和 POSITION_TP,这些特性应与请求中的特性进行比较。问题是,我们还没有获得对请求对象的访问权,必须以某种方式为标记为“.?.”的仓位传递一些值。

基本上,因为我们正在设计 MqlTradeResultSync 结构体,所以我们可以向其中添加 sltp 字段,并在发送请求之前用 MqlTradeRequestSync 中的值填充它们(内核并不“知道”我们添加的字段,在 OrderSend 调用期间不会对它们进行修改)。但是为了简单起见,我们将使用已经可用的值。MqlTradeResultSync 结构体中的 bidask 字段仅用于报告重新报价价格(TRADE_RETCODE_REQUOTE 状态),这与 TRADE_ACTION_SLTP 请求无关,因此我们可以将来自已完成的 MqlTradeRequestSyncsltp 存储在其中。

MqlTradeRequestSync 结构体的 completed 方法中这样做是合乎逻辑的,该方法使用预定义的超时启动对交易操作结果的阻塞等待。目前为止,其代码只有一个 TRADE_ACTION_DEAL 操作的分支。为了继续,让我们为 TRADE_ACTION_SLTP 添加一个分支。

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool completed()
   {
      if(action == TRADE_ACTION_DEAL)
      {
         const bool success = result.opened(timeout);
         if(successposition = result.position;
         return success;
      }
      else if(action == TRADE_ACTION_SLTP)
      {
         // pass the original request data for comparison with the position properties,
         // by default they are not in the result structure
         result.position = position;
         result.bid = sl// bid field is free in this result type, use under StopLoss
         result.ask = tp// ask field is free in this type of result, we use it under TakeProfit
         return result.adjusted(timeout);
      }
      return false;
   }

正如你所看到的,在根据请求设置仓位订单号和价格水平之后,我们可调用上面讨论的adjusted 方法来检查 wait(checkSLTP)。现在,我们可以返回到 MqlTradeResultSync 结构中的辅助方法 checkSLTP,并将其完善为最终形式。

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   static bool checkSLTP(MqlTradeResultSync &ref)
   {
      if(PositionSelectByTicket(ref.position))
      {
         return TU::Equal(PositionGetDouble(POSITION_SL), ref.bid// sl from request
            && TU::Equal(PositionGetDouble(POSITION_TP), ref.ask); // tp from request
      }
      else
      {
         Print("PositionSelectByTicket failed: P=" + (string)ref.position);
      }
      return false;
   }

这样就完成了 MqlTradeRequestSyncMqlTradeResultSync 结构体用于 Stop LossTake Profit 修改操作的功能扩展。

记住这一点,让我们继续 EA 交易的示例 MarketOrderSend.mq5,我们在上一节中便已经开始。我们为其添加一个输入参数 Distance2SLTP,该参数允许你以点数为单位指定 Stop LossTake Profit 水平之间的距离。

input int Distance2SLTP = 0// Distance to SL/TP in points (0 = no)

当该参数为零时,将不设置任何保护水平。

在工作代码中,收到开仓确认后,我们可以计算 SL 和 TP 变量中的水平值,并执行同步修改:request.adjust(SL, TP) && request.completed()

   ...
   const ulong order = (wantToBuy ?
      request.buy(symbolvolumePrice) :
      request.sell(symbolvolumePrice));
   if(order != 0)
   {
      Print("OK Order: #="order);
      if(request.completed()) // waiting for position opening
      {
         Print("OK Position: P="request.result.position);
         if(Distance2SLTP != 0)
         {
            // position "selected" in the trading environment of the terminal inside 'complete',
            // so it is not required to do this explicitly on the ticket
            // PositionSelectByTicket(request.result.position);
            
            // with the selected position, you can find out its properties, but we need the price,
            // to step back from it by a given number of points
            const double price = PositionGetDouble(POSITION_PRICE_OPEN);
            const double point = SymbolInfoDouble(symbolSYMBOL_POINT);
            // we count the levels using the auxiliary class TradeDirection
            TU::TradeDirection dir((ENUM_ORDER_TYPE)Type);
            // SL is always "worse" and TP is always "better" of the price: the code is the same for buying and selling
            const double SL = dir.negative(priceDistance2SLTP * point);
            const double TP = dir.positive(priceDistance2SLTP * point);
            if(request.adjust(SLTP) && request.completed())
            {
               Print("OK Adjust");
            }
         }
      }
   }
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
}

在成功的买入或卖出操作后第一次调用 completed 时,仓位订单号保存在请求结构体的 position 字段中。因此,要修改止损点,只有价格水平就足够了,并且仓位的交易品种和订单号已经存在于 request 中。

我们来尝试使用默认设置的 EA 交易执行买入操作,但 Distance2SLTP 设置为 500 点。

OK Order: #=1273913958
Waiting for position for deal D=1256506526
OK Position: P=1273913958
OK Adjust
TRADE_ACTION_SLTP, EURUSD, ORDER_TYPE_BUY, V=0.01, ORDER_FILLING_FOK, @ 1.10889, »
»  SL=1.10389, TP=1.11389, P=1273913958
DONE, Bid=1.10389, Ask=1.11389, Request executed, Req=26

最后两行对应于 requestrequest.result 结构体(在函数结束时启动)内容日志的调试输出。在这些行中,有趣的是这些字段存储了来自两个查询的共生值:首先是进行了开仓,然后对其进行了修改。具体来说,请求中具有交易量 (0.01) 和价格 (1.10889) 的字段保留在 TRADE_ACTION_DEAL 之后,但是不会阻止 TRADE_ACTION_SLTP 的执行。理论上,通过重置两个请求之间的结构体很容易解决这个问题,但是,我们更喜欢让它们保持原样,因为在填充的字段中也有有用的字段:position 字段接收了我们请求修改所需的订单号。如果我们重置这个结构体,那么就需要引入一个变量作为该订单号的中间存储。

当然,在一般情况下,坚持严格的数据初始化策略是可取的,但是知道如何在特定的场景中使用它们(比如两个或多个预定义类型的相关请求)可以让你优化代码。

此外,在包含结果的结构体中,我们能在 BidAsk 价格字段中看到请求水平 sltp,这一点也不奇怪:它们是由 MqlTradeRequestSync::completed 方法写入的,目的是与实际的仓位变化进行比较。当执行请求时,系统内核只在 result 结构体中填充 retcode(已完成)、comment(“请求已执行”)和 request_id (26)。

接下来,我们将考虑另一个实施 跟踪止损的水平修改示例。