
Desarrollamos un asesor experto multidivisa (Parte 12): Gestor de riesgos como en las empresas de prop-trading
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
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso
Gracias por los comentarios.
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():
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):
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. 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.
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! )
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
¡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.