preview
Gestión de Riesgo (Parte 4): Finalizando los Métodos Clave de la Clase

Gestión de Riesgo (Parte 4): Finalizando los Métodos Clave de la Clase

MetaTrader 5Ejemplos |
347 0
Niquel Mendoza
Niquel Mendoza


Introducción

En continuación al artículo anterior sobre gestión de riesgo, explicamos las principales variables y algunos métodos iniciales para nuestra clase especializada, en esta ocasión nos enfocaremos en completar los métodos necesarios para verificar si se superaron los límites máximos establecidos de pérdida o ganancia. Adicionalmente, introduciremos dos enfoques dinámicos para gestionar el riesgo por operación.


Descripción del contenido del artículo

Iniciaremos este artículo optimizando ciertas funciones, así como el constructor y destructor de la clase. Luego definiremos nuevas estructuras, enumeraciones y constantes esenciales. A continuación, implementaremos

funciones para gestionar las posiciones abiertas por el Expert Advisor (EA), incluyendo mecanismos específicos para determinar si se han superado las pérdidas o ganancias máximas establecidas. Finalmente, como elemento adicional, incorporaremos métodos para manejar el riesgo dinámico por operación.


Nuevas enumeraciones y estructuras

Ahora comenzaremos definiendo nuevas estructuras, enumeraciones y constantes que utilizaremos más adelante en nuestro artículo.

1. Riesgo dinámico por operación

Como mencioné anteriormente, también he agregado un nuevo concepto: el riesgo dinámico por operación.

¿En qué consiste el riesgo dinámico por operación?

El riesgo dinámico por operación, como su nombre indica, no es un valor fijo, sino variable, que cambia según las ganancias o pérdidas respecto a un balance inicial determinado. Esto puede resultar especialmente útil para proteger la cuenta de trading. Por ejemplo, si el balance disminuye a ciertos porcentajes respecto al balance inicial, podemos ajustar automáticamente el riesgo para minimizar pérdidas adicionales. Esto implica que cada vez que se alcance un nivel establecido, el riesgo por operación se modifica.

Veamos un ejemplo práctico representado en una tabla:

Supongamos que la cuenta tiene un balance inicial de 10000 USD.

Elegiremos los siguientes porcentajes para modificar el riesgo: 3%, 5% y 7%.

Condición Nuevo porcentaje
  Si el balance disminuye un 3% (300 USD, ósea por debajo de  9700 USD)  Riesgo ajustado a 0.7%
  Si el balance disminuye un 5% (500 USD, ósea por debajo de 9500 USD)  Riesgo ajustado a 0.5%
  Si el balance disminuye un 7% (700 USD, ósea por debajo de 9300 USD)  Riesgo ajustado a 0.25%

Notas importantes sobre los parámetros y funcionamiento del riesgo dinámico:

  • Todos los valores mencionados en este artículo son parámetros completamente ajustables según las necesidades y preferencias del usuario. Esto incluye los porcentajes que determinan cuándo se modifica el riesgo y el balance inicial sobre el cual se aplican dichos porcentajes.

El balance inicial puede determinarse de dos formas distintas:

  • Cuenta tipo PropFirm (como FTMO): Si se selecciona esta opción mediante el parámetro de manejo del riesgo (profitm_ftmo), el balance inicial debe ingresarse manualmente por medio de un parámetro input en la configuración del Expert Advisor. Este balance será fijo durante toda la operativa, es decir, no variará a lo largo del tiempo, sino que será un valor inicial estático definido por el usuario al comenzar a operar.

  • Cuenta personal: En el caso de seleccionar este tipo de cuenta (personal_account), el balance inicial será determinado automáticamente por el EA al llamar a la función AccountInfoDouble(), la cual devolverá el balance actual de la cuenta en el momento en que se inicializa el sistema. Este balance será entonces dinámico y corresponderá exactamente al valor actual disponible en la cuenta del usuario.
Ventajas de implementar un riesgo dinámico por operación:
  • Disminuye significativamente el riesgo de "quemar" o perder totalmente la cuenta de trading.

  • Aumenta la seguridad general en las operaciones, reduciendo la exposición ante rachas negativas prolongadas.

Desventajas del riesgo dinámico:

  • El proceso de recuperación tras una racha negativa o una pérdida considerable puede ser más lento, ya que se reduce la exposición de manera preventiva. Aunque esta recuperación es más lenta, es más segura y controlada.

Explicación detallada sobre la estructura necesaria para implementar el riesgo dinámico:

Para gestionar y ajustar correctamente el riesgo dinámico por operación, necesitaremos acceder y modificar la propiedad llamada assigned_percentage dentro de la variable denominada gmlpo. Para poder llevar a cabo estas modificaciones automáticas en función del balance, será necesario crear una estructura que contenga dos elementos fundamentales:

  • El balance específico en el cual se activará la modificación del riesgo (por ejemplo, cuando el balance cae a 9700 USD desde un balance inicial de 10000 USD, como indica la tabla).

  • El nuevo porcentaje de riesgo que se debe aplicar al balance al alcanzar dicho punto (por ejemplo, modificar el riesgo a 0.7% en ese caso particular).

Originalmente, podríamos pensar en crear una estructura simple con dos variables dobles (double) individuales, pero esta solución no es suficiente. En su lugar, utilizaremos dos arrays separados dentro de la estructura.

¿Por qué utilizar arrays en vez de variables individuales?

Utilizamos arrays en lugar de variables individuales porque necesitamos ordenar fácilmente múltiples valores relacionados. La necesidad del ordenamiento surge debido al método específico que emplearemos para gestionar el riesgo dinámico de manera eficiente y rápida. En particular, usaremos la función ArraySort para ordenar los balances de activación del riesgo (balance_to_activate_the_risk[]).

¿Por qué es necesario utilizar un método de ordenamiento como ArraySort?

La razón principal es que implementaremos un método sin bucles para verificar continuamente si el balance actual ha superado ciertos límites definidos para modificar el riesgo. Este enfoque sin bucles es elegido para optimizar el rendimiento y la velocidad del sistema, especialmente importante cuando se realizan comprobaciones frecuentes (por ejemplo, en cada tick o al cierre de cada operación).

Si no ordenamos los valores correctamente de menor a mayor, podríamos enfrentar serios problemas. Consideremos un escenario práctico para aclarar esta situación:

Supongamos que definimos inicialmente estos porcentajes:

  • Primer porcentaje a modificar: 3% (balance de activación: 9700 USD)

  • Segundo porcentaje a modificar: 7% (balance de activación: 9300 USD)

  • Tercer porcentaje a modificar: 5% (balance de activación: 9500 USD)

Como se puede observar, estos valores no están ordenados correctamente (el segundo porcentaje es mayor que el tercero). El problema con esta falta de orden surge porque nuestro método sin bucles utiliza una variable entera (como un contador) para llevar control del estado actual del riesgo.

Imaginemos qué sucedería:

  • Cuando el balance inicial cae un 3% (hasta 9700 USD), el contador aumenta en uno, ajustando el riesgo a 0.7%.

  • Luego, cuando el balance cae al siguiente nivel (7%), la cuenta alcanzaría 9300 USD, y el contador aumentaría nuevamente, saltándose completamente el valor intermedio (5% con balance 9500 USD).

  • El valor intermedio (9500 USD) quedaría sin utilizar, creando confusión y problemas graves en el cálculo, pues no se ajustaría correctamente el riesgo dinámico.

Además, si la cuenta comienza a recuperarse desde el nivel más bajo, en este caso erróneamente 5%, para volver al valor anterior tendría que superar el valor de 9300 (Lo cual es incorrecto). Pero como el método original no consideró correctamente el orden, la recuperación tampoco funcionaría adecuadamente, dando como resultado fallas adicionales.

Por estas razones, el ordenamiento correcto es esencial y crucial para asegurar el funcionamiento óptimo del método sin bucles. La estructura más sencilla que planteamos inicialmente es la siguiente:

struct Dynamic_gmlpo
{
   double balance_to_activate_the_risk[];
   double risk_to_be_adjusted[];
};

Sin embargo, aunque esta estructura es sencilla, todavía existe una limitación importante. Como estos dos arrays representan valores clave-valor (balance - porcentaje de riesgo ajustado), mantener esta relación intacta al ordenar es fundamental. Es aquí donde entra en juego la estructura CHashMap.

La implementación con CHashMap nos permite asociar directamente cada balance específico con su correspondiente porcentaje de riesgo. Al ordenar el array principal (de balances), automáticamente se mantendrá intacta la relación con el riesgo correspondiente. De esta manera, se garantiza la precisión total de cada operación y cálculo.

En conclusión, lo que implementamos hasta ahora es una solución simple inicial utilizando dos arrays separados para simplificar temporalmente el uso del método ArraySort. Sin embargo, para lograr una implementación más robusta y precisa, especialmente si se planean manejar múltiples pares clave-valor de manera ordenada y eficiente, recomendamos utilizar posteriormente CHashMap. Esta estructura asegura que cada balance quede correctamente enlazado a su respectivo riesgo ajustado, evitando errores potenciales al ordenar y consultar valores dinámicos.

Más adelante, en el artículo abordaremos en detalle cómo implementar correctamente esta solución con CHashMap, mostrando ejemplos prácticos y explicaciones paso a paso.

Verificación del balance:

Siguiendo con las enumeraciones necesarias para implementar adecuadamente el riesgo dinámico, debemos agregar dos opciones adicionales para definir claramente cuándo se realizará la verificación del balance. Esta verificación es fundamental porque determinará el momento exacto en el que evaluaremos si el balance actual ha caído por debajo de los porcentajes definidos previamente, activando así la modificación del riesgo dinámico.

Existen dos tipos principales de verificaciones:

  1. Verificación en cada tick del mercado: En este método, la comprobación se realiza constantemente, en cada movimiento de precio (tick) del mercado. Esta opción es muy precisa, ya que verifica continuamente si la equidad (balance actual) ha caído por debajo de un umbral específico. Sin embargo, tiene un inconveniente importante: al realizar comparaciones constantes con la equidad actual, pueden surgir situaciones incómodas o poco eficientes.

    Por ejemplo, supongamos que el balance inicial es de 10000 USD, y el primer porcentaje de activación del riesgo es cuando el balance llega a 9700 USD. Si la equidad está fluctuando entre 9701 y 9699 constantemente, la modificación del riesgo dinámico se activará y desactivará repetidamente, lo que puede ser molesto y generar un consumo innecesario de recursos debido a la alta frecuencia con la que se realiza esta verificación.

  2. Verificación al cierre de operaciones: Este segundo método realiza la verificación únicamente cuando se cierra una operación, en lugar de hacerlo en cada tick. Esta opción es más eficiente en términos de uso de recursos del sistema, ya que solo verifica el balance en momentos específicos. Sin embargo, puede ser menos precisa, ya que la verificación solo ocurre al cierre de operaciones y podría ignorar fluctuaciones intermedias importantes que podrían haber activado un ajuste de riesgo dinámico oportuno.

