Trailing stop

Una de las tareas más comunes en las que se utiliza la capacidad de cambiar los niveles de precios de protección es desplazar secuencialmente Stop Loss a un precio mejor a medida que continúa la tendencia favorable. Se trata de trailing stop. Lo implementamos utilizando las nuevas estructuras MqlTradeRequestSync y MqlTradeResultSync de las secciones anteriores.

Para poder conectar el mecanismo a cualquier Asesor Experto, vamos a declararlo como la clase Trailing Stop (véase el archivo TrailingStop.mqh). Almacenaremos el número de la posición controlada, su símbolo y el tamaño del punto de precio, así como la distancia requerida del nivel de stop loss desde el precio actual, y el paso de cambios de nivel en las variables personales de la clase.

#include <MQL5Book/MqlTradeSync.mqh>
   
class TrailingStop
{
   const ulong ticket;  // ticket of controlled position
   const string symbol// position symbol
   const double point;  // symbol price pip size
   const uint distance// distance to the stop in points
   const uint step;     // movement step (sensitivity) in points
   ...

La distancia sólo es necesaria para el algoritmo de seguimiento de posición estándar proporcionado por la clase base. Las clases derivadas podrán mover el nivel de protección según otros principios, como medias móviles, canales, el indicador SAR, etc. Después de familiarizarnos con la clase base, daremos un ejemplo de una clase derivada con una media móvil.

Vamos a crear la variable level para el nivel de precio stop actual. En la variable ok mantendremos el estado actual de la posición: true si la posición sigue existiendo y false si se ha producido un error y la posición se ha cerrado.

protected:
   double level;
   bool ok;
   virtual double detectLevel() 
   {
      return DBL_MAX;  
   }

Un método virtual detectLevel está pensado para ser sobrescrito en clases descendientes, donde el precio stop debe calcularse de acuerdo con un algoritmo arbitrario. En esta implementación se devuelve un valor especial DBL_MAX, que indica el trabajo según el algoritmo estándar (véase más abajo).

En el constructor, rellene todos los campos con los valores de los parámetros correspondientes. La función PositionSelectByTicket comprueba la existencia de una posición con un ticket dado y la asigna en el entorno del programa de manera que la llamada posterior de PositionGetString devuelve su propiedad string con el nombre del símbolo.

public:
   TrailingStop(const ulong tconst uint dconst uint s = 1) :
      ticket(t), distance(d), step(s),
      symbol(PositionSelectByTicket(t) ? PositionGetString(POSITION_SYMBOL) : NULL),
      point(SymbolInfoDouble(symbolSYMBOL_POINT))
   {
      if(symbol == NULL)
      {
         Print("Position not found: " + (string)t);
         ok = false;
      }
      else
      {
         ok = true;
      }
   }
   
   bool isOK() const
   {
      return ok;
   }

Consideremos ahora el método público principal de la clase trail. El programa MQL tendrá que llamarlo en cada tick o por temporizador para realizar un seguimiento de la posición. El método devuelve true mientras exista la posición.

