English Русский 中文 Deutsch 日本語 Português
preview
Gestor de riesgos para el trading algorítmico

Gestor de riesgos para el trading algorítmico

MetaTrader 5Trading | 8 noviembre 2024, 16:06
686 0
Aleksandr Seredin
Aleksandr Seredin

Contenido


Introducción

Este documento trata sobre la escritura de una clase de gestor de riesgos para el control de riesgos en el trading algorítmico. El objetivo de este artículo es adaptar los principios del riesgo controlado en el trading algorítmico en una clase aparte para que todo el mundo pueda comprobar por sí mismo la eficacia del enfoque de racionamiento del riesgo en el trading intradía y la inversión en los mercados financieros. El material presentado aquí utilizará y complementará la información resumida en el artículo anterior Gestor de riesgos para el trading manual. En el artículo anterior vimos que el control del riesgo puede mejorar significativamente los resultados comerciales incluso de una estrategia ya rentable y proteger a los inversores contra grandes reducciones en un corto periodo de tiempo.

Considerando las peticiones formuladas en los comentarios al artículo anterior, en este artículo prestaremos más atención a los criterios de selección de los métodos de implementación, con el fin de que el artículo resulte más comprensible para los principiantes. También daremos a conocer las definiciones de conceptos en el trading que se usarán al comentar el código. A su vez, los desarrolladores experimentados podrán usar los materiales descritos para adaptar el código a su propia arquitectura.

Conceptos y definiciones adicionales usados en este artículo:

High\low – valor superior o inferior del precio del instrumento para un determinado periodo de tiempo, limitado por una barra o vela.

Stop loss – precio límite para salir de una posición con pérdidas. Es decir, si el precio va en dirección contraria a la posición que hemos abierto, limitaremos las pérdidas de la posición abierta cerrándola antes de que la pérdida supere los valores que calculamos al abrirla.

Take profit – precio límite para salir de una posición con beneficios. Este precio se fija para salir de la posición y fijar el beneficio obtenido. Por regla general, se fija a partir del cálculo del beneficio previsto en depósito, o en la zona de agotamiento de la volatilidad diaria esperada por el instrumento. En pocas palabras, cuando se hace evidente que el potencial de un movimiento en el intervalo temporal previsto ya se ha agotado, y una nueva corrección en la dirección de la reducción de la ganancia ya obtenida es más probable.

Stop loss técnico: precio de stop loss fijado en base al análisis técnico, por ejemplo para las velas high/low, ruptura, fractal, etc., dependiendo de la estrategia comercial aplicada. La principal característica distintiva de este método es que tomamos el tamaño del stop en pips exactamente del gráfico para cualquier formación. En este caso, el punto de entrada puede cambiar, pero los valores del precio de stop loss no. Esto se debe al hecho de que estamos aceptando la afirmación de que si el precio alcanza nuestro valor de stop loss, la formación técnica se considerará rota y la dirección del instrumento se considerará irrelevante para seguir manteniendo esta posición. 

Stop loss calculado: precio de stop loss fijado sobre la base del valor calculado de la volatilidad del instrumento para un periodo determinado. Se diferencia en que no está vinculado a una formación específica en el gráfico. En este enfoque para establecer un stop loss, resulta más importante encontrar el punto de entrada del instrumento que dónde se encuentra la posición de stop en el patrón.

Getter: método de clase para acceder al valor de un campo protegido de una clase o estructura. Es necesario para encapsular el valor de la clase exactamente en la lógica establecida por el desarrollador de la clase, sin posibilidad de cambiar su funcionalidad o el valor del campo protegido con el nivel de acceso especificado en un uso posterior.

Deslizamiento: deslizamiento de una orden al precio de apertura, cuando un bróker abre una orden a un precio que difiere del precio anunciado originalmente. Esta situación puede producirse si negociamos abriendo órdenes según el mercado. Por ejemplo, al enviar una orden, el volumen de la posición se calculará según el riesgo comprometido por transacción en la divisa de depósito y el stop loss calculado/técnico en puntos. Al mismo tiempo, después de que el bróker abra la posición, puede resultar que esta se haya abierto a precios distintos de aquellos a los que se calculó el stop en pips. Digamos que en lugar de 100 puntos se ha convertido a 150 en un mercado volátil. Tales "aperturas" deben ser supervisadas, y si el riesgo resultante de la orden abierta se ha convertido en "mucho" (esto es a partir de los parámetros del gestor de riesgos) más de lo esperado, la transacción deberá cerrarse antes de tiempo, para no estropear las estadísticas comerciales y esperar a una nueva entrada.

Estilo comercial intradía: estilo comercial que implica transacciones comerciales dentro de un día comercial. Este enfoque no implica trasladar las posiciones abiertas durante la noche, es decir, hasta el siguiente día comercial a través del cierre nocturno. Este enfoque comercial no necesita considerar los riesgos asociados a los gaps matinales, las comisiones adicionales por el traslado de posiciones durante la noche, los cambios de tendencia al día siguiente, etc. Por regla general, si las posiciones abiertas se trasladan al día siguiente, este estilo comercial puede considerarse a medio plazo.

Estilo comercial posicional: estilo comercial que implica mantener una posición en un instrumento sin añadir volumen o reducir el volumen de una posición ya abierta y sin entradas adicionales. En este enfoque, al recibir una señal sobre el instrumento, el tráder calcula directamente el riesgo total sobre el instrumento para esta señal y trabaja solo con ella. Otras señales no se consideran, o se tienen en cuenta solo después de una salida completa de la posición anterior. 

Impulso en un instrumento comercial: un impulso es un movimiento unidireccional sin colisión del instrumento hacia un lado en el marco temporal seleccionado del instrumento. El punto de partida del impulso será el punto de partida del movimiento sin retroceso. Si el precio ha vuelto al punto de partida del impulso, normalmente ya se denominará retest. La magnitud del movimiento no inverso del instrumento en pips dependerá, entre otras cosas, pero no exclusivamente, de la volatilidad actual del mercado, la publicación de noticias importantes o la aproximación del precio del instrumento a valores de precios importantes y clave.


Clase sucesora para el trading algorítmico

La clase básica que escribimos en el artículo anterior RiskManagerBase ya contiene toda la funcionalidad necesaria para implementar la lógica del control del riesgo para trabajar de forma más segura en el trading intradía activo. Para evitar duplicar toda esta funcionalidad, utilizaremos uno de los principios más importantes de la programación orientada a objetos en el lenguaje mql5: la herencia. Este enfoque nos permitirá no duplicar el código ya escrito, sino simplemente complementarlo con la funcionalidad necesaria para integrar nuestra clase en cualquier algoritmo comercial.

La arquitectura de construcción del proyecto se basará en los siguientes principios:

  • conseguir cierto ahorro de tiempo en la escritura de funciones duplicadas
  • cumplir los principios de programación
  • simplificar el trabajo con nuestra arquitectura para múltiples equipos de desarrollo
  • garantizar que nuestro proyecto pueda ampliarse para adaptarse a cualquier estrategia comercial

El primer punto, como ya se ha mencionado, pretende ahorrar tiempo de desarrollo; así que utilizaremos la funcionalidad de la herencia, entre otras cosas, para no "romper" la lógica ya trabajada al tratar con límites y eventos, y no perder tiempo en copiar y probar nuevo código del que ya disponemos.

El segundo punto se refiere a los principios básicos de la construcción de clases en programación. Utilizaremos esencialmente el principio "Open closed Principle", guiándonos por la idea de que nuestra clase se amplíe sin perder los principios y enfoques básicos del control de riesgos. Cada método individual que añadamos nos permitirá garantizar el principio de responsabilidad única ("Single Responsibility Principle") para facilitar el desarrollo y una experiencia de código lógica. De ahí el principio descrito en el párrafo siguiente.

El tercer punto dice que los principios que aplicamos serán cómodos para que terceros entiendan la lógica, y tener archivos separados para cada clase hará más cómodo para el equipo de desarrollo trabajar simultáneamente, reduciendo los conflictos derivados de la fusión de cambios al trabajar con diferentes versiones en el proyecto.

Además, no restringiremos la herencia de nuestra clase RiskManagerAlgo utilizando el especificador final, para permitir una mayor mejora gracias a la posibilidad de lograr una mayor herencia. Esto nos permitirá adaptar con flexibilidad nuestra clase heredera a casi cualquier sistema comercial.

Aplicando los principios anteriores, nuestra clase mostrará el aspecto siguiente:

//+------------------------------------------------------------------+
//|                       RiskManagerAlgo                            |
//+------------------------------------------------------------------+
class RiskManagerAlgo : public RiskManagerBase
  {
protected:
   CSymbolInfo       r_symbol;                     // instance
   double            slippfits;                    // allowable slippage per trade
   double            spreadfits;                   // allowable spread relative to the opened stop level
   double            riskPerDeal;                  // risk per trade in the deposit currency

public:
                     RiskManagerAlgo(void);        // constructor
                    ~RiskManagerAlgo(void);        // destructor

   //---getters
   bool              GetRiskTradePermission() {return RiskTradePermission;};


   //---interface implementation
   virtual bool      SlippageCheck() override;  // checking the slippage for an open order
   virtual bool      SpreadMonitor(int intSL) override;           // spread control
  };