Para facilitar la selección entre estos dos métodos, definiremos claramente estas opciones utilizando una enumeración en el código:

//---
enum ENUM_REVISION_TYPE
 {
  REVISION_ON_CLOSE_POSITION, //Check GMLPO only when closing positions
  REVISION_ON_TICK //Check GMLPO on all ticks
 };

De esta manera, proporcionamos al usuario una clara opción sobre cómo y cuándo realizar la verificación del balance, permitiendo ajustar el comportamiento del riesgo dinámico de acuerdo con sus necesidades específicas y preferencias respecto al rendimiento y precisión del sistema.

Modo de GMLPO:

Para gestionar el modo en el que se aplica el riesgo dinámico por operación (GMLPO), utilizaremos una enumeración específica que ofrece tres opciones claramente diferenciadas. A continuación, explicaré detalladamente el propósito de cada una de estas opciones y por qué decidí implementarlas mediante una enumeración.

Originalmente, para establecer los porcentajes específicos en los cuales se modificaba el riesgo y los nuevos porcentajes de riesgo asociados, se empleaban cadenas de texto (strings) introducidas por el usuario. En este método inicial, el usuario debía escribir manualmente en una cadena (string input) los porcentajes negativos de balance que activarían el cambio del riesgo, y en otra cadena separada, los nuevos valores de riesgo que se aplicarían. Aunque funcional, este enfoque presentaba un problema significativo: las cadenas de texto no pueden optimizarse automáticamente mediante la función de optimización del Expert Advisor (EA). Esto hacía que la tarea de ajustar y probar diferentes escenarios fuera lenta, poco práctica y bastante tediosa.

Para superar estas limitaciones, decidí implementar una enumeración que permite seleccionar fácilmente entre tres modos claramente definidos, ofreciendo flexibilidad y facilidad en la optimización:

  1. DYNAMIC_GMLPO_FULL_CUSTOM: Este modo es completamente personalizable y permite al usuario especificar manualmente múltiples porcentajes de activación del riesgo y los nuevos porcentajes asociados. Aunque en este modo se mantiene el uso de strings, permite al usuario definir cuantas modificaciones desee, proporcionando máxima flexibilidad, pero sacrificando la posibilidad de optimización automática.

  2. DYNAMIC_GMLPO_FIXED_PARAMETERS: Este modo simplifica significativamente la configuración del riesgo dinámico al limitar el número máximo de modificaciones permitidas a cuatro. Aquí, el usuario especifica los porcentajes negativos del balance y sus respectivos porcentajes de riesgo directamente mediante parámetros numéricos, lo que facilita enormemente su optimización. Esta opción ofrece un equilibrio entre la personalización y la eficiencia en las pruebas y ajustes automáticos.

  3. NO_DYNAMIC_GMLPO: Este último modo desactiva por completo la función de riesgo dinámico. Es ideal para aquellos usuarios que prefieren mantener un riesgo fijo durante todas sus operaciones, sin cambios dinámicos basados en las fluctuaciones del balance.

La enumeración correspondiente a estas opciones es la siguiente:

//---
enum ENUM_OF_DYNAMIC_MODES_OF_GMLPO
 {
  DYNAMIC_GMLPO_FULL_CUSTOM, //Customisable dynamic risk per operation
  DYNAMIC_GMLPO_FIXED_PARAMETERS,//Risk per operation with fixed parameters
  NO_DYNAMIC_GMLPO //No dynamic risk for risk per operation
 };

Esta implementación mediante enumeración proporciona claridad, facilidad de uso y la posibilidad de optimizar fácilmente diferentes configuraciones para encontrar rápidamente la mejor solución según las preferencias y estrategias específicas del usuario.

2. Revisión de operaciones:

Para mejorar aún más nuestra gestión de riesgo, incorporaremos una funcionalidad específica que nos permitirá llevar un control preciso sobre todas las posiciones abiertas en la cuenta, ya sean estas abiertas por el usuario manualmente o por el bot.

En el caso de las posiciones abiertas por el EA, también podremos identificar claramente si un ticket corresponde a un número mágico asignado por dicho EA. Esto será particularmente útil para conocer exactamente cuántas posiciones tiene abiertas el EA, diferenciándolas fácilmente de otras posiciones abiertas manualmente.

Para lograr esto, definiremos una estructura sencilla pero efectiva, que almacenará la información básica de cada posición:

struct Positions
 {
  ulong              ticket; //position ticket
  ENUM_POSITION_TYPE type; //position type
 };

3. Superación de máximas ganancias o pérdidas:

Ahora, incluiremos una enumeración específica que indique qué criterios se tomarán en cuenta para determinar si se ha superado una máxima ganancia o pérdida establecida previamente.

La función encargada de realizar esta comprobación retornará true cuando las condiciones especificadas se cumplan según el criterio elegido.

La enumeración constará de tres posibles casos claramente diferenciados:

//--- Mode to check if a maximum loss or gain has been exceeded
enum MODE_SUPERATE 
 {
  EQUITY, //Only Equity
  CLOSE_POSITION, //Only for closed positions
  CLOSE_POSITION_AND_EQUITY//Closed positions and equity
 };
  1. EQUITY:
    Este criterio evalúa exclusivamente la equidad actual de la cuenta (es decir, el balance en tiempo real considerando posiciones abiertas y cerradas). No toma en cuenta las ganancias o pérdidas cerradas durante el día actual. La función indicará que se ha superado una máxima ganancia o pérdida únicamente cuando la equidad en tiempo real supere directamente el límite establecido.

  2. CLOSED_POSITIONS:
    Este método considera solamente las ganancias o pérdidas de posiciones que ya se han cerrado durante el día actual. Ignora completamente las posiciones abiertas y la equidad actual. Por lo tanto, la superación del límite se determina exclusivamente por el beneficio o pérdida acumulada en posiciones ya cerradas.

  3. CLOSED_POSITION_AND_EQUITY:
    Este es el método más completo y preciso, ya que combina ambas condiciones mencionadas anteriormente. La función evalúa conjuntamente tanto las ganancias o pérdidas de posiciones ya cerradas durante el día como la equidad actual en tiempo real. Esto significa que toma en cuenta el rendimiento global del día, proporcionando una evaluación más exacta y rigurosa sobre la superación de límites establecidos.

Al utilizar esta enumeración en la gestión de riesgo, permitimos que el usuario elija de manera flexible cómo desea realizar las comprobaciones de límites, adaptándose fácilmente a diversas estrategias y niveles de precisión requeridos en la gestión del riesgo.

Defines:

Continuando con las nuevas variables, es importante definir algunas constantes utilizando "defines".

Primero, estableceremos un define prefijo para identificar fácilmente las operaciones y mensajes generados por nuestro EA. Este prefijo puede incluir el nombre de tu EA, haciendo que sea fácil distinguir en los logs o comentarios generados por el sistema. Por ejemplo, en este caso utilizaremos:

#define EA_NAME "CRiksManagement | " //Prefix

Adicionalmente, será necesario definir varias constantes (flags o banderas) que usaremos posteriormente para gestionar de forma precisa tanto las posiciones abiertas como las órdenes pendientes. Estas banderas permitirán identificar rápidamente el tipo de operación (compra, venta, órdenes límite, órdenes stop, etc.), facilitando así el manejo eficiente del control de riesgo, cierres de operaciones y consultas específicas sobre la situación actual del mercado y nuestras posiciones.

A continuación se detallan estos defines específicos:

//--- positions
#define  FLAG_POSITION_BUY 2
#define  FLAG_POSITION_SELL 4

//--- orders
#define FLAG_ORDER_TYPE_BUY             1
#define FLAG_ORDER_TYPE_SELL            2
#define FLAG_ORDER_TYPE_BUY_LIMIT       4
#define FLAG_ORDER_TYPE_SELL_LIMIT      8
#define FLAG_ORDER_TYPE_BUY_STOP        16
#define FLAG_ORDER_TYPE_SELL_STOP       32
#define FLAG_ORDER_TYPE_BUY_STOP_LIMIT  64
#define FLAG_ORDER_TYPE_SELL_STOP_LIMIT 128
#define FLAG_ORDER_TYPE_CLOSE_BY        256

Estas constantes permitirán una implementación más clara y eficiente en futuras funciones, simplificando notablemente la lectura, mantenimiento y escalabilidad del código del Expert Advisor.


Mejoras en el constructor y en las funciones generales

Comenzaremos a optimizar la gestión del riesgo desde el constructor de la clase CRiskManagement. Ahora que hemos incorporado el riesgo dinámico por operación, se han añadido varias mejoras importantes:

Primero, se ha introducido claramente el tipo de lote utilizado (type_get_lot) y el parámetro relacionado con el balance inicial (account_propfirm_balance), útil cuando se usa una cuenta tipo PropFirm. Además, observarán que la definición EA_NAME aparecerá de manera constante en los comentarios informativos generados por las funciones principales de la clase. Esto nos ayudará a identificar rápidamente en los logs de la terminal.

La implementación mejorada del constructor queda de la siguiente manera:

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CRiskManagemet::CRiskManagemet(bool mdp_strict_, ENUM_GET_LOT type_get_lot_, ulong magic_number_ = NOT_MAGIC_NUMBER, ENUM_MODE_RISK_MANAGEMENT mode_risk_management_ = personal_account, double account_propfirm_balance = 0)
 {
  if(magic_number_ == NOT_MAGIC_NUMBER)
   {
    Print(EA_NAME, " (Warning) No magic number has been chosen, taking into account all the magic numbers and the user's trades");
   }
//---
  this.mdp_is_strict = mdp_strict_;
  this.type_get_lot = type_get_lot_;
//---
  this.account_balance_propfirm = account_propfirm_balance ;
  trade = new CTrade();
  trade.SetExpertMagicNumber(this.magic_number);
  this.account_profit = GetNetProfitSince(true, this.magic_number, D'1972.01.01 00:00');
  this.magic_number = magic_number_;
  this.mode_risk_managemet = mode_risk_management_;
  this.ActivateDynamicRiskPerOperation = false;
//---
  this.last_day_time = iTime(_Symbol, PERIOD_D1, 0);
  this.last_weekly_time = iTime(_Symbol, PERIOD_W1, 0);
  this.init_time = magic_number_ != NOT_MAGIC_NUMBER ? TimeCurrent() : D'1972.01.01 00:00';
//---
  this.positions_open = false;
  this.curr_profit = 0;
  UpdateProfit();
  
//---
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
    ulong position_ticket = PositionGetTicket(i);
    if(!PositionSelectByTicket(position_ticket))
      continue;
      
    ulong position_magic = PositionGetInteger(POSITION_MAGIC);
    ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

    if(position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER)
     {
      this.positions_open = true;
      Positions new_pos;
      new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      new_pos.ticket = position_ticket;
      ExtraFunctions::AddArrayNoVerification(open_positions, new_pos);
     }
   }
  
}

