Cierre de una posición: total y parcial

Técnicamente, el cierre de una posición puede considerarse una operación de trading opuesta a la utilizada para abrirla. Por ejemplo, para salir de una compra, es necesario realizar una operación de venta (ORDER_TYPE_SELL en el campo type) y para salir de la de venta es necesario comprar (ORDER_TYPE_BUY en el campo type).

El tipo de operación de trading en el campo action de la estructura MqlTradeTransaction sigue siendo el mismo: TRADE_ACTION_DEAL.

En una cuenta de cobertura, la posición que se desea cerrar debe especificarse mediante un ticket en el campo position. Para las cuentas de compensación, sólo puede especificar el nombre del símbolo en el campo symbol, ya que en ellas sólo es posible una posición de símbolo. Sin embargo, también puede cerrar posiciones aquí mediante un ticket.

Para unificar el código, tiene sentido rellenar los campos position y symbol independientemente del tipo de cuenta.

Asegúrese también de ajustar el volumen en el campo volume. Si es igual al volumen de posición, se cerrará completamente. Sin embargo, si se especifica un valor inferior, es posible cerrar sólo una parte de la posición.

En la siguiente tabla, todos los campos obligatorios de la estructura están marcados con un asterisco y los campos opcionales están marcados con un signo más.

Campo

Compensación

Cobertura

action

*

*

symbol

*

+

posición

+

*

type

*

*

type_filling

*

*

volume

*

*

price

*'

*'

deviation

±

±

magic

+

+

comment

+

+

El campo price marcado está con un asterisco con un tick porque se requiere sólo para los símbolos con los modos de ejecución Request y Instant), mientras que para la ejecución Exchange y Market, el precio en la estructura no se tiene en cuenta.

Por una razón similar, el campo deviation está marcado con «±». Sólo tiene efecto para los modos Instant y Request.

Para simplificar la implementación programática del cierre de una posición, volvamos a nuestra estructura extendida MqlTradeRequestSync en el archivo MqlTradeSync.mqh. El método para cerrar una posición por ticket tiene el siguiente código:

struct MqlTradeRequestSyncpublic MqlTradeRequest
{
   double partial// volume after partial closing
   ...
   bool close(const ulong ticketconst double lot = 0)
   {
      if(!PositionSelectByTicket(ticket)) return false;
      
      position = ticket;
      symbol = PositionGetString(POSITION_SYMBOL);
      type = (ENUM_ORDER_TYPE)(PositionGetInteger(POSITION_TYPE) ^ 1);
      price = 0
      ...

Aquí comprobamos primero la existencia de una posición llamando a la función PositionSelectByTicket. Además, esta llamada hace que la posición sea seleccionada en el entorno de trading del terminal, lo que permite leer sus propiedades mediante las funciones posteriores. En concreto, averiguamos el símbolo de una posición a partir de la propiedad POSITION_SYMBOL e «invertimos» su tipo de POSITION_TYPE al opuesto para obtener el tipo de orden requerido.

Los tipos de posición de a enumeración ENUM_POSITION_TYPE son POSITION_TYPE_BUY (valor 0) y POSITION_TYPE_SELL (valor 1). En la enumeración de tipos de órdenes ENUM_ORDER_TYPE, las operaciones de mercado ocupan exactamente los mismos valores: ORDER_TYPE_BUY y ORDER_TYPE_SELL. Por eso podemos llevar la primera enumeración a la segunda, y para obtener el sentido opuesto de la operación, basta con conmutar el bit cero mediante la operación OR exclusiva ('^'): obtenemos 1 a partir de 0, y 0 a partir de 1.

Poner a cero el campo price significa seleccionar automáticamente el precio actual correcto (Ask o Bid) antes de enviar la solicitud: esto se hace un poco más tarde, dentro del método de ayuda setVolumePrices, que se llama más adelante en el algoritmo, desde el método market.

La llamada al método _market se produce un par de líneas más abajo. El método _market genera una orden de mercado para todo el volumen o una parte, teniendo en cuenta todos los campos completos de la estructura.

      const double total = lot == 0 ? PositionGetDouble(POSITION_VOLUME) : lot;
      partial = PositionGetDouble(POSITION_VOLUME) - total;
      return _market(symboltotal);
   }

Este fragmento está ligeramente simplificado en comparación con el código fuente actual. El código completo contiene el manejo de una situación rara pero posible cuando el volumen de la posición excede el volumen máximo permitido en una orden por símbolo (propiedad SYMBOL_VOLUME_MAX). En este caso, la posición debe cerrarse por partes, mediante varias órdenes.

También hay que tener en cuenta que como la posición se puede cerrar parcialmente, hemos tenido que añadir un campo a la estructura partial, donde se coloca el saldo previsto del volumen después de la operación. Por supuesto, para un cierre completo, esto será 0. Esta información será necesaria para seguir verificando la realización de la operación.

Para las cuentas de compensación, existe una versión del método close que identifica la posición por el nombre del símbolo: selecciona una posición por símbolo, obtiene su ticket y, a continuación, remite a la versión anterior de close.

