English Русский 中文 Deutsch 日本語 Português
preview
Desarrollamos un asesor experto multidivisa (Parte 12): Gestor de riesgos como en las empresas de prop-trading

Desarrollamos un asesor experto multidivisa (Parte 12): Gestor de riesgos como en las empresas de prop-trading

MetaTrader 5Trading |
382 8
Yuriy Bykov
Yuriy Bykov

Introducción

A lo largo de este ciclo de artículos, ya hemos abordado en varias ocasiones el tema del control de riesgos. Asimismo, hemos introducido los conceptos de estrategia comercial normalizada, cuyos parámetros nos garantizan conseguir un nivel de reducción del 10% durante el periodo de prueba. Sin embargo, la normalización de instancias de estrategias comerciales, así como de grupos de estrategias comerciales, de este modo solo puede proporcionar una reducción determinada a lo largo de un periodo histórico. Cuando empezamos a probar un grupo normalizado de estrategias en un periodo forward, o lo ejecutamos ya en una cuenta comercial, no podemos estar seguros de que se cumplirá el nivel de reducción especificado.

Recientemente, el tema del control de riesgos se ha planteado, por ejemplo, en los artículos Gestor de riesgos para el trading manual y Gestor de riesgos para el trading algorítmico. En ellos, se propone una aplicación informática que supervisa el cumplimiento de diversos parámetros comerciales con indicadores predeterminados. Por ejemplo, si se supera el nivel de pérdidas establecido durante un día, una semana o un mes, se suspenderá el trading.

También me ha parecido muy interesante el artículo Aprendiendo de las compañías de Prop-Trading, en el que el autor habla de los requisitos que deben cumplir las empresas de prop-trading para superar las pruebas que se exigen a los tráders que desean obtener capital para la gestión. A pesar de la opinión ambigua respecto a las actividades de dichas empresas, que puede encontrarse en diversos recursos dedicados al trading, el uso de reglas claras de control del riesgo es uno de los componentes más importantes del éxito en el trading. Por tanto, ¿por qué no aprovechamos la experiencia ya acumulada por otros e implantamos nuestro propio gestor de riesgos, tomando como base el modelo de control de riesgos usado en las empresas de prop-trading?


Modelo y conceptos

Para un gestor de riesgos, necesitaremos los siguientes conceptos:

  • El balance básico es el balance inicial de la cuenta (o parte del balance de la cuenta) a partir del cual se pueden calcular los valores de otros parámetros. En nuestro ejemplo usaremos un valor de este indicador igual a 10 000.
  • El balance básico diario es el balance de la cuenta comercial al inicio del periodo diario actual. Para simplificar, supondremos que el inicio del periodo diario coincide con la aparición de una nueva barra en el terminal en el marco temporal D1.
  • Los fondos básicos diarios son la cantidad de fondos en la cuenta comercial al inicio del periodo diario actual.
  • El nivel diario es el máximo del balance básico diario y de los fondos. Se determina al comienzo del periodo diario y conserva su valor para el comienzo del siguiente periodo.
  • La pérdida máxima diaria es el importe de la desviación a la baja de los fondos de la cuenta con respecto al nivel diario en el que debe detenerse la negociación para el periodo diario actual. La negociación se reanudará en el siguiente periodo diario. Podemos entender por parada las diversas acciones encaminadas a reducir el tamaño de las posiciones abiertas, hasta el cierre completo. Para empezar, usaremos este sencillo modelo: cuando se alcance la pérdida máxima diaria, se cerrarán todas las posiciones abiertas en el mercado. 
  • La pérdida total máxima es el importe de la desviación a la baja de los fondos de la cuenta con respecto al valor del balance básico a partir del cual la negociación se detiene por completo, es decir, no se reanudará en los periodos siguientes. Al alcanzarse este nivel, se cerrarán todas las posiciones abiertas.

Nos limitaremos a solo dos niveles para los stops comerciales: diario y total. También podemos añadir niveles semanales o mensuales similares. Pero como las empresas de prop-trading no los tienen, no complicaremos la primera aplicación de nuestro gestor de riesgos. Si es necesario, podemos añadirlos más adelante.

Diferentes empresas de prop-trading pueden tener enfoques ligeramente distintos para calcular la pérdida máxima diaria y total. Por lo tanto, en nuestro gestor de riesgos ofreceremos tres posibles formas de establecer un valor numérico para calcular la pérdida máxima:

  • En valor fijo en la divisa del depósito. En este método, transmitiremos directamente en el parámetro el valor de la pérdida expresado en unidades de la divisa de la cuenta comercial. Lo fijaremos como un número positivo.
  • En porcentaje del balance básico. En este caso, el valor se toma como porcentaje del balance básico fijado. Como el balance básico en nuestro modelo es un valor constante (antes de reiniciar la cuenta y el asesor experto con un valor diferente del balance básico establecido manualmente), la pérdida máxima calculada de esta manera también será un valor constante. Podríamos reducir este caso al primero, pero como lo que se suele indicar es el porcentaje de pérdida máxima, lo dejaremos como un caso aparte.
  • En porcentaje del nivel diario. En esta opción, al principio de cada periodo diario, volveremos a calcular el nivel máximo de pérdidas como un porcentaje determinado del nivel diario que se acaba de calcular. A medida que aumente el balance o los fondos, también aumentará la cuantía de la pérdida máxima. Este método se usará principalmente para calcular solo la pérdida máxima diaria. La pérdida total máxima suele fijarse en relación con el balance básico.

Vamos a implementar ahora nuestra clase de gestor de riesgos, como siempre guiándonos por el principio de mínima acción. Primero implementaremos la aplicación mínima necesaria, estableciendo las posibilidades de que se complique aún más si es necesario.


