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

Gestor de riesgos para el trading manual

MetaTrader 5Ejemplos | 3 septiembre 2024, 10:16
646 0
Aleksandr Seredin
Aleksandr Seredin

Contenido


Introducción

Hola a todos. En este artículo seguiremos hablando de la metodología de gestión de riesgos. En el artículo anterior Equilibrio de riesgos en la negociación simultánea de varios instrumentos comerciales, hablamos de los conceptos básicos del riesgo. Ahora implementaremos desde cero la clase básica de Gestor de Riesgo para operar con seguridad. También veremos cómo la limitación de riesgos en los sistemas de negociación afecta a la eficacia de las estrategias comerciales.

"Risk Manager" fue la primera clase que escribí en 2019, poco después de aprender los conceptos básicos de programación. En ese momento, comprendí por mi propia experiencia que el estado psicológico de un trader influye enormemente en la efectividad del trading, especialmente en lo que respecta a la "consistencia" y la "imparcialidad" en la toma de decisiones de trading. Apostar, realizar transacciones impulsivas y aumentar los riesgos para intentar cubrir las pérdidas lo más rápido posible pueden agotar cualquier cuenta, incluso si utilizas una estrategia de trading efectiva que ha mostrado muy buenos resultados en las pruebas.

El objetivo de este artículo es demostrar que el control de riesgos mediante un gestor de riesgos aumenta su eficacia y fiabilidad. Para confirmar esta tesis, crearemos desde cero una sencilla clase base de gestor de riesgos para la negociación manual y la probaremos utilizando una estrategia de ruptura fractal muy simple.


Definir las funciones

Al implementar nuestro algoritmo específicamente para la negociación manual, sólo implementaremos el control sobre los límites de riesgo temporales para el día, la semana y el mes. Una vez que el importe de las pérdidas reales alcanza o supera los límites establecidos por el usuario, el EA debe cerrar automáticamente todas las posiciones abiertas e informar al usuario de la imposibilidad de seguir operando. Cabe señalar aquí que la información será puramente «de carácter consultivo», se mostrará en la línea de comentarios en la esquina inferior izquierda del gráfico con el EA en ejecución. Esto se debe a que estamos creando un gestor de riesgos específicamente para el trading manual, por lo que, "si es absolutamente necesario", el usuario puede eliminar este EA del gráfico en cualquier momento y continuar operando. Sin embargo, no recomendaría hacer esto, porque si el mercado va en tu contra, es mejor volver a operar al día siguiente y evitar grandes pérdidas. En lugar de eso, intenta analizar qué salió mal en tu trading manual. Si integras esta clase en tu trading algorítmico, deberás implementar la restricción de envío de órdenes cuando se alcance el límite y, preferiblemente, integrar esta clase directamente en la estructura del EA. Hablaremos de esto con más detalle un poco más adelante.


Parámetros de entrada y constructor de clase

Hemos decidido que sólo aplicaremos el control de riesgos por periodos y el criterio de alcanzar la cotización diaria de beneficios. Para ello, introducimos varias variables del tipo double con modificador de clase de memoria input para que el usuario introduzca manualmente los valores de riesgo como porcentaje del depósito para cada periodo de tiempo, así como el porcentaje de beneficio diario objetivo para bloquear los beneficios. Para indicar el control del beneficio diario objetivo, introducimos una variable adicional del tipo bool para la capacidad de activar/desactivar esta funcionalidad si el trader desea considerar cada entrada por separado y está seguro de que no existe correlación entre los instrumentos seleccionados. Este tipo de variable de conmutación también se denomina «bandera» (flag). Declaremos el siguiente código a nivel global. Para mayor comodidad, lo hemos «envuelto» previamente en un bloque con nombre utilizando la palabra clave group.

input group "RiskManagerBaseClass"
input double inp_riskperday    = 1;          // risk per day as a percentage of deposit
input double inp_riskperweek   = 3;          // risk per week
input double inp_riskpermonth  = 9;          // risk per month
input double inp_plandayprofit = 3;          // target daily profit

input bool dayProfitControl = true;          // whether to close positions after reaching daily profit

Las variables declaradas se inicializan con valores por defecto de acuerdo con la siguiente lógica. Empezaremos por el riesgo diario, ya que esta clase funciona mejor para la negociación intradía, pero también puede utilizarse para la negociación y la inversión a medio plazo. Obviamente, si opera a medio plazo o como inversor, entonces no tiene sentido que controle el riesgo intradía y puede establecer los mismos valores para los riesgos diarios y semanales. Además, si sólo realiza inversiones a largo plazo, puede establecer todos los valores límite iguales a una detracción mensual. Aquí veremos la lógica de los parámetros por defecto para la negociación intradía.

Decidimos que nos sentiríamos cómodos operando con un riesgo diario del 1% del depósito. Si se supera el límite diario, cerramos el terminal hasta mañana. A continuación, definimos el límite semanal del siguiente modo. Normalmente hay 5 días de negociación en una semana, lo que significa que si tenemos 3 días de pérdidas seguidos, dejamos de negociar hasta el comienzo de la semana siguiente. Simplemente porque lo más probable es que no haya entendido el mercado esta semana, o que algo haya cambiado y si sigue operando, acumulará una pérdida tan grande durante este periodo que no podrá cubrirla ni a costa de la semana siguiente. Una lógica similar se aplica a la fijación de un límite mensual cuando se negocia intradía. Aceptamos la condición de que si tuvimos 3 semanas no rentables en un mes, es mejor no operar la cuarta, ya que llevará mucho tiempo «mejorar» la curva de rendimiento a expensas de los periodos futuros. Tampoco queremos "asustar" a los inversionistas con una gran pérdida en un solo mes.

Fijamos el tamaño del objetivo de beneficio diario en función del riesgo diario, teniendo en cuenta las características de su sistema de negociación. Lo que hay que tener en cuenta. En primer lugar, si negocia con instrumentos correlacionados, la frecuencia con la que su sistema de negociación da señales de entrada, si negocia con proporciones fijas entre stop loss y take profit para cada operación individual, o el tamaño del depósito. Me gustaría señalar que NO RECOMIENDO EN ABSOLUTO operar sin un stop loss y sin un gestor de riesgos al mismo tiempo. En este caso, perder el depósito es sólo cuestión de tiempo. Por lo tanto, o bien fijamos stops para cada operación por separado, o bien utilizamos un gestor de riesgos para limitar el riesgo por periodos. En nuestro ejemplo actual de parámetros por defecto, establezco las condiciones para el beneficio diario de 1 a 3 en relación con el riesgo diario. También es mejor utilizar estos parámetros junto con el ajuste obligatorio de riesgo-rentabilidad para CADA operación a través de la relación entre stop loss y take profit, también de 1 a 3 (el take profit es mayor que el stop loss).

La estructura de nuestros límites puede describirse del siguiente modo.

Figura 1. Estructura de límites

Figura 1. Estructura de límites

A continuación, declaramos nuestro tipo de datos personalizado RiskManagerBase utilizando la palabra clave class. Los parámetros de entrada deberán almacenarse en nuestra clase RiskManagerBase personalizada. Dado que nuestros parámetros de entrada se miden en porcentajes, mientras que los límites se rastrean en la moneda del depósito, necesitamos introducir varios campos correspondientes de tipo double con el modificador de acceso protected a nuestra clase personalizada. 