//+------------------------------------------------------------------+

Además de los campos y métodos de la clase básica RiskManagerBase ya existente, en nuestra clase sucesora RiskManagerAlgo ofrecemos los siguientes elementos para implementar la funcionalidad adicional en los asesores expertos algorítmicos. En primer lugar, necesitaremos un getter para recuperar los datos del campo protected protegido de la clase sucesora RiskTradePermission de la clase básica RiskManagerBase. Este método supondrá la forma principal de obtener el permiso del gestor de riesgos para abrir nuevas posiciones en la sección de condiciones de colocación algorítmica de órdenes. El principio de funcionamiento es bastante simple, si esta variable contiene el valor true, entonces el asesor experto podrá seguir colocando órdenes de acuerdo con las señales de su estrategia comercial, y si contiene el valor false, no podrá colocar órdenes, incluso si la estrategia comercial indica la aparición de un nuevo punto de entrada.

También preveremos una instancia de la clase estándar CSymbolInfo del terminal MT5 con código abierto para trabajar con los campos de símbolo del instrumento. La clase CSymbolInfo ofrece acceso simplificado a las propiedades de los símbolos, lo que también nos permitirá acortar visualmente el código en nuestro EA, para facilitar la percepción y el mantenimiento posterior de la funcionalidad de la clase.

Asimismo, proporcionaremos características adicionales para las condiciones de control del deslizamiento y el spread en nuestra clase. El campo slipfits definido por el usuario almacenará la condición de control slipfits,mientras que la variable spreadfits almacenará la condición del tamaño de spread.  La tercera variable necesaria será la que contenga la magnitud del riesgo por transacción en la divisa de depósito. Cabe señalar que se declaró una variable separada para controlar el deslizamiento de órdenes, precisamente porque, por regla general, cuando negociamos intradía, el sistema comercial da muchas señales, y no es necesario limitarse a una sola transacción con el tamaño de riesgo para todo el día. Esto significa que, antes de negociar, un tráder sabrá de antemano qué señales procesará y con qué instrumentos, y considerará que el riesgo por transacción es igual al riesgo por día, teniendo en cuenta el número de reentradas en la posición.

Esto implica que la suma de todos los riesgos de todas las entradas no deberá superar el riesgo del día. O, si solo hay una entrada al día, estas cantidades pueden ser iguales, pero esto resulta bastante raro dentro de un día, por regla general, hay muchas más entradas. Vamos a declarar el código a nivel global de la siguiente forma: para mayor comodidad, "lo envolveremos" en un bloque con nombre utilizando la palabra clave group:

input group "RiskManagerAlgoClass"
input double inp_slippfits    = 2.0;  // inp_slippfits - allowable slippage per open deal
input double inp_spreadfits   = 2.0;  // inp_spreadfits - allowable spread relative to the stop level to open
input double inp_risk_per_deal   = 100;  // inp_risk_per_deal - risk per trade in the deposit currency

Esta entrada permitirá establecer de forma flexible las condiciones de supervisión de las posiciones abiertas según las condiciones especificadas por el usuario.

En la sección public de nuestra clase RiskManagerAlgo, declararemos las funciones virtuales de nuestra interfaz para la redefinición de la siguiente forma:

//--- implementation of the interface
   virtual bool      SlippageCheck() override;  // checking the slippage for an open order
   virtual bool      SpreadMonitor(int intSL) override;           // spread control

Aquí hemos utilizado la palabra clave virtual, que sirve como especificador de función que proporciona un mecanismo para seleccionar dinámicamente en la etapa de ejecución la función de miembro correspondiente entre las funciones de nuestra clase básica RiskManagerBase y la clase derivada RiskManagerAlgo, cuyo ancestro común será nuestra interfaz con funciones puramente virtuales.

La inicialización se realizará en el constructor de la clase heredera RiskManagerAlgo copiando los valores introducidos por el usuario a través de las variables de parámetros de entrada a los valores de campo correspondientes de la instancia de clase creada:

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void)
  {
   slippfits   = inp_slippfits;           // copy slippage condition
   spreadfits  = inp_spreadfits;          // copy spread condition
   riskPerDeal  = inp_risk_per_deal;      // copy risk per trade condition
  }

Aquí cabe señalar que a veces la inicialización directa de los campos de clase puede resultar más práctica, pero en este caso no importará mucho, así que dejaremos la inicialización mediante el copiado para facilitar la percepción. A su vez, en sus implementaciones, también podremos utilizar una entrada como esta:

//+------------------------------------------------------------------+
//|                        RiskManagerAlgo                           |
//+------------------------------------------------------------------+
RiskManagerAlgo::RiskManagerAlgo(void):slippfits(inp_slippfits),
                                       spreadfits(inp_spreadfits),
                                       rispPerDeal(inp_risk_per_deal)
  {

  }

En el destructor de la clase, no necesitaremos limpiar "manualmente" la memoria, así que dejaremos el cuerpo de la función vacío, como aquí:

//+------------------------------------------------------------------+
//|                         ~RiskManagerAlgo                         |
//+------------------------------------------------------------------+
RiskManagerAlgo::~RiskManagerAlgo(void)
  {

  }

Ahora que todas las funciones necesarias han sido declaradas en la clase RiskManagerAlgo, elegiremos cómo implementar nuestra interfaz para gestionar los stop loss cortos en las posiciones abiertas.


Interfaz para el trabajo con stop loss

El lenguaje de programación mql5 nos ofrece amplias posibilidades en cuanto a la flexibilidad de desarrollo y el uso de la funcionalidad necesaria en implementaciones óptimas. Parte de esta funcionalidad se ha portado desde C++, y otra parte se ha complementado y ampliado para facilitar el desarrollo. Para resolver la implementación de la funcionalidad de control de las posiciones abiertas con un stop loss corto, necesitaremos un objeto generalizado que podamos utilizar como padre, no solo para la herencia en nuestra clase gestora de riesgos, sino también para la herencia en otras arquitecturas del EA como uno de los ancestros.

Podemos usar tanto clases abstractas al estilo de C++ como un tipo de datos independiente como la interfaz para declarar un tipo de datos generalizado creado para implementar y conectar cierta funcionalidad como parte del diseño. 

Las clases abstractas, al igual que las interfaces, tienen la misión de crear entidades generalizadas a partir de las cuales se supone que posteriormente se creará una clase derivada más específica, en nuestro caso para trabajar con posiciones con stop loss cortos. Una clase abstracta es una clase que solo puede usarse como clase básica para alguna clase sucesora, por lo que no resulta posible crear un objeto de tipo clase abstracta. Si necesitamos utilizar esta entidad generalizada, el código de nuestra clase tendrá el siguiente aspecto:

//+------------------------------------------------------------------+
//|                         CShortStopLoss                           |
//+------------------------------------------------------------------+
class CShortStopLoss
  {
public:
                     CShortStopLoss(void) {};         // the class will be abstract event if at least one function in it is virtual
   virtual          ~CShortStopLoss(void) {};         // the same applies to the destructor

   virtual bool      SlippageCheck()         = NULL;  // checking slippage for the open order
   virtual bool      SpreadMonitor(int intSL)= NULL;  // spread control
  };

Considerando que el lenguaje de programación mql5 nos ofrece una interfaz de tipos de datos especial para generalizar entidades, cuyo registro tendrá un aspecto mucho más compacto y sencillo, utilizaremos dicha interfaz, ya que no habrá diferencia de funcionalidad para nosotros. En esencia, la interfaz también es una clase que no puede contener miembros/campos y no puede tener un constructor y/o destructor. Todos los métodos declarados en la interfaz son puramente virtuales, incluso sin una definición explícita, lo cual hace que su uso sea más elegante y compacto. La aplicación a través de una entidad generalizada como una interfaz se vería así:

interface IShortStopLoss
  {
   virtual bool   SlippageCheck();           // checking the slippage for an open order
   virtual bool   SpreadMonitor(int intSL);  // spread control
  };

Ahora que hemos decidido el tipo de entidad de generalización que utilizaremos, procederemos a implementar toda la funcionalidad necesaria de los métodos ya declarados en la interfaz para nuestra clase heredera.


Control de deslizamiento en las órdenes abiertas

En primer lugar, para implementar el método SlippageCheck(void), necesitaremos actualizar los datos del símbolo sobre el que estamos ejecutando nuestro gráfico. Para ello, utilizaremos el método Refresh() de una instancia de nuestra clase CSymbolInfo para actualizar todos los campos que caracterizan el instrumento para seguir trabajando con el siguiente registro:

   r_symbol.Refresh();                                                  // update symbol data

Aquí deberemos considerar que el método Refresh() actualiza todos los datos de los campos de la clase CSymbolInfo, a diferencia del método de la misma clase RefreshRates(void), que actualiza solo los datos de los precios actuales del símbolo especificado. El método Refresh() en esta implementación será llamado cada tick para usar información actualizada en cada iteración de nuestro EA.

