全部和部分平仓(对冲)

在对冲账户上,允许同时开立几个仓位,在大多数情况下,这些仓位可以是相反的方向。在某些司法管辖区,对冲账户是受限制的:一次只能持有一个方向的仓位。在这种情况下,当试图执行相反的交易操作时,你将收到 TRADE_RETCODE_HEDGE_PROHIBITED 错误代码。此外,这种限制通常与 ACCOUNT_FIFO_CLOSE 账户特性设置为 true 相关。

当两个相反的仓位同时开仓时,平台使用 TRADE_ACTION_CLOSE_BY 运算支持其同时相互平仓的机制。要执行此操作,除了 action 字段之外,你还应在 MqlTradeTransaction 结构体中填充另外两个字段: position 并且 position_by 必须包含要平仓的仓位订单号。

此功能的可用性取决于金融工具的 SYMBOL_ORDER_MODE 特性:SYMBOL_ORDER_CLOSEBY (64) 必须出现在允许标志位掩码中。

这种操作不仅简化了平仓(一次操作而不是两次),而且节省了一个点差。

我们知道,任何新仓位开始交易时的损失都等于该点差。例如,当买入一种金融工具时,交易以 Ask 价格成交,但对于退出交易,即卖出,实际价格是 Bid 价格。对于空头仓位,情况正好相反:在以 Bid 价格进场后,为了潜在的离场操作,我们会立即开始跟踪 Ask 价格。

如果你定期在同一时间平仓,它们的退出价格将与当前点差相差一段距离。但是,如果你使用 TRADE_ACTION_CLOSE_BY 操作,那么这两个仓位均将被平仓,而不考虑当前价格。仓位抵消的价格等于 position_by 仓位的开盘价(在请求结构体中)。该开盘价在由 TRADE_ACTION_CLOSE_BY 请求生成的 ORDER_TYPE_CLOSE_BY 订单中指定。

遗憾的是,在交易和仓位背景下的报告中,相反仓位/交易的收盘价和开盘价会以镜像方向成对显示,这给人以双重止盈或止亏的印象。事实上,该运算的财务结果(为该手数调整的价格之间的差异)仅针对第一个仓位退出交易(请求结构体中的 position 字段)进行记录。第二次退出交易的结果总是为 0,而与价差无关。

这种不对称的另一个结果是,通过改变在 positionposition_by 字段中的订单号位置,在交易报告中的多头交易和空头交易背景下的损益统计会发生变化,例如,盈利的多头交易可以增加正好与盈利的空头交易的数量减少一样多。但如果我们假设订单执行的延迟不依赖于订单号的传输顺序,在理论上不会应影响整体结果。

下图显示了该过程的图解说明(点差被有意夸大)。

平仓盈利仓位时的点差会计

平仓盈利仓位时的点差会计

这是一对盈利仓位的情况。如果仓位方向相反,处于亏损状态,那么当它们分别平仓时,点差将被考虑两次(分别考虑)。对冲平仓可让你减少一个点差的损失。

平仓无盈利仓位时的点差会计

平仓无盈利仓位时的点差会计

反向仓位无需大小相等。反向平仓操作将在两个交易量中的最小值上进行。

MqlTradeSync.mqh 文件中,使用带有两个仓位订单号参数的 closeby 方法来实现邻近运算。

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool closeby(const ulong ticket1const ulong ticket2)
   {
      if(!PositionSelectByTicket(ticket1)) return false;
      double volume1 = PositionGetDouble(POSITION_VOLUME);
      if(!PositionSelectByTicket(ticket2)) return false;
      double volume2 = PositionGetDouble(POSITION_VOLUME);
   
      action = TRADE_ACTION_CLOSE_BY;
      position = ticket1;
      position_by = ticket2;
      
      ZeroMemory(result);
      if(volume1 != volume2)
      {
         // remember which position should disappear
         if(volume1 < volume2)
            result.position = ticket1;
         else
            result.position = ticket2;
      }
      return OrderSend(thisresult);
   }

为了控制平仓结果,我们可在 result.position 变量中存储一个较小仓位的订单号。completed 方法和 MqlTradeResultSync 结构体中的一切都是为同步平仓跟踪做准备:相同的算法适用于正常的平仓。

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   ...
   bool completed()
   {
      ...
      else if(action == TRADE_ACTION_CLOSE_BY)
      {
         return result.closed(timeout);
      }
      return false;
   }

反向仓位通常被用来代替止损订单,或试图在短期修正时止盈,同时留在市场上并跟随主要走势。使用伪止损订单的选项允许你将实际平仓的决定推迟一段时间,继续分析市场走势,期待价格向正确的方向反转。但是,应记住,“锁定”仓位需要增加存款,并受掉期的影响。这就是为什么很难想象一个纯粹建立在反向仓位上的交易策略的原因,这可以作为本节的一个示例。

我们开发上一个示例中所述的基于价格-操作柱线的策略。新的 EA 交易为 TradeCloseBy.mq5