Clase CVirtualRiskManager

En el desarrollo de esta clase se han superado varias etapas. Al principio se hizo completamente estática para que pudiera utilizarse libremente desde todos los objetos. Luego estaba la hipótesis de que también podríamos optimizar los parámetros del gestor de riesgos, y estaría bien poder guardarlos como una cadena de inicialización. Para ello, la clase se hizo heredera de la clase CFactorable. Para poder utilizar el gestor de riesgos en objetos de distintas clases, se implementó el patrón Singleton. Pero luego resultó que el gestor de riesgos solo es necesario en una única clase: la clase de experto CVirtualAdvisor. Por lo tanto, eliminamos la implementación de la plantilla Singleton de la clase gestora de riesgos.

En primer lugar, hoy crearemos las enumeraciones necesarias para los posibles estados del gestor de riesgos y las posibles formas de cálculo de los límites:

// Possible risk manager states
enum ENUM_RM_STATE {
   RM_STATE_OK,            // Limits are not exceeded 
   RM_STATE_DAILY_LOSS,    // Daily limit is exceeded
   RM_STATE_OVERALL_LOSS   // Overall limit is exceeded
};


// Possible methods for calculating limits
enum ENUM_RM_CALC_LIMIT {
   RM_CALC_LIMIT_FIXED,          // Fixed (USD)
   RM_CALC_LIMIT_FIXED_PERCENT,  // Fixed (% from Base Balance)
   RM_CALC_LIMIT_PERCENT         // Relative (% from Daily Level)
};

En la descripción de la clase de gestor de riesgos, tendremos varias propiedades para almacenar los parámetros de entrada transmitidos a través de la cadena de inicialización al constructor. También añadiremos propiedades para almacenar diversas características de cálculo: el balance actual, los fondos, el beneficio y otros. Asimismo, declararemos algunos métodos auxiliares en la sección protegida. En la sección abierta tendremos esencialmente solo el constructor y el método de procesamiento de ticks. Por ahora solo mencionaremos los métodos de guardado/carga y el operador de conversión de cadenas, y escribiremos la implementación más adelante.

Entonces la descripción de la clase será algo así:

//+------------------------------------------------------------------+
//| Risk management class (risk manager)                             |
//+------------------------------------------------------------------+
class CVirtualRiskManager : public CFactorable {
protected:
// Main constructor parameters
   bool              m_isActive;             // Is the risk manager active?

   double            m_baseBalance;          // Base balance

   ENUM_RM_CALC_LIMIT m_calcDailyLossLimit;  // Method of calculating the maximum daily loss
   double            m_maxDailyLossLimit;    // Parameter of calculating the maximum daily loss

   ENUM_RM_CALC_LIMIT m_calcOverallLossLimit;// Method of calculating the total daily loss
   double            m_maxOverallLossLimit;  // Parameter of calculating the maximum total loss

// Current state
   ENUM_RM_STATE     m_state;

// Updated values
   double            m_balance;              // Current balance
   double            m_equity;               // Current equity
   double            m_profit;               // Current profit
   double            m_dailyProfit;          // Daily profit
   double            m_overallProfit;        // Total profit
   double            m_baseDailyBalance;     // Daily basic balance
   double            m_baseDailyEquity;      // Daily base balance
   double            m_baseDailyLevel;       // Daily base level
   double            m_virtualProfit;        // Profit of open virtual positions

// Managing the size of open positions
   double            m_prevDepoPart;         // Used part of the total balance

// Protected methods
   double            DailyLoss();            // Maximum daily loss
   double            OverallLoss();          // Maximum total loss

   void              UpdateProfit();         // Update current profit values
   void              UpdateBaseLevels();     // Updating daily base levels

   void              CheckLimits();          // Check for excess of permissible losses
   void              CheckDailyLimit();      // Check for excess of the permissible daily loss
   void              CheckOverallLimit();    // Check for excess of the permissible total loss

   double            VirtualProfit();        // Determine the real size of the virtual position

public:
                     CVirtualRiskManager(string p_params);     // Constructor

   virtual void      Tick();                 // Tick processing in risk manager 

   virtual bool      Load(const int f);      // Load status
   virtual bool      Save(const int f);      // Save status

   virtual string    operator~() override;   // Convert object to string
};

El constructor del objeto de gestor de riesgos esperará tener seis valores numéricos en la cadena de inicialización, que, tras la conversión a los tipos de datos correspondientes, se asignarán a las propiedades principales del objeto. También configuraremos el estado en el momento de la creación para que coincida con el estado normal (es decir, no se superan los límites). Si se volvemos a crear un objeto cuando se reinicia el EA en algún momento a mitad del día, al cargar la información guardada, el estado debería corregirse al que tenía en el momento del último guardado. Lo mismo se aplicará al establecimiento de la parte del balance de la cuenta asignada a la negociación: el valor establecido en el constructor podrá predefinirse al cargar la información guardada sobre el gestor de riesgos.

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualRiskManager::CVirtualRiskManager(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string and set the property values
   m_isActive = (bool) ReadLong(p_params);
   m_baseBalance = ReadDouble(p_params);
   m_calcDailyLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxDailyLossLimit = ReadDouble(p_params);
   m_calcOverallLossLimit = (ENUM_RM_CALC_LIMIT) ReadLong(p_params);
   m_maxOverallLossLimit = ReadDouble(p_params);

// Set the state: Limits are not exceeded
   m_state = RM_STATE_OK;

// Remember the share of the account balance allocated for trading
   m_prevDepoPart = CMoney::DepoPart();

// Update base daily levels
   UpdateBaseLevels();

// Adjust the base balance if it is not set
   if(m_baseBalance == 0) {
      m_baseBalance = m_balance;
   }
}