En el ámbito de variables del método Refresh(), necesitaremos variables dinámicas para almacenar los datos de las propiedades de una posición abierta, al enumerar todas las posiciones abiertas, para calcular el posible deslizamiento en la apertura. La información sobre las posiciones se almacenará en esta sección del método de la siguiente forma:

   double PriceClose = 0,                                               // close price for the order
          PriceStopLoss = 0,                                            // stop loss price for the order
          PriceOpen = 0,                                                // open price for the order
          LotsOrder = 0,                                                // order lot volume
          ProfitCur = 0;                                                // current order profit

   ulong  Ticket = 0;                                                   // order ticket
   string Symbl;                                                        // symbol

Para obtener los datos del valor del tick en caso de pérdidas, utilizaremos el método TickValueLoss() de la instancia de clase CSymbolInfo declarada dentro de nuestra clase RiskManagerAlgo. El valor obtenido de él indicará cuánto cambiará el balance de la cuenta cuando el precio cambie en un pip mínimo en un lote estándar. Usaremos este valor más adelante para calcular la pérdida potencial sobre los precios de posición realmente abiertos. Aquí utilizamos el término "potencial", porque este método funcionará en cada tick e inmediatamente después de abrir una posición, o mejor dicho, inmediatamente en el siguiente tick recibido podremos comprobar cuánto podemos perder en la transacción, aunque el precio siga encontrándose más cerca del precio de apertura que del precio de stop loss. 

   double lot_cost = r_symbol.TickValueLoss();                          // get tick value
   bool ticket_sc = 0;                                                  // variable for successful closing

También aquí declararemos la variable necesaria para comprobar la ejecución de la orden de cierre de una posición abierta, si el cálculo muestra que la posición debe cerrarse por deslizamiento. Esta variable será de tipo bool con el nombre ticket_sc.

Ahora podemos enumerar todas las posiciones abiertas dentro de nuestro método de control del deslizamiento. Buscaremos las posiciones abiertas usando la organización del ciclo for limitado por el número de posiciones abiertas en el terminal. Para obtener el valor del número de posiciones abiertas, utilizaremos la función predefinida del terminal PositionsTotal(). Las posiciones se seleccionarán simplemente según el índice, utilizando el método SelectByIndex() de la clase estándar CPositionInfo del terminal.

r_position.SelectByIndex(i)

Una vez seleccionada la posición, podremos empezar a consultar las propiedades de esta posición utilizando la misma clase de terminal estándar CPositionInfo, pero antes de nada deberemos comprobar si la posición seleccionada corresponde al símbolo en el que se está ejecutando esta instancia del EA. Esto se puede hacer con el siguiente código en el ciclo de enumeración de nuestras posiciones:

         Symbl = r_position.Symbol();                                   // get the symbol
         if(Symbl==Symbol())                                            // check if it's the right symbol

Y solo después de haber verificado que la posición seleccionada según el índice es relevante para nuestro gráfico, podremos proceder a consultar otras propiedades para verificar la posición abierta. Las consultas posteriores de propiedades de posición también usarán una instancia de la clase de terminal estándar CPositionInfo de la siguiente forma:

            PriceStopLoss = r_position.StopLoss();                      // remember its stop loss
            PriceOpen = r_position.PriceOpen();                         // remember its open price
            ProfitCur = r_position.Profit();                            // remember financial result
            LotsOrder = r_position.Volume();                            // remember order lot volume
            Ticket = r_position.Ticket();

Aquí cabe señalar que la comprobación puede realizarse no solo según el símbolo, sino también según el número mágico de la estructura MqlTradeRequest, utilizada al abrir la posición seleccionada. Este enfoque se usa a menudo para separar las transacciones ejecutadas utilizando diferentes estrategias dentro de una misma cuenta. Nosotros no utilizaremos dicho enfoque, pues resulta preferible utilizar cuentas separadas para estrategias separadas, desde el punto de vista de la facilidad de análisis y el uso de recursos computacionales. Pero aquí pueden darse otros enfoques que podemos escribir en los comentarios a este artículo; nosotros, por nuestra parte, pasaremos a la descripción de la aplicación de nuestro método.

En nuestro método estaremos cerrando posiciones, por lo que tendremos que considerar el hecho de que una posición de compra se cierra vendiendo al precio Bid mientras que una posición de venta se cerrará al precio Ask. Para ello, necesitaremos implementar la lógica de obtención del precio de cierre en función del tipo de posición abierta seleccionada:

            int dir = r_position.Type();                                // define order type

            if(dir == POSITION_TYPE_BUY)                                // if it is Buy
              {
               PriceClose = r_symbol.Bid();                             // close at Bid
              }
            if(dir == POSITION_TYPE_SELL)                               // if it is Sell
              {
               PriceClose = r_symbol.Ask();                             // close at Ask
              }

No consideraremos aquí la lógica del cierre parcial de posiciones, aunque el terminal permite realizarlo técnicamente considerando el tipo de cuenta abierta en el bróker, pero esto no se corresponde con la lógica de nuestro método, por lo que no utilizaremos dicha solución.

Una vez hayamos comprobado que la posición seleccionada es la que necesitamos y obtenido todos las características necesarias, procederemos a calcular el riesgo real de la posición. En primer lugar, deberemos calcular el tamaño del stop resultante en pips mínimos, considerando el precio de apertura real, para compararlo con el inicialmente previsto.

Luego calcularemos el tamaño como la diferencia absoluta entre el precio de apertura y el stop loss de la posición. Y obtendremos la diferencia absoluta utilizando la función predefinida del terminal MathAbs(). Para obtener un valor entero en puntos a partir de un valor de precio fraccionario, dividiremos el valor MathAbs() resultante por el valor de un punto en el valor fraccionario. Después obtendremos el valor de un punto utilizando el método Point() de una instancia de nuestra clase de terminal estándar CPositionInfo.

int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop

Ahora, para obtener el valor potencial real de la pérdida en la posición seleccionada, solo nos quedará multiplicar el valor obtenido del stop en puntos por el tamaño de la posición en lotes y el coste de un tick en el instrumento de la posición de la siguiente forma:

double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level

Después nos encargaremos de comprobar si el valor obtenido cumple con la desviación del riesgo en la transacción introducida por el usuario en la variable denominada slippfits. Y si ha sobrepasado los límites de este rango, cerraremos la posición seleccionada:

             if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // if the resulting stop exceeds risk per trade given the threshold value
                  ProfitCur<0                                                  &&   // and the order is at a loss
                  PriceStopLoss != 0                                                // if stop loss is not set, don't touch
               )

En este conjunto de condiciones, también hemos añadido dos comprobaciones adicionales que contienen la siguiente lógica para resolver las situaciones comerciales.

En primer lugar, añadiremos la condición "ProfitCur<0" para garantizar que el deslizamiento solo se produzca en la zona no rentable de una posición abierta. Esto se deberá a las siguientes condiciones de la estrategia comercial. Como el deslizamiento, por regla general, tiene lugar en momentos de alta volatilidad del mercado, y la transacción se abre deslizándose exactamente en la dirección del take profit, aumentando así el stop loss y reduciendo el take profit, esto reducirá el riesgo/rendimiento esperado de la transacción y aumentará la pérdida potencial en relación con la prevista, pero al mismo tiempo aumentará la probabilidad de obtener un take profit, ya que es probable que el impulso que "ha arrastrado" nuestra posición continúe en ese momento. Esta condición dice que cerraremos la posición solo si el impulso nos "ha arrastrado" y se ha detenido antes de alcanzar el take profit, volviendo a la zona perdedora.

La segunda condición "PriceStopLoss != 0" es necesaria para implementar la siguiente lógica: si el tráder no ha establecido un stop loss, NO cerraremos esta posición debido al riesgo ilimitado de la misma. Esto significa que al abrir una posición, el tráder se dará cuenta de que esta posición podría cubrir potencialmente todo su riesgo del día si el precio va en su contra. Y esto podría ocasionar que no haya suficientes límites en todos los instrumentos previstos para el comercio para el día, lo cual potencialmente podría resultar positivo y traer beneficios, y una posición sin stops simplemente haría estas entradas imposibles. Aquí cada tráder decidirá por sí mismo si incluir esta condición o no, basándose en su estrategia personal de trading, pero en nuestra implementación no negociaremos varios instrumentos simultáneamente, por lo que no borraremos una posición si no hay un precio stop en ella.

Si se han cumplido todas las condiciones necesarias para identificar el deslizamiento en una posición, cerraremos la posición usando el método PositionClose() de la clase estándar de código abierto CTrade declarada en nuestra clase básica RiskManagerBase. Como parámetro de entrada, transmitiremos el número de ticket de la posición previamente guardado para su cierre, y el resultado de la llamada a la función de cierre se almacenará en la variable ticket_sc para controlar la ejecución de la orden.

ticket_sc = r_trade.PositionClose(Ticket);                        // close order

