Gestor de riesgos para el trading manual
Contenido
- Introducción
- Definir las funciones
- Parámetros de entrada y constructor de clase
- Trabajar con periodos límite de riesgo
- Control del uso de los límites
- Manejador de eventos de clase
- Mecanismo de control del objetivo diario de beneficios
- Definición de un método para poner en marcha la supervisión en la estructura de la EA
- La implementación final y las posibilidades de ampliación de la clase
- Ejemplo de uso
- Conclusión
Introducción
Hola a todos. En este artículo seguiremos hablando de la metodología de gestión de riesgos. En el artículo anterior Equilibrio de riesgos en la negociación simultánea de varios instrumentos comerciales, hablamos de los conceptos básicos del riesgo. Ahora implementaremos desde cero la clase básica de Gestor de Riesgo para operar con seguridad. También veremos cómo la limitación de riesgos en los sistemas de negociación afecta a la eficacia de las estrategias comerciales.
"Risk Manager" fue la primera clase que escribí en 2019, poco después de aprender los conceptos básicos de programación. En ese momento, comprendí por mi propia experiencia que el estado psicológico de un trader influye enormemente en la efectividad del trading, especialmente en lo que respecta a la "consistencia" y la "imparcialidad" en la toma de decisiones de trading. Apostar, realizar transacciones impulsivas y aumentar los riesgos para intentar cubrir las pérdidas lo más rápido posible pueden agotar cualquier cuenta, incluso si utilizas una estrategia de trading efectiva que ha mostrado muy buenos resultados en las pruebas.
El objetivo de este artículo es demostrar que el control de riesgos mediante un gestor de riesgos aumenta su eficacia y fiabilidad. Para confirmar esta tesis, crearemos desde cero una sencilla clase base de gestor de riesgos para la negociación manual y la probaremos utilizando una estrategia de ruptura fractal muy simple.
Definir las funciones
Al implementar nuestro algoritmo específicamente para la negociación manual, sólo implementaremos el control sobre los límites de riesgo temporales para el día, la semana y el mes. Una vez que el importe de las pérdidas reales alcanza o supera los límites establecidos por el usuario, el EA debe cerrar automáticamente todas las posiciones abiertas e informar al usuario de la imposibilidad de seguir operando. Cabe señalar aquí que la información será puramente «de carácter consultivo», se mostrará en la línea de comentarios en la esquina inferior izquierda del gráfico con el EA en ejecución. Esto se debe a que estamos creando un gestor de riesgos específicamente para el trading manual, por lo que, "si es absolutamente necesario", el usuario puede eliminar este EA del gráfico en cualquier momento y continuar operando. Sin embargo, no recomendaría hacer esto, porque si el mercado va en tu contra, es mejor volver a operar al día siguiente y evitar grandes pérdidas. En lugar de eso, intenta analizar qué salió mal en tu trading manual. Si integras esta clase en tu trading algorítmico, deberás implementar la restricción de envío de órdenes cuando se alcance el límite y, preferiblemente, integrar esta clase directamente en la estructura del EA. Hablaremos de esto con más detalle un poco más adelante.
Parámetros de entrada y constructor de clase
Hemos decidido que sólo aplicaremos el control de riesgos por periodos y el criterio de alcanzar la cotización diaria de beneficios. Para ello, introducimos varias variables del tipo double con modificador de clase de memoria input para que el usuario introduzca manualmente los valores de riesgo como porcentaje del depósito para cada periodo de tiempo, así como el porcentaje de beneficio diario objetivo para bloquear los beneficios. Para indicar el control del beneficio diario objetivo, introducimos una variable adicional del tipo bool para la capacidad de activar/desactivar esta funcionalidad si el trader desea considerar cada entrada por separado y está seguro de que no existe correlación entre los instrumentos seleccionados. Este tipo de variable de conmutación también se denomina «bandera» (flag). Declaremos el siguiente código a nivel global. Para mayor comodidad, lo hemos «envuelto» previamente en un bloque con nombre utilizando la palabra clave group.
input group "RiskManagerBaseClass" input double inp_riskperday = 1; // risk per day as a percentage of deposit input double inp_riskperweek = 3; // risk per week input double inp_riskpermonth = 9; // risk per month input double inp_plandayprofit = 3; // target daily profit input bool dayProfitControl = true; // whether to close positions after reaching daily profit
Las variables declaradas se inicializan con valores por defecto de acuerdo con la siguiente lógica. Empezaremos por el riesgo diario, ya que esta clase funciona mejor para la negociación intradía, pero también puede utilizarse para la negociación y la inversión a medio plazo. Obviamente, si opera a medio plazo o como inversor, entonces no tiene sentido que controle el riesgo intradía y puede establecer los mismos valores para los riesgos diarios y semanales. Además, si sólo realiza inversiones a largo plazo, puede establecer todos los valores límite iguales a una detracción mensual. Aquí veremos la lógica de los parámetros por defecto para la negociación intradía.
Decidimos que nos sentiríamos cómodos operando con un riesgo diario del 1% del depósito. Si se supera el límite diario, cerramos el terminal hasta mañana. A continuación, definimos el límite semanal del siguiente modo. Normalmente hay 5 días de negociación en una semana, lo que significa que si tenemos 3 días de pérdidas seguidos, dejamos de negociar hasta el comienzo de la semana siguiente. Simplemente porque lo más probable es que no haya entendido el mercado esta semana, o que algo haya cambiado y si sigue operando, acumulará una pérdida tan grande durante este periodo que no podrá cubrirla ni a costa de la semana siguiente. Una lógica similar se aplica a la fijación de un límite mensual cuando se negocia intradía. Aceptamos la condición de que si tuvimos 3 semanas no rentables en un mes, es mejor no operar la cuarta, ya que llevará mucho tiempo «mejorar» la curva de rendimiento a expensas de los periodos futuros. Tampoco queremos "asustar" a los inversionistas con una gran pérdida en un solo mes.
Fijamos el tamaño del objetivo de beneficio diario en función del riesgo diario, teniendo en cuenta las características de su sistema de negociación. Lo que hay que tener en cuenta. En primer lugar, si negocia con instrumentos correlacionados, la frecuencia con la que su sistema de negociación da señales de entrada, si negocia con proporciones fijas entre stop loss y take profit para cada operación individual, o el tamaño del depósito. Me gustaría señalar que NO RECOMIENDO EN ABSOLUTO operar sin un stop loss y sin un gestor de riesgos al mismo tiempo. En este caso, perder el depósito es sólo cuestión de tiempo. Por lo tanto, o bien fijamos stops para cada operación por separado, o bien utilizamos un gestor de riesgos para limitar el riesgo por periodos. En nuestro ejemplo actual de parámetros por defecto, establezco las condiciones para el beneficio diario de 1 a 3 en relación con el riesgo diario. También es mejor utilizar estos parámetros junto con el ajuste obligatorio de riesgo-rentabilidad para CADA operación a través de la relación entre stop loss y take profit, también de 1 a 3 (el take profit es mayor que el stop loss).
La estructura de nuestros límites puede describirse del siguiente modo.
Figura 1. Estructura de límites
A continuación, declaramos nuestro tipo de datos personalizado RiskManagerBase utilizando la palabra clave class. Los parámetros de entrada deberán almacenarse en nuestra clase RiskManagerBase personalizada. Dado que nuestros parámetros de entrada se miden en porcentajes, mientras que los límites se rastrean en la moneda del depósito, necesitamos introducir varios campos correspondientes de tipo double con el modificador de acceso protected a nuestra clase personalizada.
protected: double riskperday, // risk per day as a percentage of deposit riskperweek, // risk per week as a percentage of deposit riskpermonth, // risk per month as a percentage of deposit plandayprofit // target daily profit as a percentage of deposit ; double RiskPerDay, // risk per day in currency RiskPerWeek, // risk per week in currency RiskPerMonth, // risk per month in currency StartBalance, // account balance at the EA start time, in currency StartEquity, // account equity at the limit update time, in currency PlanDayEquity, // target account equity value per day, in currency PlanDayProfit // target daily profit, in currency ; double CurrentEquity, // current equity value CurrentBallance; // current balance
Para la comodidad del cálculo de los límites de riesgo por periodo en la moneda de depósito, en base a los parámetros de entrada, declararemos el método RefreshLimits() dentro de nuestra clase, también con el modificador de acceso protected. Vamos a describir este método fuera de la clase de la siguiente manera. Proporcionaremos para el futuro el tipo de valor de retorno del tipo bool por si necesitamos ampliar nuestro método con la capacidad de comprobar la corrección de los datos obtenidos. Por ahora, describimos el método de la forma siguiente.
//+------------------------------------------------------------------+ //| RefreshLimits | //+------------------------------------------------------------------+ bool RiskManagerBase::RefreshLimits(void) { CurrentEquity = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2); // request current equity value CurrentBallance = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2); // request current balance StartBalance = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE),2); // set start balance StartEquity = NormalizeDouble(AccountInfoDouble(ACCOUNT_EQUITY),2); // request current equity value PlanDayProfit = NormalizeDouble(StartEquity * plandayprofit/100,2); // target daily profit, in currency PlanDayEquity = NormalizeDouble(StartEquity + PlanDayProfit/100,2); // target equity, in currency RiskPerDay = NormalizeDouble(StartEquity * riskperday/100,2); // risk per day in currency RiskPerWeek = NormalizeDouble(StartEquity * riskperweek/100,2); // risk per week in currency RiskPerMonth = NormalizeDouble(StartEquity * riskpermonth/100,2); // risk per month in currency return(true); }
Una forma conveniente es llamar a este método en código cada vez que necesitemos recalcular valores límite al cambiar periodos de tiempo, así como al cambiar inicialmente los valores de los campos al llamar al constructor de la clase. Escribimos el siguiente código en el constructor de la clase para inicializar los valores iniciales de los campos.
//+------------------------------------------------------------------+ //| RiskManagerBase | //+------------------------------------------------------------------+ RiskManagerBase::RiskManagerBase() { riskperday = inp_riskperday; // set the value for the internal variable riskperweek = inp_riskperweek; // set the value for the internal variable riskpermonth = inp_riskpermonth; // set the value for the internal variable plandayprofit = inp_plandayprofit; // set the value for the internal variable RefreshLimits(); // update limits }
Después de decidir la lógica de los parámetros de entrada y el estado inicial de los datos para nuestra clase, pasamos a implementar la contabilidad de los límites.
Trabajar con periodos límite de riesgo
Para trabajar con periodos límite de riesgo, necesitaremos una variable adicional con el tipo de acceso protected. En primer lugar, vamos a declarar nuestra propia bandera para cada periodo en forma de variables de tipo bool, que almacenarán datos sobre el alcance de los límites de riesgo establecidos, así como la bandera principal, que informará sobre la posibilidad de continuar operando sólo si todos los límites están disponibles al mismo tiempo. Esto es necesario para evitar la situación en la que ya se ha superado el límite mensual, pero sigue existiendo un límite diario y, por tanto, se permite la negociación. Esto limitará la negociación cuando se alcance cualquier límite de tiempo antes del siguiente periodo. También necesitaremos variables del mismo tipo para controlar el beneficio diario y el inicio de un nuevo día de negociación. Además añadiremos campos de tipo double para almacenar información sobre las pérdidas y ganancias reales de cada periodo: día, semana y mes. Además, proporcionaremos valores separados para los swaps y las comisiones en las operaciones comerciales.
bool RiskTradePermission; // general variable - whether opening of new trades is allowed bool RiskDayPermission; // flag prohibiting trading if daily limit is reached bool RiskWeekPermission; // flag to prohibit trading if daily limit is reached bool RiskMonthPermission; // flag to prohibit trading if monthly limit is reached bool DayProfitArrive; // variable to control if daily target profit is achieved bool NewTradeDay; // variable for a new trading day //--- actual limits double DayorderLoss; // accumulated daily loss double DayorderProfit; // accumulated daily profit double WeekorderLoss; // accumulated weekly loss double WeekorderProfit; // accumulated weekly profit double MonthorderLoss; // accumulated monthly loss double MonthorderProfit; // accumulated monthly profit double MonthOrderSwap; // monthly swap double MonthOrderCommis; // monthly commission
En concreto, no incluimos los gastos por comisiones y swaps en las pérdidas de los periodos correspondientes, para poder separar en el futuro las pérdidas derivadas de la herramienta de toma de decisiones de las pérdidas relacionadas con los requisitos de comisiones y swaps de los distintos intermediarios. Ahora que hemos declarado los campos correspondientes de nuestra clase, pasemos a controlar el uso de los límites.
Control del uso de los límites
Para controlar el uso real de los límites, tendremos que gestionar los eventos asociados al inicio de cada nuevo periodo, así como los eventos asociados a la aparición de operaciones de negociación finalizadas. Para llevar correctamente la cuenta de los límites realmente utilizados, anunciaremos el método interno ForOnTrade() en el área de acceso protected de nuestra clase.
En primer lugar, tendremos que proporcionar variables en el método para tener en cuenta la hora actual, así como la hora de inicio del día, la semana y el mes. Para estos fines, utilizaremos un tipo de datos predefinido especial del tipo de estructura struct en el formato MqlDateTime. Los inicializaremos inmediatamente con la hora terminal actual de la siguiente forma.
MqlDateTime local, start_day, start_week, start_month; // create structure to filter dates TimeLocal(local); // fill in initially TimeLocal(start_day); // fill in initially TimeLocal(start_week); // fill in initially TimeLocal(start_month); // fill in initially
Tenga en cuenta que para inicializar inicialmente la hora actual, utilizamos la función predefinida TimeLocal() en lugar de TimeCurrent() porque la primera utiliza la hora local, y la segunda toma la hora del último tick recibido del broker, lo que puede provocar una contabilización incorrecta de los límites debido a la diferencia de husos horarios entre distintos brokers. A continuación, tenemos que restablecer la hora de inicio de cada periodo para obtener los valores de fecha de inicio de cada uno de ellos. Para ello, accederemos a los campos públicos de nuestras estructuras del siguiente modo.
//--- reset to have the report from the beginning of the period start_day.sec = 0; // from the day beginning start_day.min = 0; // from the day beginning start_day.hour = 0; // from the day beginning start_week.sec = 0; // from the week beginning start_week.min = 0; // from the week beginning start_week.hour = 0; // from the week beginning start_month.sec = 0; // from the month beginning start_month.min = 0; // from the month beginning start_month.hour = 0; // from the month beginning
Para obtener correctamente los datos de la semana y el mes, necesitamos definir la lógica para encontrar el principio de la semana y del mes. En el caso de un mes, todo es bastante sencillo, sabemos que cada mes empieza el primer día. Tratar con una semana es un poco más complicado porque no hay un punto de notificación específico y la fecha cambiará cada vez. Aquí podemos utilizar el campo especial day_of_week de la estructura MqlDateTime. Permite obtener el número del día de la semana a partir de la fecha actual comenzando por cero. Conociendo este valor, podemos averiguar fácilmente la fecha de inicio de la semana actual de la siguiente manera.
//--- determining the beginning of the week int dif; // day of week difference variable if(start_week.day_of_week==0) // if this is the first day of the week { dif = 0; // then reset } else { dif = start_week.day_of_week-1; // if not the first, then calculate the difference start_week.day -= dif; // subtract the difference at the beginning of the week from the number of the day } //---month start_month.day = 1; // everything is simple with the month
Ahora que tenemos las fechas exactas de inicio de cada periodo en relación con el momento actual, podemos pasar a solicitar datos históricos sobre las transacciones realizadas en la cuenta. Inicialmente, tendremos que declarar las variables necesarias para contabilizar los pedidos cerrados y restablecer los valores de las variables en las que se recogerán los resultados financieros de las transacciones para cada periodo seleccionado.
//--- uint total = 0; // number of selected trades ulong ticket = 0; // order number long type; // order type double profit = 0, // order profit commis = 0, // order commission swap = 0; // order swap DayorderLoss = 0; // daily loss without commission DayorderProfit = 0; // daily profit WeekorderLoss = 0; // weekly loss without commission WeekorderProfit = 0; // weekly profit MonthorderLoss = 0; // monthly loss without commission MonthorderProfit = 0; // monthly profit MonthOrderCommis = 0; // monthly commission MonthOrderSwap = 0; // monthly swap
Solicitaremos datos históricos de órdenes cerradas a través de la función predefinida del terminal HistorySelect(). Los parámetros de esta función utilizarán las fechas que recibimos anteriormente para cada periodo. Para ello, tendremos que llevar nuestra variable MqlDateTime al tipo requerido por los parámetros HistorySelect() de la función, que es datetime. Para ello, utilizaremos la función predefinida del terminal StructToTime(). Solicitaremos los datos sobre las transacciones de la misma manera, sustituyendo los valores necesarios para el principio y el final del periodo requerido.
Después de cada llamada de la función HistorySelect(), necesitamos obtener el número de pedidos seleccionados usando la función del terminal predefinida HistoryDealsTotal() y poner este valor en nuestra variable local total. Tras obtener el número de operaciones cerradas, podemos organizar un bucle con el operador for, solicitando el número de cada orden a través de la función predefinida del terminal HistoryDealGetTicket(). Esto nos permitirá acceder a los datos de cada pedido. Accederemos a los datos de cada orden utilizando las funciones predefinidas del terminal HistoryDealGetDouble() y HistoryDealGetInteger(), pasándoles el número de orden recibido previamente. Tendremos que especificar el identificador de la propiedad de la operación correspondiente de las enumeraciones ENUM_DEAL_PROPERTY_INTEGER y ENUM_DEAL_PROPERTY_DOUBLE. También tendremos que añadir un filtro mediante un operador de selección booleano if para considerar sólo las operaciones de trading comprobando los valores DEAL_TYPE_BUY y DEAL_TYPE_SELL de la enumeración ENUM_DEAL_TYPE para filtrar otras operaciones de la cuenta, como las transacciones de saldo y las acumulaciones de bonificaciones. Así, terminaremos con el siguiente código para seleccionar los datos.
//--- now select data by --==DAY==-- HistorySelect(StructToTime(start_day),StructToTime(local)); // select required history //--- check total = HistoryDealsTotal(); // number number of selected deals ticket = 0; // order number profit = 0; // order profit commis = 0; // order commission swap = 0; // order swap //--- for all deals for(uint i=0; i<total; i++) // loop through all selected orders { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // get the number of each in order { //--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); // get data on operation type if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) // if the deal is form a trading operatoin { if(profit>0) // if financial result of current order is greater than 0, { DayorderProfit += profit; // add to profit } else { DayorderLoss += MathAbs(profit); // if loss, add up } } } } //--- now select data by --==WEEK==-- HistorySelect(StructToTime(start_week),StructToTime(local)); // select the required history //--- check total = HistoryDealsTotal(); // number number of selected deals ticket = 0; // order number profit = 0; // order profit commis = 0; // order commission swap = 0; // order swap //--- for all deals for(uint i=0; i<total; i++) // loop through all selected orders { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // get the number of each in order { //--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); // get data on operation type if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) // if the deal is form a trading operatoin { if(profit>0) // if financial result of current order is greater than 0, { WeekorderProfit += profit; // add to profit } else { WeekorderLoss += MathAbs(profit); // if loss, add up } } } } //--- now select data by --==MONTH==-- HistorySelect(StructToTime(start_month),StructToTime(local)); // select the required history //--- check total = HistoryDealsTotal(); // number number of selected deals ticket = 0; // order number profit = 0; // order profit commis = 0; // order commission swap = 0; // order swap //--- for all deals for(uint i=0; i<total; i++) // loop through all selected orders { //--- try to get deals ticket if((ticket=HistoryDealGetTicket(i))>0) // get the number of each in order { //--- get deals properties profit = HistoryDealGetDouble(ticket,DEAL_PROFIT); // get data on financial results commis = HistoryDealGetDouble(ticket,DEAL_COMMISSION); // get data on commission swap = HistoryDealGetDouble(ticket,DEAL_SWAP); // get swap data type = HistoryDealGetInteger(ticket,DEAL_TYPE); // get data on operation type MonthOrderSwap += swap; // sum up swaps MonthOrderCommis += commis; // sum up commissions if(type == DEAL_TYPE_BUY || type == DEAL_TYPE_SELL) // if the deal is form a trading operatoin { if(profit>0) // if financial result of current order is greater than 0, { MonthorderProfit += profit; // add to profit } else { MonthorderLoss += MathAbs(profit); // if loss, sum up } } } }
El método anterior puede ser llamado cada vez que necesitemos actualizar los valores actuales de uso de límites. Podemos actualizar los valores de los límites reales, así como llamar a esta función, al generar varios eventos terminales. Dado que el objetivo de este método es actualizar los límites, esto se puede hacer cuando se producen eventos relacionados con cambios en las órdenes actuales, como Trade y TradeTransaction, y siempre que surja un nuevo tick con el evento NewTick. Como nuestro método consume pocos recursos, actualizaremos los límites reales en cada tick. Ahora vamos a implementar el manejador de eventos necesario para manejar los eventos relacionados con la cancelación dinámica y la resolución de operaciones.
Manejador de eventos de clase
Para manejar los eventos, definimos un método interno de nuestra clase ContoEvents() con el nivel de acceso protected. Para ello, declaramos campos auxiliares adicionales con el mismo nivel de acceso. Para poder rastrear instantáneamente la hora de inicio de un nuevo período de negociación, que necesitamos para cambiar los indicadores de permiso de negociación, necesitamos almacenar los valores del último período registrado y del período actual. A estos efectos, podemos utilizar arrays simples declarados con el tipo de datos datetime para almacenar los valores de los periodos correspondientes.
//--- additional auxiliary arrays datetime Periods_old[3]; // 0-day,1-week,2-mn datetime Periods_new[3]; // 0-day,1-week,2-mn
En la primera dimensión almacenaremos los valores del día, en la segunda los de la semana y en la tercera los del mes. Si es necesario ampliar aún más los periodos controlados, puede declarar estas matrices no de forma estática, sino dinámica. Pero aquí sólo trabajamos con tres periodos de tiempo. Ahora vamos a añadir a nuestro constructor de clase la inicialización primaria de estas variables de matriz de la siguiente manera.
Periods_new[0] = iTime(_Symbol, PERIOD_D1, 1); // initialize the current day with the previous period Periods_new[1] = iTime(_Symbol, PERIOD_W1, 1); // initialize the current week with the previous period Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 1); // initialize the current month with the previous period
Inicializaremos cada periodo correspondiente utilizando una función terminal predefinida iTime() pasando en los parámetros el periodo correspondiente de ENUM_TIMEFRAMES del periodo anterior al actual. Deliberadamente no inicializamos la matriz Periods_old[]. En este caso, después de llamar al constructor y a nuestro método ContoEvents(), nos aseguramos de que se dispara el evento de inicio del nuevo periodo de negociación y se abren todas las banderas de inicio de negociación, y sólo se cierran por código si no quedan límites. De lo contrario, es posible que la clase no funcione correctamente cuando se reinicie. El método descrito contendrá una lógica simple: si el período actual no es igual al anterior, significa que ha comenzado un nuevo período correspondiente y puede restablecer los límites y permitir la negociación cambiando los valores en las banderas. Además, para cada periodo, llamaremos al método RefreshLimits() descrito anteriormente para recalcular los límites de entrada.
//+------------------------------------------------------------------+ //| ContoEvents | //+------------------------------------------------------------------+ void RiskManagerBase::ContoEvents() { // check the start of a new trading day NewTradeDay = false; // variable for new trading day set to false Periods_old[0] = Periods_new[0]; // copy to old, new Periods_new[0] = iTime(_Symbol, PERIOD_D1, 0); // update new for day if(Periods_new[0]!=Periods_old[0]) // if do not match, it's a new day { Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade day!"); // inform NewTradeDay = true; // variable to true DayProfitArrive = false; // reset flag of reaching target profit after a new day started RiskDayPermission = true; // allow opening new positions RefreshLimits(); // update limits DayorderLoss = 0; // reset daily financial result DayorderProfit = 0; // reset daily financial result } // check the start of a new trading week Periods_old[1] = Periods_new[1]; // copy data to old period Periods_new[1] = iTime(_Symbol, PERIOD_W1, 0); // fill new period for week if(Periods_new[1]!= Periods_old[1]) // if periods do not match, it's a new week { Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade week!"); // inform RiskWeekPermission = true; // allow opening new positions RefreshLimits(); // update limits WeekorderLoss = 0; // reset weekly losses WeekorderProfit = 0; // reset weekly profits } // check the start of a new trading month Periods_old[2] = Periods_new[2]; // copy the period to the old one Periods_new[2] = iTime(_Symbol, PERIOD_MN1, 0); // update new period for month if(Periods_new[2]!= Periods_old[2]) // if do not match, it's a new month { Print(__FUNCTION__+" line"+IntegerToString(__LINE__)+", New trade Month!"); // inform RiskMonthPermission = true; // allow opening new positions RefreshLimits(); // update limits MonthorderLoss = 0; // reset the month's loss MonthorderProfit = 0; // reset the month's profit } // set the permission to open new positions true only if everything is true // set to true if(RiskDayPermission == true && // if there is a daily limit available RiskWeekPermission == true && // if there is a weekly limit available RiskMonthPermission == true // if there is a monthly limit available ) // { RiskTradePermission=true; // if all are allowed, trading is allowed } // set to false if at least one of them is false if(RiskDayPermission == false || // no daily limit available RiskWeekPermission == false || // or no weekly limit available RiskMonthPermission == false || // or no monthly limit available DayProfitArrive == true // or target profit is reached ) // then { RiskTradePermission=false; // prohibit trading } }
También en este método, hemos añadido el control sobre el estado de los datos en la variable principal de la bandera para la posibilidad de abrir nuevas posiciones, RiskTradePermission. Mediante operadores lógicos de selección, implementamos la habilitación del permiso a través de esta variable sólo si todos los permisos son verdaderos, y la deshabilitación si al menos una de las banderas no permite la negociación. Esta variable será muy útil si integra esta clase en un EA algorítmico ya creado; puede simplemente recibirla a través de un getter e insertarla en el código con las condiciones para colocar sus órdenes. En nuestro caso, servirá simplemente como indicador para empezar a informar al usuario de la ausencia de límites de negociación libres. Ahora que nuestra clase ha «aprendido» a controlar los riesgos cuando se alcanzan las pérdidas especificadas, pasemos a implementar la funcionalidad para controlar la consecución del beneficio objetivo.
Mecanismo de control del objetivo diario de beneficios
En la parte anterior de nuestros artículos, hemos declarado una bandera para lanzar el control sobre el beneficio objetivo y una variable de entrada para determinar su valor relativo al tamaño del depósito de la cuenta. Según la lógica de nuestra clase que controla la consecución del beneficio objetivo, todas las posiciones abiertas se cerrarán si el beneficio total de todas las posiciones ha alcanzado el valor objetivo. Para cerrar todas las posiciones de una cuenta, declararemos en nuestra clase el método interno AllOrdersClose() con el nivel de acceso public. Para que este método funcione, necesitaremos recibir datos sobre las posiciones abiertas y enviar automáticamente órdenes para cerrarlas.
Para no perder tiempo escribiendo nuestras propias implementaciones de esta funcionalidad, utilizaremos clases internas ya creadas del terminal. Utilizaremos la clase terminal estándar interna CPositionInfo para trabajar con posiciones abiertas y la clase CTrade para cerrar posiciones abiertas. Declaremos las variables de estas dos clases también con el nivel de acceso protected sin usar puntero con constructor por defecto de la siguiente forma.
CTrade r_trade; // instance CPositionInfo r_position; // instance
Cuando trabajemos con estos objetos, en el marco de la funcionalidad que necesitamos ahora, no necesitaremos configurarlos adicionalmente, por lo que no los escribiremos en el constructor de nuestra clase. A continuación se muestra la implementación de este método utilizando las clases declaradas:
//+------------------------------------------------------------------+ //| AllOrdersClose | //+------------------------------------------------------------------+ bool RiskManagerBase::AllOrdersClose() // closing market positions { ulong ticket = 0; // order ticket string symb; for(int i = PositionsTotal(); i>=0; i--) // loop through open positoins { if(r_position.SelectByIndex(i)) // if a position selected { ticket = r_position.Ticket(); // remember position ticket if(!r_trade.PositionClose(ticket)) // close by ticket { Print(__FUNCTION__+". Error close order. "+IntegerToString(ticket)); // if not, inform return(false); // return false } else { Print(__FUNCTION__+". Order close success. "+IntegerToString(ticket)); // if not, inform continue; // if everything is ok, continue } } } return(true); // return true }
Llamaremos al método descrito tanto cuando se alcance el beneficio objetivo como cuando se alcancen los límites. También devuelve un valor bool por si fuera necesario gestionar errores en el envío de órdenes de cierre. Para proporcionar funcionalidad para controlar si se alcanza el beneficio objetivo, complementaremos nuestro método de manejo de eventos ContoEvents() con el siguiente código inmediatamente después del código ya descrito anteriormente.
//--- daily if(dayProfitControl) // check if functionality is enabled by the user { if(CurrentEquity >= (StartEquity+PlanDayProfit)) // if equity exceeds or equals start + target profit, { DayProfitArrive = true; // set flag that target profit is reached Print(__FUNCTION__+", PlanDayProfit has been arrived."); // inform about the event Print(__FUNCTION__+", CurrentEquity = "+DoubleToString(CurrentEquity)+ ", StartEquity = "+DoubleToString(StartEquity)+ ", PlanDayProfit = "+DoubleToString(PlanDayProfit)); AllOrdersClose(); // close all open orders StartEquity = CurrentEquity; // rewrite starting equity value //--- send a push notification ResetLastError(); // reset the last error if(!SendNotification("The planned profitability for the day has been achieved. Equity: "+DoubleToString(CurrentEquity)))// notification { Print(__FUNCTION__+IntegerToString(__LINE__)+", Error of sending notification: "+IntegerToString(GetLastError()));// if not, print } } }
El método incluye el envío de una notificación push al usuario para notificarle que se ha producido este evento. Para ello, utilizamos la función predefinida del terminal SendNotification. Para completar la funcionalidad mínima requerida de nuestra clase, sólo nos falta montar un método de clase más con acceso public, que será llamado cuando se conecte una gestora de riesgos a la estructura de nuestro EA.
Definición de un método para poner en marcha la supervisión en la estructura de la EA
Para añadir la funcionalidad de monitorización desde una instancia de nuestra clase gestora de riesgos a la estructura del EA, declararemos el método público ContoMonitor(). En este método, recopilaremos todos los métodos de gestión de eventos declarados anteriormente y también lo complementaremos con una funcionalidad para comparar los límites realmente utilizados con los valores aprobados por el usuario en los parámetros de entrada. Declaremos este método con el nivel de acceso public y describámoslo fuera de la clase como sigue.
//+------------------------------------------------------------------+ //| ContoMonitor | //+------------------------------------------------------------------+ void RiskManagerBase::ContoMonitor() // monitoring { ForOnTrade(); // update at each tick ContoEvents(); // event block //--- double currentProfit = AccountInfoDouble(ACCOUNT_PROFIT); if((MathAbs(DayorderLoss)+MathAbs(currentProfit) >= RiskPerDay && // if equity is less than or equal to the start balance minus the daily risk currentProfit<0 && // profit below zero RiskDayPermission==true) // day trading is allowed || // OR (RiskDayPermission==true && // day trading is allowed MathAbs(DayorderLoss) >= RiskPerDay) // loss exceed daily risk ) { Print(__FUNCTION__+", EquityControl, "+"ACCOUNT_PROFIT = " +DoubleToString(currentProfit));// notify Print(__FUNCTION__+", EquityControl, "+"RiskPerDay = " +DoubleToString(RiskPerDay)); // notify Print(__FUNCTION__+", EquityControl, "+"DayorderLoss = " +DoubleToString(DayorderLoss)); // notify RiskDayPermission=false; // prohibit opening new orders during the day AllOrdersClose(); // close all open positions } // check if there is a WEEK limit available for opening a new position if there are no open ones if( MathAbs(WeekorderLoss)>=RiskPerWeek && // if weekly loss is greater than or equal to the weekly risk RiskWeekPermission==true) // and we traded { RiskWeekPermission=false; // prohibit opening of new orders during the day AllOrdersClose(); // close all open positions Print(__FUNCTION__+", EquityControl, "+"WeekorderLoss = "+DoubleToString(WeekorderLoss)); // notify Print(__FUNCTION__+", EquityControl, "+"RiskPerWeek = "+DoubleToString(RiskPerWeek)); // notify } // check if there is a MONTH limit available for opening a new position if there are no open ones if( MathAbs(MonthorderLoss)>=RiskPerMonth && // if monthly loss is greater than or equal to the monthly risk RiskMonthPermission==true) // we traded { RiskMonthPermission=false; // prohibit opening of new orders during the day AllOrdersClose(); // close all open positions Print(__FUNCTION__+", EquityControl, "+"MonthorderLoss = "+DoubleToString(MonthorderLoss)); // notify Print(__FUNCTION__+", EquityControl, "+"RiskPerMonth = "+DoubleToString(RiskPerMonth)); // notify } }
La lógica de funcionamiento de nuestro método es muy sencilla: si el límite de pérdida real para un mes o una semana supera el establecido por el usuario, el indicador de negociación para un periodo determinado se establece como prohibido y, en consecuencia, se prohíbe la negociación. La única diferencia está en los límites diarios, donde también necesitamos controlar la presencia de posiciones abiertas; para ello, añadiremos también el control del beneficio actual de las posiciones abiertas a través del operador lógico OR. Cuando se alcanzan los límites de riesgo, llamamos a nuestro método para cerrar posiciones e imprimimos el registro sobre este evento.
En esta etapa, para completar la clase, sólo tenemos que añadir un método para que el usuario controle los límites actuales. La forma más sencilla y cómoda sería mostrar la información necesaria a través de la función predefinida estándar del terminal, Comment(). Para trabajar con esta función, necesitaremos pasarle un parámetro de tipo string que contenga información a mostrar en el gráfico. Para obtener estos valores de nuestra clase, declaramos el método Message() con el nivel de acceso public, que devolverá un string con los datos recogidos de todas las variables que el usuario necesite.
//+------------------------------------------------------------------+ //| Message | //+------------------------------------------------------------------+ string RiskManagerBase::Message(void) { string msg; // message msg += "\n"+" ----------Risk-Manager---------- "; // common //--- msg += "\n"+"RiskTradePer = "+(string)RiskTradePermission; // final trade permission msg += "\n"+"RiskDayPer = "+(string)RiskDayPermission; // daily risk available msg += "\n"+"RiskWeekPer = "+(string)RiskWeekPermission; // weekly risk available msg += "\n"+"RiskMonthPer = "+(string)RiskMonthPermission; // monthly risk available //---limits and inputs msg += "\n"+" -------------------------------- "; // msg += "\n"+"RiskPerDay = "+DoubleToString(RiskPerDay,2); // daily risk in usd msg += "\n"+"RiskPerWeek = "+DoubleToString(RiskPerWeek,2); // weekly risk in usd msg += "\n"+"RiskPerMonth = "+DoubleToString(RiskPerMonth,2); // monthly risk usd //--- current profits and losses for periods msg += "\n"+" -------------------------------- "; // msg += "\n"+"DayLoss = "+DoubleToString(DayorderLoss,2); // daily loss msg += "\n"+"DayProfit = "+DoubleToString(DayorderProfit,2); // daily profit msg += "\n"+"WeekLoss = "+DoubleToString(WeekorderLoss,2); // weekly loss msg += "\n"+"WeekProfit = "+DoubleToString(WeekorderProfit,2); // weekly profit msg += "\n"+"MonthLoss = "+DoubleToString(MonthorderLoss,2); // monthly loss msg += "\n"+"MonthProfit = "+DoubleToString(MonthorderProfit,2); // monthly profit msg += "\n"+"MonthCommis = "+DoubleToString(MonthOrderCommis,2); // monthly commissions msg += "\n"+"MonthSwap = "+DoubleToString(MonthOrderSwap,2); // monthly swaps //--- for current monitoring if(dayProfitControl) // if control daily profit { msg += "\n"+" ---------dayProfitControl-------- "; // msg += "\n"+"DayProfitArrive = "+(string)DayProfitArrive; // daily profit achieved msg += "\n"+"StartBallance = "+DoubleToString(StartBalance,2); // starting balance msg += "\n"+"PlanDayProfit = "+DoubleToString(PlanDayProfit,2); // target profit msg += "\n"+"PlanDayEquity = "+DoubleToString(PlanDayEquity,2); // target equity } return(msg); // return value }
El mensaje para el usuario creado por el método tendrá este aspecto.
Figura 2. Formato de salida de datos.
Este método puede modificarse o completarse añadiendo elementos para trabajar con gráficos en el terminal. Pero lo usaremos así ya que proporciona al usuario suficientes datos de nuestra clase para tomar una decisión. Si lo desea, puede perfeccionar este formato en el futuro y hacerlo más bonito en términos gráficos. Analicemos ahora las posibilidades de ampliar esta clase cuando se utilizan estrategias de negociación individuales.
La implementación final y las posibilidades de ampliación de la clase
Como hemos mencionado antes, la funcionalidad que describimos aquí es la mínima necesaria y la más universal para casi todas las estrategias de negociación. Permite controlar los riesgos y evitar la pérdida del depósito en un solo día. En esta parte del artículo, veremos varias posibilidades más para ampliar esta clase.
- Controle el tamaño del spread cuando opere con un stop loss corto
- Control del deslizamiento de las posiciones abiertas
- Controlar el objetivo de beneficio mensual
Para el primer punto, podemos implementar una funcionalidad adicional para los sistemas de negociación que utilizan la negociación con stop loss corto. Puede declarar el método SpreadMonitor(int intSL) que toma como parámetro el stop loss técnico o calculado para un instrumento en puntos para compararlo con el nivel de spread actual. Este método prohibirá colocar una orden si el diferencial se amplía mucho en relación con el stop loss en una proporción determinada por el usuario, para evitar el alto riesgo de cerrar la posición en el stop loss debido al diferencial.
Para controlar el deslizamiento en el momento de la apertura, de acuerdo con el segundo punto, puede declarar el método SlippageCheck(). Este método cerrará cada operación individual si el corredor la abrió a un precio muy distinto del indicado, debido a lo cual el riesgo de la operación superó el valor previsto. Esto permitirá, en caso de que se active el stop loss, no estropear las estadísticas por operaciones de alto riesgo por una entrada separada. Además, cuando se opera con una relación fija entre stop loss y take profit, esta relación empeora debido al deslizamiento y es mejor cerrar la posición con una pérdida pequeña que incurrir en pérdidas mayores más adelante.
De forma similar a la lógica de control del beneficio diario, es posible aplicar un método correspondiente para controlar el beneficio mensual objetivo. Este método puede utilizarse cuando se negocian estrategias a largo plazo. La clase que hemos descrito ya tiene toda la funcionalidad necesaria para su uso en la negociación intradía manual, y puede integrarse en la implementación final de un EA de negociación, que debe lanzarse en el gráfico del instrumento simultáneamente con el inicio de la negociación manual.
El montaje final del proyecto incluye conectar nuestra clase usando la directiva del preprocesador #include.
#include <RiskManagerBase.mqh>
A continuación, declaramos el puntero de nuestro objeto gestor de riesgos a nivel global.
RiskManagerBase *RMB;
Al inicializar nuestro EA, asignamos manualmente memoria a nuestro objeto para prepararlo antes del lanzamiento.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { RMB = new RiskManagerBase(); //--- return(INIT_SUCCEEDED); }
Cuando eliminamos nuestro EA del gráfico, necesitamos borrar la memoria de nuestro objeto para evitar una fuga de memoria. Para ello, escriba lo siguiente en la función OnDeinit del EA.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- delete RMB; }
Además, si es necesario, en el mismo evento puede llamar al método Comment(" "), pasándole una cadena vacía, para que el gráfico se limpie de comentarios cuando se elimine el EA del gráfico de símbolos.
Llamamos al método principal de monitorización de nuestra clase en el evento de recibir un nuevo tick para el símbolo.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { RMB.ContoMonitor(); Comment(RMB.Message()); } //+------------------------------------------------------------------+
Esto completa el montaje de nuestro EA con el gestor de riesgos incorporado y está completamente listo para su uso (archivo ManualRiskManager.mq5). Para probar varios casos de su uso, haremos una pequeña adición al código actual para simular el proceso de negociación manual.
Ejemplo de uso
Para visualizar el proceso de negociación manual con y sin el uso de un gestor de riesgos, necesitaremos código adicional que modele la negociación manual. Dado que en este artículo no tocaremos el tema de la elección de estrategias de negociación, no implementaremos la funcionalidad completa de negociación en código. En su lugar, tomaremos visualmente las entradas del gráfico diario y añadiremos datos ya preparados a nuestro EA. Utilizaremos una estrategia muy simple para tomar decisiones de trading y veremos el resultado financiero final de esta estrategia con la única diferencia: con y sin control de riesgo.
Como ejemplos de entradas, utilizaremos una estrategia simple con rupturas de un nivel fractal, para el instrumento USDJPY, durante un período de dos meses. Veamos cómo funciona esta estrategia con y sin control de riesgos. Esquemáticamente, las señales de estrategia para las entradas manuales serán las siguientes.
Figura 3. Entradas utilizando una estrategia de prueba
Para modelar esta estrategia, escribamos un pequeño añadido como prueba unitaria universal para cualquier estrategia manual, de forma que cada usuario pueda probar sus entradas con pequeñas modificaciones. Durante esta prueba, la estrategia ejecutará señales pre-cargadas ya preparadas, sin implementar su propia lógica para entrar al mercado. Para ello, primero tenemos que declarar una estructura adicional, struct, que almacenará nuestras entradas basadas en fractales.
//+------------------------------------------------------------------+ //| TradeInputs | //+------------------------------------------------------------------+ struct TradeInputs { string symbol; // symbol ENUM_POSITION_TYPE direction; // direction double price; // price datetime tradedate; // date bool done; // trigger flag };
La clase principal que se encargará de modelar las señales de negociación es TradeModel. El constructor de la clase aceptará un contenedor con parámetros de entrada de señal, y su método principal Processing() controlará cada tick si ha llegado la hora del punto de entrada basándose en los valores de entrada. Dado que estamos simulando la operativa intradía, al final del día eliminaremos todas las posiciones utilizando el método AllOrdersClose() previamente declarado en nuestra clase gestora de riesgos. Aquí está nuestra clase auxiliar.
//+------------------------------------------------------------------+ //| TradeModel | //+------------------------------------------------------------------+ class TradeModel { protected: CTrade *cTrade; // to trade TradeInputs container[]; // container of entries int size; // container size public: TradeModel(const TradeInputs &inputs[]); ~TradeModel(void); void Processing(); // main modeling method };
Para poder realizar pedidos cómodamente, utilizaremos la clase de terminal estándar CTrade, que contiene toda la funcionalidad que necesitamos. Así ahorraremos tiempo en el desarrollo de nuestra clase auxiliar. Para pasar parámetros de entrada al crear una instancia de clase, definimos nuestro constructor con un parámetro de entrada del contenedor de entradas.
//+------------------------------------------------------------------+ //| TradeModel | //+------------------------------------------------------------------+ TradeModel::TradeModel(const TradeInputs &inputs[]) { size = ArraySize(inputs); // get container size ArrayResize(container, size); // resize for(int i=0; i<size; i++) // loop through inputs { container[i] = inputs[i]; // copy to internal } //--- trade class cTrade=new CTrade(); // create trade instance if(CheckPointer(cTrade)==POINTER_INVALID) // if instance not created, { Print(__FUNCTION__+IntegerToString(__LINE__)+" Error creating object!"); // notify } cTrade.SetTypeFillingBySymbol(Symbol()); // fill type for the symbol cTrade.SetDeviationInPoints(1000); // deviation cTrade.SetExpertMagicNumber(123); // magic number cTrade.SetAsyncMode(false); // asynchronous method }
En el constructor, inicializamos el contenedor de parámetros de entrada con el valor deseado, recordamos su tamaño y creamos un objeto de nuestra clase CTrade con la configuración necesaria. La mayoría de los parámetros aquí no son configurados por el usuario, ya que no afectarán el propósito de crear nuestra prueba unitaria, por lo que los dejamos codificados de forma fija.
El destructor de nuestra clase TradeModel sólo requerirá la eliminación de un objeto CTrade.
//+------------------------------------------------------------------+ //| ~TradeModel | //+------------------------------------------------------------------+ TradeModel::~TradeModel(void) { if(CheckPointer(cTrade)!=POINTER_INVALID) // if there is an instance, { delete cTrade; // delete } }
Ahora vamos a implementar nuestro principal método de procesamiento para el funcionamiento de nuestra clase en la estructura de todo nuestro proyecto. Implementemos la lógica para realizar pedidos según la Figura 3:
//+------------------------------------------------------------------+ //| Processing | //+------------------------------------------------------------------+ void TradeModel::Processing(void) { datetime timeCurr = TimeCurrent(); // request current time double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID); // take bid double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK); // take ask for(int i=0; i<size; i++) // loop through inputs { if(container[i].done==false && // if we haven't traded yet AND container[i].tradedate <= timeCurr) // date is correct { switch(container[i].direction) // check trade direction { //--- case POSITION_TYPE_BUY: // if Buy, if(container[i].price >= ask) // check if price has reached and { if(cTrade.Buy(0.1)) // by the same lot { container[i].done = true; // if time has passed, put a flag Print("Buy has been done"); // notify } else // if hasn't passed, { Print("Error: buy"); // notify } } break; // complete the case //--- case POSITION_TYPE_SELL: // if Sell if(container[i].price <= bid) // check if price has reached and { if(cTrade.Sell(0.1)) // sell the same lot { container[i].done = true; // if time has passed, put a flag Print("Sell has been done"); // notify } else // if hasn't passed, { Print("Error: sell"); // notify } } break; // complete the case //--- default: Print("Wrong inputs"); // notify return; break; } } } }
La lógica de este método es bastante sencilla. Si hay entradas sin procesar en el contenedor para las que ha llegado el momento de modelar, colocamos estas órdenes de acuerdo con la dirección y el precio del fractal marcado en la Figura 3. Esta funcionalidad es suficiente para probar el gestor de riesgos, por lo que podemos integrarlo en nuestro proyecto principal.
En primer lugar, vamos a conectar nuestra clase de prueba con el código del EA de la siguiente manera.
#include <TradeModel.mqh>
Ahora, en la función OnInit(), creamos una instancia de nuestra estructura de matriz de datos TradeInputs y pasamos esta matriz al constructor de la clase TradeModel para inicializarla.
//--- TradeInputs modelInputs[] = { {"USDJPYz", POSITION_TYPE_SELL, 146.636, D'2024-01-31',false}, {"USDJPYz", POSITION_TYPE_BUY, 148.794, D'2024-02-05',false}, {"USDJPYz", POSITION_TYPE_BUY, 148.882, D'2024-02-08',false}, {"USDJPYz", POSITION_TYPE_SELL, 149.672, D'2024-02-08',false} }; //--- tModel = new TradeModel(modelInputs);
No olvides borrar la memoria de nuestro objeto tModel en la función DeInit(). La funcionalidad principal se realizará en la función OnTick(), complementada con el siguiente código.
tModel.Processing(); // place orders MqlDateTime time_curr; // current time structure TimeCurrent(time_curr); // request current time if(time_curr.hour >= 23) // if end of day { RMB.AllOrdersClose(); // close all positions }
Ahora comparemos los resultados de la misma estrategia con y sin la clase de control de riesgos. Vamos a ejecutar el archivo de prueba unitaria ManualRiskManager(UniTest1) sin el método de control de riesgos. Para el periodo de enero a marzo de 2024, obtenemos el siguiente resultado de nuestra estrategia.
Figura 4. Probar datos sin utilizar un gestor de riesgos
Como resultado, obtenemos una expectativa matemática positiva para esta estrategia con los siguientes parámetros.
# | Nombre del parámetro | Valor del parámetro |
---|---|---|
1 | EA | ManualRiskManager(UniTest1) |
2 | Símbolo | USDJPY |
3 | Timeframe | М15 |
4 | Intervalo de tiempo | 2024.01.01 - 2024.03.18 |
5 | Período Forward | NO |
6 | Retrasos | Sin retrasos, ejecución ideal |
7 | Modelado | Todos los ticks |
8 | Depósito inicial | 10.000 USD |
9 | Apalancamiento | 1:100 |
Tabla 1. Parámetros de entrada en el probador de estrategias
Ahora vamos a ejecutar el archivo de prueba unitaria ManualRiskManager(UniTest2), donde utilizamos nuestra clase de gestor de riesgos con los siguientes parámetros de entrada.
Nombre del parámetro de entrada | Valor de la variable |
---|---|
inp_riskperday | 0.25 |
inp_riskperweek | 0.75 |
inp_riskpermonth | 2.25 |
inp_plandayprofit | 0.78 |
dayProfitControl | true |
Tabla 2. Parámetros de entrada para el gestor de riesgos
La lógica para generar parámetros de entrada es similar a la lógica descrita anteriormente al diseñar la estructura de los parámetros de entrada en la Parte 3. La curva de beneficios tendrá este aspecto.
Figura 5. Datos de prueba mediante un gestor de riesgos
A continuación, se presenta un resumen de los resultados de las pruebas de los dos casos en la siguiente tabla.
# | Valor | Sin gestor de riesgos | Gestor de riesgos | Cambio |
---|---|---|---|---|
1 | Beneficio neto total: | 41.1 | 144.48 | +103.38 |
2 | Disminución máxima del balance: | 0.74% | 0.25% | Reducido 3 veces |
3 | Reducción máxima de la equidad: | 1.13% | 0.58% | Reducido 2 veces |
4 | Beneficio Esperado: | 10.28 | 36.12 | Crecimiento superior a 3 veces |
5 | Ratio Sharpe: | 0.12 | 0.67 | 5 veces el crecimiento |
6 | Operaciones rentables (% del total): | 75% | 75% | - |
7 | Beneficio medio por operación: | 38.52 | 56.65 | Crecimiento del 50% |
8 | Pérdida media por operación: | -74.47 | -25.46 | Reducido 3 veces |
9 | Riesgo-Retorno Promedio: | 0.52 | 2.23 | 4 veces el crecimiento |
Tabla 3. Comparación de los resultados financieros de la negociación con y sin gestor de riesgos
Con base en los resultados de nuestras pruebas unitarias, podemos concluir que el uso del control de riesgos a través de nuestra clase de gestor de riesgos ha aumentado significativamente la eficiencia del trading utilizando la misma estrategia simple, al limitar los riesgos y asegurar las ganancias de cada transacción en relación con el riesgo fijado. Esto permitió reducir el drawdown del balance en 3 veces y el drawdown de la equidad en 2 veces. El Beneficio Esperado de la estrategia aumentó más de 3 veces, y el Ratio Sharpe más de 5 veces. La media de operaciones rentables aumentó un 50% y la media de operaciones no rentables se redujo tres veces, lo que permitió situar la rentabilidad media del riesgo de la cuenta casi en el valor objetivo de 1 a 3. La siguiente tabla ofrece una comparación detallada de los resultados financieros de cada operación individual de nuestro conjunto.
Fecha | Símbolo | Dirección | Lotes | Sin gestor de riesgos | Gestor de riesgos | Cambio |
---|---|---|---|---|---|---|
2024.01.31 | USDJPY | buy | 0.1 | 25.75 | 78 | + 52.25 |
2024.02.05 | USDJPY | sell | 0.1 | 13.19 | 13.19 | - |
2024.02.08 | USDJPY | sell | 0.1 | 76.63 | 78.75 | + 2.12 |
2024.02.08 | USDJPY | buy | 0.1 | -74.47 | -25.46 | + 49.01 |
Total | - | - | - | 41.10 | 144.48 | + 103.38 |
Tabla 4. Comparación de las operaciones ejecutadas con y sin el gestor de riesgos
Conclusión
A partir de las tesis expuestas en el artículo, pueden extraerse las siguientes conclusiones. Utilizar el gestor de riesgos incluso en la negociación manual puede aumentar considerablemente la eficacia de las estrategias, incluidas las rentables. En el caso de una estrategia perdedora, el uso del gestor de riesgos puede ayudar a asegurar los depósitos, limitando las pérdidas. Como ya se ha dicho en la introducción, intentamos mitigar el factor psicológico. No hay que desactivar el gestor de riesgos tratando de recuperar inmediatamente las pérdidas. Puede ser mejor esperar a que finalice el periodo de limitación y, sin emociones, empezar a operar de nuevo. Aproveche el tiempo en que la negociación está prohibida por el gestor de riesgos para analizar su estrategia de negociación y comprender qué ha provocado las pérdidas y cómo evitarlas en el futuro.
Gracias a todos los que han leído este artículo hasta el final. Espero sinceramente que este artículo logre salvar al menos un depósito de perderse por completo. En ese caso, consideraré que mis esfuerzos no fueron en vano. Estaré encantado de ver sus comentarios o mensajes privados, especialmente sobre si debería comenzar un nuevo artículo en el que podamos adaptar esta clase a un EA puramente algorítmico. Agradezco sus comentarios. Gracias.
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/14340
- 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