Se han agregado nuevas variables relevantes como:

  • curr_profit: almacena el beneficio actual, facilitando así la revisión continua del rendimiento.
  • ActivateDynamicRiskPerOperation: una variable booleana encargada de determinar si se utilizará o no el riesgo dinámico durante la operación del EA.
  • mdp_is_strict: variable que determinará si la gestión de riesgo supervisará de forma estricta el mdp
  • type_get_lot: variable que almacenará el tipo de lote.
Además de todo esto, agregamos un bucle, este bucle recopila todas las posiciones abiertas por el número mágico o, en caso este no se haya definido y las agrega al array de posiciones de la gestión de riesgo.

Mejoras en  el destructor:

En el destructor también hemos agregado importantes mejoras, especialmente para gestionar adecuadamente la memoria dinámica. Ahora se verifica claramente el puntero a la clase CTrade utilizando la función CheckPointer, asegurando así que se elimine únicamente si es dinámico, evitando posibles errores al liberar memoria.

La implementación optimizada del destructor queda así:

//+------------------------------------------------------------------+
//| Destructor                                                       |
//+------------------------------------------------------------------+
CRiskManagemet::~CRiskManagemet()
 {
  if(CheckPointer(trade) == POINTER_DYNAMIC) delete trade;
  ArrayFree(this.open_positions);
  ArrayFree(this.dynamic_gmlpos.balance_to_activate_the_risk);
  ArrayFree(this.dynamic_gmlpos.risk_to_be_adjusted);
 }

El uso adecuado de CheckPointer asegura que el puntero trade se libera correctamente solo cuando corresponde. Además, mediante la función ArrayFree, liberamos eficientemente la memoria utilizada por los arrays pertenecientes a la clase, manteniendo así una correcta gestión de memoria y una operativa más estable y eficiente del ea donde se implemente esta gestión de riesgo.

Mejoras generales

He incorporado diversas mejoras importantes para fortalecer la gestión general del riesgo y prevenir posibles errores en el funcionamiento del Expert Advisor (EA). A continuación, detallo claramente estas mejoras:

1. Validaciones en la obtención del lote y del stop loss (SL)

Se han agregado validaciones esenciales para garantizar que el valor del riesgo por operación (gmlpo.assigned_percentage) no sea inválido o nulo. Estas comprobaciones permiten detectar oportunamente errores críticos y emitir mensajes informativos al usuario, facilitando la corrección rápida de configuraciones incorrectas.

El criterio principal es verificar directamente gmlpo.assigned_percentage. Si este valor es menor o igual a cero, se imprime un mensaje crítico en la consola y la función termina retornando valores seguros para evitar que el EA opere de manera incorrecta.

  if(gmlpo.assigned_percentage <= 0)
   {
    PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value);
    return 0;
   }

Ejemplo en la función GetSL:

//+----------------------------------------------------------------------------------+
//| Get the ideal stop loss based on a specified lot and the maximum loss per trade  |
//+----------------------------------------------------------------------------------+
long CRiskManagemet::GetSL(const ENUM_ORDER_TYPE type, double DEVIATION = 100, double STOP_LIMIT = 50)
 {
   if(gmlpo.assigned_percentage <= 0)
   {
    PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value);
    return 0;
   }
 
  double lot;
  return CalculateSL(type, this.gmlpo.value, lot, DEVIATION, STOP_LIMIT);
 }


Ejemplo en la función GetLote:

//+-----------------------------------------------------------------------------------------------+
//| Function to obtain the ideal lot based on the maximum loss per operation and the stop loss    |
//+-----------------------------------------------------------------------------------------------+
double CRiskManagemet::GetLote(const ENUM_ORDER_TYPE order_type)
 {
  if(gmlpo.assigned_percentage <= 0)
   {
    PrintFormat("%s Critical error, the value of gmlpo.value(%.2f) is invalid", EA_NAME, this.gmlpo.value);
    this.lote = 0.00;
    return this.lote;
   }
   
//---
  if(this.type_get_lot == GET_LOT_BY_STOPLOSS_AND_RISK_PER_OPERATION)
   {
    double MaxLote = GetMaxLote(order_type);
    SetNMPLO(this.lote, MaxLote);
    PrintFormat("%s Maximum loss in case the next operation fails %.2f ", EA_NAME, this.nmlpo);
   }
  else
   {
    this.lote = GetLotByRiskPerOperation(this.gmlpo.value, order_type);
   }

//---
  return this.lote;
 }

2. Mejoras en la función de asignación de parámetros (SetEnums):

Esta función es crítica y debe ejecutarse obligatoriamente en el evento OnInit. Ahora incluye una validación adicional que permite asignar correctamente los valores monetarios o porcentuales según la elección del usuario. Esta validación evita asignar valores incorrectos o negativos, especialmente cuando se utiliza dinero fijo (money) como criterio.

Implementación mejorada de la función SetEnums:

//+----------------------------------------------------------------------------------------+
//| Function to set how losses or gains are calculated,                                    |
//| by percentage applied to (balance, equity, free margin or net profit) or simply money. |
//+----------------------------------------------------------------------------------------+
//Note: This method is mandatory, it must be executed in the OnInit event.
void CRiskManagemet::SetEnums(ENUM_RISK_CALCULATION_MODE mode_mdl_, ENUM_RISK_CALCULATION_MODE mode_mwl_, ENUM_RISK_CALCULATION_MODE mode_gmlpo_, ENUM_RISK_CALCULATION_MODE mode_ml_, ENUM_RISK_CALCULATION_MODE mode_mdp_)
 {
  this.gmlpo.mode_calculation_risk = mode_gmlpo_;
  this.mdl.mode_calculation_risk  = mode_mdl_;
  this.mdp.mode_calculation_risk  = mode_mdp_;
  this.ml.mode_calculation_risk  = mode_ml_;
  this.mwl.mode_calculation_risk  = mode_mwl_;
//-- En caso se haya escojido el modo dinero, asignamos la variable que guarda el dinero o porcentage alas varialbes correspondientes
  if(this.gmlpo.mode_calculation_risk  == money)
   {
    this.gmlpo.value = this.gmlpo.percentage_applied_to;
    this.ActivateDynamicRiskPerOperation = false;
   }
  else
    this.gmlpo.value = 0;
    
  this.mdp.value  = this.mdp.mode_calculation_risk  == money ? (this.mdp.percentage_applied_to > 0 ? this.mdp.percentage_applied_to : 0) : 0;
  this.mdl.value  = this.mdl.mode_calculation_risk  == money ? (this.mdl.percentage_applied_to > 0 ? this.mdl.percentage_applied_to : 0) : 0;
  this.ml.value  = this.ml.mode_calculation_risk  == money   ? (this.ml.percentage_applied_to  > 0 ? this.ml.percentage_applied_to  : 0) : 0;
  this.mwl.value  = this.mwl.mode_calculation_risk  == money ? (this.mwl.percentage_applied_to > 0 ? this.mwl.percentage_applied_to : 0) : 0;
 }


Nuevas variables para la clase

Continuando con las mejoras, agregaremos variables que nos permitirán llevar un control más preciso y efectivo sobre las operaciones abiertas.

Primero, definiremos un array específico para almacenar información sobre todas las posiciones abiertas en la cuenta, ya sean abiertas manualmente por el usuario o automáticamente por el EA:

  //---
  Positions          open_positions[];    

Además, incorporaremos dos variables adicionales fundamentales:

  • Una variable booleana denominada positions_open que indicará si existen o no posiciones abiertas en el mercado. Esta variable será crucial para optimizar el rendimiento y evitar comprobaciones innecesarias cuando no hay posiciones activas:

  //--- Boolean variable to check if there are any open operations by the EA or user
  bool               positions_open;

  • Una variable dobule llamada curr_profit que almacenará el beneficio o pérdida actual acumulada en tiempo real. Esta variable facilitará realizar cálculos rápidos sobre la situación actual de las operaciones:

  //--- Variable to store the current profit of the EA or user
  double             curr_profit;

Riesgo dinámico por operación:

Para gestionar eficientemente el riesgo dinámico por operación (GMLPO dinámico), es esencial agregar y definir claramente varias variables específicas en nuestra clase. Estas variables garantizarán un control efectivo, permitiendo que el riesgo se ajuste automáticamente según los parámetros definidos por el usuario. A continuación, explicamos detalladamente cada una:

1. Estructura para almacenar balances y riesgos dinámicos:

Utilizaremos una estructura personalizada denominada Dynamic_gmlpo, que contendrá dos arrays dinámicos:

  Dynamic_gmlpo      dynamic_gmlpos;

Esta estructura permite guardar múltiples niveles de balances específicos junto con sus porcentajes de riesgo asociados, facilitando así la gestión del riesgo dinámico.

2. Variable para definir el tipo de revisión del riesgo dinámico:

Definiremos una variable enumerada llamada revision_type, que permitirá al usuario seleccionar cómo se realizarán las verificaciones del riesgo dinámico (en cada tick o al cierre de posiciones):

  ENUM_REVISION_TYPE revision_type;

3. Variable booleana para activar o desactivar el riesgo dinámico:

Esta variable booleana almacenará la decisión si utilizará o no el riesgo dinámico por operación:

  bool               ActivateDynamicRiskPerOperation;

4. Variable para guardar el índice actual del riesgo dinámico:

Para mantener un control preciso sobre la progresión del riesgo dinámico, utilizamos una variable índice que indica en qué punto del array dinámico nos encontramos actualmente:

  int                index_gmlpo;

5. Variable para almacenar el balance base de referencia (balance inicial):

Esta variable guardará el balance inicial o referencia seleccionado por el usuario, al que se aplicarán posteriormente los porcentajes definidos para activar el riesgo dinámico:

  double             chosen_balance;

6. Variable que indica el próximo balance objetivo para modificar el riesgo por operación (positivamente):

Almacenaremos en esta variable el siguiente balance que se debe superar para ajustar dinámicamente el porcentaje de riesgo:

  double             NewBalanceToOvercome;

7. Variable booleana para evitar errores por superación del rango del índice:

Esta variable booleana indica si se ha alcanzado el menor balance posible establecido por el usuario (el mayor porcentaje negativo permitido). Cuando sea true, se detendrá el incremento del índice para prevenir que este supere el rango máximo permitido:

  bool               TheMinimumValueIsExceeded;

8. Variable para almacenar el porcentaje de riesgo inicial por operación:

Esta variable guarda el porcentaje inicial definido para el riesgo por operación. Será utilizada especialmente para restablecer el valor inicial cuando el balance se recupere luego de superar niveles inferiores:

  double             gmlpo_percentage;