我们将使用之前的信号,在检测到两根连续的柱线以相同的方向收盘时进场。负责形成这个信号的一个函数还是 GetTradeDirection。但是,如果这种趋势继续下去,我们允许重新进入。最大允许仓位总数将在 PositionLimit 输入变量中设置,默认值为 5。

GetMyPositions 函数将经历一些变化:它将有两个参数,这两个参数将是对接受仓位订单号数组的引用:分别买入和卖出。

#define PUSH(A,V) (A[ArrayResize(AArraySize(A) + 1ArraySize(A) * 2) - 1] = V)
   
int GetMyPositions(const string sconst ulong m,
   ulong &ticketsLong[], ulong &ticketsShort[])
{
   for(int i = 0i < PositionsTotal(); ++i)
   {
      if(PositionGetSymbol(i) == s && PositionGetInteger(POSITION_MAGIC) == m)
      {
         if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            PUSH(ticketsLongPositionGetInteger(POSITION_TICKET));
         else
            PUSH(ticketsShortPositionGetInteger(POSITION_TICKET));
      }
   }
   
   const int min = fmin(ArraySize(ticketsLong), ArraySize(ticketsShort));
   if(min == 0return -fmax(ArraySize(ticketsLong), ArraySize(ticketsShort));
   return min;
}

该函数返回两个数组中最小数组的大小。当它大于零时,我们有机会反向平仓。

如果最小数组的大小为零,该函数将返回另一个数组的大小,但带有一个减号,只是为了告知调用代码:所有仓位都在同一方向。

如果两个方向都没有仓位,该函数将返回 0。

开仓仓位将保持在 OpenPosition函数的控制之下 - 此处不变。

在新函数 CloseByPosition 中,只有在两个相反仓位的模式下才会执行平仓。换言之,该 EA 交易不能像通常那样一次平仓一单。当然,在真实的机器人中,这样的原则不太可能出现,但非常适合作为即将介绍的平仓示例。如果我们需要平仓一单,为它开一个相反的仓位(这时浮动盈亏是固定的)并调用 CloseByPosition 两次就够了。

bool CloseByPosition(const ulong ticket1const ulong ticket2)
{
   MqlTradeRequestSync request;
   request.magic = Magic;
   
   ResetLastError();
   // send a request and wait for it to complete
   if(request.closeby(ticket1ticket2))
   {
      Print("Positions collapse initiated");
      if(request.completed())
      {
         Print("OK CloseBy Order/Deal/Position");
         return true// success
      }
   }
   
   Print(TU::StringOf(request));
   Print(TU::StringOf(request.result));
   
   return false// error
}

该代码使用了上述 request.closeby 方法。填充 positionposition_by 字段,并调用 OrderSend

OnTick 处理程序中说明了交易逻辑,该处理程序仅在新柱线形成时分析价格配置,并接收来自 GetTradeDirection 函数的信号。

void OnTick()
{
   static bool error = false;
   // waiting for the formation of a new bar, if there is no error
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBar && !errorreturn;
   lastBar = iTime(_Symbol_Period0);
   
   const ENUM_ORDER_TYPE type = GetTradeDirection();
   ...

接下来,我们用工作交易品种的仓位标签和给定的 Magic 编号填充 ticketsLongticketsShort 数组。如果 GetMyPositions 函数返回一个大于零的值,它将给出已形成的相反仓位对数量。可以使用 CloseByPosition 函数将它们封闭在一个循环中。在这种情况下,仓位对的组合是随机选择的(按照在终端环境中的仓位顺序),但在实践中,务必通过交易量或以最盈利仓位对首先平仓的方式来选择仓位对。

   ulong ticketsLong[], ticketsShort[];
   const int n = GetMyPositions(_SymbolMagicticketsLongticketsShort);
   if(n > 0)
   {
      for(int i = 0i < n; ++i)
      {
         error = !CloseByPosition(ticketsShort[i], ticketsLong[i]) && error;
      }
   }
   ...

对于 n 的任何其他值,都要检查是否有信号(可能是重复的)进场,并通过调用 OpenPosition 来执行。

   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      error = !OpenPosition(type);
   }
   ...

最后,如果仍然有未平仓仓位,但处于相同的方向,可检查其数量是否已经达到极限,在这种情况下,我们可形成相反的仓位,以便在下一根柱线上“折叠”其中的两个仓位(从而平仓旧仓位中的任何一个)。

   else if(n < 0)
   {
      if(-n >= (int)PositionLimit)
      {
         if(ArraySize(ticketsLong) > 0)
         {
            error = !OpenPosition(ORDER_TYPE_SELL);
         }
         else // (ArraySize(ticketsShort) > 0)
         {
            error = !OpenPosition(ORDER_TYPE_BUY);
         }
      }
   }
}

我们从 2022 年初开始在 XAUUSD, H1 的测试程序中运行 EA 交易,使用默认设置。以下是该程序进程中的持仓图以及余额曲线。

对 XAUUSD,H1 运行 TradeCloseBy 的测试结果