protected:

   double    riskperday,                     // risk per day as a percentage of deposit
             riskperweek,                    // risk per week as a percentage of deposit
             riskpermonth,                   // risk per month as a percentage of deposit
             plandayprofit                   // target daily profit as a percentage of deposit
             ;

   double    RiskPerDay,                     // risk per day in currency
             RiskPerWeek,                    // risk per week in currency
             RiskPerMonth,                   // risk per month in currency
             StartBalance,                   // account balance at the EA start time, in currency
             StartEquity,                    // account equity at the limit update time, in currency
             PlanDayEquity,                  // target account equity value per day, in currency
             PlanDayProfit                   // target daily profit, in currency
             ;

   double    CurrentEquity,                  // current equity value
             CurrentBallance;                // current balance

Para la comodidad del cálculo de los límites de riesgo por periodo en la moneda de depósito, en base a los parámetros de entrada, declararemos el método RefreshLimits() dentro de nuestra clase, también con el modificador de acceso protected. Vamos a describir este método fuera de la clase de la siguiente manera. Proporcionaremos para el futuro el tipo de valor de retorno del tipo bool por si necesitamos ampliar nuestro método con la capacidad de comprobar la corrección de los datos obtenidos. Por ahora, describimos el método de la forma siguiente.

//+------------------------------------------------------------------+
//|                        RefreshLimits                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::RefreshLimits(void)
  {
   CurrentEquity    = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // request current equity value
   CurrentBallance  = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // request current balance

   StartBalance     = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2);  // set start balance
   StartEquity      = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2);   // request current equity value

   PlanDayProfit    = NormalizeDouble(StartEquity * plandayprofit/100,2);     // target daily profit, in currency
   PlanDayEquity    = NormalizeDouble(StartEquity + PlanDayProfit/100,2);     // target equity, in currency

   RiskPerDay       = NormalizeDouble(StartEquity * riskperday/100,2);        // risk per day in currency
   RiskPerWeek      = NormalizeDouble(StartEquity * riskperweek/100,2);       // risk per week in currency
   RiskPerMonth     = NormalizeDouble(StartEquity * riskpermonth/100,2);      // risk per month in currency

   return(true);
  }

Una forma conveniente es llamar a este método en código cada vez que necesitemos recalcular valores límite al cambiar periodos de tiempo, así como al cambiar inicialmente los valores de los campos al llamar al constructor de la clase. Escribimos el siguiente código en el constructor de la clase para inicializar los valores iniciales de los campos.

//+------------------------------------------------------------------+
//|                        RiskManagerBase                           |
//+------------------------------------------------------------------+
RiskManagerBase::RiskManagerBase()
  {
   riskperday         = inp_riskperday;                                 // set the value for the internal variable
   riskperweek        = inp_riskperweek;                                // set the value for the internal variable
   riskpermonth       = inp_riskpermonth;                               // set the value for the internal variable
   plandayprofit      = inp_plandayprofit;                              // set the value for the internal variable

   RefreshLimits();                                                     // update limits
  }

Después de decidir la lógica de los parámetros de entrada y el estado inicial de los datos para nuestra clase, pasamos a implementar la contabilidad de los límites.


Trabajar con periodos límite de riesgo

Para trabajar con periodos límite de riesgo, necesitaremos una variable adicional con el tipo de acceso protected. En primer lugar, vamos a declarar nuestra propia bandera para cada periodo en forma de variables de tipo bool, que almacenarán datos sobre el alcance de los límites de riesgo establecidos, así como la bandera principal, que informará sobre la posibilidad de continuar operando sólo si todos los límites están disponibles al mismo tiempo. Esto es necesario para evitar la situación en la que ya se ha superado el límite mensual, pero sigue existiendo un límite diario y, por tanto, se permite la negociación. Esto limitará la negociación cuando se alcance cualquier límite de tiempo antes del siguiente periodo. También necesitaremos variables del mismo tipo para controlar el beneficio diario y el inicio de un nuevo día de negociación. Además añadiremos campos de tipo double para almacenar información sobre las pérdidas y ganancias reales de cada periodo: día, semana y mes. Además, proporcionaremos valores separados para los swaps y las comisiones en las operaciones comerciales.

   bool              RiskTradePermission;    // general variable - whether opening of new trades is allowed
   bool              RiskDayPermission;      // flag prohibiting trading if daily limit is reached
   bool              RiskWeekPermission;     // flag to prohibit trading if daily limit is reached
   bool              RiskMonthPermission;    // flag to prohibit trading if monthly limit is reached

   bool              DayProfitArrive;        // variable to control if daily target profit is achieved
   bool              NewTradeDay;            // variable for a new trading day

   //--- actual limits
   double            DayorderLoss;           // accumulated daily loss
   double            DayorderProfit;         // accumulated daily profit
   double            WeekorderLoss;          // accumulated weekly loss
   double            WeekorderProfit;        // accumulated weekly profit
   double            MonthorderLoss;         // accumulated monthly loss
   double            MonthorderProfit;       // accumulated monthly profit
   double            MonthOrderSwap;         // monthly swap
   double            MonthOrderCommis;       // monthly commission

En concreto, no incluimos los gastos por comisiones y swaps en las pérdidas de los periodos correspondientes, para poder separar en el futuro las pérdidas derivadas de la herramienta de toma de decisiones de las pérdidas relacionadas con los requisitos de comisiones y swaps de los distintos intermediarios. Ahora que hemos declarado los campos correspondientes de nuestra clase, pasemos a controlar el uso de los límites.


Control del uso de los límites

Para controlar el uso real de los límites, tendremos que gestionar los eventos asociados al inicio de cada nuevo periodo, así como los eventos asociados a la aparición de operaciones de negociación finalizadas. Para llevar correctamente la cuenta de los límites realmente utilizados, anunciaremos el método interno ForOnTrade() en el área de acceso protected de nuestra clase.

En primer lugar, tendremos que proporcionar variables en el método para tener en cuenta la hora actual, así como la hora de inicio del día, la semana y el mes. Para estos fines, utilizaremos un tipo de datos predefinido especial del tipo de estructura struct en el formato MqlDateTime. Los inicializaremos inmediatamente con la hora terminal actual de la siguiente forma.

   MqlDateTime local, start_day, start_week, start_month;               // create structure to filter dates
   TimeLocal(local);                                                    // fill in initially
   TimeLocal(start_day);                                                // fill in initially
   TimeLocal(start_week);                                               // fill in initially
   TimeLocal(start_month);                                              // fill in initially

Tenga en cuenta que para inicializar inicialmente la hora actual, utilizamos la función predefinida TimeLocal() en lugar de TimeCurrent() porque la primera utiliza la hora local, y la segunda toma la hora del último tick recibido del broker, lo que puede provocar una contabilización incorrecta de los límites debido a la diferencia de husos horarios entre distintos brokers. A continuación, tenemos que restablecer la hora de inicio de cada periodo para obtener los valores de fecha de inicio de cada uno de ellos. Para ello, accederemos a los campos públicos de nuestras estructuras del siguiente modo.