El trabajo principal del gestor de riesgos se realizará en cada tick en este manejador de eventos. Consistirá en comprobar la actividad del gestor de riesgos y, si está activo, realizar una actualización de los valores de beneficio actuales y de los niveles de referencia diarios si es necesario, así como comprobar si se han superado los límites de pérdidas:

//+------------------------------------------------------------------+
//| Tick processing in the risk manager                              |
//+------------------------------------------------------------------+
void CVirtualRiskManager::Tick() {
// If the risk manager is inactive, exit
   if(!m_isActive) {
      return;
   }

// Update the current profit values
   UpdateProfit();

// If a new daily period has begun, then we update the base daily levels
   if(IsNewBar(Symbol(), PERIOD_D1)) {
      UpdateBaseLevels();
   }

// Check for exceeding loss limits
   CheckLimits();
}

Destacaremos este punto por separado. Gracias al esquema desarrollado con el uso de posiciones virtuales que el receptor de volúmenes comerciales convierte en posiciones reales de mercado, y al módulo de gestión de capital, que permite establecer el factor de escala necesario entre los tamaños de las posiciones virtuales y reales, podremos realizar muy fácilmente un cierre seguro de posiciones de mercado sin alterar la lógica comercial de las estrategias funcionales. Para ello, bastará con ajustar a 0 el factor de escala en el módulo de gestión de capital:

CMoney::DepoPart(0);               // Set the used portion of the total balance to 0

Si recordamos de antemano el coeficiente anterior en la propiedad m_prevDepoPart, después de que llegue un nuevo día y se actualice el límite diario, podremos restablecer las posiciones reales cerradas anteriormente simplemente retornando este coeficiente a su valor anterior: 

CMoney::DepoPart(m_prevDepoPart);  // Return the used portion of the total balance

Dicho esto, por supuesto, no podemos saber de antemano si las posiciones se reabrirán a un precio peor o mejor. Pero podemos estar seguros de que la adición del gestor de riesgos no ha influido en modo alguno en el rendimiento de todos los casos de estrategias comerciales.

Ahora empezaremos a ver el resto de métodos de la clase de gestor de riesgos.

En el método UpdateProfits(), actualizaremos los valores actuales de balance, fondos y beneficios, y calcularemos el beneficio diario como la diferencia de los fondos actuales y el nivel diario. Debemos considerar que este valor no siempre coincidirá con los beneficios actuales. La diferencia aparecerá si ya se han cerrado algunas transacciones desde el inicio del nuevo periodo diario. Luego calcularemos la pérdida total como la diferencia entre los fondos actuales y el balance básico.

//+------------------------------------------------------------------+
//| Updating current profit values                                   |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateProfit() {
   m_equity = AccountInfoDouble(ACCOUNT_EQUITY);
   m_balance = AccountInfoDouble(ACCOUNT_BALANCE);
   m_profit = m_equity - m_balance;
   m_dailyProfit = m_equity - m_baseDailyLevel;
   m_overallProfit = m_equity - m_baseBalance;
   m_virtualProfit = VirtualProfit();

   if(IsNewBar(Symbol(), PERIOD_H1) && PositionsTotal() > 0) {
      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
   }
}

En este método también calcularemos el llamado beneficio virtual actual. Este calcula a partir de las posiciones virtuales abiertas. Si dejamos posiciones virtuales abiertas cuando se activan las restricciones del gestor de riesgos, aunque no haya posiciones reales abiertas, podremos estimar en cualquier momento cuál sería el beneficio aproximado ahora si las posiciones reales cerradas por el gestor de riesgos hubieran permanecido abiertas. Desgraciadamente, esta característica calculada no ofrece un resultado completamente exacto (tiene un error de unos pocos puntos porcentuales). No obstante, incluso esta información resulta mucho mejor que la ausencia de esta.

El cálculo del beneficio virtual actual se realizará mediante el método VirtualProfit(). En él obtendremos el puntero al objeto receptor del volumen virtual, ya que necesitamos averiguar de este el número total de posiciones virtuales y poder acceder a cada posición virtual. A continuación, iteraremos todas las posiciones virtuales y pediremos a nuestro módulo de gestión de capital que calcule el beneficio virtual escalado de cada posición para el tamaño actual de los fondos comerciales:

//+------------------------------------------------------------------+
//| Determine the profit of open virtual positions                   |
//+------------------------------------------------------------------+
double CVirtualRiskManager::VirtualProfit() {
   // Access the receiver object
   CVirtualReceiver *m_receiver = CVirtualReceiver::Instance();
   
   double profit = 0;
   
   // Find the profit sum for all virtual positions
   FORI(m_receiver.OrdersTotal(), profit += CMoney::Profit(m_receiver.Order(i)));
   
   return profit;
}

En este método, hemos aplicado la nueva macro FORI, de la que hablaremos a continuación.

Cuando surge un nuevo periodo diario, calculamos nuevamente el balance básico diario, los fondos y el nivel. También comprobaremos que si el límite de pérdidas diarias se ha alcanzado el día anterior, entonces deberemos restablecer la negociación y reabrir las posiciones reales según las posiciones virtuales abiertas. Esto será gestionado por el método UpdateBaseLevels():