En general, nuestro método se describirá de la siguiente forma:

//+------------------------------------------------------------------+
//|                         SlippageCheck                            |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SlippageCheck(void) override
  {
   r_symbol.Refresh();                                                  // update symbol data

   double PriceClose = 0,                                               // close price for the order
          PriceStopLoss = 0,                                            // stop loss price for the order
          PriceOpen = 0,                                                // open price for the order
          LotsOrder = 0,                                                // order lot volume
          ProfitCur = 0;                                                // current order profit

   ulong  Ticket = 0;                                                   // order ticket
   string Symbl;                                                        // symbol
   double lot_cost = r_symbol.TickValueLoss();                          // get tick value
   bool ticket_sc = 0;                                                  // variable for successful closing

   for(int i = PositionsTotal(); i>=0; i--)                             // start loop through orders
     {
      if(r_position.SelectByIndex(i))
        {
         Symbl = r_position.Symbol();                                   // get the symbol
         if(Symbl==Symbol())                                            // check if it's the right symbol
           {
            PriceStopLoss = r_position.StopLoss();                      // remember its stop loss
            PriceOpen = r_position.PriceOpen();                         // remember its open price
            ProfitCur = r_position.Profit();                            // remember financial result
            LotsOrder = r_position.Volume();                            // remember order lot volume
            Ticket = r_position.Ticket();

            int dir = r_position.Type();                                // define order type

            if(dir == POSITION_TYPE_BUY)                                // if it is Buy
              {
               PriceClose = r_symbol.Bid();                             // close at Bid
              }
            if(dir == POSITION_TYPE_SELL)                               // if it is Sell
              {
               PriceClose = r_symbol.Ask();                             // close at Ask
              }

            if(dir == POSITION_TYPE_BUY || dir == POSITION_TYPE_SELL)
              {
               int curr_sl_ord = (int) NormalizeDouble(MathAbs(PriceStopLoss-PriceOpen)/r_symbol.Point(),0); // check the resulting stop

               double potentionLossOnDeal = NormalizeDouble(curr_sl_ord * lot_cost * LotsOrder,2); // calculate risk upon reaching the stop level

               if(
                  potentionLossOnDeal>NormalizeDouble(riskPerDeal*slippfits,0) &&   // if the resulting stop exceeds risk per trade given the threshold value
                  ProfitCur<0                                                  &&   // and the order is at a loss
                  PriceStopLoss != 0                                                // if stop loss is not set, don't touch
               )
                 {
                  ticket_sc = r_trade.PositionClose(Ticket);                        // close order

                  Print(__FUNCTION__+", RISKPERDEAL: "+DoubleToString(riskPerDeal));                  //
                  Print(__FUNCTION__+", slippfits: "+DoubleToString(slippfits));                      //
                  Print(__FUNCTION__+", potentionLossOnDeal: "+DoubleToString(potentionLossOnDeal));  //
                  Print(__FUNCTION__+", LotsOrder: "+DoubleToString(LotsOrder));                      //
                  Print(__FUNCTION__+", curr_sl_ord: "+IntegerToString(curr_sl_ord));                 //

                  if(!ticket_sc)
                    {
                     Print(__FUNCTION__+", Error Closing Orders №"+IntegerToString(ticket_sc)+" on slippage. Error №"+IntegerToString(GetLastError())); // output to log
                    }
                  else
                    {
                     Print(__FUNCTION__+", Orders №"+IntegerToString(ticket_sc)+" closed by slippage."); // output to log
                    }
                  continue;
                 }
              }
           }
        }
     }
   return(ticket_sc);
  }
//+------------------------------------------------------------------+

Con esto completaremos la redefinición del método de control del deslizamiento, y pasaremos a describir el método de control del tamaño del spread antes de abrir una nueva posición.


Control de apertura de posiciones

El control del spread en nuestra implementación del método SpreadMonitor() consistirá en comparar de forma preliminar el spread actual justo antes de abrir una transacción con el stop calculado/técnico, transmitido como parámetro del método en un valor entero. La función retornará true si el tamaño actual de la dispersión se encuentra dentro del rango permitido por el usuario, y si el tamaño de la dispersión supera este rango, el método devolverá false.

El resultado de la función se almacenará en una variable de tipo bool inicializada con un valor por defecto igual a true:

   bool SpreadAllowed = true;

El valor del spread actual del instrumento se obtendrá usando el método Spread() de la clase CSymbolInfo de la siguiente forma:

   int SpreadCurrent = r_symbol.Spread();

La comprobación sobre la condición de cumplimiento del rango especificado por el usuario se realizará mediante una comparación lógica y tendrá el siguiente aspecto:

if(SpreadCurrent>intSL*spreadfits)

Esto significa que si el spread actual en un instrumento es mayor que el producto del stop requerido por el coeficiente definido por el usuario, el método debería retornar false, y esto debería limitar la apertura de una posición con el tamaño de spread actual hasta el siguiente tick. En términos generales, el método se describirá del siguiente modo:

//+------------------------------------------------------------------+
//|                          SpreadMonitor                           |
//+------------------------------------------------------------------+
bool RiskManagerAlgo::SpreadMonitor(int intSL)
  {
//--- spread control
   bool SpreadAllowed = true;                                           // allow spread trading and check ratio further
   int SpreadCurrent = r_symbol.Spread();                               // current spread values

   if(SpreadCurrent>intSL*spreadfits)                                   // if the current spread is greater than the stop and the coefficient
     {
      SpreadAllowed = false;                                            // prohibit trading
      Print(__FUNCTION__+IntegerToString(__LINE__)+
            ". Spread is to high! Spread:"+
            IntegerToString(SpreadCurrent)+", SL:"+IntegerToString(intSL));// notify
     }
   return SpreadAllowed;                                                // return result
  }
//+------------------------------------------------------------------+

Al trabajar con este método, deberemos considerar que si la condición de spread es muy ajustada, el asesor experto no abrirá posiciones, y este hecho quedará registrado constantemente mediante mensajes en el registro del asesor experto. Por regla general, se usará un valor del coeficiente de al menos 2, lo cual significa que si el spread es la mitad del stop, deberemos esperar a un spread menor, o negarnos a entrar con un stop tan corto, ya que cuanto más cerca esté el stop del tamaño del spread, mayor será la probabilidad de obtener una pérdida en dicha posición.


Implementación de la interfaz

La interfaz que declararemos será el primer ancestro de nuestra clase básica, ya que mql5 no soporta herencia múltiple. Pero en este caso no nos limitará en absoluto, ya que podemos organizar un esquema de herencia coherente para nuestro proyecto. 

Para ello, necesitaremos completar nuestra clase básica RiskManagerBase con la herencia de la interfaz IShortStopLoss descrita anteriormente de la siguiente forma:

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
class RiskManagerBase:IShortStopLoss                        // the purpose of the class is to control risk in terminal

Esta entrada nos permitirá transferir la funcionalidad que necesitamos al sucesor de RiskManagerAlgo. En esta situación, no nos importará el nivel de acceso a la herencia, ya que nuestra interfaz posee funciones puramente virtuales y no tiene campos, ni constructor ni destructor.

La estructura de herencia final de nuestra clase RiskManagerAlgo personalizada, que muestra la encapsulación de métodos públicos para ofrecer una funcionalidad completa, se representa en la figura 1.

Figura 1. Jerarquía de la estructura de herencia de la clase RiskManagerAlgo

Figura 1. Jerarquía de la estructura de herencia de la clase RiskManagerAlgo

Ahora, antes de montar nuestro algoritmo, solo tendremos que implementar una herramienta de decisión para probar la funcionalidad algorítmica de control de riesgos que hemos descrito.


Implementación del bloque comercial

En el artículo anterior Gestor de riesgos para el trading manual, utilizamos una entidad TradeModel bastante simple como bloque comercial para la elaboración elemental de las entradas obtenidas de los fractales. Como este artículo, a diferencia del anterior, trata sobre el trading algorítmico, haremos que la herramienta de toma de decisiones basada en fractales sea también algorítmica. Se basará en la misma lógica, pero ahora implementaremos todo en código en lugar de generar señales manualmente. Como ventaja añadida, podremos probar lo que obtengamos en un mayor intervalo de datos históricos, ya que no tendremos que generar manualmente las entradas necesarias.

Vamos a declarar la clase CFractalsSignal, que se encargará de recibir las señales de los fractales. La lógica sigue siendo la misma, si el precio rompe el fractal superior del gráfico diario, el asesor experto ejecutará una transacción de compra, si el precio actual rompe el precio del fractal inferior, también del gráfico diario, tendremos una señal de venta. Las transacciones se cerrarán dentro del día, al final del día comercial en el que se abrieron.

En primer lugar, nuestra clase CFractalsSignal contendrá un campo con la información sobre el marco temporal utilizado, del que tomaremos los fractales. Esto es necesario para poder distinguir entre el marco temporal del que el asesor experto tomará la información sobre los fractales, y el marco temporal en el que se ejecutará el asesor experto, solo para facilitar su uso. Luego declararemos la variable de enumeración ENUM_TIMEFRAMES de la siguiente forma:

ENUM_TIMEFRAMES   TF;                     // timeframe used

A continuación, declararemos el puntero variable de la clase estándar del terminal de código abierto para trabajar con el indicador técnico CiFractals, que implementará cómodamente todas las funciones necesarias; no tendremos que escribirlo todo de nuevo:

   CiFractals        *cFractals;             // fractals

Además, necesitaremos almacenar los datos sobre las señales con la posibilidad de tener en cuenta su procesamiento por parte del asesor experto. Para ello, podremos usar la misma estructura personalizada TradeInputs que utilizamos en el artículo anterior, solo que la última vez la formamos manualmente. Ahora será formada por la clase CFractalsSignal por sí misma:

//+------------------------------------------------------------------+
//|                         TradeInputs                              |
//+------------------------------------------------------------------+

struct TradeInputs
  {
   string             symbol;                                           // symbol
   ENUM_POSITION_TYPE direction;                                        // direction
   double             price;                                            // price
   datetime           tradedate;                                        // date
   bool               done;                                             // trigger flag
  };

Luego declararemos las variables internas de nuestra estructura por separado para las señales de compra y de venta, de manera que podamos tenerlas en cuenta simultáneamente, ya que no podemos saber de antemano qué precio funcionará antes:

   TradeInputs       fract_Up, fract_Dn;     // current signal

Ya solo nos necesitaremos declarar las variables que almacenarán los valores actuales obtenidos de la clase CiFractals para obtener los datos sobre los fractales recién formados en el gráfico diario.

Para ofrecer la funcionalidad necesaria, necesitaremos disponer de varios métodos de la clase CFractalsSignal de dominio público, que se encargarán de monitorizar las últimas rupturas fractales reales del precio, emitir la señal de apertura de posiciones y controlar el éxito de estas señales.

Después llamaremos al método para controlar la actualización del estado de los datos de la clase Process(). Este no retornará nada y no tomará ningún parámetro, sino que simplemente realizará una actualización del estado de los datos en cada tick entrante. Los métodos para obtener una señal de compra y venta se llamarán BuySignal() y SellSignal(). No aceptarán ningún parámetro, pero retornarán un valor de tipo bool, en caso de que sea necesario abrir una posición en la dirección correspondiente. Los métodos BuyDone() y SellDone() deberán ser llamados después de comprobar la respuesta del servidor del bróker sobre la apertura exitosa de la posición correspondiente. En términos generales, la descripción de nuestra clase será la siguiente:

//+------------------------------------------------------------------+
//|                       CFractalsSignal                            |
//+------------------------------------------------------------------+
class CFractalsSignal
  {
protected:
   ENUM_TIMEFRAMES   TF;                     // timeframe used
   CiFractals        *cFractals;             // fractals

   TradeInputs       fract_Up, fract_Dn;     // current signal

   double            FrUp;                   // upper fractals
   double            FrDn;                   // lower fractals

public:
                     CFractalsSignal(void);  // constructor
                    ~CFractalsSignal(void);  // destructor

   void              Process();              // method to start updates

   bool              BuySignal();            // buy signal
   bool              SellSignal();           // sell signal

   void              BuyDone();              // buy done
   void              SellDone();             // sell done
  };

En el constructor de la clase tendremos que inicializar el campo de marco temporal usado TF con el valor del intervalo diario PERIOD_D1, ya que los niveles del gráfico diario son lo suficientemente fuertes como para darnos el impulso necesario para alcanzar el take profit, y resultan mucho más comunes que los niveles más fuertes de los gráficos semanales y mensuales. Aquí podemos dejar la oportunidad de que todos prueben marcos temporales más pequeños, y nos centraremos en los marcos temporales diarios. Asimismo, también crearemos instancias del objeto de clase para trabajar con el indicador fractal de nuestra clase e inicializaremos por defecto todos los campos necesarios en la siguiente secuencia:

//+------------------------------------------------------------------+
//|                        CFractalsSignal                           |
//+------------------------------------------------------------------+
CFractalsSignal::CFractalsSignal(void)
  {
   TF  =  PERIOD_D1;                                                    // timeframe used

//--- fractal class
   cFractals=new CiFractals();                                          // created fractal instance
   if(CheckPointer(cFractals)==POINTER_INVALID ||                       // if instance not created OR
      !cFractals.Create(Symbol(),TF))                                   // variant not created
      Print("INIT_FAILED");                                             // don't proceed
   cFractals.BufferResize(4);                                           // resize fractal buffer
   cFractals.Refresh();                                                 // update

//---
   FrUp = EMPTY_VALUE;                                                  // leveled upper at start
   FrDn = EMPTY_VALUE;                                                  // leveled lower at start

   fract_Up.done  = true;                                               //
   fract_Up.price = EMPTY_VALUE;                                        //

   fract_Dn.done  = true;                                               //
   fract_Dn.price = EMPTY_VALUE;                                        //
  }

En el destructor, simplemente borraremos la memoria del objeto de indicador fractal que creamos en el constructor:

//+------------------------------------------------------------------+
//|                        ~CFractalsSignal                          |
//+------------------------------------------------------------------+
CFractalsSignal::~CFractalsSignal(void)
  {
//---
   if(CheckPointer(cFractals)!=POINTER_INVALID)                         // if instance was created,
      delete cFractals;                                                 // delete
  }

En el método de actualización de datos, solo llamaremos al método Refresh() de una instancia de la clase CiFractals para actualizar los datos de precios del fractal que ella misma heredó de una de sus clases padre CIndicator.

//+------------------------------------------------------------------+
//|                         Process                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::Process(void)
  {
//---
   cFractals.Refresh();                                                 // update fractals
  }

Aquí me gustaría señalar que existe la posibilidad de optimizar adicionalmente este enfoque desde el punto de vista de que no tenemos que actualizar estos datos cada tick, ya que los niveles de ruptura fractal provienen del gráfico diario. Podríamos implementar de forma adicional un método de evento de aparición de una nueva barra en el gráfico diario y actualizar estos datos solo cuando se active. No obstante, mantendremos esta implementación porque no supone una gran carga adicional para el sistema, y la implementación de funcionalidad adicional requerirá costes adicionales con una ganancia de rendimiento bastante mediocre.

En el método BuySignal(void) que abre una señal de compra, primero solicitamos el último precio Ask actual usando la siguiente entrada:

   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // Buy price

A continuación, tendremos que solicitar el valor real del fractal superior a través del método Upper() de la instancia de la clase CiFractals, transmitiendo como parámetro el índice de la barra que necesitamos en la serie temporal:

   FrUp=cFractals.Upper(3);                                             // request the current value

Así, transmitiremos el valor `3` a este método precisamente porque solo usaremos los fractales finalmente formados. Como en las series temporales el cálculo del búfer se realizará desde los más recientes `0` hacia los más tardíos de manera ascendente, el valor `tres` en el gráfico diario indicará el día anterior a anteayer, para excluir rupturas fractales que se puedan formar en el gráfico diario en un momento determinado, y que luego el precio reescriba el high/low en el mismo día, y el nivel fractal desaparezca.

Ahora vamos a hacer una comprobación lógica para actualizar la ruptura fractal real si el precio de la última ruptura real en el gráfico diario ha cambiado. Para ello, compararemos el valor actual del indicador fractal actualizado anteriormente en la variable FrUp con el último valor actual del fractal superior almacenado en el campo de precio de nuestra estructura personalizada TradeInputs. Para que el campo price almacene siempre el valor del último precio real y no se "reinicie" en ausencia de datos retornados por el indicador, si no se detecta la fractura, añadiremos una comprobación más para un valor vacío del indicador FrUp != EMPTY_VALUE. La combinación de estas dos condiciones nos permitirá actualizar solo los valores significativos del último precio fractal (distintos de cero, que en el indicador se corresponde con el valor EMPTY_VALUE) y no sobrescribir esta variable con un valor vacío. En términos generales, estos controles serán los siguientes:

   if(FrUp != fract_Up.price           &&                               // if the data has been updated
      FrUp != EMPTY_VALUE)                                              // skip empty value

Para concluir el método, la comprobación de la lógica para una señal de compra se parecerá a lo siguiente:

   if(fract_Up.price != EMPTY_VALUE    &&                               // skip zero values
      ask            >= fract_Up.price &&                               // if the buy price is greater than or equal to the fractal
      fract_Up.done  == false)                                          // the signal has not been processed yet
     {
      return true;                                                      // generate a signal to process
     }

En este bloque, la comprobación comenzará también con la presencia del valor cero de la variable del último fractal real fract_Up en el campo price, en caso de arranque inicial del EA tras inicializarse esta variable en el constructor de la clase. La siguiente condición comprobará si el precio de compra actual en el mercado ha superado el último valor real de la ruptura de fractal del día en forma de ask >= fract_Up.price; podemos decir que esta será la principal condición lógica de este método. Por último, tendremos que comprobar la señal actual de la condición si este nivel fractal ya se ha procesado. La cuestión aquí es que las señales de ruptura fractal provienen del gráfico diario, y si el precio actual de compra en el mercado ha alcanzado el valor necesario, deberíamos procesar esta señal una vez al día, ya que practicamos trading intradía, pero posicional, sin tomar posiciones abiertas simultáneamente. Si las tres condiciones se cumplen al mismo tiempo, nuestro método debería retornar true para que nuestro EA funcione con esta señal.