//--- reset to have the report from the beginning of the period
   start_day.sec     = 0;                                               // from the day beginning
   start_day.min     = 0;                                               // from the day beginning
   start_day.hour    = 0;                                               // from the day beginning

   start_week.sec    = 0;                                               // from the week beginning
   start_week.min    = 0;                                               // from the week beginning
   start_week.hour   = 0;                                               // from the week beginning

   start_month.sec   = 0;                                               // from the month beginning
   start_month.min   = 0;                                               // from the month beginning
   start_month.hour  = 0;                                               // from the month beginning

Para obtener correctamente los datos de la semana y el mes, necesitamos definir la lógica para encontrar el principio de la semana y del mes. En el caso de un mes, todo es bastante sencillo, sabemos que cada mes empieza el primer día. Tratar con una semana es un poco más complicado porque no hay un punto de notificación específico y la fecha cambiará cada vez. Aquí podemos utilizar el campo especial day_of_week de la estructura MqlDateTime. Permite obtener el número del día de la semana a partir de la fecha actual comenzando por cero. Conociendo este valor, podemos averiguar fácilmente la fecha de inicio de la semana actual de la siguiente manera.

//--- determining the beginning of the week
   int dif;                                                             // day of week difference variable
   if(start_week.day_of_week==0)                                        // if this is the first day of the week
     {
      dif = 0;                                                          // then reset
     }
   else
     {
      dif = start_week.day_of_week-1;                                   // if not the first, then calculate the difference
      start_week.day -= dif;                                            // subtract the difference at the beginning of the week from the number of the day
     }

//---month
   start_month.day         = 1;                                         // everything is simple with the month

Ahora que tenemos las fechas exactas de inicio de cada periodo en relación con el momento actual, podemos pasar a solicitar datos históricos sobre las transacciones realizadas en la cuenta. Inicialmente, tendremos que declarar las variables necesarias para contabilizar los pedidos cerrados y restablecer los valores de las variables en las que se recogerán los resultados financieros de las transacciones para cada periodo seleccionado.

//---
   uint     total  = 0;                                                 // number of selected trades
   ulong    ticket = 0;                                                 // order number
   long     type;                                                       // order type
   double   profit = 0,                                                 // order profit
            commis = 0,                                                 // order commission
            swap   = 0;                                                 // order swap

   DayorderLoss      = 0;                                               // daily loss without commission
   DayorderProfit    = 0;                                               // daily profit
   WeekorderLoss     = 0;                                               // weekly loss without commission
   WeekorderProfit   = 0;                                               // weekly profit
   MonthorderLoss    = 0;                                               // monthly loss without commission
   MonthorderProfit  = 0;                                               // monthly profit
   MonthOrderCommis  = 0;                                               // monthly commission
   MonthOrderSwap    = 0;                                               // monthly swap

Solicitaremos datos históricos de órdenes cerradas a través de la función predefinida del terminal HistorySelect(). Los parámetros de esta función utilizarán las fechas que recibimos anteriormente para cada periodo. Para ello, tendremos que llevar nuestra variable MqlDateTime al tipo requerido por los parámetros HistorySelect() de la función, que es datetime. Para ello, utilizaremos la función predefinida del terminal StructToTime(). Solicitaremos los datos sobre las transacciones de la misma manera, sustituyendo los valores necesarios para el principio y el final del periodo requerido.

Después de cada llamada de la función HistorySelect(), necesitamos obtener el número de pedidos seleccionados usando la función del terminal predefinida HistoryDealsTotal() y poner este valor en nuestra variable local total. Tras obtener el número de operaciones cerradas, podemos organizar un bucle con el operador for, solicitando el número de cada orden a través de la función predefinida del terminal HistoryDealGetTicket(). Esto nos permitirá acceder a los datos de cada pedido. Accederemos a los datos de cada orden utilizando las funciones predefinidas del terminal HistoryDealGetDouble() y HistoryDealGetInteger(), pasándoles el número de orden recibido previamente. Tendremos que especificar el identificador de la propiedad de la operación correspondiente de las enumeraciones ENUM_DEAL_PROPERTY_INTEGER y ENUM_DEAL_PROPERTY_DOUBLE. También tendremos que añadir un filtro mediante un operador de selección booleano if para considerar sólo las operaciones de trading comprobando los valores DEAL_TYPE_BUY y DEAL_TYPE_SELL de la enumeración ENUM_DEAL_TYPE para filtrar otras operaciones de la cuenta, como las transacciones de saldo y las acumulaciones de bonificaciones. Así, terminaremos con el siguiente código para seleccionar los datos.

//--- now select data by --==DAY==--
   HistorySelect(StructToTime(start_day),StructToTime(local));          // select required history 
//--- check
   total  = HistoryDealsTotal();                                        // number number of selected deals
   ticket = 0;                                                          // order number
   profit = 0;                                                          // order profit
   commis = 0;                                                          // order commission
   swap   = 0;                                                          // order swap

//--- for all deals
   for(uint i=0; i<total; i++)                                          // loop through all selected orders
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // get the number of each in order
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // get data on financial results
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // get data on commission
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // get swap data
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // get data on operation type

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // if the deal is form a trading operatoin
           {
            if(profit>0)                                                // if financial result of current order is greater than 0,
              {
               DayorderProfit += profit;                                // add to profit
              }
            else
              {
               DayorderLoss += MathAbs(profit);                         // if loss, add up
              }
           }
        }
     }

//--- now select data by --==WEEK==--
   HistorySelect(StructToTime(start_week),StructToTime(local));         // select the required history
//--- check
   total  = HistoryDealsTotal();                                        // number number of selected deals
   ticket = 0;                                                          // order number
   profit = 0;                                                          // order profit
   commis = 0;                                                          // order commission
   swap   = 0;                                                          // order swap

//--- for all deals
   for(uint i=0; i<total; i++)                                          // loop through all selected orders
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // get the number of each in order
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // get data on financial results
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // get data on commission
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // get swap data
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // get data on operation type

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // if the deal is form a trading operatoin
           {
            if(profit>0)                                                // if financial result of current order is greater than 0,
              {
               WeekorderProfit += profit;                               // add to profit
              }
            else
              {
               WeekorderLoss += MathAbs(profit);                        // if loss, add up
              }
           }
        }
     }

//--- now select data by --==MONTH==--
   HistorySelect(StructToTime(start_month),StructToTime(local));        // select the required history
//--- check
   total  = HistoryDealsTotal();                                        // number number of selected deals
   ticket = 0;                                                          // order number
   profit = 0;                                                          // order profit
   commis = 0;                                                          // order commission
   swap   = 0;                                                          // order swap