//+------------------------------------------------------------------+
//| Update daily base levels                                         |
//+------------------------------------------------------------------+
void CVirtualRiskManager::UpdateBaseLevels() {
// Update balance, funds and base daily level
   m_baseDailyBalance = m_balance;
   m_baseDailyEquity = m_equity;
   m_baseDailyLevel = MathMax(m_baseDailyBalance, m_baseDailyEquity);

   PrintFormat(__FUNCTION__" | DAILY UPDATE: Balance = %.2f | Equity = %.2f | Level = %.2f",
               m_baseDailyBalance, m_baseDailyEquity, m_baseDailyLevel);

// If the daily loss level was reached earlier, then
   if(m_state == RM_STATE_DAILY_LOSS) {
      // Restore the status to normal:
      CMoney::DepoPart(m_prevDepoPart);         // Return the used portion of the total balance
      m_state = RM_STATE_OK;                    // Set the risk manager to normal
      CVirtualReceiver::Instance().Changed();   // Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESTORE: depoPart = %.2f",
                  m_prevDepoPart);
   }
}

Tendremos dos métodos para calcular las pérdidas máximas de los métodos indicados en los parámetros: DailyLoss() y OverallLoss(). Su implementación resulta muy similar entre sí, la única diferencia será el parámetro numérico y el parámetro del método utilizado para el cálculo:

//+------------------------------------------------------------------+
//| Maximum daily loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::DailyLoss() {
   if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxDailyLossLimit;
   } else if(m_calcDailyLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxDailyLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxDailyLossLimit / 100;
   }
}

//+------------------------------------------------------------------+
//| Maximum total loss                                               |
//+------------------------------------------------------------------+
double CVirtualRiskManager::OverallLoss() {
   if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED) {
      // To get a fixed value, just return it 
      return m_maxOverallLossLimit;
   } else if(m_calcOverallLossLimit == RM_CALC_LIMIT_FIXED_PERCENT) {
      // To get a given percentage of the base balance, calculate it 
      return m_baseBalance * m_maxOverallLossLimit / 100;
   } else { // if(m_calcDailyLossLimit == RM_CALC_LIMIT_PERCENT)
      // To get a specified percentage of the daily level, calculate it
      return m_baseDailyLevel * m_maxOverallLossLimit / 100;
   }
}

El método CheckLimits() simplemente llamará a dos métodos auxiliares para comprobar la pérdida diaria y total:

//+------------------------------------------------------------------+
//| Check loss limits                                                |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckLimits() {
   CheckDailyLimit();      // Check daily limit
   CheckOverallLimit();    // Check total limit
}

El método de comprobación de pérdidas diarias usará el método DailyLoss() para obtener el límite máximo de pérdidas diarias permitido y lo comparará con el beneficio diario actual. Si se sobrepasa el límite, el gestor de riesgos pasará al estado "Límite diario superado" y se iniciará el cierre de las posiciones abiertas ajustando a 0 el tamaño del balance comercial utilizado:

//+------------------------------------------------------------------+
//| Check daily loss limit                                           |
//+------------------------------------------------------------------+
void CVirtualRiskManager::CheckDailyLimit() {
// If daily loss is reached and positions are still open
   if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) {
   // Switch the risk manager to the achieved daily loss state:
      m_prevDepoPart = CMoney::DepoPart();   // Save the previous value of the used part of the total balance
      CMoney::DepoPart(0);                   // Set the used portion of the total balance to 0
      m_state = RM_STATE_DAILY_LOSS;         // Set the risk manager to the achieved daily loss state
      CVirtualReceiver::Instance().Changed();// Notify the recipient about changes

      PrintFormat(__FUNCTION__" | VirtualProfit = %.2f | Profit = %.2f | Daily Profit = %.2f",
                  m_virtualProfit, m_profit, m_dailyProfit);
      PrintFormat(__FUNCTION__" | RESET: depoPart = %.2f",
                  CMoney::DepoPart());
   }
}

El método de prueba de pérdida total funciona de forma similar, con la única diferencia de que el beneficio total se comparará con la pérdida total permitida. Si se supera el límite total, el gestor de riesgos pasará al estado "Límite total superado".

Guardaremos el código obtenido en el archivo VirtualRiskManager.mqh en la carpeta actual.

Ahora vamos a analizar los cambios y adiciones que deberemos hacer a los archivos de proyecto creados anteriormente para poder utilizar nuestra nueva clase de gestor de riesgos.


Macros útiles

Como parte de las macros útiles para trabajar con matrices, hemos añadido una nueva macro FORI(N, D), que organiza un ciclo con la variable i, que ejecuta N veces la expresión D:

// Useful macros for array operations
#ifndef __MACROS_INCLUDE__
#define APPEND(A, V)    A[ArrayResize(A, ArraySize(A) + 1) - 1] = V;
#define FIND(A, V, I)   { for(I=ArraySize(A)-1;I>=0;I--) { if(A[I]==V) break; } }
#define ADD(A, V)       { int i; FIND(A, V, i) if(i==-1) { APPEND(A, V) } }
#define FOREACH(A, D)   { for(int i=0, im=ArraySize(A);i<im;i++) {D;} }
#define FORI(N, D)      { for(int i=0; i<N;i++) {D;} }
#define REMOVE_AT(A, I) { int s=ArraySize(A);for(int i=I;i<s-1;i++) { A[i]=A[i+1]; } ArrayResize(A, s-1);}
#define REMOVE(A, V)    { int i; FIND(A, V, i) if(i>=0) REMOVE_AT(A, i) }

#define __MACROS_INCLUDE__
#endif

Vamos a guardar estos cambios en el archivo Macros.mqh en la carpeta actual.


Clase de gestión del capital СMoney

En esta clase añadiremos un método para calcular el beneficio de una posición virtual considerando el factor de escala de su volumen. En realidad, realizaremos una operación similar en el método Volume() para determinar el tamaño calculado de la posición virtual: basándonos en la información sobre el tamaño actual del balance disponible para negociar y el tamaño del balance correspondiente al volumen de la posición virtual, encontraremos un factor de escala igual a la relación de estos balances. A continuación, el volumen de la posición virtual se multiplicará por este coeficiente para obtener el volumen calculado, es decir, el volumen que se abrirá en la cuenta comercial.