La implementación completa del método con su secuenciación lógica sería la siguiente:

//+------------------------------------------------------------------+
//|                         BuySignal                                |
//+------------------------------------------------------------------+
bool CFractalsSignal::BuySignal(void)
  {
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // Buy price

//--- check fractals update
   FrUp=cFractals.Upper(3);                                             // request the current value

   if(FrUp != fract_Up.price           &&                               // if the data has been updated
      FrUp != EMPTY_VALUE)                                              // skip empty value
     {
      fract_Up.price = FrUp;                                            // process the new fractal
      fract_Up.done = false;                                            // not processed
     }

//--- check the signal
   if(fract_Up.price != EMPTY_VALUE    &&                               // skip zero values
      ask            >= fract_Up.price &&                               // if the buy price is greater than or equal to the fractal
      fract_Up.done  == false)                                          // the signal has not been processed yet
     {
      return true;                                                      // generate a signal to process
     }

   return false;                                                        // otherwise false
  }

Como hemos mencionado antes, el método de obtención de una señal de compra deberá funcionar en tándem con el método de control del procesamiento de esta señal por el servidor del bróker. El método al que se llamará cuando se active una señal de compra tendrá un aspecto bastante compacto:

//+------------------------------------------------------------------+
//|                         BuyDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::BuyDone(void)
  {
   fract_Up.done = true;                                                // processed
  }

La lógica es muy sencilla, cuando llamemos a este método público pondremos una bandera de procesamiento exitoso de la señal en el campo de la última señal correspondiente de la instancia de la estructura fract_Up en el campo done. Como consecuencia, este método será llamado por el código del asesor experto principal solo cuando el servidor del bróker compruebe que la apertura de la orden se ha efectuado correctamente.

La lógica del método correspondiente para la venta se implementará de forma similar. La única diferencia es que ahora pediremos el precio Bid en lugar de Ask. Y el estado al precio actual se comprobará de la forma correspondiente no para un fractal mayor, sino para uno menor para la venta.

El método de señal de venta correspondiente tendrá la misma lógica con una corrección para la dirección:

//+------------------------------------------------------------------+
//|                         SellSignal                               |
//+------------------------------------------------------------------+
bool CFractalsSignal::SellSignal(void)
  {
   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // bid price

//--- check fractals update
   FrDn=cFractals.Lower(3);                                             // request the current value

   if(FrDn != EMPTY_VALUE        &&                                     // skip empty value
      FrDn != fract_Dn.price)                                           // if the data has been updated
     {
      fract_Dn.price = FrDn;                                            // process the new fractal
      fract_Dn.done = false;                                            // not processed
     }

//--- check the signal
   if(fract_Dn.price != EMPTY_VALUE    &&                               // skip empty value
      bid            <= fract_Dn.price &&                               // if the ask price is less than or equal to the fractal AND
      fract_Dn.done  == false)                                          // signal has not been processed
     {
      return true;                                                      // generate a signal to process
     }

   return false;                                                        // otherwise false
  }

El procesamiento de la señal de venta también será similar a la lógica de la compra, solo que el campo implementado se rellenará con la instancia de la estructura fract_Dn, que es responsable del último fractal real para la venta:

//+------------------------------------------------------------------+
//|                        SellDone                                  |
//+------------------------------------------------------------------+
void CFractalsSignal::SellDone(void)
  {
   fract_Dn.done = true;                                                // processed
  }
//+------------------------------------------------------------------+

Con esto completaremos la aplicación de nuestro método de obtención de entradas según las rupturas fractales desde el gráfico diario, y podremos proceder al ensamblar el proyecto.


Ensamblaje y pruebas del proyecto

Empezaremos a construir el proyecto conectando todos los archivos descritos anteriormente, incluyendo el código necesario al principio del archivo del punto de entrada principal del proyecto, usando el comando #include del preprocesador. Los archivos <RiskManagerAlgo.mqh>, <TradeModel.mqh>, y <CFractalsSignal.mqh> son nuestras clases personalizadas, discutidas en capítulos anteriores. Las dos entradas restantes <Indicators\BillWilliams.mqh> y <Trade\Trade.mqh> son clases estándar de terminal de código abierto, para trabajar con los fractales y las transacciones comerciales, respectivamente.

#include <RiskManagerAlgo.mqh>
#include <Indicators\BillWilliams.mqh>
#include <Trade\Trade.mqh>
#include <TradeModel.mqh>
#include <CFractalsSignal.mqh>

Para configurar el método de control de deslizamiento, introduciremos una variable entera adicional de tipo int de la clase de memoria input para permitir al usuario introducir el valor de stop loss que considere aceptable en la cantidad de puntos mínimos del instrumento:

input group "RiskManagerAlgoExpert"
input int inp_sl_in_int       = 2000;  // inp_sl_in_int - a stop loss level for a separate trade

En integraciones o implementaciones totalmente automáticas más detalladas, al usar nuestro método de control de deslizamiento, será mejor implementar la transmisión de este parámetro no a través de los parámetros de entrada del usuario, sino a través del retorno de un valor de stop desde la clase encargada de establecer un stop técnico, o un stop calculado desde la clase que trabaja con la volatilidad. En esta implementación, dejaremos una opción adicional para que el usuario vaya más allá y cambie esta configuración según su estrategia particular.

Ahora declararemos los punteros necesarios a las clases de gestor de riesgos, el trabajo con posiciones y fractales con la siguiente entrada:

RiskManagerAlgo *RMA;                                                   // risk manager
CTrade          *cTrade;                                                // trade
CFractalsSignal *cFract;                                                // fractal

Inicializaremos los punteros en la función OnInit() de nuestro manejador de eventos de inicialización del EA:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   RMA = new RiskManagerAlgo();                                         // algorithmic risk manager

//---
   cFract =new CFractalsSignal();                                       // fractal signal

//--- trade class
   cTrade=new CTrade();                                                 // create trade instance
   if(CheckPointer(cTrade)==POINTER_INVALID)                            // if instance not created,
     {
      Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!");   // notify
     }
   cTrade.SetTypeFillingBySymbol(Symbol());                             // fill type for the symbol
   cTrade.SetDeviationInPoints(1000);                                   // deviation
   cTrade.SetExpertMagicNumber(123);                                    // magic number
   cTrade.SetAsyncMode(false);                                          // asynchronous method

//---
   return(INIT_SUCCEEDED);
  }

Al configurar el objeto CTrade, especificaremos en el método SetTypeFillingBySymbol() como parámetro el símbolo del instrumento actual sobre el que se ejecutará el asesor experto. Luego retornaremos el símbolo del instrumento actual sobre el que se iniciará el asesor experto, utilizando el método predefinido Symbol() del terminal. En el método SetDeviationInPoints(), especificaremos un valor con un margen como desviación máxima aceptable del precio solicitado. Como este parámetro no es tan importante para nuestro análisis, no lo pondremos en los parámetros de entrada, sino que lo dejaremos como hardcode. Después escribiremos el número mágico estáticamente para todas las posiciones abiertas en el EA.

En el destructor escribiremos el borrado del objeto y el vaciado de memoria según el puntero si el este es válido:

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // if there is an instance,
     {
      delete cTrade;                                                    // delete
     }

//---
   if(CheckPointer(cFract)!=POINTER_INVALID)                            // if an instance is found,
     {
      delete cFract;                                                    // delete
     }

//---
   if(CheckPointer(RMA)!=POINTER_INVALID)                               // if an instance is found,
     {
      delete RMA;                                                       // delete
     }
  }

Ahora describiremos el cuerpo principal de nuestro EA en el punto de entrada del evento de llegada de nuevo tick OnTick(). En primer lugar, necesitaremos ejecutar el método principal de monitorización de eventos ContoMonitor() de la clase básica RiskManagerBase, que no hemos sobreescrito en el heredero, desde la instancia heredera de la siguiente forma:

   RMA.ContoMonitor();                                                  // run the risk manager

El siguiente método llamará a SlippageCheck(), que, como hemos mencionado antes, en esta implementación procesará cada nuevo tick y comprobará que las posiciones abiertas cumplan con los valores de riesgo previstos y su valor real en relación con el stop loss establecido:

   RMA.SlippageCheck();                                                 // check slippage

En las peculiaridades de este método hay que señalar que, como nuestra herramienta de toma de decisiones fractal no implica una aplicación profunda y sirve más bien como demostración de la capacidad del gestor de riesgos, este no fijará stops, sino que simplemente cerrará posiciones al final de la jornada comercial, por lo que este método resolverá todas las transacciones que se le transfieran. Para que este método funcione correctamente en su implementación, podremos enviar órdenes al servidor del bróker solo con un valor de stop loss distinto de cero.