Estas variables permiten implementar eficazmente un mecanismo robusto, seguro y fácil de gestionar, ofreciendo claridad y control preciso sobre el riesgo dinámico por operación en diferentes escenarios operativos.

Variable específica para la ganancia máxima diaria:

Para un control más estricto de las ganancias diarias, introduciremos una variable booleana llamada mdp_is_strict. Esta variable ayudará a la gestión de riesgo a determinar si el cálculo de la ganancia máxima debe ser ajustado basándose en las pérdidas del día.

  • Si mdp_is_strict es verdadero (true): La ganancia máxima diaria se considerará superada solo si se recupera cualquier pérdida previa y luego se excede el objetivo inicial. Por ejemplo, si la ganancia máxima diaria es de 50 USD y durante el día se pierden 20 USD, necesitaríamos ganar un total de 70 USD (recuperar los 20 USD perdidos y alcanzar los 50 USD de ganancia neta) para considerar que se ha superado la meta de ganancias.
  • Si mdp_is_strict es falso (false): Las pérdidas diarias no afectan el cálculo de la ganancia máxima. En este caso, si se establece un objetivo de 50 USD y se pierden 40 USD, solo será necesario obtener 10 USD adicionales (recuperar los 40 USD perdidos más 10 USD de ganancia) para alcanzar la ganancia máxima diaria establecida.

  bool               mdp_is_strict;

Variable para el tipo de lote:

Para simplificar la funcionalidad de asignación de lotes, modificaremos la función GetLote() para que ya no requiera que se especifique el tipo de lote manualmente. En lugar de ello, el tipo de lote será inicializado en el constructor o podrá ser modificado mediante funciones adicionales que desarrollaremos. Esto permitirá una configuración más directa y menos propensa a errores manuales.

  ENUM_GET_LOT       type_get_lot;  

Con estas mejoras, esperamos aumentar la eficiencia y la precisión en la gestión de riesgos y en la asignación de lotes en nuestra plataforma de trading.


Gestión de posiciones abiertas

En esta sección nos centraremos en gestionar adecuadamente todas las posiciones abiertas por el número mágico o por el usuario.

Para lograrlo, crearemos varias funciones útiles y claras, que facilitarán mantener el control de las operaciones abiertas en todo momento.

1. Función para verificar si un ticket existe en el array interno (open_positions)

La función TheTicketExists tiene la finalidad de comprobar rápidamente si un ticket específico se encuentra actualmente almacenado en nuestro array interno de posiciones abiertas (open_positions). Esta operación es crucial cuando necesitamos confirmar si ya estamos gestionando una posición específica o si debemos tomar medidas adicionales.

La lógica es sencilla: iteramos a través del array y comparamos cada elemento con el ticket proporcionado. Si encontramos una coincidencia, retornamos true; en caso contrario, retornamos false.

Declaración:

  bool               TheTicketExists(const ulong ticket); //Check if a ticket is in the operations array

Implementación:

//+------------------------------------------------------------------+
//| Function to check if the ticket is in the array                  |
//+------------------------------------------------------------------+
bool CRiskManagemet::TheTicketExists(const ulong ticket)
 {
  for(int i = 0; i < this.GetPositionsTotal() ; i++)
    if(this.open_positions[i].ticket == ticket)
      return true;
      
  return false;
 }

2. Función para obtener el número total de posiciones abiertas

La función GetPositionsTotal simplemente retorna la cantidad de elementos presentes en el array interno open_positions. Esta función facilita obtener rápidamente cuántas operaciones están siendo gestionadas en tiempo real.

Declaración e implementación:

  inline int         GetPositionsTotal() const { return (int)this.open_positions.Size(); } //Get the total number of open positions

3. Función para obtener las posiciones totales con banderas

La función GetPositions utiliza un sistema de banderas (flags) para ofrecer mayor flexibilidad al momento de contar posiciones abiertas según criterios específicos, tales como el tipo de operación (compra o venta). Para esto, convertimos el tipo de posición (ENUM_POSITION_TYPE) en valores compatibles con banderas binarias (normalmente potencias de dos, como 2, 4, 8, etc.).

La lógica consiste en recorrer todas las posiciones abiertas y verificar si cada una coincide con las banderas indicadas, incrementando un contador por cada coincidencia encontrada.

Implementación:

//+------------------------------------------------------------------+
//| Function to obtain the number of open positions                  |
//+------------------------------------------------------------------+
int CRiskManagemet::GetPositions(int flags) const
{
int count = 0;

 for(int i = 0; i < ArraySize(this.open_positions) ; i++)
  {
    if(this.open_positions[i].type == POSITION_TYPE_BUY && (flags & FLAG_POSITION_BUY) != 0 )
    {
     count++;
    }
    else if(this.open_positions[i].type == POSITION_TYPE_SELL && (flags & FLAG_POSITION_SELL) != 0 )
    {
     count++;
    }
  } 

return count;
}

4. Función para verificar si hay posiciones abiertas actualmente

Finalmente, la función ThereAreOpenOperations ofrece una manera rápida y eficiente de saber si nuestro EA está gestionando operaciones abiertas en este momento. Esta función simplemente retorna el valor de la variable booleana interna (positions_open).

Declaración e implementación:

  inline bool        ThereAreOpenOperations() const { return this.positions_open; } //Check if there are any open operations

5. Funciones adicionales útiles

Además de las funciones que forman parte directamente de la clase CRiskManagement, se crearán funciones externas adicionales que facilitarán tareas específicas, como cerrar órdenes basadas en banderas.

5.1 Función para cerrar órdenes pendientes según banderas

Antes de cerrar órdenes en función de ciertos criterios, necesitaremos convertir los tipos de órdenes (ENUM_ORDER_TYPE) a banderas binarias compatibles. Este paso es crucial para evitar errores cuando se realiza la operación bit a bit (&).

A continuación, se muestra una función simple que convierte un tipo de orden específico a una bandera correspondiente.

Si se pasa un valor inválido o genérico (WRONG_VALUE), la función retorna todas las banderas combinadas:

// Converts an order type to its corresponding flag
int OrderTypeToFlag(ENUM_ORDER_TYPE type)
 {
  if(type == ORDER_TYPE_BUY)
    return FLAG_ORDER_TYPE_BUY;
  else
    if(type == ORDER_TYPE_SELL)
      return FLAG_ORDER_TYPE_SELL;
    else
      if(type == ORDER_TYPE_BUY_LIMIT)
        return FLAG_ORDER_TYPE_BUY_LIMIT;
      else
        if(type == ORDER_TYPE_SELL_LIMIT)
          return FLAG_ORDER_TYPE_SELL_LIMIT;
        else
          if(type == ORDER_TYPE_BUY_STOP)
            return FLAG_ORDER_TYPE_BUY_STOP;
          else
            if(type == ORDER_TYPE_SELL_STOP)
              return FLAG_ORDER_TYPE_SELL_STOP;
            else
              if(type == ORDER_TYPE_BUY_STOP_LIMIT)
                return FLAG_ORDER_TYPE_BUY_STOP_LIMIT;
              else
                if(type == ORDER_TYPE_SELL_STOP_LIMIT)
                  return FLAG_ORDER_TYPE_SELL_STOP_LIMIT;
                else
                  if(type == ORDER_TYPE_CLOSE_BY)
                    return FLAG_ORDER_TYPE_CLOSE_BY;
                    
  return (FLAG_ORDER_TYPE_BUY | FLAG_ORDER_TYPE_SELL | FLAG_ORDER_TYPE_BUY_LIMIT |
          FLAG_ORDER_TYPE_SELL_LIMIT | FLAG_ORDER_TYPE_BUY_STOP | FLAG_ORDER_TYPE_SELL_STOP |
          FLAG_ORDER_TYPE_BUY_STOP_LIMIT | FLAG_ORDER_TYPE_SELL_STOP_LIMIT | FLAG_ORDER_TYPE_CLOSE_BY);
 }

La función principal recorre todas las órdenes existentes, cerrando aquellas que coincidan con las banderas especificadas:

// Close all orders that match the flags in `flags`
void CloseAllOrders(int flags, CTrade &obj_trade, ulong magic_number_ = NOT_MAGIC_NUMBER)
 {
  ResetLastError();
  for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
    ulong ticket = OrderGetTicket(i);
    
    if(OrderSelect(ticket))
     {
      ENUM_ORDER_TYPE type_order = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
      ulong magic = OrderGetInteger(ORDER_MAGIC);
      int bandera = OrderTypeToFlag(type_order);
      if((bandera & flags) != 0 && (magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER))
       {
        if(type_order == ORDER_TYPE_BUY || type_order == ORDER_TYPE_SELL)
          obj_trade.PositionClose(ticket);
        else
          obj_trade.OrderDelete(ticket);
       }
     }
    else
     {
      PrintFormat("Error selecting order %d, last error %d", ticket, GetLastError());
     }
   }
 }

5.2 Funciones para obtener el número total de posiciones abiertas

Se incluye también una función externa sencilla para contar el número total de posiciones abiertas sin necesidad de depender exclusivamente de la clase CRiskManagement. Primero, necesitamos convertir los tipos de posiciones (ENUM_POSITION_TYPE) en banderas compatibles.

5.2.1 Función para convertir ENUM_POSITION_TYPE a una bandera válida

int PositionTypeToFlag(ENUM_POSITION_TYPE type)
 {
  if(type == POSITION_TYPE_BUY)
    return FLAG_POSITION_BUY;
  else
    if(type == POSITION_TYPE_SELL)
      return FLAG_POSITION_SELL;
      
  return FLAG_POSITION_BUY | FLAG_POSITION_SELL;
 }

5.2.2 Función para obtener el número total de posiciones abiertas según banderas

Esta función recorre todas las posiciones existentes y cuenta únicamente aquellas que cumplen con las banderas especificadas:

//---
int GetPositions(int flags = FLAG_POSITION_BUY | FLAG_POSITION_SELL, ulong magic_number_ = NOT_MAGIC_NUMBER)
 {
  int counter = 0;
  for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
    ulong position_ticket = PositionGetTicket(i);
    if(!PositionSelectByTicket(position_ticket))
      continue; // Si la selección falla, pasa a la siguiente posición
    ulong position_magic = PositionGetInteger(POSITION_MAGIC);
    ENUM_POSITION_TYPE type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
    // Check if the position type matches the flags
    if((flags & PositionTypeToFlag(type)) != 0 &&
       (position_magic == magic_number_ || magic_number_ == NOT_MAGIC_NUMBER))
     {
      counter++;
     }
   }
  return counter;
 }


Métodos de superación

Ahora vamos a definir unas funciones para determinar si hemos superado una pérdida máxima o la ganancia máxima diaria.