//--- for all deals
   for(uint i=0; i<total; i++)                                          // loop through all selected orders
     {
      //--- try to get deals ticket
      if((ticket=HistoryDealGetTicket(i))>0)                            // get the number of each in order
        {
         //--- get deals properties
         profit    = HistoryDealGetDouble(ticket,DEAL_PROFIT);          // get data on financial results
         commis    = HistoryDealGetDouble(ticket,DEAL_COMMISSION);      // get data on commission
         swap      = HistoryDealGetDouble(ticket,DEAL_SWAP);            // get swap data
         type      = HistoryDealGetInteger(ticket,DEAL_TYPE);           // get data on operation type

         MonthOrderSwap    += swap;                                     // sum up swaps
         MonthOrderCommis  += commis;                                   // sum up commissions

         if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL)            // if the deal is form a trading operatoin
           {
            if(profit>0)                                                // if financial result of current order is greater than 0,
              {
               MonthorderProfit += profit;                              // add to profit
              }
            else
              {
               MonthorderLoss += MathAbs(profit);                       // if loss, sum up
              }
           }
        }
     }

El método anterior puede ser llamado cada vez que necesitemos actualizar los valores actuales de uso de límites. Podemos actualizar los valores de los límites reales, así como llamar a esta función, al generar varios eventos terminales. Dado que el objetivo de este método es actualizar los límites, esto se puede hacer cuando se producen eventos relacionados con cambios en las órdenes actuales, como Trade y TradeTransaction, y siempre que surja un nuevo tick con el evento NewTick. Como nuestro método consume pocos recursos, actualizaremos los límites reales en cada tick. Ahora vamos a implementar el manejador de eventos necesario para manejar los eventos relacionados con la cancelación dinámica y la resolución de operaciones.


Manejador de eventos de clase

Para manejar los eventos, definimos un método interno de nuestra clase ContoEvents() con el nivel de acceso protected. Para ello, declaramos campos auxiliares adicionales con el mismo nivel de acceso. Para poder rastrear instantáneamente la hora de inicio de un nuevo período de negociación, que necesitamos para cambiar los indicadores de permiso de negociación, necesitamos almacenar los valores del último período registrado y del período actual. A estos efectos, podemos utilizar arrays simples declarados con el tipo de datos datetime para almacenar los valores de los periodos correspondientes.

   //--- additional auxiliary arrays
   datetime          Periods_old[3];         // 0-day,1-week,2-mn
   datetime          Periods_new[3];         // 0-day,1-week,2-mn

En la primera dimensión almacenaremos los valores del día, en la segunda los de la semana y en la tercera los del mes. Si es necesario ampliar aún más los periodos controlados, puede declarar estas matrices no de forma estática, sino dinámica. Pero aquí sólo trabajamos con tres periodos de tiempo. Ahora vamos a añadir a nuestro constructor de clase la inicialización primaria de estas variables de matriz de la siguiente manera.

   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 1);                       // initialize the current day with the previous period
   Periods_new[1] = iTime(_Symbol, PERIOD_W1, 1);                       // initialize the current week with the previous period
   Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 1);                      // initialize the current month with the previous period

Inicializaremos cada periodo correspondiente utilizando una función terminal predefinida iTime() pasando en los parámetros el periodo correspondiente de ENUM_TIMEFRAMES del periodo anterior al actual. Deliberadamente no inicializamos la matriz Periods_old[]. En este caso, después de llamar al constructor y a nuestro método ContoEvents(), nos aseguramos de que se dispara el evento de inicio del nuevo periodo de negociación y se abren todas las banderas de inicio de negociación, y sólo se cierran por código si no quedan límites. De lo contrario, es posible que la clase no funcione correctamente cuando se reinicie. El método descrito contendrá una lógica simple: si el período actual no es igual al anterior, significa que ha comenzado un nuevo período correspondiente y puede restablecer los límites y permitir la negociación cambiando los valores en las banderas. Además, para cada periodo, llamaremos al método RefreshLimits() descrito anteriormente para recalcular los límites de entrada.

//+------------------------------------------------------------------+
//|                     ContoEvents                                  |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoEvents()
  {
// check the start of a new trading day
   NewTradeDay    = false;                                              // variable for new trading day set to false
   Periods_old[0] = Periods_new[0];                                     // copy to old, new
   Periods_new[0] = iTime(_Symbol, PERIOD_D1, 0);                       // update new for day
   if(Periods_new[0]!=Periods_old[0])                                   // if do not match, it's a new day
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade day!");  // inform
      NewTradeDay = true;                                               // variable to true

      DayProfitArrive     = false;                                      // reset flag of reaching target profit after a new day started
      RiskDayPermission = true;                                         // allow opening new positions

      RefreshLimits();                                                  // update limits

      DayorderLoss = 0;                                                 // reset daily financial result
      DayorderProfit = 0;                                               // reset daily financial result
     }

// check the start of a new trading week
   Periods_old[1]    = Periods_new[1];                                  // copy data to old period
   Periods_new[1]    = iTime(_Symbol, PERIOD_W1, 0);                    // fill new period for week
   if(Periods_new[1]!= Periods_old[1])                                  // if periods do not match, it's a new week
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade week!"); // inform

      RiskWeekPermission = true;                                        // allow opening new positions

      RefreshLimits();                                                  // update limits

      WeekorderLoss = 0;                                                // reset weekly losses
      WeekorderProfit = 0;                                              // reset weekly profits
     }

// check the start of a new trading month
   Periods_old[2]    = Periods_new[2];                                  // copy the period to the old one
   Periods_new[2]    = iTime(_Symbol, PERIOD_MN1, 0);                   // update new period for month
   if(Periods_new[2]!= Periods_old[2])                                  // if do not match, it's a new month
     {
      Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade Month!");   // inform

      RiskMonthPermission = true;                                       // allow opening new positions

      RefreshLimits();                                                  // update limits

      MonthorderLoss = 0;                                               // reset the month's loss
      MonthorderProfit = 0;                                             // reset the month's profit
     }

// set the permission to open new positions true only if everything is true
// set to true
   if(RiskDayPermission    == true &&                                   // if there is a daily limit available
      RiskWeekPermission   == true &&                                   // if there is a weekly limit available
      RiskMonthPermission  == true                                      // if there is a monthly limit available
     )                                                                  //
     {
      RiskTradePermission=true;                                         // if all are allowed, trading is allowed
     }

// set to false if at least one of them is false
   if(RiskDayPermission    == false ||                                  // no daily limit available
      RiskWeekPermission   == false ||                                  // or no weekly limit available
      RiskMonthPermission  == false ||                                  // or no monthly limit available
      DayProfitArrive      == true                                      // or target profit is reached
     )                                                                  // then
     {
      RiskTradePermission=false;                                        // prohibit trading
     }
   }

También en este método, hemos añadido el control sobre el estado de los datos en la variable principal de la bandera para la posibilidad de abrir nuevas posiciones, RiskTradePermission. Mediante operadores lógicos de selección, implementamos la habilitación del permiso a través de esta variable sólo si todos los permisos son verdaderos, y la deshabilitación si al menos una de las banderas no permite la negociación. Esta variable será muy útil si integra esta clase en un EA algorítmico ya creado; puede simplemente recibirla a través de un getter e insertarla en el código con las condiciones para colocar sus órdenes. En nuestro caso, servirá simplemente como indicador para empezar a informar al usuario de la ausencia de límites de negociación libres. Ahora que nuestra clase ha «aprendido» a controlar los riesgos cuando se alcanzan las pérdidas especificadas, pasemos a implementar la funcionalidad para controlar la consecución del beneficio objetivo.


Mecanismo de control del objetivo diario de beneficios