   virtual bool trail()
   {
      if(!PositionSelectByTicket(ticket))
      {
         ok = false;
         return false// position closed
      }
   
      // find out prices for calculations: current quote and stop level
      const double current = PositionGetDouble(POSITION_PRICE_CURRENT);
      const double sl = PositionGetDouble(POSITION_SL);
      ...

Aquí y más abajo utilizamos las funciones de lectura de las propiedades de posición. Se abordarán en detalle en una sección por separado. En concreto, debemos averiguar la dirección de la negociación -compra y venta- para saber en qué dirección debe fijarse el nivel de stop.

      // POSITION_TYPE_BUY  = 0 (false)
      // POSITION_TYPE_SELL = 1 (true)
      const bool sell = (bool)PositionGetInteger(POSITION_TYPE);
      TU::TradeDirection dir(sell);
      ...

Para los cálculos y comprobaciones, utilizaremos la clase de ayuda TU::TradeDirection y su objeto dir. Por ejemplo, su método negative permite calcular el precio situado a una distancia determinada del precio actual en una dirección perdedora, con independencia del tipo de operación. Esto simplifica el código porque, de lo contrario, habría que hacer cálculos «espejo» para las compras y las ventas.

      level = detectLevel();
      // we can't trail without a level: removing the stop level must be done by the calling code
      if(level == 0return true;
      // if there is a default value, make a standard offset from the current price
      if(level == DBL_MAXlevel = dir.negative(currentpoint * distance);
      level = TU::NormalizePrice(levelsymbol);
      
      if(!dir.better(currentlevel))
      {
         return true// you can't set a stop level on the profitable side<
      }
      ...

El método better de la clase TU::TradeDirection comprueba que el nivel de stop recibido se encuentra a la derecha del precio. Sin este método, tendríamos que volver a escribir la comprobación dos veces (para las compras y las ventas).

Podemos obtener un valor de nivel de stop incorrecto ya que el método detectLevel puede sobrescribirse en clases derivadas. Con el cálculo estándar, este problema desaparece porque el nivel lo calcula el objeto dir.

Por último, una vez calculado el nivel, es necesario aplicarlo a la posición. Si la posición aún no tiene un stop loss, cualquier nivel válido servirá. Si ya se ha fijado el stop loss, entonces el nuevo valor debe ser mejor que el anterior y diferir en más que el paso especificado.

      if(sl == 0)
      {
         PrintFormat("Initial SL: %f"level);
         move(level);
      }
      else
      {
         if(dir.better(levelsl) && fabs(level - sl) >= point * step)
         {
            PrintFormat("SL: %f -> %f"sllevel);
            move(level);
         }
      }
      
      return true// success
   }

El envío de una solicitud de modificación de posición se implementa en el método move, que utiliza el conocido método adjust de la estructura MqlTradeRequestSync (véase la sección Modificación de los niveles de Stop Loss y/o Take Profit).

   bool move(const double sl)
   {
      MqlTradeRequestSync request;
      request.position = ticket;
      if(request.adjust(sl0) && request.completed())
      {
         Print("OK Trailing: "TU::StringOf(sl));
         return true;
      }
      return false;
   }
};

Ahora todo está listo para añadir trailing al Asesor Experto TrailingStop.mq5 de prueba. En los parámetros de entrada, puede especificar la dirección de trading, la distancia al nivel de stop en puntos y el paso en puntos. El parámetro TrailingDistance es igual a 0 por defecto, lo que significa cálculo automático del rango diario de cotizaciones y uso de la mitad del mismo como distancia.

#include <MQL5Book/MqlTradeSync.mqh>
#include <MQL5Book/TrailingStop.mqh>
   
enum ENUM_ORDER_TYPE_MARKET
{
   MARKET_BUY = ORDER_TYPE_BUY,   // ORDER_TYPE_BUY
   MARKET_SELL = ORDER_TYPE_SELL  // ORDER_TYPE_SELL
};
   
input int TrailingDistance = 0;   // Distance to Stop Loss in points (0 = autodetect)
input int TrailingStep = 10;      // Trailing Step in points
input ENUM_ORDER_TYPE_MARKET Type;
input string Comment;
input ulong Deviation;
input ulong Magic = 1234567890;

Al iniciarse, el Asesor Experto buscará si existe una posición en el símbolo actual con el número Magic especificado y la creará si no existe.

El trailing se realizará mediante un objeto de la clase TrailingStop envuelto en un puntero inteligente AutoPtr. Gracias a esto último, no necesitamos eliminar manualmente el objeto antiguo cuando se necesita un nuevo objeto de seguimiento que lo sustituya para la nueva posición que se está creando. Cuando se asigna un nuevo objeto a un puntero inteligente, el objeto antiguo se elimina automáticamente. Recordemos que la desreferenciación de un puntero inteligente, es decir, el acceso al objeto de trabajo almacenado en su interior, se realiza mediante el operador sobrecargado [].

#include <MQL5Book/AutoPtr.mqh>
   
AutoPtr<TrailingStoptr;

En el manejador OnTick comprobamos si hay un objeto. Si lo hay, comprueba si existe una posición (el atributo se devuelve desde el método trail). Inmediatamente después de iniciarse el programa, el objeto no está ahí, y el puntero es NULL. En este caso, debe crear una nueva posición o encontrar una ya abierta y crear un objeto Trailing Stop para ella; de ello se encarga la función Setup. En llamadas posteriores de OnTick, el objeto se inicia y continúa el seguimiento, evitando que el programa entre en el bloque if mientras la posición esté «viva».

void OnTick()
{
   if(tr[] == NULL || !tr[].trail())
   {
      // if there is no trailing yet, create or find a suitable position
      Setup();
   }
}

Y aquí está la función Setup propiamente dicha:

void Setup()
{
   int distance = 0;
   const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   
   if(trailing distance == 0// auto-detect the daily range of prices
   {
      distance = (int)((iHigh(_SymbolPERIOD_D11) - iLow(_SymbolPERIOD_D11))
         / point / 2);
      Print("Autodetected daily distance (points): "distance);
   }
   else
   {
      distance = TrailingDistance;
   }
   
   // process only the position of the current symbol and our Magic
   if(GetMyPosition(_SymbolMagic))
   {
      const ulong ticket = PositionGetInteger(POSITION_TICKET);
      Print("The next position found: "ticket);
      tr = new TrailingStop(ticketdistanceTrailingStep);
   }
   else // there is no our position
   {
      Print("No positions found, lets open it...");
      const ulong ticket = OpenPosition();
      if(ticket)
      {
         tr = new TrailingStop(ticketdistanceTrailingStep);
      }
   }
   
   if(tr[] != NULL)
   {
      // Execute trailing for the first time immediately after creating or finding a position
      tr[].trail();
   }
}

La búsqueda de una posición abierta adecuada se implementa en la función GetMyPosition, y la apertura de una nueva posición se realiza mediante la función OpenPosition. Ambas se presentan a continuación. En cualquier caso, obtenemos un ticket de posición y creamos un objeto de trailing para él.

bool GetMyPosition(const string sconst ulong m)
{
   for(int i = 0i < PositionsTotal(); ++i)
   {
      if(PositionGetSymbol(i) == s && PositionGetInteger(POSITION_MAGIC) == m)
      {
         return true;
      }
   }
   return false;
}

El propósito y el significado general del algoritmo deben quedar claros a partir de los nombres de las funciones integradas. En el bucle a través de todas las posiciones abiertas (PositionsTotal), seleccionamos secuencialmente cada una de ellas utilizando PositionGetSymbol y obtenemos su símbolo. Si el símbolo coincide con el solicitado, leemos y comparamos la propiedad de posición POSITION_MAGIC con la «magic» pasada. Todas las funciones para trabajar con posiciones se abordarán en una sección aparte.

La función devolverá true en cuanto se encuentre la primera posición coincidente. Al mismo tiempo, la posición permanecerá seleccionada en el entorno de trading del terminal, lo que hace posible que el resto del código lea sus otras propiedades si es necesario.

Ya conocemos el algoritmo para abrir una posición.

ulong OpenPosition()
{
   MqlTradeRequestSync request;
   
   // default values
   const bool wantToBuy = Type == MARKET_BUY;
   const double volume = SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN);
   // optional fields are filled directly in the structure
   request.magic = Magic;
   request.deviation = Deviation;
   request.comment = Comment;
   ResetLastError();
   // execute the selected trade operation and wait for its confirmation
   if((bool)(wantToBuy ? request.buy(volume) : request.sell(volume))
      && request.completed())
   {
      Print("OK Order/Deal/Position");
   }
   
   return request.position// non-zero value - sign of success
}

Para mayor claridad, veamos cómo funciona este programa en el probador, en modo visual.

Después de la compilación, abramos el panel del probador de estrategias en el terminal, en la pestaña Review, y elijamos la primera opción: Single test.

En la pestaña Settings, seleccione lo siguiente:

  • en la lista desplegable Expert Advisor, MQL5Book\p6\TralingStop
  • Symbol: EURUSD
  • Timeframe: H1
  • Interval: último año, mes o personalizado
  • Forward: No
  • Delays: deshabilitado
  • Modeling: basado en ticks reales o generados
  • Optimization: deshabilitado
  • Visual mode: habilitado

Una vez que pulse Start verá algo parecido a esto en una ventana de comprobación independiente:

Trailing stop estándar en el probador

Trailing stop estándar en el probador

El registro mostrará entradas como las siguientes:

2022.01.10 00:02:00 Autodetected daily distance (points): 373

2022.01.10 00:02:00 No positions found, let's open it...

2022.01.10 00:02:00 instant buy 0.01 EURUSD at 1.13612 (1.13550 / 1.13612 / 1.13550)

2022.01.10 00:02:00 deal #2 buy 0.01 EURUSD at 1.13612 done (based on order #2)

2022.01.10 00:02:00 deal performed [#2 buy 0.01 EURUSD at 1.13612]

2022.01.10 00:02:00 order performed buy 0.01 at 1.13612 [#2 buy 0.01 EURUSD at 1.13612]

2022.01.10 00:02:00 Waiting for position for deal D=2

2022.01.10 00:02:00 OK Order/Deal/Position

2022.01.10 00:02:00 Initial SL: 1.131770

2022.01.10 00:02:00 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13177]

2022.01.10 00:02:00 OK Trailing: 1.13177

2022.01.10 00:06:13 SL: 1.131770 -> 1.131880

2022.01.10 00:06:13 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13188]

2022.01.10 00:06:13 OK Trailing: 1.13188

2022.01.10 00:09:17 SL: 1.131880 -> 1.131990

2022.01.10 00:09:17 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13199]

2022.01.10 00:09:17 OK Trailing: 1.13199

2022.01.10 00:09:26 SL: 1.131990 -> 1.132110

2022.01.10 00:09:26 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13211]

