Cierre de posiciones opuestas: total y parcial (cobertura)

En las cuentas de cobertura se permite abrir varias posiciones al mismo tiempo y, en la mayoría de los casos, estas posiciones pueden ser en sentido opuesto. En algunas jurisdicciones, las cuentas de cobertura están restringidas: sólo se pueden tener posiciones en una dirección al mismo tiempo. En este caso, recibirá el código de error TRADE_RETCODE_HEDGE_PROHIBITED cuando intente ejecutar una operación de trading opuesta. Además, esta restricción a menudo se correlaciona con el ajuste de la propiedad de cuenta ACCOUNT_FIFO_CLOSE a true.

Cuando se abren dos posiciones opuestas al mismo tiempo, la plataforma admite el mecanismo de su cierre mutuo simultáneo mediante la operación TRADE_ACTION_CLOSE_BY. Para realizar esta acción, debe rellenar dos campos más en la estructura MqlTradeTransaction además del campo action: position y position_by deben contener los tickets de las posiciones que se van a cerrar.

La disponibilidad de esta característica depende de la propiedad SYMBOL_ORDER_MODE del instrumento financiero: SYMBOL_ORDER_CLOSEBY (64) debe estar presente en la máscara de bits de las banderas permitidas.

Esta operación no sólo simplifica el cierre (una operación en lugar de dos), sino que también ahorra un diferencial.

Como sabe, cualquier posición nueva comienza a operar con una pérdida igual al diferencial. Por ejemplo, cuando se compra un instrumento financiero, la transacción se concluye al precio Ask, pero para una transacción de salida, es decir, una venta, el precio real es Bid. Para una posición corta, la situación es inversa: inmediatamente después de entrar al precio Bid, empezamos a realizar un seguimiento del precio Ask para una posible salida.

Si cierra posiciones al mismo tiempo de forma regular, sus precios de salida estarán a una distancia del diferencial actual entre sí. Sin embargo, si utiliza la operación TRADE_ACTION_CLOSE_BY, ambas posiciones se cerrarán sin tener en cuenta los precios actuales. El precio al que se compensan las posiciones es igual al precio de apertura de la posición position_by (en la estructura de solicitudes). Se especifica en la orden ORDER_TYPE_CLOSE_BY generada por la solicitud TRADE_ACTION_CLOSE_BY.

Lamentablemente, en los informes en el contexto de transacciones y posiciones, los precios de cierre y apertura de posiciones/transacciones opuestas se muestran en pares de valores idénticos, en sentido espejo, lo que da la impresión de una doble ganancia o pérdida. De hecho, el resultado financiero de la operación (la diferencia entre los precios ajustados por el lote) sólo se registra para la operación de salida de la primera posición (el campo position de la estructura de la solicitud). El resultado de la segunda operación de salida es siempre 0, independientemente de la diferencia de precios.

Otra consecuencia de esta asimetría es que al cambiar los lugares de los tickets en los campos position y position_by, las estadísticas de pérdidas y ganancias en el contexto de operaciones largas y cortas cambian en el informe de trading; por ejemplo, las operaciones largas rentables pueden aumentar exactamente tanto como disminuye el número de operaciones cortas rentables. Pero esto, en teoría, no debería afectar al resultado global, si suponemos que el retraso en la ejecución de la orden no depende del orden de transferencia de los tickets.

En el siguiente diagrama se muestra una explicación gráfica del proceso (los diferenciales están intencionadamente exagerados).

Contabilidad de diferenciales al cerrar posiciones rentables

Contabilidad de diferenciales al cerrar posiciones rentables

He aquí un caso de un par de posiciones rentables. Si las posiciones tenían direcciones opuestas y eran deficitarias, al cerrarlas por separado, el diferencial se tendría en cuenta dos veces (en cada una). El contracierre permite reducir la pérdida en un diferencial.

Contabilización del diferencial al cerrar posiciones no rentables

Contabilización del diferencial al cerrar posiciones no rentables

Las posiciones invertidas no tienen por qué ser del mismo tamaño. La operación de cierre opuesto funcionará en el mínimo de los dos volúmenes.

En el archivo MqlTradeSync.mqh, la operación del cierre opuesto se implementa utilizando el método closeby con dos parámetros para los tickets de posición.

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);
   }

Para controlar el resultado del cierre, almacenamos el ticket de una posición menor en la variable result.position. Todo en el método completed y en la estructura MqlTradeResultSync está listo para el seguimiento síncrono del cierre de posición: el mismo algoritmo funcionó para el cierre normal de una posición.

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

Las posiciones opuestas suelen utilizarse como sustituto de una orden de stop o un intento de obtener beneficios en una corrección a corto plazo mientras se permanece en el mercado y se sigue la tendencia principal. La opción de utilizar una orden de pseudo-stop le permite posponer la decisión de cerrar realmente las posiciones durante algún tiempo, continuando el análisis de los movimientos del mercado a la espera de que el precio se invierta en la dirección correcta. Sin embargo, hay que tener en cuenta que las posiciones «bloqueadas» requieren mayores depósitos y están sujetas a swaps. Por eso es difícil imaginar una estrategia de trading basada en posiciones opuestas en su forma pura, que pueda servir de ejemplo para esta sección.

Vamos a desarrollar la idea de la estrategia basada en barras precio-acción esbozada en el ejemplo anterior. El nuevo Asesor Experto es TradeCloseBy.mq5.