对 XAUUSD,H1 运行 TradeCloseBy 的测试结果

在日志中很容易找到一个趋势结束的时刻(用 2 号至 4 号的订单号买入),交易开始以相反的方向产生(卖出 5 号),之后触发对冲平仓。

2022.01.03 01:05:00 instant buy 0.01 XAUUSD at 1831.13 (1830.63 / 1831.13 / 1830.63)

2022.01.03 01:05:00 deal #2 buy 0.01 XAUUSD at 1831.13 done (based on order #2)

2022.01.03 01:05:00 deal performed [#2 buy 0.01 XAUUSD at 1831.13]

2022.01.03 01:05:00 order performed buy 0.01 at 1831.13 [#2 buy 0.01 XAUUSD at 1831.13]

2022.01.03 01:05:00 Waiting for position for deal D=2

2022.01.03 01:05:00 OK New Order/Deal/Position

2022.01.03 2:00:00 instant buy 0.01 XAUUSD at 1828.77 (1828.47 / 1828.77 / 1828.47)

2022.01.03 2:00:00 deal #3 buy 0.01 XAUUSD at 1828.77 done (based on order #3)

2022.01.03 2:00:00 deal performed [#3 buy 0.01 XAUUSD at 1828.77]

2022.01.03 2:00:00 order performed buy 0.01 at 1828.77 [#3 buy 0.01 XAUUSD at 1828.77]

2022.01.03 2:00:00 Waiting for position for deal D=3

2022.01.03 2:00:00 OK New Order/Deal/Position

2022.01.03 3:00:00 instant buy 0.01 XAUUSD at 1830.40 (1830.16 / 1830.40 / 1830.16)

2022.01.03 3:00:00 deal #4 buy 0.01 XAUUSD at 1830.40 done (based on order #4)

2022.01.03 3:00:00 deal performed [#4 buy 0.01 XAUUSD at 1830.40]

2022.01.03 3:00:00 order performed buy 0.01 at 1830.40 [#4 buy 0.01 XAUUSD at 1830.40]

2022.01.03 3:00:00 Waiting for position for deal D=4

2022.01.03 3:00:00 OK New Order/Deal/Position

2022.01.03 05:00:00 instant sell 0.01 XAUUSD at 1826.22 (1826.22 / 1826.45 / 1826.22)

2022.01.03 05:00:00 deal #5 sell 0.01 XAUUSD at 1826.22 done (based on order #5)

2022.01.03 05:00:00 deal performed [#5 sell 0.01 XAUUSD at 1826.22]

2022.01.03 05:00:00 order performed sell 0.01 at 1826.22 [#5 sell 0.01 XAUUSD at 1826.22]

2022.01.03 5:00:00 Waiting for position for deal D=5

2022.01.03 5:00:00 OK New Order/Deal/Position

2022.01.03 06:00:00 close position #5 sell 0.01 XAUUSD by position #2 buy 0.01 XAUUSD (1825.64 / 1825.86 / 1825.64)

2022.01.03 6:00:00 deal #6 buy 0.01 XAUUSD at 1831.13 done (based on order #6)

2022.01.03 06:00:00 deal #7 sell 0.01 XAUUSD at 1826.22 done (based on order #6)

2022.01.03 06:00:00 Positions collapse initiated

2022.01.03 06:00:00 OK CloseBy Order/Deal/Position

交易 #3 是个很有趣的异常情况。细心的读者会注意到,它的开盘价比前一个低,似乎违反了我们的策略。事实上,此处没有错误,这是因为信号的条件写得尽可能简单:仅基于该柱线的收盘价。因此,一根空头反转柱线 (D) 以向上跳空开盘,收于前一根多头柱线 (C) 的末端上方,产生了买入信号。下面的截图说明了这种情况。

收盘价呈上升趋势的交易

收盘价呈上升趋势的交易

序列 A、B、C、D 和 E 中的所有柱线收盘都比前一根柱线高,鼓励继续买入。为了排除这种异常情况,应另外分析柱线本身的方向。

本例中最后需要注意的是 OnInit 函数。由于 EA 交易使用 TRADE_ACTION_CLOSE_BY 操作,因此需要在此检查相关账户和工作交易品种设置。

int OnInit()
{
   ...
   if(AccountInfoInteger(ACCOUNT_MARGIN_MODE) != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
   {
      Alert("An account with hedging is required for this EA!");
      return INIT_FAILED;
   }
   
   if((SymbolInfoInteger(_SymbolSYMBOL_ORDER_MODE) & SYMBOL_ORDER_CLOSEBY) == 0)
   {
      Alert("'Close By' mode is not supported for "_Symbol);
      return INIT_FAILED;
   }
   
   return INIT_SUCCEEDED;
}

如果其中一个特性不支持交叉平仓,EA 交易将无法继续工作。创建工作机器人时,这些检查通常在交易算法内部进行,并将程序切换到替代模式,特别是单次平仓并在净额结算时保持总仓位的情况下。