2022.01.10 00:09:26 OK Trailing: 1.13211

2022.01.10 00:09:35 SL: 1.132110 -> 1.132240

2022.01.10 00:09:35 position modified [#2 buy 0.01 EURUSD 1.13612 sl: 1.13224]

2022.01.10 00:09:35 OK Trailing: 1.13224

2022.01.10 10:06:38 stop loss triggered #2 buy 0.01 EURUSD 1.13612 sl: 1.13224 [#3 sell 0.01 EURUSD at 1.13224]

2022.01.10 10:06:38 deal #3 sell 0.01 EURUSD at 1.13221 done (based on order #3)

2022.01.10 10:06:38 deal performed [#3 sell 0.01 EURUSD at 1.13221]

2022.01.10 10:06:38 order performed sell 0.01 at 1.13221 [#3 sell 0.01 EURUSD at 1.13224]

2022.01.10 10:06:38 Autodetected daily distance (points): 373

2022.01.10 10:06:38 No positions found, let's open it...

Observe cómo el algoritmo desplaza el nivel de SL hacia arriba con un movimiento favorable del precio, hasta el momento en que la posición se cierra por stop loss. Inmediatamente después de liquidar una posición, el programa abre otra nueva.

Para comprobar la posibilidad de utilizar mecanismos de seguimiento no estándar, aplicamos un ejemplo de algoritmo sobre una media móvil. Para ello, volvamos al archivo TrailingStop.mqh y describamos la clase derivada TrailingStopByMA.

class TrailingStopByMApublic TrailingStop
{
   int handle;
   
public:
   TrailingStopByMA(const ulong tconst int period,
      const int offset = 1,
      const ENUM_MA_METHOD method = MODE_SMA,
      const ENUM_APPLIED_PRICE type = PRICE_CLOSE): TrailingStop(t01)
   {
      handle = iMA(_SymbolPERIOD_CURRENTperiodoffsetmethodtype);
   }
   
   virtual double detectLevel() override
   {
      double array[1];
      ResetLastError();
      if(CopyBuffer(handle001array) != 1)
      {
         Print("CopyBuffer error: "_LastError);
         return 0;
      }
      return array[0];
   }
};

Crea la instancia del indicador iMA en el constructor: el periodo, el método de promediación y el tipo de precio se pasan como parámetros.

En el método overridden detectLevel, leemos el valor del búfer del indicador, y por defecto, esto se hace con un offset de 1 barra, es decir, la barra está cerrada, y sus lecturas no cambian cuando llegan los ticks. Aquellos que lo deseen pueden tomar el valor de la barra cero, pero tales señales son inestables para todos los tipos de precio, excepto para PRICE_OPEN.

Para utilizar una nueva clase en el mismo Asesor Experto TrailingStop.mq5 de prueba, vamos a añadir otro parámetro de entrada MATrailingPeriod con un período móvil (vamos a dejar otros parámetros del indicador sin cambios).

input int MATrailingPeriod = 0;   // Period for Trailing by MA (0 = disabled)

El valor 0 en este parámetro desactiva la media móvil de trailing. Si está activada, se ignoran los ajustes de distancia del parámetro TrailingDistance.

Dependiendo de este parámetro, crearemos un objeto de trailing estándar TrailingStop o el derivado de iMA -TrailingStopByMA.

      ...
      tr = MATrailingPeriod > 0 ?
         new TrailingStopByMA(ticketMATrailingPeriod) :
         new TrailingStop(ticketdistanceTrailingStep);
      ...

Veamos cómo se comporta el programa actualizado en el probador. En la configuración del Asesor Experto, establezca un período distinto de cero para MA, por ejemplo, 10.

Trailing stop en la media móvil en el probador

Trailing stop en la media móvil en el probador

Tenga en cuenta que en los momentos en los que la media se acerca al precio, se produce un efecto de activación frecuente del stop-loss y de cierre de la posición. Cuando la media está por encima de las cotizaciones, no se fija ningún nivel de protección, porque no es correcto para comprar. Esto es consecuencia del hecho de que nuestro Asesor Experto no tiene ninguna estrategia y siempre abre posiciones del mismo tipo, independientemente de la situación en el mercado. En el caso de las ventas, de vez en cuando se produce la misma situación paradójica cuando la media se sitúa por debajo del precio, lo que significa que el mercado está creciendo, y el robot se pone «obstinadamente» en posición corta.

En las estrategias de trabajo, por regla general, la dirección de la posición se elige teniendo en cuenta el movimiento del mercado, y la media móvil se sitúa a la derecha del precio actual, donde se permite colocar un stop loss.