Utilizaremos la señal anterior para entrar en el mercado al detectar dos velas consecutivas que cerraron en la misma dirección. Una función responsable de su formación es de nuevo GetTradeDirection. No obstante, vamos a permitir reingresos si la tendencia continúa. El número máximo total de posiciones permitidas se fijará en la variable de entrada PositionLimit; el valor por defecto es 5.

La función GetMyPositions sufrirá algunos cambios: tendrá dos parámetros, que serán referencias a arrays que aceptan tickets de posición: compra y venta por separado.

#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;
}

La función devuelve el tamaño del array más pequeño de los dos. Cuando es mayor que cero, tenemos la oportunidad de cerrar posiciones opuestas.

Si el array mínimo es de tamaño cero, la función devolverá el tamaño de otro array, pero con un signo menos, para que el código que llama sepa que todas las posiciones están en la misma dirección.

Si no hay posiciones en ninguna dirección, la función devolverá 0.

Las posiciones de apertura seguirán bajo el control de la función OpenPosition; aquí no hay cambios.

El cierre sólo se realizará en el modo de dos posiciones opuestas en la nueva función CloseByPosition. En otras palabras: este Asesor Experto no es capaz de cerrar posiciones de una en una, de la forma habitual. Por supuesto, en un robot real es improbable que se produzca un principio así, pero como ejemplo de un cierre que se aproxima, encaja muy bien. Si necesitamos cerrar una sola posición, basta con abrir una posición opuesta para ella (en este momento la ganancia o pérdida flotante es fija) y llamar a CloseByPosition para dos.

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
}

El código utiliza el método request.closeby descrito anteriormente. Se rellenan los campos position y position_by y se llama a OrderSend.

La lógica de trading se describe en el manejador OnTick que analiza la configuración de precios sólo en el momento de la formación de una nueva barra y recibe una señal de la función 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();
   ...

A continuación, rellenamos los arrays ticketsLong y ticketsShort con los tickets de posición del símbolo de trabajo y con el número Magic dado. Si la función GetMyPositions devuelve un valor mayor que cero, da el número de pares formados de posiciones opuestas. Pueden cerrarse en bucle mediante la función CloseByPosition. La combinación de pares en este caso se elige aleatoriamente (por orden de posiciones en el entorno del terminal); sin embargo, en la práctica, puede ser importante seleccionar los pares por volumen o de una forma tal que se cierren primero los más rentables.

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

Para cualquier otro valor de n, debe comprobar si existe una señal (posiblemente repetida) para entrar en el mercado y ejecutarla llamando a OpenPosition.

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

Por último, si todavía hay posiciones abiertas, pero están en la misma dirección, comprobamos si su número ha alcanzado el límite, en cuyo caso formamos una posición opuesta para «colapsar» dos de ellas en la siguiente barra (cerrando así una de cualquier posición de las antiguas).

   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);
         }
      }
   }
}

Vamos a ejecutar el Asesor Experto en el probador en XAUUSD, H1 desde principios de 2022, con la configuración predeterminada. A continuación se muestra el gráfico con las posiciones en el proceso del programa, así como la curva de balance.

Resultados de la prueba TradeCloseBy en XAUUSD,H1

Resultados de la prueba TradeCloseBy en XAUUSD, H1

Es fácil encontrar en el registro los momentos en los que finaliza una tendencia (compra con tickets del nº 2 al nº 4), y comienzan a generarse transacciones en sentido opuesto (venta nº 5), tras lo cual se activa un cierre de contador.

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 02:00:00 instant buy 0.01 XAUUSD at 1828.77 (1828.47 / 1828.77 / 1828.47)

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

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

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

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

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

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

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

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

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

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

2022.01.03 03: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 05:00:00 Waiting for position for deal D=5

2022.01.03 05: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 06: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

La transacción nº 3 es un artefacto interesante. Un lector atento observará que abrió por debajo de la anterior, violando aparentemente nuestra estrategia. De hecho, aquí no hay ningún error, y esto es consecuencia de que las condiciones de las señales están escritas de la forma más sencilla posible: sólo en función de los precios de cierre de las barras. Por lo tanto, una vela de reversión bajista (D), que abrió con un precio más alto y cerró por encima del final de la vela alcista anterior (C), generó una señal de compra. Esta situación se ilustra en la siguiente captura de pantalla:

Transacciones con tendencia al alza a precios de cierre

Transacciones con tendencia al alza a precios de cierre

Todas las velas de la secuencia A, B, C, D y E cierran por encima de la anterior y animan a seguir comprando. Para excluir tales artefactos, habría que analizar además la dirección de las propias barras.

Lo último a lo que hay que prestar atención en este ejemplo es la función OnInit. Dado que el Asesor Experto utiliza la operación TRADE_ACTION_CLOSE_BY, aquí se comprueba la configuración de la cuenta y del símbolo de trabajo correspondientes.

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;
}

Si una de las propiedades no admite cierre cruzado, el Asesor Experto no podrá seguir funcionando. Al crear robots de trabajo, estas comprobaciones, por regla general, se llevan a cabo dentro del algoritmo de trading y cambian el programa a modos alternativos; en concreto, a un único cierre de posiciones y al mantenimiento de una posición agregada en caso de compensación.