
Automatización de estrategias de trading en MQL5 (Parte 3): Sistema RSI de recuperación de zona para la gestión dinámica de operaciones
Introducción
En el artículo anterior (Parte 2), mostramos cómo transformar la estrategia Kumo Breakout en un asesor experto (EA) totalmente funcional utilizando MetaQuotes Language 5 (MQL5). En este artículo (Parte 3), nos centramos en el sistema Zone Recovery RSI, una estrategia avanzada diseñada para gestionar operaciones y recuperarse de pérdidas de forma dinámica. Este sistema combina el Índice de Fuerza Relativa (Relative Strength Index, RSI) para activar señales de entrada con un mecanismo de "Recuperación de Zona" que realiza operaciones contrarias cuando el mercado se mueve en contra de la posición inicial. El objetivo es mitigar las caídas y mejorar la rentabilidad general adaptándose a las condiciones del mercado.
Recorremos el proceso de codificación de la lógica de recuperación, gestionando posiciones con tamaños de lote dinámicos y utilizando el RSI para la entrada en operaciones y las señales de recuperación. Al final de este artículo, comprenderá claramente cómo implementar el sistema Zone Recovery RSI, probar su rendimiento con el probador de estrategias MQL5 y optimizarlo para mejorar la gestión de riesgos y la rentabilidad. El artículo está estructurado de la siguiente manera para facilitar su comprensión.
- Diseño de estrategias y conceptos clave
- Implementación en MQL5
- Backtesting y análisis de rendimiento
- Conclusión
Diseño de estrategias y conceptos clave
El sistema Zone Recovery RSI combina el indicador Índice de fuerza relativa (Relative Strength Index, RSI) para las entradas en operaciones con un mecanismo de recuperación de zona para gestionar los movimientos adversos de los precios. Las entradas de operaciones se activan cuando el RSI cruza umbrales clave, normalmente 30 para condiciones de sobreventa (compra) y 70 para condiciones de sobrecompra (venta). Sin embargo, el verdadero poder del sistema reside en su capacidad para recuperarse de las operaciones perdidas utilizando un modelo de recuperación por zonas bien estructurado.
El sistema Zone Recovery establece cuatro niveles de precios críticos para cada operación: Zona Alta, Zona Baja, Objetivo Alto y Objetivo Bajo. Cuando se abre una operación, estos niveles se calculan en relación con el precio de entrada. Para una operación de compra, el mínimo de la zona se establece por debajo del precio de entrada, mientras que el máximo de la zona se sitúa en el precio de entrada. Por el contrario, para una operación de venta, el máximo de la zona se sitúa por encima del precio de entrada, mientras que el mínimo de la zona se alinea con él. Si el mercado se mueve más allá de la zona baja (para compras) o la zona alta (para ventas), se activa una operación contraria en la dirección opuesta con un tamaño de lote mayor basado en un multiplicador predefinido. El objetivo alto y el objetivo bajo definen los puntos de obtención de beneficios para las posiciones de compra y venta, lo que garantiza que las operaciones se cierren con beneficios una vez que el mercado se mueva favorablemente. Este enfoque permite recuperar pérdidas y al mismo tiempo controlar el riesgo mediante ajustes sistemáticos del tamaño de las posiciones y de los niveles. A continuación se muestra una ilustración para resumir todo el modelo.
Implementación en MQL5
Después de aprender todas las teorías sobre la estrategia de trading Zone Recovery, automatizamos la teoría y creamos un Asesor Experto (Expert Advisor, EA) en MetaQuotes Language 5 (MQL5) para MetaTrader 5.
Para crear un asesor experto (EA), en su terminal MetaTrader 5, haga clic en la pestaña Herramientas y marque MetaQuotes Language Editor, o simplemente pulse F4 en su teclado. También puede hacer clic en el icono IDE (Integrated Development Environment) de la barra de herramientas. Esto abrirá el entorno MetaQuotes Language Editor, que permite escribir robots de trading, indicadores técnicos, scripts y librerías de funciones. Una vez abierto MetaEditor, en la barra de herramientas, vaya a la pestaña Archivo y seleccione Nuevo archivo, o simplemente pulse CTRL + N, para crear un nuevo documento. Alternativamente, puede hacer clic en el icono Nuevo en la pestaña de herramientas. Esto generará una ventana emergente del Asistente MQL (MQL Wizard).
En el Asistente que aparece, marque Asesor experto (plantilla) y haga clic en Siguiente. En las propiedades generales del Asesor Experto, en la sección «Nombre», introduzca el nombre del archivo de su experto. Tenga en cuenta que para especificar o crear una carpeta si no existe, debe utilizar la barra invertida antes del nombre del EA. Por ejemplo, aquí tenemos "Experts\" por defecto. Esto significa que nuestro EA se creará en la carpeta Expertos (Experts) y podremos encontrarlo allí. Las demás secciones son bastante sencillas, pero puedes seguir el enlace en la parte inferior del Asistente para saber cómo realizar el proceso con precisión.
Después de proporcionar el nombre de archivo del Asesor Experto deseado, haga clic en Siguiente, luego en Siguiente y luego en Finalizar. Después de hacer todo esto, ahora estamos listos para codificar y programar nuestra estrategia.
Primero, comenzamos definiendo algunos metadatos sobre el Asesor Experto (EA). Esto incluye el nombre del EA, la información de derechos de autor y un enlace al sitio web de MetaQuotes. También especificamos la versión del EA, que se establece en "1.00".
//+------------------------------------------------------------------+ //| 1. Zone Recovery RSI EA.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00"
Esto mostrará los metadatos del sistema al cargar el programa. Luego podemos pasar a agregar algunas variables globales que usaremos dentro del programa. En primer lugar, incluimos una instancia comercial utilizando #include al principio del código fuente. Esto nos da acceso a la clase «CTrade», que utilizaremos para crear un objeto comercial. Esto es crucial, ya que lo necesitamos para abrir operaciones.
#include <Trade/Trade.mqh>
CTrade obj_Trade;
El preprocesador sustituirá la línea #include <Trade/Trade.mqh> por el contenido del archivo Trade.mqh. Los corchetes angulares indican que el archivo Trade.mqh se tomará del directorio estándar (normalmente es directorio_de_instalación_del_terminal\MQL5\Include). El directorio actual no se incluye en la búsqueda. La línea se puede colocar en cualquier parte del programa, pero normalmente todas las inclusiones se colocan al principio del código fuente, para mejorar la estructura del código y facilitar la consulta. La declaración del objeto «obj_Trade» de la clase CTrade nos dará acceso a los métodos contenidos en esa clase fácilmente, gracias a los desarrolladores de MQL5.
Después de eso, necesitamos declarar varias variables globales importantes que utilizaremos en el sistema comercial.
// Global variables for RSI logic int rsiPeriod = 14; //--- The period used for calculating the RSI indicator. int rsiHandle; //--- Handle for the RSI indicator, used to retrieve RSI values. double rsiBuffer[]; //--- Array to store the RSI values retrieved from the indicator. datetime lastBarTime = 0; //--- Holds the time of the last processed bar to prevent duplicate signals.
Para gestionar la generación de señales, configuramos las variables globales necesarias para gestionar la lógica del indicador del índice de fuerza relativa (Relative Strength Index, RSI). En primer lugar, definimos «rsiPeriod» como 14, lo que determina el número de barras de precios utilizadas para calcular el RSI. Se trata de un parámetro estándar en el análisis técnico que nos permite evaluar las condiciones de sobrecompra o sobreventa del mercado. A continuación, creamos «rsiHandle», una referencia para el indicador RSI. Este identificador nos permitirá solicitar y recuperar valores RSI de la plataforma MetaTrader, lo que nos permitirá realizar un seguimiento de los movimientos del indicador en tiempo real.
Para almacenar estos valores RSI, utilizamos «rsiBuffer», una matriz que contiene el resultado del indicador. Analizaremos este búfer para detectar puntos de cruce clave, como cuando el RSI se mueve por debajo de 30 (posible señal de compra) o por encima de 70 (posible señal de venta). Por último, presentamos «lastBarTime», que almacena la hora de la barra procesada más recientemente. Esta variable garantizará que solo procesemos una señal por barra, evitando que se activen múltiples operaciones dentro de la misma barra. Después de eso, podemos definir una clase que se encargará del mecanismo de recuperación.
// Global ZoneRecovery object class ZoneRecovery { //--- };
Aquí, creamos un sistema de recuperación de zona utilizando una clase llamada «ZoneRecovery», que sirve como contenedor de todas las variables, funciones y lógica necesarias para gestionar el proceso de recuperación. Al utilizar una clase, podemos organizar el código en un objeto autónomo, lo que nos permite gestionar las operaciones, realizar un seguimiento del progreso de la recuperación y calcular los niveles esenciales para cada ciclo operativo. Este enfoque proporciona una mejor estructura, reutilización y escalabilidad para gestionar múltiples posiciones comerciales simultáneamente. Una clase puede contener encapsulaciones de tres miembros en forma de miembros privados, protegidos y públicos. Primero definamos los miembros privados.
private: CTrade trade; //--- Object to handle trading operations. double initialLotSize; //--- The initial lot size for the first trade. double currentLotSize; //--- The lot size for the current trade in the sequence. double zoneSize; //--- Distance in points defining the range of the recovery zone. double targetSize; //--- Distance in points defining the target profit range. double multiplier; //--- Multiplier to increase lot size in recovery trades. string symbol; //--- Symbol for trading (e.g., currency pair). ENUM_ORDER_TYPE lastOrderType; //--- Type of the last executed order (BUY or SELL). double lastOrderPrice; //--- Price at which the last order was executed. double zoneHigh; //--- Upper boundary of the recovery zone. double zoneLow; //--- Lower boundary of the recovery zone. double zoneTargetHigh; //--- Upper boundary for target profit range. double zoneTargetLow; //--- Lower boundary for target profit range. bool isRecovery; //--- Flag indicating whether the recovery process is active.
Aquí definimos las variables miembro privadas de la clase «ZoneRecovery», que almacena datos esenciales para gestionar el proceso de recuperación de la zona. Estas variables nos permiten realizar un seguimiento del estado de la estrategia, calcular los niveles clave de la zona de recuperación y gestionar la lógica de ejecución de las operaciones.
Utilizamos el objeto «CTrade» para gestionar todas las operaciones bursátiles, como la realización, modificación y cierre de operaciones. El «initialLotSize» representa el tamaño del lote de la primera operación, mientras que el «currentLotSize» realiza un seguimiento del tamaño del lote para las operaciones de recuperación posteriores, que aumenta en función del «multiplicador». Los parámetros «zoneSize» y «targetSize» definen los límites críticos del sistema de recuperación. Concretamente, la zona de recuperación está delimitada por «zoneHigh» y «zoneLow», mientras que el objetivo de beneficio se define mediante «zoneTargetHigh» y «zoneTargetLow».
Para realizar un seguimiento del flujo de operaciones, almacenamos el «lastOrderType» (COMPRA o VENTA) y el «lastOrderPrice» al que se ejecutó la operación anterior. Esta información nos ayuda a determinar cómo posicionar las operaciones futuras en respuesta a los movimientos del mercado. La variable «symbol» identifica el instrumento de negociación utilizado, mientras que el indicador «isRecovery» indica si el sistema se encuentra activamente en proceso de recuperación. Al mantener estas variables privadas, nos aseguramos de que solo la lógica interna de la clase pueda modificarlas, manteniendo la integridad y precisión de los cálculos del sistema. Después de eso, ahora podemos definir las funciones de la clase directamente, en lugar de tener que llamarlas más adelante y definirlas, solo por simplicidad. Entonces, en lugar de declarar las funciones que necesitamos y definirlas más tarde, simplemente las declaramos y definimos de una vez por todas. Definamos primero la función encargada de calcular las zonas de recuperación.
// Calculate dynamic zones and targets void CalculateZones() { if (lastOrderType == ORDER_TYPE_BUY) { zoneHigh = lastOrderPrice; //--- Upper boundary starts from the last BUY price. zoneLow = zoneHigh - zoneSize; //--- Lower boundary is calculated by subtracting zone size. zoneTargetHigh = zoneHigh + targetSize; //--- Profit target above the upper boundary. zoneTargetLow = zoneLow - targetSize; //--- Buffer below the lower boundary for recovery trades. } else if (lastOrderType == ORDER_TYPE_SELL) { zoneLow = lastOrderPrice; //--- Lower boundary starts from the last SELL price. zoneHigh = zoneLow + zoneSize; //--- Upper boundary is calculated by adding zone size. zoneTargetLow = zoneLow - targetSize; //--- Buffer below the lower boundary for profit range. zoneTargetHigh = zoneHigh + targetSize; //--- Profit target above the upper boundary. } Print("Zone recalculated: ZoneHigh=", zoneHigh, ", ZoneLow=", zoneLow, ", TargetHigh=", zoneTargetHigh, ", TargetLow=", zoneTargetLow); }
Aquí diseñamos la función «CalculateZones», que desempeña un papel fundamental en la definición de los niveles clave para nuestra estrategia de recuperación de zonas. El objetivo principal de esta función es calcular los cuatro límites esenciales: «zoneHigh», «zoneLow», «zoneTargetHigh» y «zoneTargetLow», que guían nuestros puntos de entrada, recuperación y salida con beneficios. Estos límites son dinámicos y se ajustan según el tipo y precio de la última orden ejecutada, lo que garantiza que mantengamos el control sobre el proceso de recuperación.
Si nuestra última orden fue una COMPRA, establecemos «zoneHigh» en el precio al que se ejecutó la orden de COMPRA. A partir de este punto, calculamos el «zoneLow» restando el «zoneSize» del «zoneHigh», creando un rango de recuperación por debajo del precio de COMPRA original. Para establecer nuestros objetivos de beneficio, calculamos «zoneTargetHigh» sumando «targetSize» a «zoneHigh», mientras que «zoneTargetLow» se sitúa por debajo de «zoneLow» con el mismo «targetSize». Esta estructura nos permitirá posicionar las operaciones de recuperación por debajo de la entrada de COMPRA original y definir los límites superior e inferior de nuestro rango de beneficios.
Si nuestra última orden fue de VENTA, invertimos la lógica. Aquí, establecemos «zoneLow» al precio de la última orden de VENTA. A continuación, calculamos «zoneHigh» sumando «zoneSize» a «zoneLow», lo que forma el límite superior del rango de recuperación. Los objetivos de beneficio se establecen calculando «zoneTargetLow» como un valor inferior a «zoneLow», mientras que «zoneTargetHigh» se establece por encima de «zoneHigh», ambos según el «targetSize». Esta configuración nos permitirá iniciar operaciones de recuperación por encima de la entrada de VENTA original, al tiempo que definimos la zona de toma de ganancias.
Al final de este proceso, hemos establecido nuestros límites de recuperación de zona y nuestros objetivos de beneficio tanto para las operaciones de COMPRA como para las de VENTA. Para facilitar la depuración y la evaluación de la estrategia, utilizamos la función Print para mostrar los valores de «zoneHigh», «zoneLow», «zoneTargetHigh» y «zoneTargetLow» en el registro. De esa manera, podemos definir otra función que se encargue de la lógica de ejecución de las operaciones.
// Open a trade based on the given type bool OpenTrade(ENUM_ORDER_TYPE type) { if (type == ORDER_TYPE_BUY) { if (trade.Buy(currentLotSize, symbol)) { lastOrderType = ORDER_TYPE_BUY; //--- Mark the last trade as BUY. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price. CalculateZones(); //--- Recalculate zones after placing the trade. Print(isRecovery ? "RECOVERY BUY order placed" : "INITIAL BUY order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize); isRecovery = true; //--- Set recovery state to true after the first trade. return true; } } else if (type == ORDER_TYPE_SELL) { if (trade.Sell(currentLotSize, symbol)) { lastOrderType = ORDER_TYPE_SELL; //--- Mark the last trade as SELL. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price. CalculateZones(); //--- Recalculate zones after placing the trade. Print(isRecovery ? "RECOVERY SELL order placed" : "INITIAL SELL order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize); isRecovery = true; //--- Set recovery state to true after the first trade. return true; } } return false; //--- Return false if the trade fails. }
Aquí definimos una función llamada «OpenTrade» que devuelve un valor booleano. El objetivo de esta función será abrir una operación dependiendo de si queremos ejecutar una orden de COMPRA o VENTA. Primero comprobamos si el tipo de orden solicitado es una COMPRA. Si es así, utilizamos la función «trade.Buy» para intentar abrir una posición de compra con el tamaño de lote actual y el símbolo especificado. Si la operación se abre correctamente, establecemos «lastOrderType» en BUY y, a continuación, almacenamos el precio actual del símbolo utilizando la función SymbolInfoDouble para obtener el precio de compra. Este precio representa el precio al que abrimos la posición. A continuación, recalculamos las zonas de recuperación llamando a la función «CalculateZones», que ajusta los niveles de las zonas en función de la nueva posición.
A continuación, imprimimos un mensaje en el registro indicando si se trató de una COMPRA inicial o una COMPRA de recuperación. Utilizamos un operador ternario para comprobar si el indicador «isRecovery» es verdadero o falso: si es verdadero, el mensaje indicará que se trata de un pedido de recuperación; de lo contrario, indicará que se trata del pedido inicial. A continuación, establecemos el indicador «isRecovery» en verdadero, lo que indica que cualquier operación posterior se considerará parte del proceso de recuperación. Finalmente, la función devuelve verdadero, lo que confirma que la operación se ha realizado correctamente.
Si el tipo de orden es VENTA, seguimos los mismos pasos. Intentamos abrir una posición de VENTA llamando a la función «trade.Sell» con los mismos parámetros y, tras una ejecución satisfactoria, almacenamos el «lastOrderPrice» y ajustamos las zonas de recuperación de la misma manera. Imprimimos un mensaje que indica si se trataba de una VENTA inicial o una VENTA de recuperación, utilizando de nuevo un operador ternario para comprobar el indicador «isRecovery». A continuación, se establece el indicador «isRecovery» en verdadero y la función devuelve verdadero para indicar que la operación se ha realizado correctamente. Si, por cualquier motivo, la operación no se abre correctamente, la función devuelve falso, lo que indica que el intento de operación ha fallado. Éstas son las funciones cruciales que debemos tener como privados. Otros los podemos tener como públicos, sin problemas.
public: // Constructor ZoneRecovery(double initialLot, double zonePts, double targetPts, double lotMultiplier, string _symbol) { initialLotSize = initialLot; currentLotSize = initialLot; //--- Start with the initial lot size. zoneSize = zonePts * _Point; //--- Convert zone size to points. targetSize = targetPts * _Point; //--- Convert target size to points. multiplier = lotMultiplier; symbol = _symbol; //--- Initialize the trading symbol. lastOrderType = ORDER_TYPE_BUY; lastOrderPrice = 0.0; //--- No trades exist initially. isRecovery = false; //--- No recovery process active at initialization. }
Aquí declaramos la sección pública de la clase «ZoneRecovery», que contiene el constructor. El constructor se utiliza para inicializar un objeto de la clase «ZoneRecovery» con parámetros específicos cuando se crea. El constructor toma como entradas «initialLot», «zonePts», «targetPts», «lotMultiplier» y «_symbol».
Comenzamos asignando el valor «initialLot» a «initialLotSize» y «currentLotSize», asegurándonos de que ambos comiencen con el mismo valor, que representa el tamaño del lote para la primera operación. A continuación, calculamos el «zoneSize» multiplicando «zonePts» (la distancia de la zona en puntos) por _Point, que es una constante integrada que representa el movimiento mínimo del precio para el símbolo. Del mismo modo, «targetSize» se calcula convirtiendo «targetPts» (distancia de beneficio objetivo) en puntos utilizando el mismo enfoque. El «multiplicador» se establece en «lotMultiplier», que se utilizará más adelante para ajustar el tamaño del lote para la operación de recuperación.
A continuación, se asigna el «símbolo» a la variable «symbol» para indicar qué instrumento de negociación se utilizará. «lastOrderType» se establece inicialmente en ORDER_TYPE_BUY, suponiendo que la primera operación será una orden de compra. «lastOrderPrice» se establece en «0,0» porque aún no se ha ejecutado ninguna operación. Por último, «isRecovery» se establece en «false», lo que indica que el proceso de recuperación aún no está activo. Este constructor garantiza que el objeto «ZoneRecovery» se inicialice correctamente y esté preparado para gestionar las operaciones y los procesos de recuperación. A continuación, definimos una función para activar operaciones basadas en señales externas.
// Trigger trade based on external signals void HandleSignal(ENUM_ORDER_TYPE type) { if (lastOrderPrice == 0.0) //--- Open the first trade if no trades exist. OpenTrade(type); }
Aquí definimos una función llamada «HandleSignal» que toma como parámetro un tipo ENUM_ORDER_TYPE, que representa el tipo de operación que se va a ejecutar (ya sea una COMPRA o una VENTA). En primer lugar, comprobamos si «lastOrderPrice» es «0,0», lo que indica que no se ha ejecutado ninguna operación anterior. Si esta condición es verdadera, significa que esta es la primera operación que se abre, por lo que llamamos a la función «OpenTrade» y le pasamos el parámetro «type». La función «OpenTrade» se encargará entonces de la lógica para abrir una orden de COMPRA o VENTA en función de la señal recibida. Ahora podemos gestionar las zonas abriendo operaciones de recuperación mediante la lógica que se muestra a continuación.
// Manage zone recovery positions void ManageZones() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current BID price. // Open recovery trades based on zones if (lastOrderType == ORDER_TYPE_BUY && currentPrice <= zoneLow) { currentLotSize *= multiplier; //--- Increase lot size for recovery. OpenTrade(ORDER_TYPE_SELL); //--- Open a SELL order for recovery. } else if (lastOrderType == ORDER_TYPE_SELL && currentPrice >= zoneHigh) { currentLotSize *= multiplier; //--- Increase lot size for recovery. OpenTrade(ORDER_TYPE_BUY); //--- Open a BUY order for recovery. } }
Para gestionar las operaciones abiertas, definimos una función void llamada «ManageZones» que se encarga de gestionar las operaciones de recuperación basándose en zonas de precios predefinidas. Dentro de esta función, primero recuperamos el precio BID actual para el símbolo especificado utilizando la función SymbolInfoDouble con el parámetro SYMBOL_BID. Esto nos da el precio actual de mercado al que se negocia el activo.
A continuación, comprobamos el tipo de operación de la última orden ejecutada utilizando la variable «lastOrderType». Si la última operación fue una COMPRA y el precio actual de mercado ha caído hasta o por debajo de la «zoneLow» (el límite inferior de la zona de recuperación), aumentamos el «currentLotSize» multiplicándolo por el «multiplier» para asignar más capital a la operación de recuperación. A continuación, llamamos a la función «OpenTrade» con el parámetro ORDER_TYPE_SELL, indicando que necesitamos abrir una posición de VENTA para gestionar la pérdida de la operación de COMPRA anterior.
Del mismo modo, si la última operación fue una VENTA y el precio actual de mercado ha subido hasta o por encima del «zoneHigh» (el límite superior de la zona de recuperación), volvemos a aumentar el «currentLotSize» multiplicándolo por el «multiplier» para ampliar el tamaño de la operación con el fin de recuperarla. A continuación, llamamos a la función «OpenTrade» con el parámetro ORDER_TYPE_BUY, abriendo una posición de COMPRA para recuperarnos de la operación de VENTA anterior. Así de fácil. Ahora, después de abrir las operaciones iniciales y de recuperación, necesitamos lógica para cerrarlas en un punto. Así que definamos la lógica de cierre o objetivo a continuación.
// Check and close trades at zone targets void CheckCloseAtTargets() { double currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get the current BID price. // Close BUY trades at target high if (lastOrderType == ORDER_TYPE_BUY && currentPrice >= zoneTargetHigh) { for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop through all open positions. if (PositionGetSymbol(i) == symbol) { //--- Check if the position belongs to the current symbol. ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Retrieve the ticket number. int retries = 10; while (retries > 0) { if (trade.PositionClose(ticket)) { //--- Attempt to close the position. Print("Closed BUY position with ticket: ", ticket); break; } else { Print("Failed to close BUY position with ticket: ", ticket, ". Reintentando... Error: ", GetLastError()); retries--; Sleep(100); //--- Wait 100ms before retrying. } } if (retries == 0) Print("Gave up on closing BUY position with ticket: ", ticket); } } Reset(); //--- Reset the strategy after closing all positions. } // Close SELL trades at target low else if (lastOrderType == ORDER_TYPE_SELL && currentPrice <= zoneTargetLow) { for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop through all open positions. if (PositionGetSymbol(i) == symbol) { //--- Check if the position belongs to the current symbol. ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Retrieve the ticket number. int retries = 10; while (retries > 0) { if (trade.PositionClose(ticket)) { //--- Attempt to close the position. Print("Closed SELL position with ticket: ", ticket); break; } else { Print("Failed to close SELL position with ticket: ", ticket, ". Reintentando... Error: ", GetLastError()); retries--; Sleep(100); //--- Wait 100ms before retrying. } } if (retries == 0) Print("Gave up on closing SELL position with ticket: ", ticket); } } Reset(); //--- Reset the strategy after closing all positions. } }
Aquí definimos una función llamada «CheckCloseAtTargets» que se encarga de comprobar si alguna operación abierta ha alcanzado su precio objetivo predefinido y cerrarla en consecuencia.
En primer lugar, recuperamos el precio BID actual para el símbolo dado utilizando la función SymbolInfoDouble con el parámetro SYMBOL_BID. Esto nos da el precio de mercado actual del símbolo, que utilizaremos para compararlo con los niveles de precios objetivo (ya sea «zoneTargetHigh» o «zoneTargetLow») y decidir si se deben cerrar las operaciones.
A continuación, comprobamos si el último tipo de orden es COMPRA y si el precio actual ha alcanzado o superado el «zoneTargetHigh» (el nivel de precio objetivo para una operación de COMPRA). Si se cumplen estas condiciones, recorremos todas las posiciones abiertas utilizando la función PositionsTotal, comenzando por la última posición. Para cada posición abierta, comprobamos si la posición pertenece al mismo símbolo utilizando la función PositionGetSymbol. Si el símbolo coincide, recuperamos el número de ticket de la posición utilizando la función PositionGetInteger con el parámetro «POSITION_TICKET».
A continuación, intentamos cerrar la posición llamando a la función «trade.PositionClose» con el ticket recuperado. Si la posición se cierra correctamente, imprimimos un mensaje de confirmación indicando que la posición de COMPRA se ha cerrado, incluyendo el número de ticket. Si el cierre falla, reintentamos hasta 10 veces, imprimiendo un mensaje de error cada vez y utilizando la función Sleep para esperar 100 milisegundos antes de volver a intentarlo. Si aún no podemos cerrar la posición después de 10 reintentos, imprimimos un mensaje de error y procedemos a la siguiente posición abierta. Una vez que se cierran todas las posiciones o se alcanza el límite de reintentos, llamamos a la función «Reset» para restablecer la estrategia, asegurándonos de que el estado se borra para cualquier operación futura.
Del mismo modo, si el último tipo de orden es VENTA y el precio actual ha alcanzado o caído por debajo del «zoneTargetLow» (el nivel de precio objetivo para una operación de VENTA), el proceso se repite para todas las posiciones de VENTA. La función intentará cerrar las posiciones de VENTA de la misma manera, reintentándolo si es necesario e imprimiendo mensajes de estado en cada paso. Utilizamos una función externa para restablecer el estado, pero aquí está la lógica adoptada.
// Reset the strategy after hitting targets void Reset() { currentLotSize = initialLotSize; //--- Reset lot size to the initial value. lastOrderType = -1; //--- Clear the last order type. lastOrderPrice = 0.0; //--- Clear the last order price. isRecovery = false; //--- Set recovery state to false. Print("Strategy reset after closing trades."); }
Definimos una función llamada «Reset», que se encarga de restablecer las variables internas de la estrategia y preparar el sistema para la siguiente operación o escenario de restablecimiento. Comenzamos restableciendo el «currentLotSize» al «initialLotSize», lo que significa que, tras una serie de operaciones de recuperación o alcanzar los niveles objetivo, devolvemos el tamaño del lote a su valor original. Esto garantiza que la estrategia comience desde cero con el tamaño inicial del lote para cualquier nueva operación.
A continuación, borramos el «lastOrderType» estableciéndolo en -1, lo que indica efectivamente que no hay ningún tipo de orden anterior (ni COMPRA ni VENTA). Esto ayuda a garantizar que no haya confusión ni dependencia del tipo de orden anterior en la lógica de negociación futura. Del mismo modo, restablecemos el «lastOrderPrice» a .0.0, borrando el último precio al que se ejecutó una operación. A continuación, establecemos el indicador «isRecovery» en falso, lo que indica que el proceso de recuperación ya no está activo. Esto es especialmente importante, ya que garantiza que cualquier operación futura se trate como una operación inicial y no como parte de una estrategia de recuperación.
Por último, imprimimos un mensaje utilizando la función Print, indicando que la estrategia se ha restablecido correctamente tras cerrar todas las operaciones. Esto proporciona información en la terminal, lo que ayuda al operador a realizar un seguimiento de cuándo se ha restablecido la estrategia y garantiza el estado adecuado para futuras operaciones. En esencia, la función borra todas las variables esenciales que registran las condiciones comerciales, los estados de recuperación y los volúmenes comerciales, devolviendo el sistema a su configuración predeterminada para nuevas operaciones. Y eso es todo lo que necesitamos para que la clase gestione todas las señales entrantes. Ahora podemos proceder a inicializar el objeto de clase pasando los parámetros predeterminados.
ZoneRecovery zoneRecovery(0.1, 200, 400, 2.0, _Symbol); //--- Initialize the ZoneRecovery object with specified parameters.
Aquí, creamos una instancia de la clase «ZoneRecovery» llamando a su constructor y pasando los parámetros necesarios. Concretamente, inicializamos el objeto «zoneRecovery» con los siguientes valores:
- «0.1» para el tamaño inicial del lote. Esto significa que la primera operación utilizará un tamaño de lote de 0.1.
- «200» para el tamaño de la zona, que es el número de puntos que definen el rango de la zona de recuperación. A continuación, se multiplica por _Point para convertir este valor en puntos reales para el símbolo especificado.
- «400» para el tamaño objetivo, que define la distancia en puntos hasta el nivel de beneficio objetivo. Al igual que el tamaño de la zona, esto también se convierte en puntos utilizando _Point.
- «2.0» para el multiplicador, que se utilizará para aumentar el tamaño del lote en operaciones de recuperación posteriores si fuera necesario.
- «_Symbol» se utiliza como símbolo comercial para este caso concreto de ZoneRecovery, que se corresponde con el símbolo del instrumento que utiliza el operador.
Al inicializar «zoneRecovery» con estos parámetros, configuramos el objeto para que gestione la lógica de negociación de esta estrategia de negociación específica, incluida la gestión de las zonas de recuperación, los ajustes del tamaño de los lotes y los niveles objetivo para cualquier operación que se abra o gestione. Este objeto está listo para gestionar operaciones comerciales basadas en la estrategia de recuperación definida una vez que se ejecute el sistema. Ahora podemos pasar a los controladores de eventos, donde nos centraremos en la generación de señales. Comenzamos con el controlador de eventos OnInit. Aquí solo tenemos que inicializar el identificador del indicador y configurar la matriz de almacenamiento como una serie temporal.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Initialize RSI indicator rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, rsiPeriod, PRICE_CLOSE); //--- Create RSI indicator handle. if (rsiHandle == INVALID_HANDLE) { //--- Check if RSI handle creation failed. Print("Failed to create RSI handle. Error: ", GetLastError()); return(INIT_FAILED); //--- Return failure status if RSI initialization fails. } ArraySetAsSeries(rsiBuffer, true); //--- Set the RSI buffer as a time series to align values. Print("Zone Recovery Strategy initialized."); //--- Log successful initialization. return(INIT_SUCCEEDED); //--- Return success status. }
Aquí, inicializamos el indicador RSI y preparamos el sistema para operar realizando una serie de tareas de configuración en la función OnInit. En primer lugar, creamos un indicador RSI llamando a la función iRSI y pasando el símbolo actual (_Symbol), el intervalo de tiempo PERIOD_CURRENT, el «rsiPeriod» especificado y el tipo de precio PRICE_CLOSE. Este paso configura el indicador RSI para su uso en la estrategia.
A continuación, comprobamos si la creación del identificador se ha realizado correctamente verificando que el identificador no sea igual a INVALID_HANDLE. Si la creación falla, imprimimos un mensaje de error con el código de error específico utilizando la función GetLastError y devolvemos «INIT_FAILED» para indicar el fallo. Si la creación del identificador se realiza correctamente, continuamos configurando el búfer RSI como una serie temporal utilizando ArraySetAsSeries para alinear el búfer con la serie temporal del gráfico, asegurándonos de que los valores más recientes se encuentren en el índice 0. Por último, imprimimos un mensaje de éxito que confirma la inicialización de la «Zone Recovery Strategy» y devolvemos INIT_SUCCEEDED, lo que indica que la configuración se ha realizado correctamente y que el Asesor Experto está listo para comenzar a funcionar. Aquí hay una ilustración.
Sin embargo, dado que creamos e inicializamos un indicador, debemos liberarlo una vez que ya no necesitemos el programa para liberar recursos. Esta es la lógica que adoptamos.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (rsiHandle != INVALID_HANDLE) //--- Check if RSI handle is valid. IndicatorRelease(rsiHandle); //--- Release RSI indicator handle to free resources. Print("Zone Recovery Strategy deinitialized."); //--- Log deinitialization message. }
Aquí, desinicializamos la estrategia y liberamos cualquier recurso utilizado por el indicador RSI cuando se elimina o se detiene el Asesor Experto. En la función OnDeinit, primero comprobamos si «rsiHandle» es válido confirmando que no es igual a INVALID_HANDLE. Esto garantiza que el indicador RSI exista antes de intentar liberarlo.
Si el identificador es válido, utilizamos la función IndicatorRelease para liberar los recursos asociados al indicador RSI, asegurándonos de que la memoria se gestiona correctamente y no se deja en uso después de que el EA deje de funcionar. Por último, imprimimos el mensaje «Zone Recovery Strategy deinitialized» para registrar que el proceso de desinicialización se ha completado, lo que confirma que el sistema se ha apagado correctamente. Esto garantiza que la EA se pueda eliminar de forma segura sin dejar recursos innecesarios asignados. Aquí hay un ejemplo de resultado.
Después de ocuparnos de la instancia en la que se detiene el programa, podemos pasar al controlador de eventos final, que es el principal, donde se procesan los ticks, es decir, el controlador de eventos OnTick.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Copy RSI values if (CopyBuffer(rsiHandle, 0, 1, 2, rsiBuffer) <= 0) { //--- Attempt to copy RSI buffer values. Print("Failed to copy RSI buffer. Error: ", GetLastError()); //--- Log failure if copying fails. return; //--- Exit the function on failure to avoid processing invalid data. } //--- }
En la función OnTick, primero intentamos copiar los valores RSI en la matriz «rsiBuffer» utilizando la función CopyBuffer. La función CopyBuffer se invoca con los siguientes parámetros: el identificador del indicador RSI «rsiHandle», el índice del búfer 0 (que indica el búfer RSI principal), la posición inicial 1 (donde comenzar a copiar los datos), el número de valores a copiar 2 y la matriz «rsiBuffer», que almacenará los datos copiados. Esta función recupera los dos valores RSI más recientes y los almacena en el búfer.
A continuación, comprobamos si la operación de copia se ha realizado correctamente evaluando si el valor devuelto es mayor que 0. Si la operación falla (es decir, devuelve un valor menor o igual a 0), registramos un mensaje de error indicando que la «RSI buffer copy» ha fallado utilizando la función Print y mostramos el código GetLastError para proporcionar detalles sobre el fallo. Después de registrar el error, salimos inmediatamente de la función utilizando «return» para evitar cualquier procesamiento adicional basado en datos RSI no válidos o faltantes. Esto garantiza que el EA no intente tomar decisiones comerciales con datos incompletos o defectuosos, evitando así posibles errores o pérdidas. Si no finalizamos el proceso significará que tenemos los datos solicitados necesarios y podemos seguir tomando decisiones comerciales.
//--- Check RSI crossover signals datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); //--- Get the time of the current bar. if (currentBarTime != lastBarTime) { //--- Ensure processing happens only once per bar. lastBarTime = currentBarTime; //--- Update the last processed bar time. if (rsiBuffer[1] > 30 && rsiBuffer[0] <= 30) { //--- Check for RSI crossing below 30 (oversold signal). Print("BUY SIGNAL"); //--- Log a BUY signal. zoneRecovery.HandleSignal(ORDER_TYPE_BUY); //--- Trigger the Zone Recovery BUY logic. } else if (rsiBuffer[1] < 70 && rsiBuffer[0] >= 70) { //--- Check for RSI crossing above 70 (overbought signal). Print("SELL SIGNAL"); //--- Log a SELL signal. zoneRecovery.HandleSignal(ORDER_TYPE_SELL); //--- Trigger the Zone Recovery SELL logic. } }
Aquí, verificamos las señales de cruce del RSI en cada nueva barra del mercado para activar posibles operaciones. Comenzamos recuperando la marca de tiempo de la barra actual utilizando la función iTime. La función toma el símbolo (_Symbol), el marco temporal (PERIOD_CURRENT) y el índice de la barra (0 para la barra actual). Esto proporciona el «currentBarTime», que representa la marca de tiempo de la barra completada más reciente.
A continuación, nos aseguramos de que la lógica de negociación se ejecute solo una vez por barra comparando «currentBarTime» con «lastBarTime». Si los tiempos son diferentes, significa que se ha formado una nueva barra, por lo que procedemos con el procesamiento. A continuación, actualizamos «lastBarTime» para que coincida con «currentBarTime» y así realizar un seguimiento de la barra procesada más recientemente y evitar ejecuciones repetitivas durante la misma barra.
El siguiente paso es detectar señales de cruce del RSI. Primero comprobamos si el valor del RSI ha cruzado por debajo de 30 (una condición de sobreventa) comparando «rsiBuffer[1]» (el valor del RSI de la barra anterior) con «rsiBuffer[0]» (el valor del RSI de la barra actual). Si el RSI de la barra anterior estaba por encima de 30 y el RSI de la barra actual está en 30 o por debajo, esto indica una posible señal de COMPRA, por lo que imprimimos un mensaje que dice «BUY SIGNAL» y luego llamamos a la función «HandleSignal» del objeto «zoneRecovery» para activar el proceso de recuperación de una orden de COMPRA.
Del mismo modo, comprobamos si el RSI ha superado los 70 puntos (una condición de sobrecompra). Si el RSI de la barra anterior estaba por debajo de 70 y el RSI de la barra actual está en 70 o por encima, esto indica una posible señal de VENTA, y mostramos «SELL SIGNAL». A continuación, volvemos a llamar a «HandleSignal», pero esta vez para una orden de VENTA, lo que activa la lógica correspondiente de recuperación de zona de VENTA. Por último, solo tenemos que llamar a las funciones correspondientes para gestionar las zonas abiertas y cerrarlas cuando se alcancen los objetivos.
//--- Manage zone recovery logic zoneRecovery.ManageZones(); //--- Perform zone recovery logic for active positions. //--- Check and close at zone targets zoneRecovery.CheckCloseAtTargets(); //--- Evaluate and close trades when target levels are reached.
Aquí utilizamos el operador punto («.») para llamar a funciones que forman parte de la clase «ZoneRecovery». En primer lugar, utilizamos «zoneRecovery.ManageZones()» para ejecutar el método «ManageZones», que gestiona la lógica para gestionar las operaciones de recuperación de zona basándose en el precio actual y las zonas de recuperación definidas. Este método ajusta el tamaño del lote para las operaciones de recuperación y abre nuevas posiciones según sea necesario.
A continuación, llamamos a «zoneRecovery.CheckCloseAtTargets()» para activar el método «CheckCloseAtTargets», que comprueba si el precio ha alcanzado los niveles objetivo para cerrar las posiciones. Si se cumplen las condiciones, intenta cerrar las operaciones abiertas, asegurando que la estrategia se mantenga en línea con sus límites de ganancias o pérdidas objetivo. Mediante el uso del operador punto, accedemos y ejecutamos estos métodos en el objeto «zoneRecovery» para gestionar el proceso de recuperación de forma eficaz. Para asegurarnos de que los métodos se llaman correctamente en cada tick, ejecutamos el programa y este es el resultado.
En la imagen podemos ver que hemos llamado correctamente a los métodos de clase para preparar el programa en el primer tick, lo que confirma que nuestra clase de programa está conectada y lista para funcionar. Para confirmarlo también, ejecutamos el programa y aquí están las confirmaciones de las operaciones.
En la imagen, podemos ver que confirmamos una señal de compra, abrimos una posición a partir de ella, la añadimos al sistema de recuperación de zonas, recalculamos los niveles de las zonas, identificamos que se trata de una posición inicial y, cuando se alcanza el objetivo, cerramos la posición y reiniciamos el sistema para la siguiente operación. Probemos y veamos un caso en el que entramos en un sistema de recuperación de zona.
En la imagen podemos ver que, cuando el mercado se mueve en contra nuestra 200 puntos, asumimos que la tendencia es alcista y la seguimos abriendo una posición de compra con un tamaño de lote mayor, en este caso 0.2.
Podemos ver nuevamente que cuando el mercado alcanza los niveles objetivo, cerramos las operaciones y nos preparamos para otra. Mientras el sistema está en modo de recuperación, ignoramos cualquier señal entrante. Esto confirma que hemos logrado nuestro objetivo con éxito, y lo que queda por hacer es realizar una prueba retrospectiva del programa y analizar su rendimiento. Esto se trata en la siguiente sección.
Backtesting y análisis de rendimiento
En esta sección, nos centramos en el proceso de backtesting y análisis del rendimiento de nuestro sistema Zone Recovery RSI. El backtesting nos permite evaluar la eficacia de nuestra estrategia con datos históricos, identificar posibles fallos y ajustar los parámetros para obtener mejores resultados en el trading real.
Comenzamos configurando el Probador de estrategias en la plataforma MetaTrader 5. El simulador de estrategias nos permite simular condiciones históricas del mercado y ejecutar operaciones como si se estuvieran realizando en tiempo real. Para ejecutar una prueba retrospectiva, seleccionamos el símbolo, el marco temporal y el periodo de prueba pertinentes. También nos aseguramos de que el «modo visual» esté habilitado si queremos ver las operaciones a medida que se producen en el gráfico.
Una vez que el entorno de backtesting está listo, configuramos las entradas para nuestro programa. Especificamos el depósito inicial, el tamaño del lote y los parámetros específicos relacionados con la lógica de recuperación de la zona. Los datos clave incluyen «tamaño inicial del lote», «tamaño de la zona», «tamaño objetivo» y «multiplicador». Al variar estos datos, podemos analizar cómo afectan a la rentabilidad global de la estrategia. Esto es lo que hemos cambiado.
ZoneRecovery zoneRecovery(0.1, 700, 1400, 2.0, _Symbol); //--- Initialize the ZoneRecovery object with specified parameters.
Configuramos el sistema para que funcionara desde el primer día de enero de 2024 durante todo un año y estos son los resultados.
Gráfico del Probador de estrategias:
Informe del Probador de estrategia:
A partir de los resultados obtenidos en el gráfico y el informe, podemos estar seguros de que nuestra estrategia está funcionando según lo previsto. Sin embargo, aún podemos aumentar el rendimiento del programa asegurándonos de limitarnos a la maximización de beneficios añadiendo una lógica de seguimiento, por la que, en lugar de tener que esperar a alcanzar el objetivo de beneficio total, lo que nos expone a casos de recuperación la mayor parte del tiempo, podemos proteger los pequeños beneficios que tenemos y maximizarlos aplicando un stop dinámico. Dado que solo podemos rastrear las primeras posiciones, podemos aplicar una lógica que garantice que solo rastreamos las primeras posiciones y, si entramos en modo de recuperación, esperamos a que se complete la recuperación. Por lo tanto, primero necesitaremos una función de trailing stop.
//+------------------------------------------------------------------+ //| FUNCTION TO APPLY TRAILING STOP | //+------------------------------------------------------------------+ void applyTrailingStop(double slPoints, CTrade &trade_object, int magicNo=0, double minProfitPoints=0){ double buySl = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - slPoints*_Point, _Digits); //--- Calculate the stop loss price for BUY trades double sellSl = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + slPoints*_Point, _Digits); //--- Calculate the stop loss price for SELL trades for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Loop through all open positions ulong ticket = PositionGetTicket(i); //--- Get the ticket number of the current position if (ticket > 0){ //--- Check if the ticket is valid if (PositionSelectByTicket(ticket)){ //--- Select the position by its ticket number if (PositionGetString(POSITION_SYMBOL) == _Symbol && //--- Check if the position belongs to the current symbol (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check if the position matches the given magic number or if no magic number is specified double positionOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the opening price of the position double positionSl = PositionGetDouble(POSITION_SL); //--- Get the current stop loss of the position if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ //--- Check if the position is a BUY trade double minProfitPrice = NormalizeDouble(positionOpenPrice + minProfitPoints * _Point, _Digits); //--- Calculate the minimum price at which profit is locked if (buySl > minProfitPrice && //--- Check if the calculated stop loss is above the minimum profit price buySl > positionOpenPrice && //--- Check if the calculated stop loss is above the opening price (buySl > positionSl || positionSl == 0)){ //--- Check if the calculated stop loss is greater than the current stop loss or if no stop loss is set trade_object.PositionModify(ticket, buySl, PositionGetDouble(POSITION_TP)); //--- Modify the position to update the stop loss } } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ //--- Check if the position is a SELL trade double minProfitPrice = NormalizeDouble(positionOpenPrice - minProfitPoints * _Point, _Digits); //--- Calculate the minimum price at which profit is locked if (sellSl < minProfitPrice && //--- Check if the calculated stop loss is below the minimum profit price sellSl < positionOpenPrice && //--- Check if the calculated stop loss is below the opening price (sellSl < positionSl || positionSl == 0)){ //--- Check if the calculated stop loss is less than the current stop loss or if no stop loss is set trade_object.PositionModify(ticket, sellSl, PositionGetDouble(POSITION_TP)); //--- Modify the position to update the stop loss } } } } } } }
Aquí creamos una función llamada «applyTrailingStop» que nos permite aplicar un trailing stop a todas las posiciones abiertas de COMPRA y VENTA. El objetivo de este trailing stop es proteger y asegurar las ganancias a medida que el mercado se mueve a favor de nuestras operaciones. Utilizamos el objeto «CTrade» para modificar automáticamente los niveles de stop-loss de las operaciones. Para garantizar que el trailing stop no se active demasiado pronto, incluimos una condición que exige alcanzar un beneficio mínimo antes de que el stop-loss comience a seguir la tendencia. Este enfoque evita ajustes prematuros de stop-loss y garantiza que obtengamos una cierta cantidad de ganancias antes de realizar el trailing.
Definimos cuatro parámetros clave en esta función. El parámetro «slPoints» especifica la distancia, en puntos, entre el precio de mercado actual y el nuevo nivel de stop-loss. El parámetro «trade_object» hace referencia al objeto «CTrade», que nos permite gestionar posiciones abiertas, modificar el stop-loss y ajustar el take-profit. El parámetro «magicNo» sirve como identificador único para filtrar operaciones. Si «magicNo» se establece en 0, aplicamos el trailing stop a todas las operaciones, independientemente de su número mágico. Por último, el parámetro «minProfitPoints» define el beneficio mínimo (en puntos) que debe alcanzarse antes de que se active el trailing stop. Esto garantiza que solo ajustemos el stop-loss después de que la posición haya obtenido suficientes beneficios.
Aquí, comenzamos calculando los precios de stop-loss dinámico para las operaciones de COMPRA y VENTA. Para las operaciones de COMPRA, calculamos el nuevo precio de stop-loss restando «slPoints» del precio BID actual. Para las operaciones de VENTA, lo calculamos añadiendo «slPoints» al precio ASK actual. Estos precios de stop-loss se normalizan utilizando _Digits para garantizar la precisión en función de la precisión del precio del símbolo. Este paso de normalización garantiza que los precios se ajusten al número correcto de decimales para el instrumento financiero específico.
A continuación, recorremos todas las posiciones abiertas, comenzando por la última y avanzando hacia la primera. Este enfoque de bucle inverso es esencial porque modificar las posiciones durante un bucle hacia adelante puede provocar errores en la indexación de posiciones. Para cada posición, obtenemos su «ticket», que es el identificador único de esa posición. Si el ticket es válido, utilizamos la función PositionSelectByTicket para seleccionar y acceder a los detalles de la posición.
Una vez seleccionada la posición, comprobamos si coincide con el símbolo actual y si su número mágico coincide con el «magicNo» dado. Si «magicNo» se establece en 0, aplicamos el trailing stop a todas las operaciones, independientemente de su número mágico. Después de identificar una posición coincidente, determinamos si se trata de una operación de COMPRA o VENTA.
Si la posición es una operación de COMPRA, calculamos el precio mínimo que debe alcanzar el mercado antes de que el stop-loss comience a seguir la tendencia. Este valor se obtiene sumando «minProfitPoints» al precio de apertura de la posición. A continuación, comprobamos si el precio del trailing stop calculado está por encima tanto del precio de apertura de la posición como del stop-loss actual. Si se cumplen estas condiciones, modificamos la posición utilizando «trade_object.PositionModify» y actualizamos el precio de stop-loss para la operación de COMPRA.
Si la posición es una operación de VENTA, seguimos un proceso similar. Calculamos el precio mínimo de beneficio restando «minProfitPoints» del precio de apertura de la posición. Comprobamos si el precio del trailing stop calculado está por debajo tanto del precio de apertura de la posición como del stop-loss actual. Si se cumplen estas condiciones, modificamos la posición utilizando «trade_object.PositionModify» y actualizamos el stop-loss para la operación de VENTA.
Ahora que contamos con esta función, necesitamos una lógica para encontrar primero las posiciones iniciales y, a esas funciones, podemos añadir la lógica de trailing stop. Para ello, tendremos que definir una variable booleana en la clase de recuperación de zona, pero hay algo importante: hay que hacerla accesible desde cualquier parte del programa, por lo que hay que hacerla pública.
public: bool isFirstPosition;
Aquí tenemos una variable pública llamada «isFirstPosition» dentro de la clase «ZoneRecovery». Esta variable es de tipo booleano (bool), lo que significa que solo puede contener dos valores posibles: verdadero o falso. El objetivo de la función es realizar un seguimiento de si la operación actual se encuentra en la primera posición del proceso de recuperación de la zona. Cuando «isFirstPosition» es verdadero, indica que no se han abierto operaciones anteriores y que esta es la posición inicial. Esta distinción es esencial porque la lógica para gestionar la primera operación cambiará, ya que queremos aplicar la lógica de trailing stop a la misma.
Dado que declaramos «isFirstPosition» como público, se puede acceder a él y modificarlo desde fuera de la clase «ZoneRecovery». Esto permite que otras partes del programa comprueben si una posición es la primera de una serie o actualicen su estado en consecuencia. Ahora, dentro de la función responsable de abrir operaciones, necesitamos asignar los indicadores booleanos para indicar si se trata de una primera posición o no, una vez que se abre una posición.
if (trade.Buy(currentLotSize, symbol)) { lastOrderType = ORDER_TYPE_BUY; //--- Mark the last trade as BUY. lastOrderPrice = SymbolInfoDouble(symbol, SYMBOL_BID); //--- Store the current BID price. CalculateZones(); //--- Recalculate zones after placing the trade. Print(isRecovery ? "RECOVERY BUY order placed" : "INITIAL BUY order placed", " at ", lastOrderPrice, " with lot size ", currentLotSize); isFirstPosition = isRecovery ? false : true; isRecovery = true; //--- Set recovery state to true after the first trade. return true; }
Aquí, establecemos la variable «isFirstPosition» en falso si la posición está registrada como posición de recuperación, o en verdadero si la variable «isRecovery» es falsa. De nuevo, en las funciones constructor y reset, establecemos por defecto la variable objetivo en false. A partir de ahí, podemos ir al controlador de eventos «OnTick» y aplicar el trailing stop cuando tengamos una posición inicial.
if (zoneRecovery.isFirstPosition == true){ //--- Check if this is the first position in the Zone Recovery process applyTrailingStop(100, obj_Trade, 0, 100); //--- Apply a trailing stop with 100 points, passing the "obj_Trade" object, a magic number of 0, and a minimum profit of 100 points }
Aquí, verificamos si la variable «zoneRecovery.isFirstPosition» es verdadera, lo que indica que esta es la primera posición en el proceso de recuperación de zona. Si es así, llamamos a la función «applyTrailingStop». Los parámetros pasados son «100» puntos para la distancia del trailing stop, «obj_Trade» como objeto de la operación, un número mágico de «0» para identificar la operación y un beneficio mínimo de «100» puntos. Esto garantiza que, una vez que la operación alcance una ganancia de 100 puntos, se aplique el trailing stop para proteger las ganancias, siguiendo el stop loss a medida que el precio se mueve a favor de la operación. Sin embargo, cuando cerramos las operaciones mediante trailing stop, aún nos quedan restos de la lógica de recuperación de la zona, ya que no las restablecemos. Esto hace que el sistema abra operaciones de recuperación incluso cuando no tenemos operaciones existentes. Esto es lo que queremos decir con eso.
En la visualización se puede ver que tenemos que reiniciar el sistema una vez que se ha alcanzado la posición inicial. Esta es la lógica que debemos adoptar para ello.
if (zoneRecovery.isFirstPosition == true && PositionsTotal() == 0){ //--- Check if this is the first position and if there are no open positions zoneRecovery.Reset(); //--- Reset the Zone Recovery system, restoring initial settings and clearing previous trade data }
Aquí, comprobamos si la variable «isFirstPosition» es verdadera y si no hay posiciones existentes. Si se cumplen ambas condiciones, significa que teníamos una posición inicial, que se cerró por cualquier motivo, y ahora que ya no existe, llamamos a la función «zoneRecovery.Reset()». Esto restablece el sistema Zone Recovery restaurando su configuración inicial y borrando cualquier dato comercial anterior, lo que garantiza que el proceso de recuperación comience desde cero. Estas modificaciones hacen que el sistema sea perfecto. Tras realizar las pruebas finales, obtenemos los siguientes resultados.
Gráfico del Probador de estrategias:
Informe del Probador de estrategia:
En la imagen podemos ver que hemos reducido el número de posiciones de recuperación, lo que aumenta significativamente nuestra tasa de aciertos. Esto confirma que hemos logrado nuestro objetivo de crear un sistema de recuperación de zonas con una lógica de gestión comercial dinámica.
Conclusión
En conclusión, hemos demostrado cómo crear un asesor experto MetaQuotes Language 5 (MQL5) utilizando la estrategia Zone Recovery. Al combinar el indicador Índice de fuerza relativa (Relative Strength Index, RSI) con la lógica de «recuperación de zona», creamos un sistema capaz de detectar señales comerciales, gestionar posiciones de recuperación y asegurar ganancias con stops dinámicos. Los elementos clave incluían la identificación de señales, la ejecución automatizada de operaciones y la recuperación dinámica de posiciones.
Descargo de responsabilidad: Este artículo sirve como guía educativa para desarrollar programas MQL5. Aunque la estrategia «Zone Recovery RSI» ofrece un enfoque estructurado para la gestión de operaciones, las condiciones del mercado siguen siendo impredecibles. El trading conlleva riesgos financieros, y los resultados pasados no garantizan resultados futuros. Es fundamental realizar pruebas adecuadas y gestionar los riesgos antes de operar en vivo.
Al dominar los conceptos descritos en esta guía, podrá crear sistemas de negociación más adaptables y explorar nuevas estrategias para la negociación algorítmica. ¡Feliz programación y éxito en tus operaciones!
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/16705
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
no encuentro los archivos mq de tradq donde esta
Está perfectamente explicado incluso con imagen.
Bien dicho. La estrategia asume que el precio se romperá fuera del rango inicialmente negociado con el fin de recuperar las pérdidas. Como sabemos, al menos en lo que respecta al mercado de divisas, el precio oscila más a menudo de lo que tiende. El método para mitigar las catastróficas pérdidas de Martingale sería añadir un filtro de volatilidad para las operaciones iniciales.
Bien dicho. La estrategia supone que el precio saldrá del rango negociado inicialmente para recuperar las pérdidas. Como sabemos, al menos en lo que respecta al mercado de divisas, el precio oscila más a menudo de lo que tiende. El método para mitigar las catastróficas pérdidas de Martingale sería añadir un filtro de volatilidad para las operaciones iniciales.
Por supuesto.
Hojeo el código y me pregunto ¿por qué no utilizar el precio de venta para calcular en el caso de compra? ¿Hay algún conflicto si lo cambio? Gracias.