En la parte anterior de nuestros artículos, hemos declarado una bandera para lanzar el control sobre el beneficio objetivo y una variable de entrada para determinar su valor relativo al tamaño del depósito de la cuenta. Según la lógica de nuestra clase que controla la consecución del beneficio objetivo, todas las posiciones abiertas se cerrarán si el beneficio total de todas las posiciones ha alcanzado el valor objetivo. Para cerrar todas las posiciones de una cuenta, declararemos en nuestra clase el método interno AllOrdersClose() con el nivel de acceso public. Para que este método funcione, necesitaremos recibir datos sobre las posiciones abiertas y enviar automáticamente órdenes para cerrarlas. 

Para no perder tiempo escribiendo nuestras propias implementaciones de esta funcionalidad, utilizaremos clases internas ya creadas del terminal. Utilizaremos la clase terminal estándar interna CPositionInfo para trabajar con posiciones abiertas y la clase CTrade para cerrar posiciones abiertas. Declaremos las variables de estas dos clases también con el nivel de acceso protected sin usar puntero con constructor por defecto de la siguiente forma.

   CTrade            r_trade;                // instance
   CPositionInfo     r_position;             // instance

Cuando trabajemos con estos objetos, en el marco de la funcionalidad que necesitamos ahora, no necesitaremos configurarlos adicionalmente, por lo que no los escribiremos en el constructor de nuestra clase. A continuación se muestra la implementación de este método utilizando las clases declaradas:

//+------------------------------------------------------------------+
//|                       AllOrdersClose                             |
//+------------------------------------------------------------------+
bool RiskManagerBase::AllOrdersClose()                                  // closing market positions
  {
   ulong ticket = 0;                                                    // order ticket
   string symb;

   for(int i = PositionsTotal(); i>=0; i--)                             // loop through open positoins
     {
      if(r_position.SelectByIndex(i))                                   // if a position selected
        {
         ticket = r_position.Ticket();                                  // remember position ticket

         if(!r_trade.PositionClose(ticket))                             // close by ticket
           {
            Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); // if not, inform
            return(false);                                              // return false
           }
         else
           {
            Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); // if not, inform
            continue;                                                   // if everything is ok, continue
           }
        }
     }
   return(true);                                                        // return true
  }

Llamaremos al método descrito tanto cuando se alcance el beneficio objetivo como cuando se alcancen los límites. También devuelve un valor bool por si fuera necesario gestionar errores en el envío de órdenes de cierre. Para proporcionar funcionalidad para controlar si se alcanza el beneficio objetivo, complementaremos nuestro método de manejo de eventos ContoEvents() con el siguiente código inmediatamente después del código ya descrito anteriormente.

//--- daily
   if(dayProfitControl)							// check if functionality is enabled by the user
     {
      if(CurrentEquity >= (StartEquity+PlanDayProfit))                  // if equity exceeds or equals start + target profit,
        {
         DayProfitArrive = true;                                        // set flag that target profit is reached
         Print(__FUNCTION__+", PlanDayProfit has been arrived.");       // inform about the event
         Print(__FUNCTION__+", CurrentEquity = "+DoubleToString(CurrentEquity)+
               ", StartEquity = "+DoubleToString(StartEquity)+
               ", PlanDayProfit = "+DoubleToString(PlanDayProfit));
         AllOrdersClose();                                              // close all open orders

         StartEquity = CurrentEquity;                                   // rewrite starting equity value

         //--- send a push notification
         ResetLastError();                                              // reset the last error
         if(!SendNotification("The planned profitability for the day has been achieved. Equity: "+DoubleToString(CurrentEquity)))// notification
           {
            Print(__FUNCTION__+IntegerToString(__LINE__)+", Error of sending notification: "+IntegerToString(GetLastError()));// if not, print
           }
        }
     }

El método incluye el envío de una notificación push al usuario para notificarle que se ha producido este evento. Para ello, utilizamos la función predefinida del terminal SendNotification. Para completar la funcionalidad mínima requerida de nuestra clase, sólo nos falta montar un método de clase más con acceso public, que será llamado cuando se conecte una gestora de riesgos a la estructura de nuestro EA.


Definición de un método para poner en marcha la supervisión en la estructura de la EA

Para añadir la funcionalidad de monitorización desde una instancia de nuestra clase gestora de riesgos a la estructura del EA, declararemos el método público ContoMonitor(). En este método, recopilaremos todos los métodos de gestión de eventos declarados anteriormente y también lo complementaremos con una funcionalidad para comparar los límites realmente utilizados con los valores aprobados por el usuario en los parámetros de entrada. Declaremos este método con el nivel de acceso public y describámoslo fuera de la clase como sigue.

//+------------------------------------------------------------------+
//|                       ContoMonitor                               |
//+------------------------------------------------------------------+
void RiskManagerBase::ContoMonitor()                                    // monitoring
  {
   ForOnTrade();                                                        // update at each tick

   ContoEvents();                                                       // event block

//---
   double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT);
   
   if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay &&    // if equity is less than or equal to the start balance minus the daily risk
       currentProfit<0                                            &&    // profit below zero
       RiskDayPermission==true)                                         // day trading is allowed
      ||                                                                // OR
      (RiskDayPermission==true &&                                       // day trading is allowed
       MathAbs(DayorderLoss) >= RiskPerDay)                             // loss exceed daily risk
   )                                                                    

     {
      Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = "  +DoubleToString(currentProfit));// notify
      Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = "      +DoubleToString(RiskPerDay));   // notify
      Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = "    +DoubleToString(DayorderLoss)); // notify
      RiskDayPermission=false;                                          // prohibit opening new orders during the day
      AllOrdersClose();                                                 // close all open positions
     }

// check if there is a WEEK limit available for opening a new position if there are no open ones
   if(
      MathAbs(WeekorderLoss)>=RiskPerWeek &&                            // if weekly loss is greater than or equal to the weekly risk
      RiskWeekPermission==true)                                         // and we traded
     {
      RiskWeekPermission=false;                                         // prohibit opening of new orders during the day
      AllOrdersClose();                                                 // close all open positions

      Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss));  // notify
      Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek));      // notify
     }

// check if there is a MONTH limit available for opening a new position if there are no open ones
   if(
      MathAbs(MonthorderLoss)>=RiskPerMonth &&                          // if monthly loss is greater than or equal to the monthly risk
      RiskMonthPermission==true)                                        // we traded
     {
      RiskMonthPermission=false;                                        // prohibit opening of new orders during the day
      AllOrdersClose();                                                 // close all open positions

      Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss));  // notify
      Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth));      // notify
     }
  }

La lógica de funcionamiento de nuestro método es muy sencilla: si el límite de pérdida real para un mes o una semana supera el establecido por el usuario, el indicador de negociación para un periodo determinado se establece como prohibido y, en consecuencia, se prohíbe la negociación. La única diferencia está en los límites diarios, donde también necesitamos controlar la presencia de posiciones abiertas; para ello, añadiremos también el control del beneficio actual de las posiciones abiertas a través del operador lógico OR. Cuando se alcanzan los límites de riesgo, llamamos a nuestro método para cerrar posiciones e imprimimos el registro sobre este evento.