Comenzaremos desarrollando dos funciones esenciales: una dedicada a verificar si hemos superado pérdidas y otra para confirmar si hemos alcanzado nuestras máximas ganancias esperadas.

Cada uno de estos modos se encapsula en una función que, dependiendo de la opción que elijamos, nos dirá si hemos logrado superar el desafío (devolviendo true) o si aún nos falta camino por recorrer (devolviendo false). Por ejemplo, para el modo "CLOSED_POSITIONS", es vital saber cuánto hemos ganado, ya que necesitamos comparar esto con nuestras pérdidas o ganancias máximas diarias o semanales.

Veamos cómo podríamos implementar esto en código:

//+------------------------------------------------------------------+
//| Boolean function to check if a loss was overcome                 |
//+------------------------------------------------------------------+
bool CRiskManagemet::IsSuperated(double profit_, double loss_, const MODE_SUPERATE mode) const
 {
  if(loss_ <= 0 || !this.positions_open)
    return false; //if loss is zero return false (the loss is not being used)
//---
  if(mode == EQUITY) //---
   {
    if(this.curr_profit * -1 > loss_)
      return true;
   }
  else
    if(mode == CLOSE_POSITION)
     {
      if(profit_ * -1 > loss_)
        return true;
     }
    else
      if(mode == CLOSE_POSITION_AND_EQUITY)
       {
        double new_loss = profit_ < 0 ? loss_ - MathAbs(profit_) : loss_;
        if(this.curr_profit * -1 > new_loss)
          return true;
       }

  return false;
 }

Desglose de los modos:

  1. EQUITY: Aquí comparamos directamente el beneficio actual (la equidad menos el balance de la cuenta). Si este valor negativo supera la pérdida definida, hemos superado nuestro límite.

  2. CLOSE_POSITION: En este modo, nos centramos en el beneficio de posiciones que ya hemos cerrado, multiplicando este beneficio por -1 para hacer la comparación adecuada.

  3. CLOSE_POSITION_AND_EQUITY: Este modo es un poco más complejo. Aquí, ajustamos la pérdida máxima basándonos en el beneficio actual. Si hemos tenido un mal día y el beneficio es negativo, restamos ese valor del límite de pérdida que podemos tolerar. Si el total ajustado es superado por el beneficio actual negativo, entonces también hemos superado este umbral.

Funciones especializadas para las ganancias máximas

Al igual que con las pérdidas, también necesitamos una manera de verificar si hemos superado nuestras ganancias máximas esperadas. Para ello, establecemos una función dedicada que se asegura de que nuestras operaciones no alteren de manera involuntaria el valor máximo de ganancia diaria (mdp).

A continuación, te presento el código de la función:

//+------------------------------------------------------------------+
//| Function to check if the maximum profit per day was exceeded     |
//+------------------------------------------------------------------+
bool CRiskManagemet::MDP_IsSuperated(const MODE_SUPERATE mode) const
 {
  if(this.mdp.value  <= 0 || !this.positions_open)
    return false; //if loss is zero return false (the loss is not being used)
//---
  if(mode == EQUITY) //---
   {
    if(this.curr_profit > this.mdp.value)
      return true;
   }
  else
    if(mode == CLOSE_POSITION)
     {
      if(this.daily_profit > this.mdp.value)
        return true;
     }
    else
      if(mode == CLOSE_POSITION_AND_EQUITY)
       {
        double new_mdp = this.daily_profit > 0 ? this.mdp.value - this.daily_profit : (this.mdp_is_strict == false ? this.mdp.value : this.mdp.value + (this.daily_profit * -1));
        if(this.curr_profit > new_mdp)
          return true;
       }
//---
  return false;
 }

Detalles de la función:

  1. EQUITY: Directamente, comparamos el beneficio actual (curr_profit) con el valor máximo diario de ganancia (mdp). Si el beneficio es mayor, hemos superado la meta.

  2. CLOSE_POSITION: Aquí evaluamos si el beneficio obtenido solo de las posiciones cerradas durante el día (daily_profit) supera el mdp.

  3. CLOSE_POSITION_AND_EQUITY: Este caso es más complejo. Consideramos dos situaciones:

    • Si el beneficio diario es positivo, simplemente restamos este valor del mdp para establecer un nuevo objetivo ajustado.
    • Si el beneficio diario es negativo y la política es estricta (mdp_is_strict), sumamos el valor absoluto del beneficio negativo al mdp para compensar las pérdidas antes de poder declarar que hemos superado la meta. En cambio, si no es estricta, mantenemos el mdp original, ignorando las pérdidas del día.

Esta función nos permite una gestión precisa de nuestras metas de ganancias, asegurándonos de que incluso en días volátiles podamos evaluar correctamente si hemos alcanzado o superado nuestras expectativas financieras

Máxima perdida en propfirms

Continuando con la gestión de pérdidas, encontramos una particularidad importante en las cuentas PropFirm, especialmente en FTMO. En estas cuentas, el límite de pérdida máximo no varía; se mantiene fijo desde el inicio de la prueba. Por ejemplo, si comenzamos con una cuenta de 10000 USD, nuestro límite máximo de pérdida será de 9000 USD. Esto significa que, si en cualquier momento la equidad de la cuenta cae por debajo de este umbral, automáticamente se pierde la oportunidad de obtener financiación.

Para facilitar el control de este límite y evitar complicaciones adicionales, implementaremos un método específico que se encargará de verificar si se ha alcanzado o superado dicha pérdida máxima:

  //--- Function to check if the maximum loss has been exceeded in a PropFirm account of the FTMO type
  inline bool        IsSuperatedMLPropFirm() const { return (this.ml.value == 0 || !this.positions_open) ? false : AccountInfoDouble(ACCOUNT_EQUITY) < (account_balance_propfirm - (this.ml.value)); }

La lógica es sencilla y clara: si no tenemos posiciones abiertas o la variable que controla la máxima pérdida (ml) está en cero, no se realiza ninguna verificación y se retorna false. Por el contrario, si existen posiciones abiertas y ml tiene un valor asignado, la función compara la equidad actual con la diferencia entre el saldo inicial de la prueba y la pérdida máxima permitida.

Funciones para verificar

Además del método mencionado anteriormente, crearemos diversas funciones prácticas que servirán para verificar rápidamente si se han superado o alcanzado distintos niveles de pérdida o ganancia preestablecidos. Estas funciones actúan como accesos directos al método general IsSuperated:

  //--- functions to verify if the established losses were exceeded
  inline bool        ML_IsSuperated(const MODE_SUPERATE mode)   const  {return this.mode_risk_managemet == personal_account ? IsSuperated(this.gross_profit, this.ml.value, mode) : IsSuperatedMLPropFirm();  }
  inline bool        MWL_IsSuperated(const MODE_SUPERATE mode)  const  {return IsSuperated(this.weekly_profit, this.mwl.value, mode);    }
  inline bool        MDL_IsSuperated(const MODE_SUPERATE mode)  const  {return IsSuperated(this.daily_profit, this.mdl.value, mode);     }
  inline bool        GMLPO_IsSuperated()                        const  {return IsSuperated(0, this.gmlpo.value, EQUITY);                 }
  inline bool        NMLPO_IsSuperated()                        const  {return IsSuperated(0, this.nmlpo, EQUITY);                       }
  bool               MDP_IsSuperated(const MODE_SUPERATE mode)  const;

Estas funciones proporcionan una manera eficiente y directa de evaluar las condiciones específicas para pérdidas diarias, semanales, etc.

Máxima pérdida diaria dinámica en FTMO

Un aspecto clave en la gestión del riesgo en FTMO es comprender que la pérdida máxima diaria no es un valor estático, sino dinámico. Esta pérdida cambia directamente en relación con los beneficios acumulados durante la jornada. Es decir, mientras más beneficios obtengamos en el día, mayor será el margen que tendremos para posibles pérdidas, haciendo que este límite fluctúe y no se mantenga fijo.

A continuación, se presenta una función que actualiza esta pérdida máxima diaria en función del beneficio obtenido:

  //--- Update Loss (only if ftmo propfirm FTMO is selected)
  inline void        UpdateDailyLossFTMO() {  this.mdl.value += this.daily_profit > 0 ? this.daily_profit : 0; 
                                              PrintFormat("%s The maximum loss per operation has been modified, its new value: %.2f",EA_NAME,this.mdl.value); }

Esta función debe incorporarse dentro del método que maneja las transacciones del EA, específicamente en OnTradeTransaction.

//+------------------------------------------------------------------+
//| TradeTransaction function                                        |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)


Gestión de eventos

Ahora continuaremos con nuevos "eventos" que se ejecutaran en eventos del Asesor Experto.

OnTradeTransaction

El método OnTradeTransaction juega un papel central en la gestión dinámica del riesgo, ya que se ejecuta ante cualquier evento relacionado con transacciones en la cuenta, como la apertura o cierre de posiciones, modificaciones en órdenes existentes, o depósitos en la cuenta.

La función tiene tres parámetros importantes:

  • trans: Información detallada sobre la transacción actual.

  • request: Detalles sobre solicitudes comerciales pendientes o recientes.

  • result: Resultados obtenidos tras procesar dichas solicitudes.

Para gestionar adecuadamente el riesgo, principalmente nos interesa la información contenida en trans, pues allí encontraremos el detalle exacto del tipo de transacción y la operación asociada.

Existen varios tipos de transacciones comerciales, especificadas en la enumeración: 

ENUM_TRADE_TRANSACTION_TYPE

Tipos de transacciones:

Identificador
Descripción
 TRADE_TRANSACTION_ORDER_ADD
  Agregación de una nueva orden abierta.
 TRADE_TRANSACTION_ORDER_UPDATE Modificación de orden abierta. A estas modificaciones les corresponden no solo los cambios explícitos por parte del terminal de cliente o servidor de trading, sino también   alteraciones de su estado durante la colocación (por ejemplo, paso del estado ORDER_STATE_STARTED a ORDER_STATE_PLACED o de ORDER_STATE_PLACED a     ORDER_STATE_PARTIAL, etc.).
 TRADE_TRANSACTION_ORDER_DELETE Eliminación de la orden de la lista de órdenes abiertas. Una orden puede ser eliminada de las abiertas como resultado de colocación de la solicitud correspondiente, o bien una     vez ejecutada (llena) y pasada al historial.
 TRADE_TRANSACTION_DEAL_ADD Agregación de una transacción (deal) al historial. Se hace como resultado de ejecución de la orden o realización de la operación con el balance de la cuenta.
 TRADE_TRANSACTION_DEAL_UPDATE Modificación de una transacción (deal) en el historial. Puede pasar que una transacción (deal) ejecutada antes se cambie en el servidor. Por ejemplo, la transacción (deal) fue       modificada en el sistema externo de trading (bolsa) a donde había sido pasada por el broker.
 TRADE_TRANSACTION_DEAL_DELETE Eliminación de una transacción (deal) del historial. Puede pasar que una transacción (deal) ejecutada antes se elimine en el servidor. Por ejemplo, la transacción (deal) fue   eliminada en el sistema externo de trading (bolsa) a donde había sido pasada por el broker.
 TRADE_TRANSACTION_HISTORY_ADD Agregación de una orden al historial como resultado de su ejecución o cancelación.
   TRADE_TRANSACTION_HISTORY_UPDATE Modificación de una orden que se encuentra en el historial de órdenes. Este tipo está previsto para ampliar la funcionalidad en la parte del servidor.
   TRADE_TRANSACTION_HISTORY_DELETE Eliminación de una orden del historial de órdenes. Este tipo está previsto para ampliar la funcionalidad en la parte del servidor.
 TRADE_TRANSACTION_POSITION Modificación de la posición que no está relacionada con la ejecución de la transacción (deal). Este tipo de transacción (transaction) quiere decir que la posición ha sido   modificada en la parte del servidor de trading. La posición puede sufrir el cambio del volumen, precio de apertura, así como de los niveles Stop Loss y Take Profit. La   información sobre los cambios se envía en la estructura MqlTradeTransaction usando el manejador OnTradeTransaction. La modificación de una posición (agregación, cambio o   eliminación) como resultado de ejecución de la transacción (deal) no supone la aparición tras sí la transacción (transaction) TRADE_TRANSACTION_POSITION.
 TRADE_TRANSACTION_REQUEST Aviso de que la solicitud comercial ha sido procesada por el servidor, y el resultado de su procesamiento ha sido recibido. Para las transacciones de este tipo en la estructura   MqlTradeTransaction hay que analizar solo un campo - type (tipo de transacción). Para obtener la información adicional hay que analizar el segundo y el tercer parámetro de la   función OnTradeTransaction (request y result).

Sin embargo, el tipo de transacción que nos interesa es:

  • TRADE_TRANSACTION_DEAL_ADD: Indica que una nueva transacción ha sido agregada al historial (deal cerrado o apertura de posición confirmada).

Creación de la función OnTradeTransactionEvent

A continuación, definiremos una función específica para gestionar este evento de manera clara y eficiente:

  void               OnTradeTransactionEvent(const MqlTradeTransaction& trans);

La función únicamente requerirá del parámetro trans, que contiene toda la información necesaria sobre la transacción actual.

La estructura básica de nuestra función comenzará verificando primero que se trate del tipo de transacción TRADE_TRANSACTION_DEAL_ADD, seleccionando previamente el deal del historial:

//+------------------------------------------------------------------+
//| OnTradeTransaction Event                                         |
//+------------------------------------------------------------------+
void CRiskManagemet::OnTradeTransactionEvent(const MqlTradeTransaction &trans)
 {
  HistoryDealSelect(trans.deal);
  
  if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
   {
    ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
    ulong position_magic = (ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
    bool is_select = PositionSelectByTicket(trans.position);

    if(entry == DEAL_ENTRY_IN && is_select && (this.magic_number == position_magic || this.magic_number == NOT_MAGIC_NUMBER))
     {
      Print(EA_NAME, " New position opened with ticket: ", trans.position);
      this.positions_open = true;   
      
      Positions new_pos;
      new_pos.type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      new_pos.ticket = trans.position;
      
      AddArrayNoVerification(open_positions, new_pos);
      return;
     }

    if(entry == DEAL_ENTRY_OUT && TheTicketExists(trans.position) == true  && !is_select)
     {
      Print(EA_NAME, " Position with ticket ", trans.position, " has been closed");
      DeleteTicket(trans.position);

      //---
      if(this.revision_type == REVISION_ON_CLOSE_POSITION)
        CheckAndModifyThePercentageOfGmlpo();

      //---
      if(GetPositionsTotal() == 0)
        this.positions_open = false;

      //---
      UpdateProfit();

      //---
      if(this.mode_risk_managemet == propfirm_ftmo)
        UpdateDailyLossFTMO();

      SetGMLPO();
      Print(StringFormat("%-6s| %.2f", "GMLPO", this.gmlpo.value));
     }
   }
   
 }

Dentro de esta función, manejamos dos escenarios clave:

  • Apertura de posiciones: Confirmamos que la posición está realmente abierta (is_select) y que su número mágico coincide con el establecido, antes de incluirla en nuestro registro interno (open_positions).

  • Cierre de posiciones: Validamos que la posición ha sido efectivamente cerrada (no seleccionable) y que está en nuestro listado de posiciones gestionadas. En este caso, eliminamos el ticket del registro, actualizamos variables clave como la ganancia acumulada y el estado general de posiciones abiertas, y también ajustamos dinámicamente la pérdida diaria máxima si estamos en modo FTMO.

De esta forma, nuestro sistema podrá responder proactivamente ante cada transacción registrada, garantizando una gestión de riesgo clara, eficiente y dinámica.

OnTickEvent

El evento OnTick es esencial en los Expert Advisors (EA), ya que se activa cada vez que se produce un nuevo tick en el mercado.

En nuestro caso específico, utilizaremos este evento para actualizar el beneficio total acumulado de todas las posiciones abiertas, filtrando por número mágico si este está definido. De no existir un número mágico establecido, la actualización abarcará todas las posiciones abiertas sin importar dicho número.

La definición básica del método será la siguiente:

  void               OnTickEvent();

Esta función únicamente se ejecutará cuando existan posiciones abiertas, para evitar procesos innecesarios.

La estructura de la función quedaría implementada de la siguiente manera:

//+------------------------------------------------------------------+
//| Function to execute in OnTick                                    |
//+------------------------------------------------------------------+
void CRiskManagemet::OnTickEvent(void)
 {
  if(!positions_open)
    return;
//---
  GetPositionsProfit();

//---
  if(this.revision_type == REVISION_ON_TICK)
    CheckAndModifyThePercentageOfGmlpo();
 }

Explicación detallada del proceso:
  1. Actualización del beneficio total:

    • El método GetPositionsProfit() se encarga de obtener y actualizar la información de ganancias o pérdidas actuales de las posiciones gestionadas. Esto garantiza que siempre contemos con información fresca y precisa sobre el rendimiento acumulado de las operaciones abiertas.

  2. Verificación y modificación dinámica del GMLPO:

    • En caso de que hayamos seleccionado la opción REVISION_ON_TICK, el EA revisará constantemente en cada nuevo tick si se ha superado un umbral de ganancias o pérdidas definido previamente, ajustando así el riesgo permitido por operación de forma dinámica. Esto permite adaptar el nivel de exposición en tiempo real y mejorar la eficiencia en la gestión de riesgos.


Funciones para el riesgo dinámico y trabajo con el array de posiciones

En esta sección, implementaremos funciones fundamentales que facilitarán trabajar eficientemente con el riesgo dinámico y realizar tareas prácticas relacionadas con arrays de posiciones. Estas herramientas serán útiles en diversas partes de nuestra estrategia de gestión de riesgos.

Función para convertir un string a un tipo

Esta función utiliza plantillas para adaptarse fácilmente a diferentes tipos de datos simples (exceptuando clases o estructuras complejas). Su principal propósito es transformar un string en el tipo específico deseado, facilitando la manipulación dinámica de información en tiempo real.

Implementación:

template <typename S>
void StringToType(string token, S &value, ENUM_DATATYPE type)
 {
  if(StringLen(token) == 0)
   {
    Print("Error: String is empty.");
    return;
   }
  switch(type)
   {
    case TYPE_BOOL:
      value = (S)(StringToInteger(token) != 0);  // Convertir a bool
      break;
    case TYPE_CHAR:
      value = (S)((char)StringToInteger(token));  // Convertir a char
      break;
    case TYPE_UCHAR:
      value = (S)((uchar)StringToInteger(token));  // Convertir a uchar
      break;
    case TYPE_SHORT:
      value = (S)((short)StringToInteger(token));  // Convertir a short
      break;
    case TYPE_USHORT:
      value = (S)((ushort)StringToInteger(token));  // Convertir a ushort
      break;
    case TYPE_COLOR:
      value = (S)((color)StringToInteger(token));  // Convertir a color
      break;
    case TYPE_INT:
      value = (S)(StringToColor(token));  // Convertir a int
      break;
    case TYPE_UINT:
      value = (S)((uint)StringToInteger(token));  // Convertir a uint
      break;
    case TYPE_DATETIME:
      value = (S)(StringToTime(token));  // Convertir a datetime
      break;
    case TYPE_LONG:
      value = (S)((long)StringToInteger(token));  // Convertir a long
      break;
    case TYPE_ULONG:
      value = (S)((ulong)StringToInteger(token));  // Convertir a ulong
      break;
    case TYPE_FLOAT:
      value = (S)((float)StringToDouble(token));  // Convertir a float
      break;
    case TYPE_DOUBLE:
      value = (S)(StringToDouble(token));  // Convertir a double
      break;
    case TYPE_STRING:
      value = (S)(token);  // Mantener como string
      break;
    default:
      Print("Error: Unsupported data type in ConvertToType.");
      break;
   }
 }   

Esta función será particularmente importante cuando trabajemos con el riesgo dinámico, ya que recibiremos parámetros como cadenas de texto y será necesario convertirlas en variables numéricas para su posterior uso.

Función para convertir una cadena de texto a un array de tipos simples

Similarmente, esta función aprovecha la versatilidad de las plantillas para simplificar el proceso de conversión de una cadena de texto en un array del tipo deseado. La función realiza lo siguiente:

  • Divide el string original usando un separador específico (por defecto una coma ',').

  • Almacena cada fragmento resultante en un array temporal.

  • Convierte individualmente cada fragmento al tipo especificado y los asigna al array de destino.

Implementación:
//---
template <typename S>
void StringToArray(S &array_receptor[], string cadena, ENUM_DATATYPE type_data, ushort separator = ',')
 {
  string result[];
  int num = StringSplit(cadena, separator, result);
  ArrayResize(array_receptor, ArraySize(result));
  for(int i = 0; i < ArraySize(array_receptor) ; i++)
   {
    S value;
    StringToType(result[i], value, type_data);
    array_receptor[i]  = value;
   }
 }

Por ejemplo, en situaciones prácticas relacionadas con el riesgo dinámico, podríamos recibir datos como "5.0,4.5,3.0", que serían convertidos automáticamente a un array de tipo double. Esto simplifica considerablemente la gestión de parámetros dinámicos dentro de nuestro sistema.

Funciones avanzadas para manipular arrays

A continuación, presentamos diversas funciones útiles que facilitarán la gestión eficiente de arrays, especialmente cuando trabajamos con estrategias dinámicas y estructuras complejas en nuestra gestión del riesgo.

Función para eliminar múltiples índices de un array

Esta función permite eliminar varios elementos de un array simultáneamente, mejorando significativamente la eficiencia y claridad del código. La lógica principal consiste en ordenar previamente los índices que deseamos eliminar y luego copiar únicamente los elementos que queremos conservar en un nuevo índice del array original.
Implementación:
//---
template <typename T>
void RemoveMultipleIndexes(T &arr[], int &indexes_to_remove[])
 {
  int oldSize = ArraySize(arr);
  int removeSize = ArraySize(indexes_to_remove);
  if(removeSize == 0 || oldSize == 0)
    return;
// Ordenamos los índices para garantizar eficiencia al recorrerlos
  ArraySort(indexes_to_remove);
  int writeIndex = 0, readIndex = 0, removeIndex = 0;
  while(readIndex < oldSize)
   {
    if(removeIndex < removeSize && readIndex == indexes_to_remove[removeIndex])
     {
      removeIndex++;
     }
    else
     {
      arr[writeIndex] = arr[readIndex];
      writeIndex++;
     }
    readIndex++;
   }
  ArrayResize(arr, writeIndex);
 }

Función para añadir elementos a un array

La siguiente función permite añadir fácilmente nuevos elementos a cualquier tipo de array, adaptándose automáticamente al tipo de datos.

Implementación:

//---
template <typename X>
void AddArrayNoVerification(X &array[], const X &value)
 {
  ArrayResize(array, array.Size() + 1);
  array[array.Size() - 1] = value;
 }

Es sencilla: aumentamos el tamaño del array en una unidad y asignamos el nuevo valor al último elemento.

Función especializada para eliminar un solo elemento basado en ticket

Esta función específica está diseñada especialmente para arrays de estructuras que tengan un campo denominado "ticket".

Permite eliminar un elemento específico basándose en este identificador único.

Implementación:

//---
template<typename T>
bool RemoveIndexFromAnArrayOfPositions(T &array[], const ulong ticket)
 {
  int size = ArraySize(array);
  int index = -1;
// Search index and move elements in a single loop
  for(int i = 0; i < size; i++)
   {
    if(array[i].ticket == ticket)
     {
      index = i;
     }
    if(index != -1 && i < size - 1)
     {
      array[i] = array[i + 1]; // Move the elements
     }
   }
  if(index == -1)
    return false;
// Reducir el tamaño del array
  if(size > 1)
    ArrayResize(array, size - 1);
  else
    if(size <= 1)
      ArrayFree(array);
      
  return true;
 }

Es importante tener en cuenta que esta función requiere obligatoriamente que la estructura incluya un miembro llamado "ticket". Si intentamos usarla con estructuras que no lo tengan, se generará un error:

'ticket' - undeclared identifier        
in template 'bool RemoveIndexFromAnArrayOfPositions(T&[],const ulong)' specified with [T=Message]      
see template instantiation 'ExtraFunctions::RemoveIndexFromAnArrayOfPositions<Message>' 
1 errors, 0 warnings            

En este caso, la estructura "Message" no contiene un miembro "ticket".

Función adicional para repetir una cadena de texto

Por último, tenemos una función útil que genera un texto repetido tantas veces como deseemos. Es especialmente práctica para imprimir tablas o separar visualmente información:

Implementación:

string StringRepeat(string str, int count)
 {
  string result = "";
  for(int i = 0; i < count; i++)
    result += str;

  return result;
 } 

Por ejemplo, al llamar 

StringRepeat("-", 10) obtendremos "----------".

Estas funciones avanzadas simplifican considerablemente el manejo de arrays y mejoran la claridad del código, proporcionando herramientas eficaces y versátiles para una gestión dinámica y precisa del riesgo en nuestros sistemas de trading.


Construcción de un riesgo dinámico

Finalmente, llegamos a una parte esencial: la implementación del riesgo dinámico. Para ello, utilizaremos dos funciones clave que facilitarán enormemente su manejo y adaptabilidad.

Antes de empezar, necesitamos incluir la siguiente librería:

#include <Generic\HashMap.mqh>

Esta librería nos ayudará a ordenar y gestionar adecuadamente los datos relacionados con el riesgo dinámico

Función de inicialización del riesgo dinámico

Siguiendo con el concepto inicial presentado en artículos anteriores, nuestro sistema dinámico de riesgo estará basado en una estructura que contendrá dos arrays de tipo double: uno para almacenar los nuevos valores de riesgo a aplicar y otro para indicar los niveles específicos de balance o porcentaje que activarán dichos cambios.

Debido a limitaciones de MQL5, no es posible pasar directamente arrays de tipo double como parámetros del EA. Para solucionar esto, utilizaremos cadenas de texto (strings), separadas por comas, que luego convertiremos en arrays numéricos mediante funciones que previamente implementamos.

La función principal de inicialización del riesgo dinámico se encargará de convertir estos strings en arrays numéricos, verificando además que no existan duplicados y que los valores estén ordenados adecuadamente.

La declaración será la siguiente:

  void               SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_);