Por lo tanto, primero sacaremos del método Volume() la parte del código que encuentra el factor de escala al método Coeff() aparte:

//+------------------------------------------------------------------+
//| Calculate the virtual position volume scaling factor             |
//+------------------------------------------------------------------+
double CMoney::Coeff(CVirtualOrder *p_order) {
   // Request the normalized strategy balance for the virtual position
   double fittedBalance = p_order.FittedBalance();

   // If it is 0, then the scaling factor is 1
   if(fittedBalance == 0.0) {
      return 1;
   }

   // Otherwise, find the value of the total balance for trading
   double totalBalance = s_fixedBalance > 0 ? s_fixedBalance : AccountInfoDouble(ACCOUNT_BALANCE);

   // Return the volume scaling factor
   return totalBalance * s_depoPart / fittedBalance;
}

Después, la implementación de los métodos Volume() y Profit() se volverá muy similar: tomamos el valor deseado (volumen o beneficio) de la posición virtual y lo multiplicaremos por el factor de escala obtenido:

//+------------------------------------------------------------------+
//| Determine the calculated size of the virtual position            |
//+------------------------------------------------------------------+
double CMoney::Volume(CVirtualOrder *p_order) {
   return p_order.Volume() * Coeff(p_order);
}

//+------------------------------------------------------------------+
//| Determining the calculated profit of a virtual position          |
//+------------------------------------------------------------------+
double CMoney::Profit(CVirtualOrder *p_order) {
   return p_order.Profit() * Coeff(p_order);
}

Y, por supuesto, tendremos que añadir nuevos métodos a la descripción de la clase:

//+------------------------------------------------------------------+
//| Basic money management class                                     |
//+------------------------------------------------------------------+
class CMoney {
   ...
   
   // Calculate the scaling factor of the virtual position volume
   static double     Coeff(CVirtualOrder *p_order);

public:
   CMoney() = delete;                  // Disable the constructor
   
   // Determine the calculated size of the virtual position
   static double     Volume(CVirtualOrder *p_order);
   
   // Determine the calculated profit of a virtual position  
   static double     Profit(CVirtualOrder *p_order);  

   ...
};

Guardaremos los cambios realizados en el archivo Money.mqh en la carpeta actual.


Clase CVirtualFactory

Como la clase de gestor de riesgos que hemos creado es heredera de la clase CFactorable, tendremos que extender los objetos creados por CVirtualFactory para permitir su creación. Después añadiremos un bloque de código dentro del método estático Create() responsable de crear un objeto de la clase CVirtualRiskManager:

//+------------------------------------------------------------------+
//| Object factory class                                             |
//+------------------------------------------------------------------+
class CVirtualFactory {
public:
   // Create an object from the initialization string
   static CFactorable* Create(string p_params) {
      // Read the object class name
      string className = CFactorable::ReadClassName(p_params);
      
      // Pointer to the object being created
      CFactorable* object = NULL;

      // Call the corresponding constructor  depending on the class name
      if(className == "CVirtualAdvisor") {
         object = new CVirtualAdvisor(p_params);
      } else if(className == "CVirtualRiskManager") {
         object = new CVirtualRiskManager(p_params);
      } else if(className == "CVirtualStrategyGroup") {
         object = new CVirtualStrategyGroup(p_params);
      } else if(className == "CSimpleVolumesStrategy") {
         object = new CSimpleVolumesStrategy(p_params);
      }
      
      ...

      return object;
   }
};

Guardaremos el código obtenido en el archivo VirtualFactory.mqh en la carpeta actual.


Clase CVirtualAdvisor

En la clase CVirtualAdvisor tendremos que hacer cambios más significativos. Como hemos decidido que el objeto de gestor de riesgos solo se utilizará dentro de esta clase, añadiremos la propiedad correspondiente a la descripción de la clase:

//+------------------------------------------------------------------+
//| Class of the EA handling virtual positions (orders)              |
//+------------------------------------------------------------------+
class CVirtualAdvisor : public CAdvisor {
protected:
   CVirtualReceiver     *m_receiver;      // Receiver object that brings positions to the market
   CVirtualInterface    *m_interface;     // Interface object to show the status to the user
   CVirtualRiskManager  *m_riskManager;   // Risk manager object

   ...
};

Asimismo, acordaremos que la cadena de inicialización del gestor de riesgos se incorporará en la cadena de inicialización del asesor experto justo después de la línea de inicialización del grupo de estrategias. A continuación, en el constructor añadiremos la lectura de esta cadena de inicialización a la variable riskManagerParams, y luego crearemos el gestor de riesgos de la misma:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CVirtualAdvisor::CVirtualAdvisor(string p_params) {
// Save the initialization string
   m_params = p_params;

// Read the initialization string of the strategy group object
   string groupParams = ReadObject(p_params);

// Read the initialization string of the risk manager object
   string riskManagerParams = ReadObject(p_params);

// Read the magic number
   ulong p_magic = ReadLong(p_params);

// Read the EA name
   string p_name = ReadString(p_params);

// Read the work flag only at the bar opening
   m_useOnlyNewBar = (bool) ReadLong(p_params);

// If there are no read errors,
   if(IsValid()) {
      ...
      
      // Create the risk manager object 
      m_riskManager = NEW(riskManagerParams);
   }
}