En esta etapa, para completar la clase, sólo tenemos que añadir un método para que el usuario controle los límites actuales. La forma más sencilla y cómoda sería mostrar la información necesaria a través de la función predefinida estándar del terminal, Comment(). Para trabajar con esta función, necesitaremos pasarle un parámetro de tipo string que contenga información a mostrar en el gráfico. Para obtener estos valores de nuestra clase, declaramos el método Message() con el nivel de acceso public, que devolverá un string con los datos recogidos de todas las variables que el usuario necesite.

//+------------------------------------------------------------------+
//|                        Message                                   |
//+------------------------------------------------------------------+
string RiskManagerBase::Message(void)
  {
   string msg;                                                          // message

   msg += "\n"+" ----------Risk-Manager---------- ";                    // common
//---
   msg += "\n"+"RiskTradePer = "+(string)RiskTradePermission;           // final trade permission
   msg += "\n"+"RiskDayPer   = "+(string)RiskDayPermission;             // daily risk available
   msg += "\n"+"RiskWeekPer  = "+(string)RiskWeekPermission;            // weekly risk available
   msg += "\n"+"RiskMonthPer = "+(string)RiskMonthPermission;           // monthly risk available

//---limits and inputs
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"RiskPerDay   = "+DoubleToString(RiskPerDay,2);          // daily risk in usd
   msg += "\n"+"RiskPerWeek  = "+DoubleToString(RiskPerWeek,2);         // weekly risk in usd
   msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,2);        // monthly risk usd
//--- current profits and losses for periods
   msg += "\n"+" -------------------------------- ";                    //
   msg += "\n"+"DayLoss     = "+DoubleToString(DayorderLoss,2);         // daily loss
   msg += "\n"+"DayProfit   = "+DoubleToString(DayorderProfit,2);       // daily profit
   msg += "\n"+"WeekLoss    = "+DoubleToString(WeekorderLoss,2);        // weekly loss
   msg += "\n"+"WeekProfit  = "+DoubleToString(WeekorderProfit,2);      // weekly profit
   msg += "\n"+"MonthLoss   = "+DoubleToString(MonthorderLoss,2);       // monthly loss
   msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,2);     // monthly profit
   msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,2);     // monthly commissions
   msg += "\n"+"MonthSwap   = "+DoubleToString(MonthOrderSwap,2);       // monthly swaps
//--- for current monitoring

   if(dayProfitControl)                                                 // if control daily profit
     {
      msg += "\n"+" ---------dayProfitControl-------- ";                //
      msg += "\n"+"DayProfitArrive = "+(string)DayProfitArrive;         // daily profit achieved
      msg += "\n"+"StartBallance   = "+DoubleToString(StartBalance,2);  // starting balance
      msg += "\n"+"PlanDayProfit   = "+DoubleToString(PlanDayProfit,2); // target profit
      msg += "\n"+"PlanDayEquity   = "+DoubleToString(PlanDayEquity,2); // target equity
     }
   return(msg);                                                         // return value
  }

El mensaje para el usuario creado por el método tendrá este aspecto.

Figura 2. Formato de salida de datos.

Figura 2. Formato de salida de datos.

Este método puede modificarse o completarse añadiendo elementos para trabajar con gráficos en el terminal. Pero lo usaremos así ya que proporciona al usuario suficientes datos de nuestra clase para tomar una decisión. Si lo desea, puede perfeccionar este formato en el futuro y hacerlo más bonito en términos gráficos. Analicemos ahora las posibilidades de ampliar esta clase cuando se utilizan estrategias de negociación individuales.


La implementación final y las posibilidades de ampliación de la clase

Como hemos mencionado antes, la funcionalidad que describimos aquí es la mínima necesaria y la más universal para casi todas las estrategias de negociación. Permite controlar los riesgos y evitar la pérdida del depósito en un solo día. En esta parte del artículo, veremos varias posibilidades más para ampliar esta clase. 

  • Controle el tamaño del spread cuando opere con un stop loss corto
  • Control del deslizamiento de las posiciones abiertas
  • Controlar el objetivo de beneficio mensual

Para el primer punto, podemos implementar una funcionalidad adicional para los sistemas de negociación que utilizan la negociación con stop loss corto. Puede declarar el método SpreadMonitor(int intSL) que toma como parámetro el stop loss técnico o calculado para un instrumento en puntos para compararlo con el nivel de spread actual. Este método prohibirá colocar una orden si el diferencial se amplía mucho en relación con el stop loss en una proporción determinada por el usuario, para evitar el alto riesgo de cerrar la posición en el stop loss debido al diferencial.

Para controlar el deslizamiento en el momento de la apertura, de acuerdo con el segundo punto, puede declarar el método SlippageCheck(). Este método cerrará cada operación individual si el corredor la abrió a un precio muy distinto del indicado, debido a lo cual el riesgo de la operación superó el valor previsto. Esto permitirá, en caso de que se active el stop loss, no estropear las estadísticas por operaciones de alto riesgo por una entrada separada. Además, cuando se opera con una relación fija entre stop loss y take profit, esta relación empeora debido al deslizamiento y es mejor cerrar la posición con una pérdida pequeña que incurrir en pérdidas mayores más adelante.

De forma similar a la lógica de control del beneficio diario, es posible aplicar un método correspondiente para controlar el beneficio mensual objetivo. Este método puede utilizarse cuando se negocian estrategias a largo plazo. La clase que hemos descrito ya tiene toda la funcionalidad necesaria para su uso en la negociación intradía manual, y puede integrarse en la implementación final de un EA de negociación, que debe lanzarse en el gráfico del instrumento simultáneamente con el inicio de la negociación manual.

El montaje final del proyecto incluye conectar nuestra clase usando la directiva del preprocesador #include.

#include <RiskManagerBase.mqh>

A continuación, declaramos el puntero de nuestro objeto gestor de riesgos a nivel global.

RiskManagerBase *RMB;

Al inicializar nuestro EA, asignamos manualmente memoria a nuestro objeto para prepararlo antes del lanzamiento.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {

   RMB = new RiskManagerBase();

//---
   return(INIT_SUCCEEDED);
  }

Cuando eliminamos nuestro EA del gráfico, necesitamos borrar la memoria de nuestro objeto para evitar una fuga de memoria. Para ello, escriba lo siguiente en la función OnDeinit del EA.

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---

   delete RMB;

  }

Además, si es necesario, en el mismo evento puede llamar al método Comment(" "), pasándole una cadena vacía, para que el gráfico se limpie de comentarios cuando se elimine el EA del gráfico de símbolos.

Llamamos al método principal de monitorización de nuestra clase en el evento de recibir un nuevo tick para el símbolo.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   RMB.ContoMonitor();

   Comment(RMB.Message());
  }
//+------------------------------------------------------------------+

Esto completa el montaje de nuestro EA con el gestor de riesgos incorporado y está completamente listo para su uso (archivo ManualRiskManager.mq5). Para probar varios casos de su uso, haremos una pequeña adición al código actual para simular el proceso de negociación manual.


Ejemplo de uso