El procedimiento interno será:

1. Validar el uso del riesgo dinámico

Antes de realizar cualquier operación, verificamos que el porcentaje del GMLPO esté definido adecuadamente:

 if(this.gmlpo.assigned_percentage == 0)
    return;
    
  if(this.gmlpo.mode_calculation_risk == money)
  {
  this.ActivateDynamicRiskPerOperation = false;
  Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' ");
  return;
  } 

2. Asignar el tipo de revisión seleccionado

  this.revision_type = revision_type_;

3. Convertir los strings en arrays numéricos

Usando las funciones anteriormente creadas, transformamos las cadenas en arrays de tipo double:

//---
  ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ',');
  ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');

4. Validar los arrays resultantes

//---
  if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 && this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1)
   {
    Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1");
    this.ActivateDynamicRiskPerOperation = false;
    return;
   }
  if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size())
   {
    Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal");
    this.ActivateDynamicRiskPerOperation = false;
    return;
   }

  Print(EA_NAME, " Arrays before revision");
  PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
  PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");

A continuación, confirmamos que ambos arrays tienen la misma longitud y no están vacíos. En caso contrario, desactivaremos el riesgo dinámico para evitar errores:

5. Limpiar y preparar la estructura final

Finalmente, limpiamos la estructura HashMap, seleccionamos el balance de referencia adecuado según el tipo de cuenta (FTMO o personal), y preparamos un array auxiliar para posibles índices duplicados o inválidos:

 balanceRiskMap.Clear();
  this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm  : AccountInfoDouble(ACCOUNT_BALANCE);
  int indexes_to_remove[];

Con esta función aseguramos una configuración robusta y fiable del riesgo dinámico, adaptada a las necesidades específicas de cada cuenta o estrategia de trading, ofreciendo una gestión eficiente y precisa.

6. Bucle para agregar elementos válidos al HashMap

A continuación, implementaremos un bucle esencial que añadirá únicamente aquellos elementos que sean válidos a nuestro HashMap, garantizando así una gestión ordenada y eficiente del riesgo dinámico. Los elementos se consideran inválidos bajo las siguientes circunstancias:

  • Si el porcentaje que activa el riesgo es menor o igual a cero.

  • Si el nuevo valor de riesgo es menor o igual a cero.

  • Si el elemento ya está presente en el HashMap (duplicado).

Al identificar un elemento inválido, lo añadiremos temporalmente al array indexes_to_remove para eliminarlo posteriormente. Al evaluar ambos arrays (balance_to_activate_the_risk y risk_to_be_adjusted), basta que un solo valor del par sea incorrecto para descartar completamente dicho par, manteniendo la integridad y coherencia de los datos.

Implementación del bucle:

//---
  for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
   {

    if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0)
     {
      Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)");
      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
      continue;
     }

    if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0)
     {
      Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)");
      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
      continue;
     }

    if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false)
      balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]);
    else
      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
   }

Por ejemplo, supongamos que tenemos los arrays:

  • [1, 3, 5]

  • [0.1, 0.3, 0.0]

En este caso, el último par (5 - 0.0) será considerado inválido debido a que el valor de riesgo ajustado es cero, y por ello "5" será eliminado posteriormente del array correspondiente.

7. Eliminación de duplicados y elementos inválidos

Una vez completada la identificación de elementos inválidos o repetidos, procederemos a eliminarlos. Además, reorganizaremos el array principal balance_to_activate_the_risk para asegurar un orden adecuado y consistencia en nuestros datos:

//---
  ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove);
  ArraySort(dynamic_gmlpos.balance_to_activate_the_risk);
  ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk)); 
 

El ajuste en el tamaño del array risk_to_be_adjusted es necesario para mantener coherencia entre ambos arrays después de la eliminación.

8. Ajuste y conversión de valores del riesgo

A continuación, ajustaremos y transformaremos los porcentajes almacenados en balance_to_activate_the_risk en cifras monetarias concretas, basadas en el saldo seleccionado de la cuenta. El array risk_to_be_adjusted será actualizado con los valores correspondientes:

//---
  for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
   {
    double value;
    balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value);
    dynamic_gmlpos.risk_to_be_adjusted[i] = value;
    dynamic_gmlpos.balance_to_activate_the_risk[i] =  this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0));
   }

9. Inicialización final del riesgo dinámico

Finalmente, inicializaremos las variables requeridas para asegurar que el riesgo dinámico esté listo para funcionar correctamente:

//---
  this.index_gmlpo = 0;
  this.ActivateDynamicRiskPerOperation = true;
  this.TheMinimumValueIsExceeded = false;
  this.NewBalanceToOvercome = 0.00;
  Print(EA_NAME, " Arrays ready: ");
  PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
  PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");

De esta manera, la configuración dinámica del riesgo será precisa, eficiente y adaptada constantemente al rendimiento actual de nuestra estrategia de trading.

Función completa:

//+------------------------------------------------------------------+
//| Function to set dynamic risks per operation                      |
//+------------------------------------------------------------------+
void CRiskManagemet::SetDynamicGMLPO(string percentages_to_activate, string risks_to_be_applied, ENUM_REVISION_TYPE revision_type_)
 {
  if(this.gmlpo.assigned_percentage <= 0)
    return;
  
  if(this.gmlpo.mode_calculation_risk == money)
  {
  this.ActivateDynamicRiskPerOperation = false;
  Print(EA_NAME, __FUNCTION__, "::'Money' mode is not valid for dynamic risk, change it to 'Percentage %' or change the group mode to 'No dynamic risk for risk per operation' ");
  return;
  }
  
//---
  this.revision_type = revision_type_;

//---
  ExtraFunctions::StringToArray(this.dynamic_gmlpos.balance_to_activate_the_risk, percentages_to_activate, TYPE_DOUBLE, ',');
  ExtraFunctions::StringToArray(this.dynamic_gmlpos.risk_to_be_adjusted, risks_to_be_applied, TYPE_DOUBLE, ',');

//---
  if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() < 1 || this.dynamic_gmlpos.balance_to_activate_the_risk.Size() < 1)
   {
    Print(EA_NAME, __FUNCTION__, "::Critical error: the size of the array is less than 1");
    this.ActivateDynamicRiskPerOperation = false;
    return;
   }
  if(this.dynamic_gmlpos.risk_to_be_adjusted.Size() != this.dynamic_gmlpos.balance_to_activate_the_risk.Size())
   {
    Print(EA_NAME, __FUNCTION__, "::Critical error the double arrays for the risk due to dynamic operation are not equal");
    this.ActivateDynamicRiskPerOperation = false;
    return;
   }

  Print(EA_NAME, " Arrays before revision");
  PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
  PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");