Ya que hemos creado el objeto en el constructor, deberemos encargarnos de su eliminación en el destructor:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
void CVirtualAdvisor::~CVirtualAdvisor() {
   if(!!m_receiver)     delete m_receiver;      // Remove the recipient
   if(!!m_interface)    delete m_interface;     // Remove the interface
   if(!!m_riskManager)  delete m_riskManager;   // Remove risk manager
   DestroyNewBar();           // Remove the new bar tracking objects 
}

Pues bien, lo más importante será llamar al manejador Tick() del gestor de riesgos desde el manejador del asesor experto correspondiente. Tenga en cuenta que el gestor de riesgos se inicia antes de que se corrijan los volúmenes de mercado, de modo que si se superan los límites de pérdidas o, por el contrario, si se actualizan los límites, el receptor puede corregir los volúmenes abiertos de las posiciones de mercado en el mismo tick:

//+------------------------------------------------------------------+
//| OnTick event handler                                             |
//+------------------------------------------------------------------+
void CVirtualAdvisor::Tick(void) {
// Define a new bar for all required symbols and timeframes
   bool isNewBar = UpdateNewBar();

// If there is no new bar anywhere, and we only work on new bars, then exit
   if(!isNewBar && m_useOnlyNewBar) {
      return;
   }

// Receiver handles virtual positions
   m_receiver.Tick();

// Start handling in strategies
   CAdvisor::Tick();

// Risk manager handles virtual positions
   m_riskManager.Tick();

// Adjusting market volumes
   m_receiver.Correct();

// Save status
   Save();

// Render the interface
   m_interface.Redraw();
}

Guardaremos los cambios realizados en el archivo VirtualAdvisor.mqh en la carpeta actual.


Asesor experto SimpleVolumesExpertSingle

Para probar el gestor de riesgos, solo tendremos que añadir la posibilidad de especificar sus parámetros en el asesor experto y la formación de la cadena de inicialización necesaria. Por ahora, pondremos los seis parámetros del gestor de riesgos en variables de entrada separadas del asesor experto:

input group "===  Risk management"
input bool        rmIsActive_             = true;
input double      rmStartBaseBalance_     = 10000;
input ENUM_RM_CALC_LIMIT 
                  rmCalcDailyLossLimit_   = RM_CALC_LIMIT_FIXED;
input double      rmMaxDailyLossLimit_    = 200;
input ENUM_RM_CALC_LIMIT 
                  rmCalcOverallLossLimit_ = RM_CALC_LIMIT_FIXED;
input double      rmMaxOverallLossLimit_  = 500;

En la función OnInit() tendremos que añadir la creación de la cadena de inicialización del gestor de riesgos e incorporarla en la cadena de inicialización del asesor experto. Al mismo tiempo, reescribiremos ligeramente el código para crear cadenas de inicialización para una estrategia y un grupo que incluya una de estas estrategias, asignando las cadenas de inicialización de los objetos individuales a variables diferentes:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   CMoney::FixedBalance(fixedBalance_);
   CMoney::DepoPart(1.0);

// Prepare the initialization string for a single strategy instance
   string strategyParams = StringFormat(
                              "class CSimpleVolumesStrategy(\"%s\",%d,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%d)",
                              Symbol(), Period(),
                              signalPeriod_, signalDeviation_, signaAddlDeviation_,
                              openDistance_, stopLevel_, takeLevel_, ordersExpiration_,
                              maxCountOfOrders_
                           );

// Prepare the initialization string for a group with one strategy instance
   string groupParams = StringFormat(
                           "class CVirtualStrategyGroup(\n"
                           "       [\n"
                           "        %s\n"
                           "       ],%f\n"
                           "    )",
                           strategyParams, scale_
                        );

// Prepare the initialization string for the risk manager
   string riskManagerParams = StringFormat(
                                 "class CVirtualRiskManager(\n"
                                 "       %d,%.2f,%d,%.2f,%d,%.2f"
                                 "    )",
                                 rmIsActive_, rmStartBaseBalance_,
                                 rmCalcDailyLossLimit_, rmMaxDailyLossLimit_,
                                 rmCalcOverallLossLimit_, rmMaxOverallLossLimit_
                              );

// Prepare the initialization string for an EA with a group of a single strategy and the risk manager
   string expertParams = StringFormat(
                            "class CVirtualAdvisor(\n"
                            "    %s,\n"
                            "    %s,\n"
                            "    %d,%s,%d\n"
                            ")",
                            groupParams,
                            riskManagerParams,
                            magic_, "SimpleVolumesSingle", true
                         );

   PrintFormat(__FUNCTION__" | Expert Params:\n%s", expertParams);

// Create an EA handling virtual positions
   expert = NEW(expertParams);

   if(!expert) return INIT_FAILED;

   return(INIT_SUCCEEDED);
}

Guardaremos el código obtenido en el archivo SimpleVolumesExpertSingle.mq5 en la carpeta actual. Ahora todo está listo para poner a prueba el funcionamiento del gestor de riesgos.


Pruebas de funcionamiento

Tomaremos los parámetros de una de las instancias de la estrategia comercial obtenidos durante el proceso de optimización en las fases anteriores de desarrollo. Llamaremos a este ejemplo de estrategia comercial estrategia modelo. Los parámetros de la estrategia modelo se muestran en la fig. 1.

Fig. 1. Parámetros de la estrategia modelo

Vamos a realizar una única pasada del simulador con estos parámetros y el gestor de riesgos desactivado para el intervalo 2021-2022. Obtendremos los siguientes resultados:

Fig. 2. Resultados de la estrategia modelo sin gestor de riesgos

El gráfico muestra que se han producido varias reducciones notables en los fondos durante el periodo de tiempo seleccionado. Las mayores se han reunido a finales de octubre de 2021 (~380 $) y junio de 2022 (~840 $).