A continuación, deberemos actualizar los datos del indicador de ruptura fractal a través de nuestra clase personalizada CFractalsSignal utilizando el método público Process() con la siguiente entrada:

   cFract.Process();                                                    // start the fractal process

Ahora que todos los métodos de los eventos del estado de los datos de todas las clases se considerarán en el código; ahora pasaremos al bloque de monitorización de la aparición de señales de entrada y colocación de órdenes. La comprobación de las señales de compra y de venta se separará del mismo modo que se separan los métodos correspondientes de nuestra clase de herramienta de decisión comercial CFractalsSignal. En primer lugar, describiremos la comprobación de la compra a través de las dos condiciones siguientes como en el código de abajo:

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // if there is a buy signal

Primero comprobaremos si existe una señal de compra a través del método BuySignal() de una instancia de la clase CFractalsSignal, y si da esta señal, comprobaremos si existe permiso simultáneo del gestor de riesgos para ajustar el spread al valor permitido por el usuario a través del método SpreadMonitor(). Luego transmitiremos el parámetro de entrada de usuario inp_sl_in_int como único parámetro al método SpreadMonitor().

Si se cumplen las dos condiciones descritas anteriormente, procedemos a colocar órdenes en la siguiente construcción lógica simplificada:

      if(cTrade.Buy(0.1))                                               // if Buy executed,
        {
         cFract.BuyDone();                                              // the signal has been processed
         Print("Buy has been done");                                    // notify
        }
      else                                                              // if Buy not executed,
        {
         Print("Error: buy");                                           // notify
        }

A continuación, colocaremos una orden utilizando el método Buy() de la instancia de la clase CTrade, transmitiendo como parámetro un valor de lote igual a 0,1. Para una evaluación más objetiva de los resultados del gestor de riesgos, no modificaremos este valor para "suavizar" las estadísticas del parámetro de volumen. Esto significará que todas las entradas tendrán el mismo peso en las estadísticas de nuestro EA.

Si el método Buy() haya funcionado como debería, es decir, si hemos obtenido una respuesta positiva del bróker y se ha abierto la transacción, llamaremos inmediatamente al método BuyDone() para informar a nuestra clase CFractalsSignal de que la señal que ha recibido ha funcionado correctamente y no necesitamos volver a enviar una señal a este precio. Si la colocación de una orden de compra falla, informaremos de ello en el registro del asesor experto y no llamaremos al método de procesamiento de señales con éxito, por la posibilidad de un posterior intento de reapertura.

Luego implementaremos la misma lógica en la dirección de la venta, solo que en la secuencia de código llamaremos a los métodos correspondientes a la venta.

El bloque de órdenes de cierre al final del día comercial lo tomaremos sin cambios del artículo anterior en la misma forma:

   MqlDateTime time_curr;                                               // current time structure
   TimeCurrent(time_curr);                                              // request current time

   if(time_curr.hour >= 23)                                             // if end of day
     {
      RMA.AllOrdersClose();                                             // close all positions
     }

La tarea principal de este código será cerrar todas las posiciones abiertas a las 23 horas según la última hora conocida del servidor, porque, repito, estamos implementando la lógica comercial intradía sin traslado de posiciones por la noche (overnights). La lógica de este código se describió con más detalle en el último artículo. Si un tráder quiere dejar posiciones durante la noche, podrá simplemente comentar este bloque de código en el asesor experto o excluirlo por completo.

Además, no deberemos olvidarnos de mostrar el estado actual de los datos del gestor de riesgos a través de la función predefinida Comment() del terminal transmitiéndole el método Message() de la clase gestor de riesgos:

   Comment(RMA.Message());                                              // display the data state in a comment
En su forma final, el código del nuevo procesamiento de eventos de ticks tendrá el aspecto siguiente:
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMA.ContoMonitor();                                                  // run the risk manager

   RMA.SlippageCheck();                                                 // check slippage

   cFract.Process();                                                    // start the fractal process

   if(cFract.BuySignal() &&
      RMA.SpreadMonitor(inp_sl_in_int))                                 // if there is a buy signal
     {
      if(cTrade.Buy(0.1))                                               // if Buy executed,
        {
         cFract.BuyDone();                                              // the signal has been processed
         Print("Buy has been done");                                    // notify
        }
      else                                                              // if Buy not executed,
        {
         Print("Error: buy");                                           // notify
        }
     }

   if(cFract.SellSignal())                                              // if there is a sell signal
     {
      if(cTrade.Sell(0.1))                                              // if sell executed,
        {
         cFract.SellDone();                                             // the signal has been processed
         Print("Sell has been done");                                   // notify
        }
      else                                                              // if sell failed,
        {
         Print("Error: sell");                                          // notify
        }
     }

   MqlDateTime time_curr;                                               // current time structure
   TimeCurrent(time_curr);                                              // request current time

   if(time_curr.hour >= 23)                                             // if end of day
     {
      RMA.AllOrdersClose();                                             // close all positions
     }

   Comment(RMA.Message());                                              // display the data state in a comment
  }
//+------------------------------------------------------------------+

Ahora podremos montar el proyecto y probarlo con datos históricos. Para el ejemplo de prueba, tomaremos el par USDJPY y lo probaremos durante 2023 en el optimizador de estrategias con la siguiente configuración de entrada (ver Tabla 2):

Nombre del ajuste Valor del ajuste
 1  Asesor  RiskManagerAlgo.ex5
 2  Símbolo  USDJPY
 3  Periodo del gráfico  M15
 4  Intervalo  2023.01.01 - 2023.12.31
 5  Adelante  no
 6  Retrasos  Sin retrasos, ejecución ideal
 7  Modelado  Todos los tics
 8  Depósito inicial  10 000 usd
 9  Apalancamiento  1:100
 10  Optimización  Lenta (iteración completa de parámetros) 

Tabla 1. Configuración del simulador de estrategias para el asesor experto RiskManagerAlgo

Seleccionaremos los parámetros para optimizar el simulador de estrategias según el principio del paso más pequeño con el fin de acortar el tiempo de entrenamiento, pero al mismo tiempo, también poder trazar la misma dependencia de la que hablamos en el último artículo, y que el gestor de riesgos permita mejorar los resultados comerciales de estrategias incluso rentables. Los parámetros de entrada de la optimización del simulador de estrategias MetaTrader 5 se presentan en la Tabla 2:


Nombre del parámetro Inicio Paso Parada
 1  inp_riskperday  0.1  0.5  1
 2  inp_riskperweek  0.5  0.5  3
 3  inp_riskpermonth  2  1  8
 4  inp_plandayprofit  0.1  0.5  3
 5  dayProfitControl  false  -  true

Tabla 2 Parámetros del optimizador de estrategias del asesor experto RiskManagerAlgo

Los parámetros de optimización no incluyen aquellos parámetros que no dependen directamente de la eficacia de la estrategia y no son significativos para el modelado en el simulador. Por ejemplo, inp_slippfits dependerá principalmente de la "limpieza" (no confundir con "frecuencia") de la ejecución de órdenes por parte del bróker, más que de nuestras entradas. El parámetro inp_spreadfits dependerá directamente del valor del spread, que varía en función de muchos factores, entre otros, el tipo de cuenta del bróker, la hora de publicación de noticias importantes, etc. Cada cual puede optimizar estos parámetros para sí mismo, basándose en los supuestos del bróker prioritario con el que negocie.

Los resultados de la optimización se muestran en las figuras 2 y 3.

Figura 2: Gráfico con los resultados de la optimización del asesor experto RiskManagerAlgo

Figura 2. Gráfico con los resultados de la optimización del asesor experto RiskManagerAlgo

El gráfico de los resultados de la optimización muestra que la mayoría de los resultados de las ejecuciones de los resultados de la estrategia se encuentran en la zona de esperanza matemática positiva. Esto se debe a la lógica de la estrategia, cuando un gran volumen de posiciones de los participantes en el mercado se concentra en rupturas fractales fuertes y, como consecuencia, la prueba de estas rupturas por parte del precio provoca un aumento de la actividad del mercado, lo cual ofrece impulsos al instrumento.

Para confirmar la tesis de la presencia de impulso en los niveles fractales de trading, podemos comparar la mejor y la peor iteración en este conjunto del optimizador de estrategias comerciales con los parámetros anteriores. Y nuestro gestor de riesgos será precisamente lo que nos permita normalizar este impulso en relación con los riesgos para entrar en una posición. Para comprender mejor la esencia de la función que desempeña el gestor de riesgos en la normalización del riesgo en relación con el impulso del mercado, podemos ver la relación entre los parámetros del valor diario del riesgo y el beneficio diario previsto en la Figura 3.

Figura 3: Esquema de dependencia entre los parámetros de riesgo diario y los parámetros de beneficio diario previsto

Figura 3. Esquema de dependencia entre los parámetros de riesgo diario y los parámetros de beneficio diario previsto