//---
  balanceRiskMap.Clear();
  this.chosen_balance = this.mode_risk_managemet == propfirm_ftmo ? this.account_balance_propfirm  : AccountInfoDouble(ACCOUNT_BALANCE);
  int indexes_to_remove[];

//---
  for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
   {

    if(dynamic_gmlpos.balance_to_activate_the_risk[i] <= 0)
     {
      Print(EA_NAME, " (Warning) The percentage value that will be exceeded to modify the risk is 0 or less than this (it will not be taken into account)");
      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
      continue;
     }

    if(dynamic_gmlpos.risk_to_be_adjusted[i] <= 0)
     {
      Print(EA_NAME, " (Warning) The new percentage to which the field is modified is 0 or less than this (it will not be taken into account)");
      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
      continue;
     }

    if(balanceRiskMap.ContainsKey(dynamic_gmlpos.balance_to_activate_the_risk[i]) == false)
      balanceRiskMap.Add(dynamic_gmlpos.balance_to_activate_the_risk[i], dynamic_gmlpos.risk_to_be_adjusted[i]);
    else
      ExtraFunctions::AddArrayNoVerification(indexes_to_remove, i);
   }

//---
  ExtraFunctions::RemoveMultipleIndexes(dynamic_gmlpos.balance_to_activate_the_risk, indexes_to_remove);
  ArraySort(dynamic_gmlpos.balance_to_activate_the_risk);
  ArrayResize(dynamic_gmlpos.risk_to_be_adjusted, ArraySize(dynamic_gmlpos.balance_to_activate_the_risk));

//---
  for(int i = 0 ; i < ArraySize(dynamic_gmlpos.balance_to_activate_the_risk) ; i++)
   {
    double value;
    balanceRiskMap.TryGetValue(this.dynamic_gmlpos.balance_to_activate_the_risk[i], value);
    dynamic_gmlpos.risk_to_be_adjusted[i] = value;
    dynamic_gmlpos.balance_to_activate_the_risk[i] =  this.chosen_balance - (this.chosen_balance * (dynamic_gmlpos.balance_to_activate_the_risk[i] / 100.0));
   }

//---
  this.index_gmlpo = 0;
  this.ActivateDynamicRiskPerOperation = true;
  this.TheMinimumValueIsExceeded = false;
  this.NewBalanceToOvercome = 0.00;
  Print(EA_NAME, " Arrays ready: ");
  PrintArrayAsTable(dynamic_gmlpos.balance_to_activate_the_risk, "Negative percentages to modify the risk", "balance");
  PrintArrayAsTable(dynamic_gmlpos.risk_to_be_adjusted, "Risk to be adjusted", "new risk");
 }

Explicación humanizada y detallada de la función para modificar el porcentaje dinámico de riesgo (GMLPO)

A continuación, explicaremos paso a paso la función CheckAndModifyThePercentageOfGmlpo, la cual permite gestionar de forma dinámica el riesgo por operación, adaptándose automáticamente según los niveles de equidad (equity) alcanzados en la cuenta de trading.

1. Verificación inicial

La función inicia comprobando si la gestión dinámica del riesgo está activa. Si está desactivada, la función termina su ejecución inmediatamente.

if(!this.ActivateDynamicRiskPerOperation)
   return;

2. Comprobación del balance actual

Luego, obtiene la equidad actual de la cuenta (el valor real de la cuenta considerando ganancias o pérdidas flotantes). Esta equidad se compara con el balance previamente seleccionado (chosen_balance).

Si la equidad actual es superior al balance elegido y aún no se ha alcanzado el siguiente nivel de balance objetivo, la función no lleva a cabo cambios y termina su ejecución.

double account_equity = AccountInfoDouble(ACCOUNT_EQUITY);

if(account_equity > this.chosen_balance && this.NewBalanceToOvercome != this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo])
   return;

3. Modificación del riesgo cuando la equidad disminuye

Si la equidad actual disminuye por debajo de los niveles establecidos, el sistema busca el siguiente nivel de balance inferior.

  • Si se detecta que el balance ha bajado por debajo del nivel configurado:

    1. Se identifica el nuevo porcentaje de riesgo correspondiente a ese nivel.

    2. Se actualizan las variables internas para reflejar este nuevo nivel de riesgo.

  if(this.TheMinimumValueIsExceeded == false)
   {
    if(account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo])
     {
      PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);

      while(IsStopped() == false && index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size())
       {
        if(index_gmlpo < (int)this.dynamic_gmlpos.balance_to_activate_the_risk.Size() - 1)
         {
          if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]  > account_equity
             && account_equity > this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo + 1])
           {
            this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo];
            this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo];
            index_gmlpo ++;
            PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome);
            break;
           }
         }
        else
          if(index_gmlpo == this.dynamic_gmlpos.balance_to_activate_the_risk.Size() -  1)
           {
            if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]  > account_equity)
             {
              this.NewBalanceToOvercome = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo];
              this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo];
              this.TheMinimumValueIsExceeded = true;
              PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome);
              PrintFormat("%s The minimum value %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
              break;
             }
           }

        index_gmlpo++;
       }

      PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage);
      SetGMLPO();
     }
   }

4. Restauración del riesgo cuando la equidad vuelve a subir


Si posteriormente la equidad aumenta nuevamente por encima del nivel objetivo previamente alcanzado.

  • El sistema ajusta nuevamente el riesgo por operación, restaurando niveles previos según los niveles definidos en los arrays.

  if(this.NewBalanceToOvercome > 0.00)
   {
    if(account_equity > this.NewBalanceToOvercome)
     {
      PrintFormat("%s Equity %.2f exceeded balance to shift risk to %.2f", EA_NAME, account_equity, NewBalanceToOvercome);
      while(!IsStopped() && index_gmlpo > 0)
       {
        if(index_gmlpo > 0)
         {
          if(this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]  < account_equity
             && account_equity < this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1])
           {
            break;
           }

          this.index_gmlpo--;
         }
       }

      this.TheMinimumValueIsExceeded = false;
      if(this.index_gmlpo == 0)
       {
        Print(EA_NAME, " Excellent, the balance has been positively exceeded");
        PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
        this.gmlpo.assigned_percentage = this.gmlpo_percentage;
        this.NewBalanceToOvercome = 0.00;

        PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage);
        SetGMLPO();
       }
      else
        if(index_gmlpo > 0)
         {
          Print(EA_NAME, " Excellent, the balance has been positively exceeded");
          PrintFormat("%s The risk percentage per operation has been modified because the value of %.2f was exceeded", EA_NAME, this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo]);
          this.NewBalanceToOvercome  = this.dynamic_gmlpos.balance_to_activate_the_risk[index_gmlpo - 1];
          this.gmlpo.assigned_percentage = this.dynamic_gmlpos.risk_to_be_adjusted[index_gmlpo - 1];

          PrintFormat("%s The new risk per operation %.2f", EA_NAME, this.gmlpo.assigned_percentage);
          PrintFormat("%s The new balance to overcome: %.2f", EA_NAME, NewBalanceToOvercome);
          SetGMLPO();
         }
     }
   }


Conclusión

A lo largo de este artículo, hemos aprendido paso a paso cómo desarrollar una gestión completa y robusta del riesgo desde cero. En esta última sección, nos enfocamos en cerrar cada detalle y presentamos un concepto avanzado y muy útil: el riesgo dinámico por operación. Esta herramienta permite adaptar automáticamente el nivel de riesgo según el desempeño real de nuestra cuenta, lo que resulta clave para preservar el capital y mejorar nuestra eficiencia como traders.

Espero sinceramente que este material sea útil, especialmente para aquellos que están iniciando su camino en el fascinante mundo de la programación en MQL5.

En la siguiente entrega sobre gestión de riesgo, daremos un paso adicional y aprenderemos cómo implementar de manera práctica todo lo aprendido, integrando esta gestión avanzada en un bot.

Utilizaremos para ello el indicador de Order Blocks desarrollado previamente:

Además, tendremos la oportunidad de comprobar claramente las ventajas concretas de aplicar una gestión de riesgo efectiva en comparación con operar sin ningún tipo de control. Finalmente, configuraremos parámetros específicos que faciliten enormemente el uso cotidiano de estas herramientas avanzadas dentro de cualquier estrategia automatizada.


Archivos usados/mejorados en este artículo:

Nombre del archivo Tipo Descripción 
 Risk_Management.mqh   .mqh (archivo   incluido) Archivo principal que contiene funciones generales y la implementación de la clase CRiskManagement, responsable de gestionar el riesgo dentro del sistema. En este archivo   se definen, desarrollan y amplían todas las funcionalidades relacionadas con la gestión de pérdidas y ganancias.
Archivos adjuntos |
Risk_Management.mqh (126.71 KB)
Redes neuronales en el trading: Análisis de nubes de puntos (PointNet) Redes neuronales en el trading: Análisis de nubes de puntos (PointNet)
El análisis directo de nubes de puntos evita alcanza un tamaño de datos innecesario y mejora la eficacia de los modelos en tareas de clasificación y segmentación. Estos enfoques demuestran un alto rendimiento y solidez frente a las perturbaciones de los datos de origen.
Redes neuronales en el trading: Transformador vectorial jerárquico (Final) Redes neuronales en el trading: Transformador vectorial jerárquico (Final)
Continuamos nuestro análisis del método del Transformador Vectorial Jerárquico. En este artículo finalizaremos la construcción del modelo. También lo entrenaremos y probaremos con datos históricos reales.
Redes neuronales en el trading: Un método complejo de predicción de trayectorias (Traj-LLM) Redes neuronales en el trading: Un método complejo de predicción de trayectorias (Traj-LLM)
En este artículo, me gustaría presentarles un interesante método de predicción de trayectorias desarrollado para resolver problemas en el campo de los movimientos de vehículos autónomos. Los autores del método combinaron los mejores elementos de varias soluciones arquitectónicas.
Métodos de William Gann (Parte II): Creación del indicador Cuadrado de Gann Métodos de William Gann (Parte II): Creación del indicador Cuadrado de Gann
Crearemos un indicador basado en el Cuadrado de Gann de 9, construido elevando al cuadrado el tiempo y el precio. Prepararemos el código y probaremos el indicador en la plataforma en diferentes intervalos de tiempo.