Ahora activaremos el gestor de riesgos y fijaremos el límite máximo de pérdida diaria en 150$ y el límite máximo de pérdida total en 450$. Obtendremos los siguientes resultados:


Fig. 3. Resultados de la estrategia modelo con gestor de riesgos (pérdidas máximas: 150$ y 450$)

El gráfico muestra que en octubre de 2021, el gestor de riesgos ha cerrado dos veces posiciones de mercado no rentables, pero las posiciones virtuales han seguido abiertas. Por lo tanto, al llegar el día siguiente, se han reabierto las posiciones de mercado. Lamentablemente, la reapertura se ha producido a un precio menos favorable, por lo que la reducción total de balance y fondos ha sido ligeramente superior a la reducción de fondos con el gestor de riesgos desconectado. También podemos observar que tras cerrar posiciones con la estrategia, en lugar de obtener un pequeño beneficio (como en el caso sin gestor de riesgos), se ha producido alguna pérdida.

En junio de 2022, el gestor de riesgos ya se ha activado siete veces, cerrando posiciones de mercado cuando la pérdida diaria alcanzaba los 150 dólares. De nuevo resulta que la reapertura se ha producido a precios menos favorables, y el resultado de esta serie de transacciones ha sido una pérdida. Pero si este asesor experto negociara en una cuenta demo de una empresa de prop-trading con estos parámetros de pérdidas máximas permitidas diarias y totales, entonces sin un gestor de riesgos la cuenta se detendría por incumplir las reglas comerciales, mientras que con un gestor de riesgos la cuenta continuaría su trabajo, obteniendo un beneficio ligeramente inferior.

Aunque hemos fijado el valor de la pérdida total en 450$ y la reducción total del balance ha sido de más de 1 000 $ en junio, no se ha alcanzado la pérdida máxima total porque esta se calcula a partir del balance básico. Es decir, se alcanzará si los fondos caen por debajo de (10000 - 450) = 9550$. Sin embargo, gracias al beneficio acumulado anteriormente, el tamaño de los fondos en ese momento definitivamente no ha caído por debajo de 10 000$. Por lo tanto, el asesor experto ha continuado su trabajo, acompañado por la apertura de posiciones de mercado.

Ahora vamos a simular la activación del límite total de pérdidas. Para ello, aumentaremos el factor de escala de los tamaños de posición de modo que en octubre de 2021 aún no se supere la pérdida máxima total, pero en junio de 2022 sí. Vamos a establecer un valor del parámetro scale_ = 50 y a ver el resultado:

Fig. 4. Resultados de la estrategia modelo con gestor de riesgos (pérdidas máximas: 150$ y 450$), scale = 50

Como podemos ver, la negociación finaliza en junio de 2022, el EA no ha abierto ni una sola posición en el periodo posterior. Esto ha provocado que se alcance el límite total de pérdidas (9550 $).  También podemos observar que ahora la pérdida diaria se alcanza con más frecuencia, lo cual significa que no solo se ha encontrado en octubre de 2021, sino también en varios otros lugares de la línea temporal.

Así que ambos límites funcionan correctamente.

El gestor de riesgos desarrollado puede resultar útil incluso sin utilizar el comercio en cuentas de prop-trading. A modo de ilustración, intentaremos optimizar los parámetros del gestor de riesgos de nuestra estrategia modelo, tratando de aumentar el tamaño de las posiciones abiertas, pero sin superar al mismo tiempo la reducción permitida del 10%. Para ello, fijaremos una pérdida total máxima igual al 10% del nivel diario en los parámetros del gestor de riesgos. La pérdida máxima diaria, también calculada como porcentaje del nivel diario, se revisará igualmente durante la optimización.


Fig. 5. Resultados de la optimización de la estrategia modelo con un gestor de riesgos

Los resultados obtenidos nos permiten afirmar que la rentabilidad normalizada en un año usando el gestor de riesgos ha aumentado casi una vez y media: de 1560$ a 2276$ (este es el resultado de la segunda columna Result). Aquí tenemos la mejor pasada por separado:

Fig. 6. Resultados de la estrategia modelo con gestor de riesgos (pérdidas máximas: 7.6% y 10%, scale_ = 88)