Para visualizar el proceso de negociación manual con y sin el uso de un gestor de riesgos, necesitaremos código adicional que modele la negociación manual. Dado que en este artículo no tocaremos el tema de la elección de estrategias de negociación, no implementaremos la funcionalidad completa de negociación en código. En su lugar, tomaremos visualmente las entradas del gráfico diario y añadiremos datos ya preparados a nuestro EA. Utilizaremos una estrategia muy simple para tomar decisiones de trading y veremos el resultado financiero final de esta estrategia con la única diferencia: con y sin control de riesgo.

Como ejemplos de entradas, utilizaremos una estrategia simple con rupturas de un nivel fractal, para el instrumento USDJPY, durante un período de dos meses. Veamos cómo funciona esta estrategia con y sin control de riesgos. Esquemáticamente, las señales de estrategia para las entradas manuales serán las siguientes.

Figura 3. Entradas utilizando una estrategia de prueba

Figura 3. Entradas utilizando una estrategia de prueba

Para modelar esta estrategia, escribamos un pequeño añadido como prueba unitaria universal para cualquier estrategia manual, de forma que cada usuario pueda probar sus entradas con pequeñas modificaciones. Durante esta prueba, la estrategia ejecutará señales pre-cargadas ya preparadas, sin implementar su propia lógica para entrar al mercado. Para ello, primero tenemos que declarar una estructura adicional, struct, que almacenará nuestras entradas basadas en fractales.

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

La clase principal que se encargará de modelar las señales de negociación es TradeModel. El constructor de la clase aceptará un contenedor con parámetros de entrada de señal, y su método principal Processing() controlará cada tick si ha llegado la hora del punto de entrada basándose en los valores de entrada. Dado que estamos simulando la operativa intradía, al final del día eliminaremos todas las posiciones utilizando el método AllOrdersClose() previamente declarado en nuestra clase gestora de riesgos. Aquí está nuestra clase auxiliar.

//+------------------------------------------------------------------+
//|                        TradeModel                                |
//+------------------------------------------------------------------+
class TradeModel
  {
protected:

   CTrade               *cTrade;                                        // to trade
   TradeInputs       container[];                                       // container of entries

   int               size;                                              // container size

public:
                     TradeModel(const TradeInputs &inputs[]);
                    ~TradeModel(void);

   void              Processing();                                      // main modeling method
  };

Para poder realizar pedidos cómodamente, utilizaremos la clase de terminal estándar CTrade, que contiene toda la funcionalidad que necesitamos. Así ahorraremos tiempo en el desarrollo de nuestra clase auxiliar. Para pasar parámetros de entrada al crear una instancia de clase, definimos nuestro constructor con un parámetro de entrada del contenedor de entradas.

//+------------------------------------------------------------------+
//|                          TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::TradeModel(const TradeInputs &inputs[])
  {
   size = ArraySize(inputs);                                            // get container size
   ArrayResize(container, size);                                        // resize

   for(int i=0; i<size; i++)                                            // loop through inputs
     {
      container[i] = inputs[i];                                         // copy to internal
     }

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

En el constructor, inicializamos el contenedor de parámetros de entrada con el valor deseado, recordamos su tamaño y creamos un objeto de nuestra clase CTrade con la configuración necesaria. La mayoría de los parámetros aquí no son configurados por el usuario, ya que no afectarán el propósito de crear nuestra prueba unitaria, por lo que los dejamos codificados de forma fija. 

El destructor de nuestra clase TradeModel sólo requerirá la eliminación de un objeto CTrade.

//+------------------------------------------------------------------+
//|                         ~TradeModel                              |
//+------------------------------------------------------------------+
TradeModel::~TradeModel(void)
  {
   if(CheckPointer(cTrade)!=POINTER_INVALID)                            // if there is an instance,
     {
      delete cTrade;                                                    // delete
     }
  }

Ahora vamos a implementar nuestro principal método de procesamiento para el funcionamiento de nuestra clase en la estructura de todo nuestro proyecto. Implementemos la lógica para realizar pedidos según la Figura 3:

//+------------------------------------------------------------------+
//|                         Processing                               |
//+------------------------------------------------------------------+
void TradeModel::Processing(void)
  {
   datetime timeCurr = TimeCurrent();                                   // request current time

   double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);                  // take bid
   double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);                  // take ask

   for(int i=0; i<size; i++)                                            // loop through inputs
     {
      if(container[i].done==false &&                                    // if we haven't traded yet AND
         container[i].tradedate <= timeCurr)                            // date is correct
        {
         switch(container[i].direction)                                 // check trade direction
           {
            //---
            case  POSITION_TYPE_BUY:                                    // if Buy,
               if(container[i].price >= ask)                            // check if price has reached and
                 {
                  if(cTrade.Buy(0.1))                                   // by the same lot
                    {
                     container[i].done = true;                          // if time has passed, put a flag
                     Print("Buy has been done");                        // notify
                    }
                  else                                                  // if hasn't passed,
                    {
                     Print("Error: buy");                               // notify
                    }
                 }
               break;                                                   // complete the case
            //---
            case  POSITION_TYPE_SELL:                                   // if Sell
               if(container[i].price <= bid)                            // check if price has reached and
                 {
                  if(cTrade.Sell(0.1))                                  // sell the same lot
                    {
                     container[i].done = true;                          // if time has passed, put a flag
                     Print("Sell has been done");                       // notify
                    }
                  else                                                  // if hasn't passed,
                    {
                     Print("Error: sell");                              // notify
                    }
                 }
               break;                                                   // complete the case

            //---
            default:
               Print("Wrong inputs");                                   // notify
               return;
               break;
           }
        }
     }
  }

La lógica de este método es bastante sencilla. Si hay entradas sin procesar en el contenedor para las que ha llegado el momento de modelar, colocamos estas órdenes de acuerdo con la dirección y el precio del fractal marcado en la Figura 3. Esta funcionalidad es suficiente para probar el gestor de riesgos, por lo que podemos integrarlo en nuestro proyecto principal.

En primer lugar, vamos a conectar nuestra clase de prueba con el código del EA de la siguiente manera.

#include <TradeModel.mqh>

Ahora, en la función OnInit(), creamos una instancia de nuestra estructura de matriz de datos TradeInputs y pasamos esta matriz al constructor de la clase TradeModel para inicializarla.

//---
   TradeInputs modelInputs[] =
     {
        {"USDJPYz", POSITION_TYPE_SELL, 146.636, D'2024-01-31',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.794, D'2024-02-05',false},
        {"USDJPYz", POSITION_TYPE_BUY,  148.882, D'2024-02-08',false},
        {"USDJPYz", POSITION_TYPE_SELL, 149.672, D'2024-02-08',false}
     };

//---
   tModel = new TradeModel(modelInputs);

No olvides borrar la memoria de nuestro objeto tModel en la función DeInit(). La funcionalidad principal se realizará en la función OnTick(), complementada con el siguiente código.

   tModel.Processing();                                                 // place orders

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

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

Ahora comparemos los resultados de la misma estrategia con y sin la clase de control de riesgos. Vamos a ejecutar el archivo de prueba unitaria ManualRiskManager(UniTest1) sin el método de control de riesgos. Para el periodo de enero a marzo de 2024, obtenemos el siguiente resultado de nuestra estrategia.

Figura 4. Probar datos sin utilizar un gestor de riesgos

