Modificar los niveles de Stop Loss y/o Take Profit de una posición

Un programa MQL puede cambiar los niveles de precios de protección Stop Loss y Take Profit para una posición abierta. El elemento TRADE_ACTION_SLTP de la enumeración ENUM_TRADE_REQUEST_ACTIONS está pensado para este fin, es decir, cuando se rellena la estructura MqlTradeRequest debemos escribir TRADE_ACTION_SLTP en el campo action.

Este es el único campo obligatorio. La necesidad de rellenar otros campos viene determinada por el modo de funcionamiento de la cuenta ENUM_ACCOUNT_MARGIN_MODE. En las cuentas de cobertura, debe rellenar el campo symbol, pero puede omitir el ticket de posición. En las cuentas de cobertura, por el contrario, es obligatorio indicar el ticket de posición position, pero puede omitir el símbolo. Esto se debe a las particularidades de la identificación de posiciones en cuentas de distintos tipos. Durante la compensación, sólo puede existir una posición para cada símbolo.

Para unificar el código, se recomienda rellenar ambos campos si se dispone de información.

Los niveles de precios de protección se fijan en los campos sl y tp. Es posible calcular sólo uno de los campos. Para eliminar los niveles de protección, asígneles valores cero.

En la siguiente tabla se resumen los requisitos para rellenar los campos en función de los modos de recuento. Los campos obligatorios están marcados con un asterisco, los campos opcionales están marcados con un signo más.

Campo

Compensación

Cobertura

action

*

*

symbol

*

+

posición

+

*

sl

+

+

tp

+

+

Para realizar la operación de modificación de los niveles de protección, introducimos varias sobrecargas del método adjust en la estructura MqlTradeRequestSync.

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

Como vimos anteriormente, dependiendo del entorno, la modificación puede hacerse sólo por ticket o sólo por símbolo de posición. Estas opciones se tienen en cuenta en los dos primeros prototipos.

Además, dado que la estructura puede haberse utilizado ya para solicitudes anteriores, es posible que haya rellenado los campos position y symbols. A continuación, puede llamar al método con el último prototipo.

Todavía no mostramos la implementación de estos tres métodos, porque está claro que debe tener un cuerpo común con el envío de una solicitud. Esta parte se enmarca como un método de ayuda privado _adjust con un conjunto completo de opciones. Aquí se da su código con algunas abreviaturas que no afectan a la lógica de trabajo.

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

Rellenamos todos los campos de la estructura según las reglas anteriores, llamando a los métodos anteriormente descritos setSymbol y setSLTP, y a continuación enviamos una solicitud al servidor. El resultado es un estado de éxito (true) o error (false).

Cada uno de los métodos adjust sobrecargados prepara por separado los parámetros de origen para la solicitud. Así es como se hace en presencia de un ticket de posición:

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

Aquí, utilizando la función integrada PositionSelectByTicket, comprobamos la presencia de una posición y su selección en el entorno de trading del terminal, lo cual es necesario para la posterior lectura de sus propiedades, en este caso, el símbolo (PositionGetString(POSITION_SYMBOL)). Entonces la variante universal se denomina adjust.

Al modificar una posición por nombre de símbolo (que sólo está disponible en una cuenta de compensación), puede utilizar otra opción 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);
   }

En este caso, la selección de la posición se realiza mediante la función integrada PositionSelect, y el número de ticket se obtiene a partir de sus propiedades (PositionGetInteger(POSITION_TICKET)).

Todas estas características se abordarán en detalle en sus respectivas secciones sobre trabajar con posiciones y propiedades de posiciones.

La versión del método adjust con el conjunto de parámetros más minimalista, es decir, con sólo los niveles stop y take, es la siguiente:

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

Este código garantiza que los campos position y symbols se rellenan correctamente en varios modos o que sale antes de tiempo con un mensaje de error en el registro. Al final, se llama a la versión privada de _adjust, que envía la solicitud a través de OrderSend.

De forma similar a los métodos de buy/sell, el conjunto de métodos de adjust funciona de forma «asíncrona»: a su finalización, sólo se conoce el estado de envío de la solicitud, pero no hay confirmación de la modificación de los niveles. Como sabemos, para la bolsa, el nivel Take Profit puede transmitirse como una orden Limit. Por lo tanto, en la estructura MqlTradeResultSync, debemos proporcionar una espera «sincrónica» hasta que los cambios surtan efecto.

El mecanismo general de espera formado como el método MqlTradeResultSync::wait ya está listo y se ha utilizado para esperar la apertura de una posición. El método wait recibe como primer parámetro un puntero a otro método con un prototipo predefinido condition para sondear en un bucle hasta que se cumpla la condición requerida o se produzca un timeout. En este caso, este método compatible con condition debe realizar una comprobación aplicada de los niveles de parada de la posición.