En la figura 3 se observa un giro en el valor del parámetro de riesgo diario, en el que la eficacia de la estrategia de rupturas fractales aumenta primero cuando aumenta este valor, y luego empieza a disminuir. Este será el punto extremo (ruptura de la función) de nuestro modelo para estos dos parámetros. En un momento determinado, también la propia existencia de ese momento en el modelo (cuando el aumento del valor del parámetro de riesgo para un día empieza a disminuir en lugar de aumentar el beneficio) demuestra que el impulso del mercado se hace menor en relación con los riesgos que hemos introducido en nuestro modelo. Existe un claro exceso del coste del riesgo en relación con la rentabilidad esperada. Esta ruptura resulta claramente visible en el gráfico y no requiere de matemáticas adicionales para calcular el extremo, como las derivadas de las funciones.

Ahora, para que podamos ver realmente si hay impulso del mercado, vamos a ver los parámetros de la mejor y la peor iteración de los resultados de optimización de nuestro EA por separado, con el fin de evaluar la relación riesgo/rentabilidad. Obviamente, si el beneficio esperado a la entrada es varias veces superior al riesgo previsto, los impulsos se habrán producido como un movimiento de retroceso unidireccional del instrumento en una dirección. Si en nuestros niveles el valor del riesgo es igual o inferior al rendimiento, ahí no habrá impulsos.

La ruptura del valor de riesgo en el día será obviamente el punto óptimo de optimización según la mejor pasada con los siguientes parámetros presentados en la Tabla 3, como el optimizador nos ha mostrado:


Nombre del parámetro Valor del parámetro
 1  inp_riskperday  0.6
 2  inp_riskperweek  3
 3  inp_riskpermonth  8
 4  inp_plandayprofit  3.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

Tabla 3 Parámetros de la mejor pasada del optimizador de estrategias para el asesor experto RiskManagerAlgo

Podemos ver que el beneficio previsto de 3,1 supera en cinco veces el valor del coste de riesgo necesario para obtenerlo con un valor de 0,6. Dicho de otro modo, arriesgamos el 0,6% del depósito y obtenemos el 3,1%. Esto indica claramente la presencia de impulsos de precios en los niveles diarios de las rupturas fractales, que dan una esperanza matemática positiva.

El gráfico de ejecución de la mejor iteración se muestra en la Figura 4.

Figura 4: Gráfico de ejecución de la mejor iteración del optimizador de estrategias

Figura 4. Gráfico de ejecución de la mejor iteración del optimizador de estrategias

A partir del gráfico de crecimiento de los depósitos, podemos ver que la estrategia que utiliza el control de riesgos forma un gráfico bastante suave en el que cada retroceso posterior no sobrescribe el valor mínimo del retroceso anterior, lo cual nos indica que es necesario aplicar la normalización del riesgo y nuestro gestor de riesgos para garantizar la seguridad de la inversión. Ahora, para confirmar por fin la tesis de la presencia del impulso y la necesidad de normalizar los riesgos en relación con él, veamos los resultados de la peor ejecución del optimizador.

A partir de los datos de la Tabla 4, podemos estimar la rentabilidad del riesgo en la peor pasada con los siguientes parámetros:


Nombre del parámetro
Valor del parámetro
 1  inp_riskperday  1.1
 2  inp_riskperweek  0.5
 3  inp_riskpermonth  2
 4  inp_plandayprofit  0.1
 5  dayProfitControl  true
 6  inp_slippfits  2
 7  inp_spreadfits  2
 8  inp_risk_per_deal  100
 9  inp_sl_in_int  2000

Tabla 4 Parámetros de la mejor pasada del optimizador de estrategias para el asesor experto RiskManagerAlgo

Los datos de la Tabla 4 muestran que la peor iteración se sitúa exactamente en el plano de la figura 3 en el que no normalizamos el riesgo en relación con el impulso. Es decir, estamos en una zona en la que el riesgo marginal no ofrece el rendimiento necesario para su amortización, por lo que no utilizamos al máximo el riesgo potencial recibido, mientras gastamos una gran cantidad de depósito en estas entradas.

En la figura 5 se muestra un gráfico de la peor iteración:

Figura 5: Gráfico de la peor iteración del optimizador de estrategias

Figura 5. Gráfico de ejecución de la peor iteración del optimizador de estrategias

El gráfico 5 muestra que un desequilibrio entre el riesgo y la rentabilidad potencial puede provocar grandes pérdidas en la cuenta, tanto en el balance como en los fondos. Basándonos en los resultados de optimización presentados en este capítulo del artículo, podemos concluir que el uso de un gestor de riesgos es obligatorio para controlar los riesgos. Asimismo, resulta esencial seleccionar riesgos lógicamente justificados en relación con las capacidades de nuestra estrategia comercial. Pasemos ahora a las conclusiones generales de nuestro artículo.


Conclusión

Partiendo de los materiales, modelos, argumentos y cálculos presentados en el artículo, podemos extraer las siguientes conclusiones. No basta con encontrar una estrategia de inversión o un algoritmo rentables para que nuestro capital aumente con eficiencia y se mantenga seguro, incluso así podemos perder dinero si no aplicamos el riego de forma razonable. Incluso con una estrategia rentable, la clave de la eficacia y la seguridad al trabajar en los mercados financieros consiste en atenerse a la gestión de riesgos. Un requisito previo para la eficacia y la seguridad de un funcionamiento estable a largo plazo es la normalización del riesgo adaptándose a las capacidades de la estrategia utilizada. Además, le recomiendo encarecidamente no negociar en cuentas reales con el gestor de riesgos desactivado y sin stop loss establecidos en cada posición abierta. 

Obviamente, no todos los riesgos de los mercados financieros pueden controlarse y minimizarse, pero siempre deberán medirse en relación con los rendimientos esperados. Siempre es posible controlar los riesgos normalizados con la ayuda de un gestor de riesgos, y en este artículo, como en los anteriores, le recomiendo encarecidamente aplicar los principios de la gestión de capital y riesgos a su trabajo.

Si practica un trading asistemático sin control de riesgos, podrá hacer una estrategia perdedora de cualquier estrategia rentable, al tiempo que de casi cualquier estrategia perdedora, podrá hacer una rentable si utilizas un gestor de riesgos. Si los materiales presentados en este artículo ayudan a salvar aunque sea un depósito de la pérdida total, consideraré que el trabajo no se ha hecho en vano.

Le agradeceré sus comentario a este artículo, especialmente sobre temas que puedan seguir interesándole. Que la tendencia os acompañe, amigos.


Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14634

Archivos adjuntos |
IShortStopLoss.mqh (1.21 KB)
RiskManagerAlgo.mq5 (10.53 KB)
RiskManagerAlgo.mqh (16.59 KB)
RiskManagerBase.mqh (61.49 KB)
TradeModel.mqh (12.99 KB)
CFractalsSignal.mqh (13.93 KB)
Uso del algoritmo de aprendizaje automático PatchTST para predecir la acción del precio durante las próximas 24 horas Uso del algoritmo de aprendizaje automático PatchTST para predecir la acción del precio durante las próximas 24 horas
En este artículo, aplicamos un algoritmo de red neuronal relativamente complejo lanzado en 2023 llamado PatchTST para predecir la acción del precio durante las próximas 24 horas. Utilizaremos el repositorio oficial, haremos ligeras modificaciones, entrenaremos un modelo para EURUSD y lo aplicaremos para realizar predicciones futuras tanto en Python como en MQL5.
Características del Wizard MQL5 que debe conocer (Parte 25): Pruebas y operaciones en múltiples marcos temporales Características del Wizard MQL5 que debe conocer (Parte 25): Pruebas y operaciones en múltiples marcos temporales
Las estrategias que se basan en múltiples marcos de tiempo no se pueden probar en los Asesores Expertos ensamblados por defecto debido a la arquitectura de código MQL5 utilizada en las clases de ensamblaje. Exploramos una posible solución a esta limitación para las estrategias que buscan utilizar múltiples marcos temporales en un estudio de caso con la media móvil cuadrática.
Redes neuronales: así de sencillo (Parte 89): Transformador de descomposición de la frecuencia de señal (FEDformer) Redes neuronales: así de sencillo (Parte 89): Transformador de descomposición de la frecuencia de señal (FEDformer)
Todos los modelos de los que hemos hablado anteriormente analizan el estado del entorno como una secuencia temporal. Sin embargo, las propias series temporales también pueden representarse como características de frecuencia. En este artículo, presentaremos un algoritmo que utiliza las características de frecuencia de una secuencia temporal para predecir los estados futuros.
Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 1): Creación del panel Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 1): Creación del panel
Este artículo explora los pasos fundamentales en la elaboración e implementación de un panel de Interfaz Gráfica de Usuario (GUI) utilizando MetaQuotes Language 5 (MQL5). Los paneles de utilidades personalizados mejoran la interacción del usuario en la negociación simplificando las tareas habituales y visualizando la información esencial de la negociación. Al crear paneles personalizados, los operadores pueden agilizar su flujo de trabajo y ahorrar tiempo durante las operaciones.