Figura 4. Probar datos sin utilizar un gestor de riesgos

Como resultado, obtenemos una expectativa matemática positiva para esta estrategia con los siguientes parámetros.

# Nombre del parámetro Valor del parámetro
 1  EA  ManualRiskManager(UniTest1)
 2  Símbolo  USDJPY
 3  Timeframe  М15
 4  Intervalo de tiempo  2024.01.01 - 2024.03.18
 5  Período Forward  NO 
 6  Retrasos   Sin retrasos, ejecución ideal
 7  Modelado  Todos los ticks 
 8  Depósito inicial  10.000 USD 
 9  Apalancamiento  1:100 

Tabla 1. Parámetros de entrada en el probador de estrategias


Ahora vamos a ejecutar el archivo de prueba unitaria ManualRiskManager(UniTest2), donde utilizamos nuestra clase de gestor de riesgos con los siguientes parámetros de entrada.

Nombre del parámetro de entrada Valor de la variable
inp_riskperday 0.25
inp_riskperweek 0.75
inp_riskpermonth 2.25
inp_plandayprofit  0.78 
dayProfitControl  true

Tabla 2. Parámetros de entrada para el gestor de riesgos

La lógica para generar parámetros de entrada es similar a la lógica descrita anteriormente al diseñar la estructura de los parámetros de entrada en la Parte 3. La curva de beneficios tendrá este aspecto.

Figura 5. Datos de prueba mediante un gestor de riesgos

Figura 5. Datos de prueba mediante un gestor de riesgos


A continuación, se presenta un resumen de los resultados de las pruebas de los dos casos en la siguiente tabla.

# Valor Sin gestor de riesgos Gestor de riesgos Cambio
 1  Beneficio neto total:  41.1 144.48  +103.38
 2  Disminución máxima del balance:  0.74% 0.25%  Reducido 3 veces
 3  Reducción máxima de la equidad:  1.13% 0.58%  Reducido 2 veces
 4  Beneficio Esperado:  10.28 36.12  Crecimiento superior a 3 veces
 5  Ratio Sharpe:  0.12 0.67  5 veces el crecimiento
 6  Operaciones rentables (% del total):  75% 75%  -
 7  Beneficio medio por operación:  38.52 56.65  Crecimiento del 50%
 8  Pérdida media por operación:  -74.47 -25.46  Reducido 3 veces
 9  Riesgo-Retorno Promedio:  0.52  2.23  4 veces el crecimiento

Tabla 3. Comparación de los resultados financieros de la negociación con y sin gestor de riesgos

Con base en los resultados de nuestras pruebas unitarias, podemos concluir que el uso del control de riesgos a través de nuestra clase de gestor de riesgos ha aumentado significativamente la eficiencia del trading utilizando la misma estrategia simple, al limitar los riesgos y asegurar las ganancias de cada transacción en relación con el riesgo fijado. Esto permitió reducir el drawdown del balance en 3 veces y el drawdown de la equidad en 2 veces. El Beneficio Esperado de la estrategia aumentó más de 3 veces, y el Ratio Sharpe más de 5 veces. La media de operaciones rentables aumentó un 50% y la media de operaciones no rentables se redujo tres veces, lo que permitió situar la rentabilidad media del riesgo de la cuenta casi en el valor objetivo de 1 a 3. La siguiente tabla ofrece una comparación detallada de los resultados financieros de cada operación individual de nuestro conjunto.


Fecha Símbolo Dirección Lotes Sin gestor de riesgos Gestor de riesgos Cambio
2024.01.31 USDJPY buy 0.1 25.75 78 + 52.25
2024.02.05
USDJPY sell 0.1
13.19 13.19 -
2024.02.08
USDJPY sell 0.1
76.63 78.75 + 2.12
2024.02.08
USDJPY buy 0.1
-74.47 -25.46 + 49.01
Total - - - 41.10 144.48 + 103.38

Tabla 4. Comparación de las operaciones ejecutadas con y sin el gestor de riesgos


Conclusión

A partir de las tesis expuestas en el artículo, pueden extraerse las siguientes conclusiones. Utilizar el gestor de riesgos incluso en la negociación manual puede aumentar considerablemente la eficacia de las estrategias, incluidas las rentables. En el caso de una estrategia perdedora, el uso del gestor de riesgos puede ayudar a asegurar los depósitos, limitando las pérdidas. Como ya se ha dicho en la introducción, intentamos mitigar el factor psicológico. No hay que desactivar el gestor de riesgos tratando de recuperar inmediatamente las pérdidas. Puede ser mejor esperar a que finalice el periodo de limitación y, sin emociones, empezar a operar de nuevo. Aproveche el tiempo en que la negociación está prohibida por el gestor de riesgos para analizar su estrategia de negociación y comprender qué ha provocado las pérdidas y cómo evitarlas en el futuro.

Gracias a todos los que han leído este artículo hasta el final. Espero sinceramente que este artículo logre salvar al menos un depósito de perderse por completo. En ese caso, consideraré que mis esfuerzos no fueron en vano. Estaré encantado de ver sus comentarios o mensajes privados, especialmente sobre si debería comenzar un nuevo artículo en el que podamos adaptar esta clase a un EA puramente algorítmico. Agradezco sus comentarios. Gracias.


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

El problema del desacuerdo: profundizando en la explicabilidad de la complejidad en la IA El problema del desacuerdo: profundizando en la explicabilidad de la complejidad en la IA
En este artículo hablaremos de los problemas relacionados con los explicadores y la explicabilidad en la IA. Los modelos de IA suelen tomar decisiones difíciles de explicar. Además, el uso de múltiples explicadores suele provocar el llamado "problema del desacuerdo". Al fin y al cabo, la comprensión clara del funcionamiento de los modelos resulta fundamental para aumentar la confianza en la IA.
Modelo de aprendizaje profundo GRU en Python usando ONNX en asesores expertos, GRU vs LSTM Modelo de aprendizaje profundo GRU en Python usando ONNX en asesores expertos, GRU vs LSTM
El artículo está dedicado al desarrollo de un modelo de aprendizaje profundo GRU ONNX en Python. En la parte práctica, implementaremos este modelo en un asesor comercial y, a continuación, compararemos el rendimiento del modelo GRU con LSTM (memoria a largo plazo).
El papel de la calidad del generador de números aleatorios en la eficiencia de los algoritmos de optimización El papel de la calidad del generador de números aleatorios en la eficiencia de los algoritmos de optimización
En este artículo, analizaremos el generador de números aleatorios Mersenne Twister y lo compararemos con el estándar en MQL5. También determinaremos la influencia de la calidad del generador de números aleatorios en los resultados de los algoritmos de optimización.
Aprendizaje automático y Data Science (Parte 21): Desbloqueando las redes neuronales: desmitificando los algoritmos de optimización Aprendizaje automático y Data Science (Parte 21): Desbloqueando las redes neuronales: desmitificando los algoritmos de optimización
Sumérjase en el corazón de las redes neuronales mientras desmitificamos los algoritmos de optimización utilizados dentro de la red neuronal. En este artículo, descubra las técnicas clave que liberan todo el potencial de las redes neuronales, impulsando sus modelos a nuevas cotas de precisión y eficacia.