Vamos a añadir un nuevo método llamado 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;
   }

En primer lugar, por supuesto, comprobamos el estado en el campo retcode. Si hay un estado estándar, seguimos comprobando los niveles en sí, pasando a wait un método auxiliar checkSLTP.

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

Este código garantiza que la posición se selecciona mediante ticket en el entorno de trading del terminal utilizando PositionSelectByTicket y lee las propiedades de posición POSITION_SL y POSITION_TP, que deben compararse con lo que había en la solicitud. El problema es que aquí no tenemos acceso al objeto de solicitud y debemos pasar aquí de alguna manera un par de valores para los lugares marcados con '.?.'.

Básicamente, dado que estamos diseñando la estructura MqlTradeResultSync, podemos añadirle los campos sl y tp y rellenarlos con los valores de MqlTradeRequestSync antes de enviar la solicitud (el núcleo no «sabe» de nuestros campos añadidos y los dejará intactos durante la llamada aOrderSend). Pero para simplificar, utilizaremos lo que ya está disponible. Los campos bid y ask de la estructura MqlTradeResultSync sólo se utilizan para informar de los precios de recotización (estado TRADE_RETCODE_REQUOTE), que no está relacionado con la solicitud TRADE_ACTION_SLTP, por lo que podemos almacenar en ellos sl y tp de la MqlTradeRequestSync completada.

Es lógico hacerlo en el método completed de la estructura MqlTradeRequestSync que inicia una espera de bloqueo para los resultados de la operación de trading con un tiempo de espera predefinido. Hasta ahora, su código sólo tenía una rama para la acción TRADE_ACTION_DEAL. Para continuar, vamos a añadir una rama para 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;
   }

Como puede ver, después de establecer el ticket de posición y los niveles de precio desde la solicitud, llamamos al método adjusted comentado anteriormente que comprueba wait(checkSLTP). Ahora podemos volver al método de ayuda checkSLTP en la estructura MqlTradeResultSync y llevarlo a su forma final.

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

Esto completa la ampliación de la funcionalidad de las estructuras MqlTradeRequestSync y MqlTradeResultSync para la operación de modificación de Stop Loss y Take Profit.

Con esto en mente, vamos a continuar con el ejemplo del Asesor Experto MarketOrderSend.mq5 que comenzamos en la sección anterior. Añadámosle un parámetro de entrada Distance2SLTP, que permite especificar la distancia en puntos a los niveles Stop Loss y Take Profit.

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

Si es cero, no se establecerá ningún nivel de protección.

En el código de trabajo, tras recibir la confirmación de apertura de una posición, calculamos los valores de los niveles en las variables SL y TP y realizamos una modificación sincrónica: 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));
}

En la primera llamada de completed tras una operación de compra o venta exitosa, el ticket de posición se guarda en el campo position de la estructura de solicitud. Por lo tanto, para modificar los stops, bastan sólo los niveles de precios, y el símbolo y el ticket de la posición ya están presentes en request.

Vamos a intentar ejecutar una operación de compra utilizando el Asesor Experto con la configuración por defecto pero con Distance2SLTP fijado en 500 puntos.

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

Las dos últimas líneas corresponden a la salida de depuración al registro del contenido de las estructuras request y request.result, iniciada al final de la función. En estas líneas, es interesante que los campos almacenen una simbiosis de valores a partir de dos consultas: primero se abrió una posición y luego se modificó. En particular, los campos con volumen (0.01) y precio (1.10889) de la solicitud permanecieron después de TRADE_ACTION_DEAL, pero no impidieron la ejecución de TRADE_ACTION_SLTP. En teoría, es fácil deshacerse de esto reseteando la estructura entre dos solicitudes; sin embargo, hemos preferido dejarlos como están, porque entre los campos rellenados también los hay útiles: el campo position recibió el ticket que necesitamos para solicitar la modificación. Si restableciéramos la estructura tendríamos que introducir una variable para el almacenamiento intermedio del ticket.

En casos generales, por supuesto, es deseable adherirse a una estricta política de inicialización de datos, pero saber cómo usarlos en escenarios específicos (como dos o más solicitudes relacionadas de un tipo predefinido) le permite optimizar su código.

Además, no debe sorprendernos que en la estructura con el resultado, veamos los niveles solicitados sl y tp en los campos para los precios Bid y Ask: fueron escritos allí por el método MqlTradeRequestSync::completed con el propósito de compararlos con los cambios de posición reales. Al ejecutar la solicitud, el núcleo del sistema sólo rellena retcode (DONE), comment («Solicitud ejecutada») y request_id (26) en la estructura result.

A continuación veremos otro ejemplo de modificación de nivel que implementa trailing stop.