Cabe señalar que durante todo el periodo de prueba el asesor experto ha continuado abriendo operaciones. Por tanto, nunca se ha superado el límite global del 10%. Está claro que no tiene sentido aplicar el gestor de riesgos a instancias individuales de estrategias comerciales, ya que no tenemos previsto ejecutarlas en una cuenta real de forma individual. Pero lo que funciona para una única instancia debería funcionar de forma similar para un asesor experto con muchas instancias. Así pues, incluso estos someros resultados sugieren que un gestor de riesgos puede aportar valor añadido.


    Conclusión

    Bien, ya hemos realizado un implementación básica de un gestor de riesgos para la negociación que nos permite respetar los niveles especificados de pérdidas máximas diarias y totales permitidas. Todavía no permite guardar y cargar el estado al reiniciar el asesor experto, así que no resulta aconsejable utilizarlo al negociar en una cuenta real. No obstante, esta mejora no es muy difícil, volveremos a ella en el futuro.

    Al mismo tiempo, podríamos intentar añadir posibilidades para restringir la negociación en distintos periodos de tiempo, empezando por la exclusión de la negociación en determinadas horas de determinados días de la semana, y terminando con la prohibición de abrir nuevas posiciones en intervalos de tiempo en torno a los momentos de publicación de noticias económicas importantes. Otras posibles direcciones de desarrollo del gestor de riesgos son un cambio más suave del tamaño de las posiciones (por ejemplo, reducción en 2 veces cuando se supera la mitad del límite), y una recuperación más "inteligente" de los volúmenes (por ejemplo, solo cuando la pérdida supere el nivel en el que se ha producido la reducción del tamaño de las posiciones).

    De momento, dejaremos esto para más adelante; por ahora vamos a volver a la automatización del proceso de optimización del asesor experto desarrollado. El primer paso ya se ha implementado en el artículo anterior, es hora de empezar el segundo paso.

    Gracias por su atención, ¡hasta la próxima!


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

    Archivos adjuntos |
    Macros.mqh (2.4 KB)
    Money.mqh (6.52 KB)
    VirtualFactory.mqh (4.46 KB)
    VirtualAdvisor.mqh (23.02 KB)
    Yuriy Bykov
    Yuriy Bykov | 29 may 2024 en 07:33

    Gracias por los comentarios.

    Правильно я понимаю, что при контроле рисков в методе DailyLoss() не учитывается риск просадки по еквити?

    Probablemente equivocado. El método DailyLoss() no evalúa la magnitud de la reducción. Sólo convierte el nivel de reducción máximo especificado a la divisa de la cuenta en porcentaje si es necesario. La comparación en sí tiene lugar en el método CheckDailyLimit():

    if(m_dailyProfit < -DailyLoss() && CMoney::DepoPart() > 0) { ... }

    El valor de m_dailyProfit se actualiza en cada tick y se calcula como la diferencia de los fondos actuales (capital) y el nivel diario(el máximo del valor del saldo y los fondos al inicio del periodo diario):

    m_dailyProfit = m_equity - m_baseDailyLevel;

    Así que parece que la reducción de los fondos sólo se tiene en cuenta. ¿O he entendido mal la pregunta?


    ¿Por qué se utilizan macros cuando se trabaja con matrices?

    Para compactar el código. Las macros también permiten pasar un bloque de código como parámetro, mientras que cuando se implementan estas operaciones mediante funciones, no se puede pasar un bloque de código a las funciones como parámetro.

    Aleksandr Seredin
    Aleksandr Seredin | 29 may 2024 en 19:12
    Yuriy Bykov cada tick y se calcula como la diferencia entre los fondos actuales (capital) y el nivel diario(el máximo del saldo y los fondos al inicio del periodo diario):

    Así que parece que la reducción de los fondos sólo se tiene en cuenta. ¿O he entendido mal la pregunta?


    Para compactar el código. Además, las macros permiten pasar un bloque de código como parámetro, mientras que cuando se implementan estas operaciones mediante funciones, no se puede pasar un bloque de código a las funciones como parámetro.

    Muchas gracias por tu extensa respuesta )) ¡Esperaremos nuevos artículos! )

    pensaval
    pensaval | 19 oct 2024 en 19:28

    Estimado Yuriy,

    Estoy tratando de compilar el código pero obtengo el siguiente error en VirtualRiskManager.mqh:

    "Changed - undeclared identifier" en la línea CVirtualReceiver::Instance().Changed(); // Notificar los cambios al destinatario

    He revisado el código múltiples veces pero no hay manera. Me podéis explicar qué me estoy perdiendo?

    Espero el próximo artículo de esta serie.

    Gracias

    Yuriy Bykov
    Yuriy Bykov | 21 oct 2024 en 16:52

    ¡Hola!

    Pido disculpas, olvidé adjuntar al menos un archivo más en el que se realizaron ediciones. A partir de la parte 16, un archivo completo de los archivos del proyecto se adjunta a cada artículo. Lo adjuntaré aquí para este artículo.

    pensaval
    pensaval | 21 oct 2024 en 20:08
    Yuriy Bykov del proyecto. Lo adjuntaré aquí para este artículo.
    Muchas gracias
    Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 2): Añadir controles y capacidad de respuesta Creación de una interfaz gráfica de usuario interactiva en MQL5 (Parte 2): Añadir controles y capacidad de respuesta
    Mejorar el panel GUI de MQL5 con funciones dinámicas puede mejorar significativamente la experiencia comercial de los usuarios. Al incorporar elementos interactivos, efectos de desplazamiento y actualizaciones de datos en tiempo real, el panel se convierte en una herramienta poderosa para los traders modernos.
    Cómo integrar los conceptos de dinero inteligente (Smart Money Concepts, SMC) junto con el indicador RSI en un EA Cómo integrar los conceptos de dinero inteligente (Smart Money Concepts, SMC) junto con el indicador RSI en un EA
    Concepto de dinero inteligente (ruptura de estructura) junto con el indicador RSI para tomar decisiones comerciales automatizadas informadas basadas en la estructura del mercado.
    Desarrollamos un asesor experto multidivisa (Parte 13): Automatización de la segunda fase: selección en grupos Desarrollamos un asesor experto multidivisa (Parte 13): Automatización de la segunda fase: selección en grupos
    Ya hemos puesto en marcha la primera fase del proceso de optimización automatizada. Para distintos símbolos y marcos temporales, realizamos la optimización utilizando varios criterios y almacenamos información sobre los resultados de cada pasada en la base de datos. Ahora vamos a seleccionar los mejores grupos de conjuntos de parámetros de entre los encontrados en la primera etapa.
    Desarrollo de un robot en Python y MQL5 (Parte 2): Selección, creación y entrenamiento de modelos, simulador personalizado en Python Desarrollo de un robot en Python y MQL5 (Parte 2): Selección, creación y entrenamiento de modelos, simulador personalizado en Python
    Hoy vamos a continuar con la serie de artículos sobre la creación de un robot comercial en Python y MQL5. En el presente artículo, resolveremos el problema de la selección y el entrenamiento de modelos, la prueba de los mismos, la aplicación de la validación cruzada, la búsqueda en cuadrícula y el problema del ensamblaje de modelos.