   bool close(const string nameconst double lot = 0)
   {
      if(!PositionSelect(name)) return false;
      return close(PositionGetInteger(POSITION_TICKET), lot);
   }

En la estructura MqlTradeRequestSync, tenemos el método completed que proporciona una espera sincrónica para la finalización de la operación, si es necesario. Ahora necesitamos complementarlo para cerrar posiciones, en la rama donde action es igual a TRADE_ACTION_DEAL. Distinguiremos entre abrir una posición y cerrarla por un valor cero en el campo position: no tiene ticket cuando se abre una posición, y tiene uno cuando se cierra.

   bool completed()
   {
      if(action == TRADE_ACTION_DEAL)
      {
         if(position == 0)
         {
            const bool success = result.opened(timeout);
            if(successposition = result.position;
            return success;
         }
         else
         {
            result.position = position;
            result.partial = partial;
            return result.closed(timeout);
         }
      }

Para comprobar el cierre real de una posición, hemos añadido el método closed en la estructura MqlTradeResultSync. Antes de llamarlo, escribimos el ticket de la posición en el campo result.position para que la estructura de resultados pueda realizar un seguimiento del momento en que el ticket correspondiente desaparece del entorno de trading del terminal, o cuando el volumen es igual a result.partial en caso de cierre parcial.

He aquí el método closed. Se basa en un principio bien conocido: comprobar primero el éxito del código de retorno del servidor y luego esperar con el método wait a que se cumpla alguna condición.

struct MqlTradeResultSyncpublic MqlTradeResult
{
   ...
   bool closed(const ulong msc = 1000)
   {
      if(retcode != TRADE_RETCODE_DONE)
      {
         return false;
      }
      if(!wait(positionRemovedmsc))
      {
         Print("Position removal timeout: P=" + (string)position);
      }
      
      return true;
   }

En este caso, para comprobar la condición para que la posición desaparezca, hemos tenido que implementar una nueva función positionRemoved.

   static bool positionRemoved(MqlTradeResultSync &ref)
   {
      if(ref.partial)
      {
         return PositionSelectByTicket(ref.position)
            && TU::Equal(PositionGetDouble(POSITION_VOLUME), ref.partial);
      }
      return !PositionSelectByTicket(ref.position);
   }

Probaremos el funcionamiento del cierre de posiciones utilizando el Asesor Experto TradeClose.mq5, que implementa una sencilla estrategia de trading: entrar en el mercado si hay dos barras consecutivas en la misma dirección, y en cuanto la siguiente barra cierre en sentido opuesto a la tendencia anterior, salir del mercado. Las señales repetitivas durante tendencias continuas serán ignoradas, es decir, habrá un máximo de una posición (lote mínimo) o ninguna en el mercado.

El Asesor Experto no tendrá ningún parámetro ajustable, a excepción de (Deviation) y un número único (Magic). Los parámetros implícitos son el marco temporal y el símbolo de trabajo del gráfico.

Para rastrear la presencia de una posición ya abierta, utilizamos la función GetMyPosition del ejemplo anterior TradeTrailing.mq5: busca entre las posiciones por símbolo y número de Asesor Experto y devuelve un true lógico si se encuentra una posición adecuada.

También tomamos la función casi sin cambios OpenPosition: abre una posición según el tipo de orden de mercado pasada en el parámetro único. Aquí, este parámetro provendrá del algoritmo de detección de tendencias, y anteriormente (en TrailingStop.mq5) el tipo de orden era establecido por el usuario a través de una variable de entrada.

Una nueva función que implementa el cierre de una posición es ClosePosition. Dado que el archivo de encabezado MqlTradeSync.mqh se hizo cargo de toda la rutina, sólo tenemos que llamar al método request.close(ticket) para el ticket de posición enviado y esperar a que se complete el borrado mediante request.completed().

En teoría, esto último puede evitarse si el Asesor Experto analiza la situación en cada tick. En este caso, un posible problema con la eliminación de la posición se revelará rápidamente en el siguiente tick, y el Asesor Experto puede intentar eliminarla de nuevo. Sin embargo, este Asesor Experto tiene una lógica de trading basada en barras, y por lo tanto no tiene sentido analizar cada tick. A continuación, implementamos un mecanismo especial para trabajar barra por barra y, en este sentido, controlamos de forma sincrónica la eliminación, ya que, de lo contrario, la posición se quedaría «colgada» durante toda una barra.

ulong LastErrorCode = 0;
   
ulong ClosePosition(const ulong ticket)
{
   MqlTradeRequestSync request// empty structure
   
   // optional fields are filled directly in the structure
   request.magic = Magic;
   request.deviation = Deviation;
   
   ResetLastError();
   // perform close and wait for confirmation
   if(request.close(ticket) && request.completed())
   {
      Print("OK Close Order/Deal/Position");
   }
   else // print diagnostics in case of problems
   {
      Print(TU::StringOf(request));
      Print(TU::StringOf(request.result));
      LastErrorCode = request.result.retcode;
      return 0// error, code to parse in LastErrorCode
   }
   
   return request.position// non-zero value - success
}

Podríamos obligar a las funciones de ClosePosition a devolver 0 en caso de borrado correcto de la posición, y un código de error en caso contrario. Este enfoque aparentemente eficaz haría que el comportamiento de las dos funciones OpenPosition y ClosePosition fuera diferente: en el código de llamada, sería necesario anidar las llamadas de estas funciones en expresiones lógicas de significado opuesto, lo que introduciría confusión. Además, necesitaríamos la variable global LastErrorCode en cualquier caso, a fin de añadir información sobre el error dentro de la función OpenPosition. Además, la comprobación if(condition) se interpreta más orgánicamente como un éxito que if(!condition).

La función que genera señales de trading según la estrategia anterior se denomina GetTradeDirection.

ENUM_ORDER_TYPE GetTradeDirection()
{
   if(iClose(_Symbol_Period1) > iClose(_Symbol_Period2)
      && iClose(_Symbol_Period2) > iClose(_Symbol_Period3))
   {
      return ORDER_TYPE_BUY// open a long position
   }
   
   if(iClose(_Symbol_Period1) < iClose(_Symbol_Period2)
      && iClose(_Symbol_Period2) < iClose(_Symbol_Period3))
   {
      return ORDER_TYPE_SELL// open a short position
   }
   
   return (ENUM_ORDER_TYPE)-1// close
}

La función devuelve un valor del tipo ENUM_ORDER_TYPE con dos elementos estándar (ORDER_TYPE_BUY y ORDER_TYPE_SELL) que activan compras y ventas, respectivamente. El valor especial -1 (no en la enumeración) se utilizará como señal de cierre.

Para activar el Asesor Experto basado en el algoritmo de trading utilizamos el manejador OnTick. Como recordamos, otras opciones son adecuadas para otras estrategias; por ejemplo, un temporizador para operar en las noticias o eventos de Profundidad de Mercado para trading de volumen.

En primer lugar, analicemos la función de forma simplificada, sin manejar posibles errores. Al principio, hay un bloque que garantiza que el algoritmo posterior sólo se active cuando se abra una nueva barra.

void OnTick()
{
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBarreturn;
   lastBar = iTime(_Symbol_Period0);
   ...

A continuación obtenemos la señal actual de la función GetTradeDirection.

   const ENUM_ORDER_TYPE type = GetTradeDirection();

Si hay una posición, comprobamos si se ha recibido una señal para cerrarla y llamamos a ClosePosition si es necesario. Si todavía no hay ninguna posición y hay una señal para entrar en el mercado, llamamos a OpenPosition.

   if(GetMyPosition(_SymbolMagic))
   {
      if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
      {
         ClosePosition(PositionGetInteger(POSITION_TICKET));
      }
   }
   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      OpenPosition(type);
   }
}

Para analizar errores, necesitará encerrar las llamadas a OpenPosition y ClosePosition en sentencias condicionales y tomar alguna acción para restaurar el estado de funcionamiento del programa. En el caso más sencillo, basta con repetir la solicitud en el siguiente tick, pero es deseable hacerlo un número limitado de veces. Por lo tanto, crearemos variables estáticas con un contador y un límite de error.

void OnTick()
{
   static int errors = 0;
   static const int maxtrials = 10// no more than 10 attempts per bar
   
   // expect a new bar to appear if there were no errors
   static datetime lastBar = 0;
   if(iTime(_Symbol_Period0) == lastBar && errors == 0return;
   lastBar = iTime(_Symbol_Period0);
   ...

El mecanismo barra a barra se desactiva temporalmente si aparecen errores, ya que conviene superarlos lo antes posible.

Los errores se cuentan en las sentencias condicionales en torno a ClosePosition y OpenPosition.

   const ENUM_ORDER_TYPE type = GetTradeDirection();
   
   if(GetMyPosition(_SymbolMagic))
   {
      if(type != ORDER_TYPE_BUY && type != ORDER_TYPE_SELL)
      {
         if(!ClosePosition(PositionGetInteger(POSITION_TICKET)))
         {
            ++errors;
         }
         else
         {
            errors = 0;
         }
      }
   }
   else if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL)
   {
      if(!OpenPosition(type))
      {
         ++errors;
      }
      else
      {
         errors = 0;
      }
   }
 // too many errors per bar
   if(errors >= maxtrialserrors = 0;
 // error serious enough to pause
   if(IS_TANGIBLE(LastErrorCode)) errors = 0;
}

Si se establece la variable errors en 0, se activa de nuevo el mecanismo barra a barra y se detienen los intentos de repetir la solicitud hasta la siguiente barra.

La macro IS_TANGIBLE se define en TradeRetcode.mqh como:

#define IS_TANGIBLE(T) ((T) >= TRADE_RETCODE_ERROR)

Los errores con códigos más pequeños son operativos, es decir, normales en cierto sentido. Los códigos grandes requieren análisis y diferentes acciones, dependiendo de la causa del problema: parámetros de solicitud incorrectos, prohibiciones permanentes o temporales en el entorno de trading, falta de fondos, etc. Presentaremos un clasificador de errores mejorado en la sección Modificación de orden pendiente.

Vamos a ejecutar el Asesor Experto en el probador en XAUUSD, H1 desde principios de 2022, simulando ticks reales. En el siguiente collage se muestra un fragmento de un gráfico con transacciones, así como la curva de balance.

Resultados de las pruebas TradeClose en XAUUSD,H1

Resultados de las pruebas TradeClose en XAUUSD, H1

Basándonos en el informe y el registro, podemos ver que la combinación de nuestra sencilla lógica de trading y las dos operaciones de apertura y cierre de posiciones funciona correctamente.

Además de simplemente cerrar una posición, la plataforma admite la posibilidad de un mutuo cierre de dos posiciones opuestas en cuentas de cobertura.