Automatización de estrategias de trading en MQL5 (Parte 5): Desarrollo de la estrategia Adaptive Crossover RSI Trading Suite
Introducción
En el artículo anterior (parte 4 de la serie), presentamos el sistema de recuperación de zonas multinivel y mostramos cómo ampliar los principios de recuperación de zonas para gestionar varias configuraciones comerciales independientes simultáneamente en MetaQuotes Language 5 (MQL5). En este artículo (Parte 5), tomamos una nueva dirección con la estrategia Adaptive Crossover RSI Trading Suite, un sistema integral diseñado para identificar y actuar sobre oportunidades de trading de alta probabilidad. Esta estrategia combina dos herramientas fundamentales de análisis técnico: los cruces de la media móvil adaptativa (14 y 50 períodos) como generador de señales principal y un indicador de fuerza relativa (RSI) de 14 períodos como filtro de confirmación.
Además, emplea un filtro de días de negociación para excluir las sesiones de negociación de baja probabilidad, lo que garantiza una mayor precisión y rendimiento. Para mejorar la usabilidad, el sistema visualiza las señales de trading confirmadas directamente en el gráfico dibujando flechas y añadiendo anotaciones con descripciones claras de las señales. También se incluye un panel de control que ofrece un resumen en tiempo real del estado de la estrategia, las métricas clave y la actividad de las señales, lo que proporciona a los operadores una visión general completa de un solo vistazo. Este artículo le guiará paso a paso por el proceso de desarrollo de esta estrategia, desde la elaboración del plan hasta su implementación en MQL5, pasando por la comprobación retrospectiva de su rendimiento y el análisis de los resultados. Estructuraremos esto a través de los siguientes temas:
- Plan estratégico
- Implementación en MQL5
- Pruebas retrospectivas
- Conclusión
Al finalizar, comprenderá de forma práctica cómo crear un sistema de negociación adaptativo basado en filtros y cómo perfeccionarlo para obtener un rendimiento sólido en diversas condiciones de mercado. ¡Comencemos!
Plan estratégico
La estrategia Adaptive Crossover RSI Trading Suite se basa en cruces de medias móviles y confirmación de impulso, creando un enfoque equilibrado para operar. Las señales principales se derivarán de la interacción entre un promedio móvil rápido de 14 períodos y un promedio móvil lento de 50 períodos. Se producirá una señal de compra cuando la media móvil rápida cruce por encima de la media móvil lenta, lo que sugiere una tendencia alcista, mientras que se generará una señal de venta cuando la media móvil rápida cruce por debajo de la media móvil lenta, lo que indica una tendencia bajista.
Para mejorar la precisión de estas señales, se utilizó un período de 14.Índice de fuerza relativa (RSI) se empleará como filtro de confirmación. El RSI garantizará que las operaciones se alineen con el impulso predominante del mercado, reduciendo la probabilidad de entrar en operaciones en condiciones de sobrecompra o sobreventa. Por ejemplo, una señal de compra solo se validará si el RSI está por encima de un umbral de 50, mientras que una señal de venta requerirá que el RSI esté por debajo de su umbral correspondiente. La estrategia también incorporará un filtro de días de negociación para optimizar el rendimiento evitando operaciones en días con una volatilidad históricamente baja o un rendimiento deficiente. Este filtro garantizará que el sistema se centre únicamente en oportunidades de negociación de alta probabilidad. En resumen, el plan estratégico es el siguiente.
Plan de confirmación de venta:

Plan de confirmación de compra:

Además, una vez confirmada una operación, el sistema marcará el gráfico con flechas de señalización y anotaciones, identificando claramente los puntos de entrada. Un panel de control proporcionará actualizaciones en tiempo real, ofreciendo una instantánea de la actividad de las señales, las métricas clave y el estado general del sistema. Este enfoque estructurado y adaptable garantizará que la estrategia sea sólida y fácil de usar. La perspectiva final será la que se muestra a continuación.

Implementación en MQL5
Después de aprender todas las teorías sobre la estrategia Adaptive Crossover RSI Trading Suite, automatizamos la teoría y creamos un Asesor Experto (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 bibliotecas 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. También puede hacer clic en el icono Nuevo de la pestaña Herramientas. Esto hará que aparezca una ventana emergente del Asistente MQL.
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. Eso significa que nuestro EA se creará en la carpeta de Expertos y podremos encontrarlo allí. Las demás secciones son bastante sencillas, pero puedes seguir el enlace que aparece al final 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, haga clic en Siguiente y, a continuación, haga clic en Finalizar. Después de hacer todo eso, ya estamos listos para codificar y programar nuestra estrategia.
En primer lugar, comenzamos definiendo algunos metadatos sobre el Asesor Experto (EA). Esto incluye el nombre del agente de bolsa, la información de derechos de autor y un enlace al sitio web de MetaQuotes. También especificamos la versión del EA, que está configurada en "1.00".
//+------------------------------------------------------------------+ //| Adaptive Crossover RSI Trading Suite.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Forex Algo-Trader, Allan" #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property description "EA that trades based on MA Crossover, RSI + Day Filter" #property strict
Esto mostrará los metadatos del sistema al cargar el programa. A continuación, podemos pasar a añadir algunas variables globales que utilizaremos 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, debemos declarar varias variables de entrada importantes que permitirán al usuario cambiar los valores de negociación a los deseados sin alterar el código en sí. Para lograrlo, organizamos los datos en grupos para mayor claridad, es decir, ajustes generales, indicadores y filtros.
sinput group "GENERAL SETTINGS" sinput double inpLots = 0.01; // LotSize input int inpSLPts = 300; // Stoploss Points input double inpR2R = 1.0; // Risk to Reward Ratio sinput ulong inpMagicNo = 1234567; // Magic Number input bool inpisAllowTrailingStop = true; // Apply Trailing Stop? input int inpTrailPts = 50; // Trailing Stop Points input int inpMinTrailPts = 50; // Minimum Trailing Stop Points sinput group "INDICATOR SETTINGS" input int inpMA_Fast_Period = 14; // Fast MA Period input ENUM_MA_METHOD inpMA_Fast_Method = MODE_EMA; // Fast MA Method input int inpMA_Slow_Period = 50; // Slow MA Period input ENUM_MA_METHOD inpMA_Slow_Method = MODE_EMA; // Slow MA Method sinput group "FILTER SETTINGS" input ENUM_TIMEFRAMES inpRSI_Tf = PERIOD_CURRENT; // RSI Timeframe input int inpRSI_Period = 14; // RSI Period input ENUM_APPLIED_PRICE inpRSI_Applied_Price = PRICE_CLOSE; // RSI Application Price input double inpRsiBUYThreshold = 50; // BUY Signal Threshold input double inpRsiSELLThreshold = 50; // SELL Signal Threshold input bool Sunday = false; // Trade on Sunday? input bool Monday = false; // Trade on Monday? input bool Tuesday = true; // Trade on Tuesday? input bool Wednesday = true; // Trade on Wednesday? input bool Thursday = true; // Trade on Thursday? input bool Friday = false; // Trade on Friday? input bool Saturday = false; // Trade on Saturday?
Aquí definimos los parámetros y configuraciones principales del programa Adaptive Crossover RSI Trading Suite, lo que permite un control preciso sobre su comportamiento. Dividimos estos ajustes en tres grupos principales: «GENERAL SETTINGS», «INDICATOR SETTINGS» y «FILTER SETTINGS», junto con controles específicos para los días de negociación. El uso de tipos de variables y enumeraciones mejora la flexibilidad y la claridad en el diseño del sistema.
En el grupo «GENERAL SETTINGS», definimos los parámetros de gestión comercial. Utilizamos la palabra clave input para los parámetros optimizables y sinput para los parámetros de cadena o no optimizables. La variable «inpLots» especifica el tamaño del lote de la operación, mientras que «inpSLPts» establece el nivel de stop-loss en puntos, lo que garantiza el control del riesgo en cada operación. La variable «inpR2R» establece la relación riesgo-recompensa deseada, manteniendo un equilibrio favorable entre el riesgo y la recompensa potencial. Se asigna un identificador comercial único mediante «inpMagicNo», que el programa utiliza para diferenciar sus órdenes. La funcionalidad de stop dinámico se gestiona mediante «inpisAllowTrailingStop», lo que permite a los usuarios activarla o desactivarla. Las variables «inpTrailPts» e «inpMinTrailPts» especifican la distancia del trailing stop y el umbral mínimo de activación, respectivamente, lo que garantiza que los trailing stops se ajusten a las condiciones del mercado.
En el grupo «INDICATOR SETTINGS» (Configuración de indicadores), configuramos los parámetros para las medias móviles, que constituyen la columna vertebral de la generación de señales. El período de la media móvil rápida se define mediante «inpMA_Fast_Period», y su método de cálculo se elige utilizando la enumeración ENUM_MA_METHOD con la variable «inpMA_Fast_Method», que admite opciones como MODE_SMA, MODE_EMA, MODE_SMMA y MODE_LWMA. Del mismo modo, la media móvil lenta se establece con «inpMA_Slow_Period», mientras que su método se determina utilizando «inpMA_Slow_Method». Estas enumeraciones garantizan que los usuarios puedan personalizar la estrategia con sus tipos de medias móviles preferidos para diferentes condiciones del mercado.
El grupo «FILTER SETTINGS» (Configuración de filtros) se centra en el indicador RSI, que sirve como filtro de impulso. La variable «inpRSI_Tf», definida mediante la enumeración ENUM_TIMEFRAMES, permite a los usuarios seleccionar el intervalo de tiempo del RSI, como PERIOD_M1, «PERIOD_H1» o «PERIOD_D1». El período RSI se especifica con «inpRSI_Period», mientras que «inpRSI_Applied_Price», una enumeración ENUM_APPLIED_PRICE, determina los datos de precios (por ejemplo, «PRICE_CLOSE», «PRICE_OPEN» o «PRICE_MEDIAN») utilizados para los cálculos. Los umbrales para validar las señales de compra y venta se establecen utilizando «inpRsiBUYThreshold» e «inpRsiSELLThreshold», lo que garantiza que el RSI se alinee con el impulso del mercado antes de ejecutar las operaciones.
Por último, implementamos un filtro de días de negociación utilizando variables booleanas, como «domingo», «lunes», etc., lo que permite controlar la actividad del EA en días específicos. Al desactivar la negociación en días menos favorables, el sistema evita una exposición innecesaria a condiciones potencialmente poco rentables. A continuación, debemos definir los indicadores que vamos a utilizar.
int handleMAFast = INVALID_HANDLE; int handleMASlow = INVALID_HANDLE; int handleRSIFilter = INVALID_HANDLE;
Inicializamos tres variables clave: «handleMAFast», «handleMASlow» y «handleRSIFilter», y las establecemos en INVALID_HANDLE. De esta forma, nos aseguramos de que nuestro EA comience en un estado limpio y controlado, evitando posibles problemas derivados de indicadores no inicializados o no válidos. Utilizamos «handleMAFast» para gestionar el indicador de media móvil rápida, que configuramos para capturar las tendencias de precios a corto plazo basándonos en los parámetros que definimos.
Del mismo modo, «handleMASlow» está diseñado para gestionar el indicador de media móvil lenta, lo que nos permite realizar un seguimiento de las tendencias de precios a más largo plazo. Estas manejaderas son vitales para recuperar y procesar dinámicamente los valores medios móviles necesarios para nuestra estrategia. Con «handleRSIFilter», nos preparamos para conectarnos al indicador RSI, que utilizamos como filtro de impulso para confirmar nuestras señales. A continuación, tendremos que definir las matrices de almacenamiento en las que guardaremos los datos recuperados de los indicadores. Esto también requerirá tres matrices.
double bufferMAFast[]; double bufferMASlow[]; double bufferRSIFilter[];
Aquí declaramos tres matrices dinámicas: «bufferMAFast[]», «bufferMASlow[]» y «bufferRSIFilter[]». Estas matrices servirán como contenedores de almacenamiento donde recopilaremos y gestionaremos los valores calculados de los indicadores utilizados en nuestra estrategia. Al organizar los datos de esta manera, nos aseguramos de que nuestro EA tenga acceso directo y eficiente a los resultados de los indicadores durante su funcionamiento. A partir de aquí, tendremos que ir a la función de inicialización y crear los identificadores del indicador.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- handleMAFast = iMA(_Symbol,_Period,inpMA_Fast_Period,0,inpMA_Fast_Method,PRICE_CLOSE); handleMASlow = iMA(_Symbol,_Period,inpMA_Slow_Period,0,inpMA_Slow_Method,PRICE_CLOSE); handleRSIFilter = iRSI(_Symbol,inpRSI_Tf,inpRSI_Period,inpRSI_Applied_Price); //---- }
Aquí inicializamos los identificadores para los indicadores que utilizaremos en la estrategia: la media móvil rápida, la media móvil lenta y el filtro RSI. Comenzamos inicializando la media móvil rápida utilizando la función iMA. Esta función requiere varios parámetros. El primero, _Symbol, indica a la función que calcule la media móvil para el instrumento de negociación actual. El segundo, _Period, especifica el intervalo de tiempo del gráfico (por ejemplo, 1 minuto, 1 hora).
También pasamos el período medio móvil rápido («inpMA_Fast_Period»), que determina cuántas barras se utilizan para calcular la media móvil. El parámetro «0» se refiere al «desplazamiento» de la media móvil, donde «0» significa que no hay desplazamiento. El método de media móvil («inpMA_Fast_Method») especifica si se trata de una media móvil exponencial o simple, y «PRICE_CLOSE» indica que estamos utilizando los precios de cierre de cada barra para calcular la media.
El resultado de esta función se asigna a «handleMAFast», lo que nos permite acceder al valor medio móvil rápido para futuros cálculos.
A continuación, inicializamos la media móvil lenta de la misma manera llamando a la función iMA. Aquí utilizamos los mismos _Symbol, _Period y el periodo medio de movimiento lento («inpMA_Slow_Period»). Una vez más, especificamos el método y el precio («PRICE_CLOSE») utilizados para calcular esta media móvil. Este valor se almacena en «handleMASlow» para su uso futuro. Por último, inicializamos el filtro RSI utilizando la función iRSI. Proporcionamos el _Symbol para especificar el instrumento, el intervalo de tiempo del RSI («inpRSI_Tf»), el período del RSI («inpRSI_Period») y el precio aplicado («inpRSI_Applied_Price»). El resultado de la función se almacena en «handleRSIFilter», lo que nos permitirá utilizar el valor RSI para confirmar las señales de trading en la estrategia.
Dado que estos manejadores son la columna vertebral de nuestra estrategia, debemos asegurarnos de que estén correctamente inicializados y, si no es así, claramente no tiene sentido que continuemos ejecutando el programa.
if (handleMAFast == INVALID_HANDLE || handleMASlow == INVALID_HANDLE || handleRSIFilter == INVALID_HANDLE){ Print("ERROR! Unable to create the indicator handles. Reveting Now!"); return (INIT_FAILED); }
Aquí comprobamos si la inicialización de los indicadores se ha realizado correctamente. Evaluamos si alguno de los identificadores («handleMAFast», «handleMASlow» o «handleRSIFilter») es igual a INVALID_HANDLE, lo que indicaría un error al crear los indicadores correspondientes. Si alguno de los manejadores falla, utilizamos la función «Print» para mostrar un mensaje de error en la terminal, alertándonos del problema. Por último, devolvemos INIT_FAILED, lo que detiene la ejecución del EA si alguno de los identificadores del indicador no es válido, garantizando que el EA no continúe ejecutándose en condiciones defectuosas.
También se produciría otro error si el usuario proporcionara períodos poco realistas, técnicamente inferiores o iguales a cero. Por lo tanto, debemos comprobar los valores de entrada definidos por el usuario para los períodos de la media móvil rápida, la media móvil lenta y el RSI, a fin de garantizar que los períodos («inpMA_Fast_Period», «inpMA_Slow_Period», «inpRSI_Period») sean superiores a cero.
if (inpMA_Fast_Period <= 0 || inpMA_Slow_Period <= 0 || inpRSI_Period <= 0){ Print("ERROR! Periods cannot be <= 0. Reverting Now!"); return (INIT_PARAMETERS_INCORRECT); }
Aquí, si los valores introducidos por el usuario no son mayores que cero, terminamos el programa devolviendo INIT_PARAMETERS_INCORRECT. Si pasamos por aquí, entonces tenemos los indicadores listos y podemos configurar las matrices de almacenamiento como series temporales.
ArraySetAsSeries(bufferMAFast,true); ArraySetAsSeries(bufferMASlow,true); ArraySetAsSeries(bufferRSIFilter,true); obj_Trade.SetExpertMagicNumber(inpMagicNo); Print("SUCCESS INITIALIZATION. ACCOUNT TYPE = ",trading_Account_Mode());
Por último, realizamos algunas acciones clave para finalizar el proceso de inicialización. En primer lugar, utilizamos la función ArraySetAsSeries para establecer las matrices («bufferMAFast», «bufferMASlow» y «bufferRSIFilter») como series temporales. Esto es importante porque garantiza que los datos de estas matrices se almacenen de forma compatible con el modo en que MetaTrader gestiona los datos de series temporales, es decir, almacenando los datos más recientes en el índice 0. Al configurar cada una de estas matrices como una serie, nos aseguramos de que se acceda a los indicadores en el orden correcto durante la negociación.
A continuación, llamamos al método «SetExpertMagicNumber» en el objeto «obj_Trade», pasando el valor «inpMagicNo» como número mágico. El número mágico es un identificador único para las operaciones del EA, lo que garantiza que se puedan diferenciar de otras operaciones realizadas manualmente o por otros EA. Por último, utilizamos la función Print para mostrar un mensaje de éxito en la terminal, confirmando que el proceso de inicialización se ha completado. El mensaje incluye el tipo de cuenta, que se recupera utilizando la función «trading_Account_Mode», indicando si la cuenta es una cuenta demo o una cuenta real. La función responsable de esto es la siguiente.
string trading_Account_Mode(){ string account_mode; switch ((ENUM_ACCOUNT_TRADE_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE)){ case ACCOUNT_TRADE_MODE_DEMO: account_mode = "DEMO"; break; case ACCOUNT_TRADE_MODE_CONTEST: account_mode = "COMPETITION"; break; case ACCOUNT_TRADE_MODE_REAL: account_mode = "REAL"; break; } return account_mode; }
Aquí definimos una función string «trading_Account_Mode» para determinar el tipo de cuenta de trading (si es una cuenta demo, una cuenta de competición o una cuenta real) basándonos en el valor del parámetro ACCOUNT_TRADE_MODE. Comenzamos declarando una variable «account_mode» para almacenar el tipo de cuenta como una cadena. A continuación, utilizamos una instrucción «switch» para evaluar el modo de negociación de la cuenta, que se obtiene llamando a la función «AccountInfoInteger» con el parámetro ACCOUNT_TRADE_MODE. Esta función devuelve el modo de negociación de la cuenta como un valor entero. La instrucción switch comprueba el valor de este entero y lo compara con los posibles modos de cuenta:
- Si el modo de cuenta es ACCOUNT_TRADE_MODE_DEMO, establecemos «account_mode» en «DEMO».
- Si el modo de cuenta es ACCOUNT_TRADE_MODE_CONTEST, establecemos «account_mode» en «COMPETITION».
- Si el modo de cuenta es ACCOUNT_TRADE_MODE_REAL, establecemos «account_mode» en «REAL».
Por último, la función devuelve «account_mode» como una cadena, que indica el tipo de cuenta a la que está conectado el EA. Por lo tanto, la función de inicialización final es la siguiente:
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ //--- handleMAFast = iMA(_Symbol,_Period,inpMA_Fast_Period,0,inpMA_Fast_Method,PRICE_CLOSE); handleMASlow = iMA(_Symbol,_Period,inpMA_Slow_Period,0,inpMA_Slow_Method,PRICE_CLOSE); handleRSIFilter = iRSI(_Symbol,inpRSI_Tf,inpRSI_Period,inpRSI_Applied_Price); if (handleMAFast == INVALID_HANDLE || handleMASlow == INVALID_HANDLE || handleRSIFilter == INVALID_HANDLE){ Print("ERROR! Unable to create the indicator handles. Reveting Now!"); return (INIT_FAILED); } if (inpMA_Fast_Period <= 0 || inpMA_Slow_Period <= 0 || inpRSI_Period <= 0){ Print("ERROR! Periods cannot be <= 0. Reverting Now!"); return (INIT_PARAMETERS_INCORRECT); } ArraySetAsSeries(bufferMAFast,true); ArraySetAsSeries(bufferMASlow,true); ArraySetAsSeries(bufferRSIFilter,true); obj_Trade.SetExpertMagicNumber(inpMagicNo); Print("SUCCESS INITIALIZATION. ACCOUNT TYPE = ",trading_Account_Mode()); //--- return(INIT_SUCCEEDED); }
Ahora pasamos al controlador de eventos OnDeinit, donde tendremos que liberar los identificadores del indicador, ya que ya no los necesitaremos.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- IndicatorRelease(handleMAFast); IndicatorRelease(handleMASlow); IndicatorRelease(handleRSIFilter); }
Para liberar los recursos asignados a los manejadores de indicadores, comenzamos llamando a la función IndicatorRelease para cada uno de los manejadores de indicadores: «handleMAFast», «handleMASlow» y «handleRSIFilter». El objetivo de la función es liberar la memoria y los recursos asociados con los identificadores de indicadores que se inicializaron durante la ejecución del EA. Esto garantiza que los recursos de la plataforma no se vean ocupados innecesariamente por indicadores que ya no se utilizan. A continuación, pasamos al controlador de eventos OnTick, que es donde se gestionará la mayor parte de nuestra lógica de negociación. Primero tendremos que recuperar los datos del indicador de los manejadores.
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Check if data can be retrieved for the fast moving average (MA) if (CopyBuffer(handleMAFast,0,0,3,bufferMAFast) < 3){ //--- Print error message for fast MA data retrieval failure Print("ERROR! Failed to retrieve the requested FAST MA data. Reverting."); //--- Exit the function if data retrieval fails return; } //--- Check if data can be retrieved for the slow moving average (MA) if (CopyBuffer(handleMASlow,0,0,3,bufferMASlow) < 3){ //--- Print error message for slow MA data retrieval failure Print("ERROR! Failed to retrieve the requested SLOW MA data. Reverting."); //--- Exit the function if data retrieval fails return; } //--- Check if data can be retrieved for the RSI filter if (CopyBuffer(handleRSIFilter,0,0,3,bufferRSIFilter) < 3){ //--- Print error message for RSI data retrieval failure Print("ERROR! Failed to retrieve the requested RSI data. Reverting."); //--- Exit the function if data retrieval fails return; } //--- }
Aquí nos centramos en recuperar los últimos datos de los indicadores para el promedio móvil rápido, el promedio móvil lento y el filtro RSI, con el fin de garantizar que el EA disponga de la información necesaria para tomar decisiones de trading. En primer lugar, utilizamos la función CopyBuffer para el controlador de media móvil rápida («handleMAFast»). La función extrae los valores del indicador al búfer correspondiente («bufferMAFast») para su procesamiento. En concreto, solicitamos 3 puntos de datos a partir del índice 0, que representa los datos más recientes del gráfico. Si el número de valores recuperados es inferior a 3, indica un fallo al acceder a los datos requeridos. En este caso, imprimimos un mensaje de error utilizando la función Print y terminamos la función antes de tiempo con el operador return.
A continuación, repetimos un proceso similar para el mango de promedio lento («handleMASlow») y su búfer («bufferMASlow»). Una vez más, si la función CopyBuffer no consigue recuperar al menos 3 puntos de datos, imprimimos un mensaje de error y salimos de la función para evitar que se siga ejecutando. Por último, utilizamos la misma función para el controlador del filtro RSI («handleRSIFilter») y su búfer («bufferRSIFilter»). Como antes, nos aseguramos de que los puntos de datos solicitados se recuperen correctamente; de lo contrario, se muestra un mensaje de error y la función se termina. Si no volvemos a este punto, disponemos de los datos necesarios y podemos seguir generando señales. Sin embargo, queremos generar señales en cada barra y no en cada tick. Por lo tanto, necesitaremos una función para detectar la generación de nuevas barras.
//+------------------------------------------------------------------+ //| Function to detect if a new bar is formed | //+------------------------------------------------------------------+ bool isNewBar(){ //--- Static variable to store the last bar count static int lastBarCount = 0; //--- Get the current bar count int currentBarCount = iBars(_Symbol,_Period); //--- Check if the bar count has increased if (currentBarCount > lastBarCount){ //--- Update the last bar count lastBarCount = currentBarCount; //--- Return true if a new bar is detected return true; } //--- Return false if no new bar is detected return false; }
Aquí definimos la función «isNewBar», diseñada para detectar la aparición de una nueva barra en el gráfico. Esta función será crucial para garantizar que nuestras operaciones se realicen solo una vez por barra, en lugar de repetidamente en cada tick. Comenzamos declarando una variable estática «lastBarCount» e inicializándola en 0. Una variable estática conserva su valor entre llamadas a funciones, lo que nos permite comparar el estado actual con el estado anterior. A continuación, recuperamos el número total de barras del gráfico utilizando la función iBars, pasando _Symbol (el instrumento de negociación actual) y _Period (el intervalo de tiempo actual). El resultado se almacena en «currentBarCount».
A continuación, comparamos «currentBarCount» con «lastBarCount». Si «currentBarCount» es mayor, indica que se ha formado una nueva barra en el gráfico. En este caso, actualizamos «lastBarCount» para que coincida con «currentBarCount» y devolvemos verdadero, lo que indica la presencia de una nueva barra. Si no se detecta ninguna barra nueva, la función devuelve falso. Ahora podemos usar esta función en el controlador de eventos de tick.
//--- Check if a new bar has formed if (isNewBar()){ //--- Print debug message for a new tick //Print("THIS IS A NEW TICK"); //--- Identify if a buy crossover has occurred bool isMACrossOverBuy = bufferMAFast[1] > bufferMASlow[1] && bufferMAFast[2] <= bufferMASlow[2]; //--- Identify if a sell crossover has occurred bool isMACrossOverSell = bufferMAFast[1] < bufferMASlow[1] && bufferMAFast[2] >= bufferMASlow[2]; //--- Check if the RSI confirms a buy signal bool isRSIConfirmBuy = bufferRSIFilter[1] >= inpRsiBUYThreshold; //--- Check if the RSI confirms a sell signal bool isRSIConfirmSell = bufferRSIFilter[1] <= inpRsiSELLThreshold; //--- }
Aquí implementamos la lógica central para detectar señales de trading específicas basadas en la relación entre las medias móviles y las confirmaciones del RSI. El proceso comienza comprobando si se ha formado una nueva barra utilizando la función «isNewBar». Esto garantiza que la lógica posterior se ejecute solo una vez por barra, evitando evaluaciones repetidas dentro de la misma barra.
Si se detecta una nueva barra, primero nos preparamos para identificar un cruce alcista evaluando la relación entre las medias móviles rápidas y lentas. Concretamente, comprobamos si el valor medio rápido de la barra anterior («bufferMAFast[1]») es mayor que el valor medio lento de la misma barra («bufferMASlow[1]»), mientras que, al mismo tiempo, el valor medio móvil rápido de hace dos barras («bufferMAFast[2]») era menor o igual que el valor medio móvil lento de esa barra («bufferMASlow[2]»). Si ambas condiciones son ciertas, establecemos la variable booleana «isMACrossOverBuy» en verdadero, lo que indica un cruce de compra.
Del mismo modo, identificamos un cruce de venta comprobando si el valor de la media móvil rápida de la barra anterior («bufferMAFast[1]») es inferior al valor de la media móvil lenta de la misma barra («bufferMASlow[1]»), mientras que el valor de la media móvil rápida de hace dos barras («bufferMAFast[2]») era mayor o igual que el valor de la media móvil lenta de esa barra («bufferMASlow[2]»). Si se cumplen estas condiciones, establecemos la variable booleana «isMACrossOverSell» en verdadero, lo que indica un cruce de venta.
A continuación, incorporamos el RSI como filtro de confirmación para los cruces detectados. Para confirmar una compra, verificamos que el valor RSI de la barra anterior («bufferRSIFilter[1]») sea mayor o igual al umbral de compra («inpRsiBUYThreshold»). Si es cierto, establecemos la variable booleana «isRSIConfirmBuy» en verdadero. Del mismo modo, para una confirmación de venta, comprobamos que el valor del RSI de la barra anterior («bufferRSIFilter[1]») sea inferior o igual al umbral de venta («inpRsiSELLThreshold»). Si es cierto, establecemos la variable booleana «isRSIConfirmSell» en verdadero. Ahora podemos utilizar estas variables para tomar decisiones comerciales.
//--- Handle buy signal conditions if (isMACrossOverBuy){ if (isRSIConfirmBuy){ //--- Print buy signal message Print("BUY SIGNAL"); //--- }
Aquí, comprobamos si hay un cruce en las medias móviles y el RSI confirma la señal, y si todas las condiciones se cumplen, imprimimos una señal de compra. Sin embargo, antes de abrir una posición de compra, debemos comprobar que se cumple el filtro de días de negociación. Por lo tanto, necesitamos una función para mantener todo modularizado.
//+------------------------------------------------------------------+ //| Function to check trading days filter | //+------------------------------------------------------------------+ bool isCheckTradingDaysFilter(){ //--- Structure to store the current date and time MqlDateTime dateTIME; //--- Convert the current time into structured format TimeToStruct(TimeCurrent(),dateTIME); //--- Variable to store the day of the week string today = "DAY OF WEEK"; //--- Assign the day of the week based on the numeric value if (dateTIME.day_of_week == 0){today = "SUNDAY";} if (dateTIME.day_of_week == 1){today = "MONDAY";} if (dateTIME.day_of_week == 2){today = "TUESDAY";} if (dateTIME.day_of_week == 3){today = "WEDNESDAY";} if (dateTIME.day_of_week == 4){today = "THURSDAY";} if (dateTIME.day_of_week == 5){today = "FRIDAY";} if (dateTIME.day_of_week == 6){today = "SATURDAY";} //--- Check if trading is allowed based on the input parameters if ( (dateTIME.day_of_week == 0 && Sunday == true) || (dateTIME.day_of_week == 1 && Monday == true) || (dateTIME.day_of_week == 2 && Tuesday == true) || (dateTIME.day_of_week == 3 && Wednesday == true) || (dateTIME.day_of_week == 4 && Thursday == true) || (dateTIME.day_of_week == 5 && Friday == true) || (dateTIME.day_of_week == 6 && Saturday == true) ){ //--- Print acceptance message for trading Print("Today is on ",today,". Trade ACCEPTED."); //--- Return true if trading is allowed return true; } else { //--- Print rejection message for trading Print("Today is on ",today,". Trade REJECTED."); //--- Return false if trading is not allowed return false; } }
Aquí, creamos una función «isCheckTradingDaysFilter» para determinar si se permite operar en el día actual según la configuración introducida por el usuario. Esto garantiza que las operaciones solo se ejecuten en los días hábiles permitidos, lo que mejora la precisión y evita operaciones no deseadas en días restringidos. En primer lugar, definimos un objeto estructurado «MqlDateTime dateTIME» para almacenar la fecha y la hora actuales. Mediante la función TimeToStruct, convertimos la hora actual del servidor (TimeCurrent) en la estructura «dateTIME», lo que nos permite acceder fácilmente a componentes como el día de la semana.
A continuación, definimos una variable «hoy» y le asignamos una cadena de marcador de posición «DÍA DE LA SEMANA». Esto almacenará posteriormente el nombre del día actual en un formato legible para los humanos. Mediante una serie de condiciones if, asignamos el valor numérico «day_of_week» (que va de 0 para el domingo a 6 para el sábado) al nombre del día correspondiente, actualizando la variable «today» con el día correcto.
A continuación, comprobamos si se permite operar en el día actual comparando «dateTIME.day_of_week» con las variables de entrada booleanas correspondientes («Sunday», «Monday», etc.). Si el día actual coincide con uno de los días hábiles habilitados, se imprime un mensaje mediante "Print" para indicar que se permite operar, incluyendo el nombre del día, y la función devuelve verdadero. Por el contrario, si no se permite el comercio, se imprime un mensaje para indicar que el comercio ha sido rechazado y la función devuelve falso. Técnicamente, esta función actúa como un filtro, asegurando que las operaciones de negociación se ajusten a las preferencias específicas del usuario para cada día. Podemos usarlo para crear el filtro del día de negociación.
//--- Verify trading days filter before placing a trade if (isCheckTradingDaysFilter()){ //--- Retrieve the current ask price double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Retrieve the current bid price double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Set the open price to the ask price double openPrice = Ask; //--- Calculate the stop-loss price double stoploss = Bid - inpSLPts*_Point; //--- Calculate the take-profit price double takeprofit = Bid + (inpSLPts*inpR2R)*_Point; //--- Define the trade comment string comment = "BUY TRADE"; //--- Execute a buy trade obj_Trade.Buy(inpLots,_Symbol,openPrice,stoploss,takeprofit,comment); //--- Initialize the ticket variable ulong ticket = 0; //--- Retrieve the order result ticket ticket = obj_Trade.ResultOrder(); //--- Print success message if the trade is opened if (ticket > 0){ Print("SUCCESS. Opened the BUY position with ticket # ",ticket); } //--- Print error message if the trade fails to open else {Print("ERROR! Failed to open the BUY position.");} }
Aquí, comprobamos si se permite operar en el día actual llamando a la función "isCheckTradingDaysFilter". Si la función devuelve verdadero, procedemos a recopilar datos de mercado y a realizar una operación, asegurándonos de que la operación se ajuste a los filtros diarios definidos por el usuario. En primer lugar, recuperamos los precios actuales de mercado utilizando la función SymbolInfoDouble. Los parámetros SYMBOL_ASK y SYMBOL_BID se utilizan para obtener los precios de compra y venta actuales del símbolo de negociación activo (_Symbol). Estos valores se almacenan en las variables «Ask» y «Bid», respectivamente, y sirven de base para cálculos posteriores.
A continuación, calculamos los niveles de precios necesarios para la operación. El precio "Ask" se establece como el "openPrice", que representa el precio de entrada para una posición de compra. Calculamos el precio de stop-loss restando «inpSLPts» (los puntos de stop-loss de entrada) multiplicados por _Point del precio de «Bid». Del mismo modo, el precio de take-profit se determina sumando el producto de «inpSLPts», la relación riesgo-recompensa («inpR2R») y _Point al precio de «Bid». Estos cálculos definen los límites de riesgo y recompensa de la operación.
Luego definimos un comentario de operación ("BUY TRADE") para etiquetar la operación como referencia futura. A continuación, ejecutamos la operación de compra utilizando el método «obj_Trade.Buy», pasando como parámetros el tamaño del lote («inpLots»), el símbolo de negociación, el precio de entrada, el precio de stop-loss, el precio de take-profit y el comentario. Esta función envía la orden de compraventa al mercado. Tras la ejecución de la operación, inicializamos la variable "ticket" en 0 y le asignamos el ticket de orden devuelto por el método "obj_Trade.ResultOrder". Si el ticket es mayor que 0, indica que la operación se abrió correctamente y se imprime un mensaje de éxito con el número del ticket. Si el ticket permanece en 0, significa que la operación ha fallado y se muestra un mensaje de error. Para una posición de venta, seguimos el mismo procedimiento, pero con condiciones inversas. Su fragmento de código es el siguiente:
//--- Handle sell signal conditions else if (isMACrossOverSell){ if (isRSIConfirmSell){ //--- Print sell signal message Print("SELL SIGNAL"); //--- Verify trading days filter before placing a trade if (isCheckTradingDaysFilter()){ //--- Retrieve the current ask price double Ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK); //--- Retrieve the current bid price double Bid = SymbolInfoDouble(_Symbol,SYMBOL_BID); //--- Set the open price to the bid price double openPrice = Bid; //--- Calculate the stop-loss price double stoploss = Ask + inpSLPts*_Point; //--- Calculate the take-profit price double takeprofit = Ask - (inpSLPts*inpR2R)*_Point; //--- Define the trade comment string comment = "SELL TRADE"; //--- Execute a sell trade obj_Trade.Sell(inpLots,_Symbol,openPrice,stoploss,takeprofit,comment); //--- Initialize the ticket variable ulong ticket = 0; //--- Retrieve the order result ticket ticket = obj_Trade.ResultOrder(); //--- Print success message if the trade is opened if (ticket > 0){ Print("SUCCESS. Opened the SELL position with ticket # ",ticket); } //--- Print error message if the trade fails to open else {Print("ERROR! Failed to open the SELL position.");} } } }
Al ejecutar el programa, obtenemos el siguiente resultado.

En la imagen podemos ver que confirmamos las operaciones. Sin embargo, sería buena idea visualizar las señales en el gráfico para mayor claridad. Por lo tanto, necesitamos una función para dibujar flechas con anotaciones.
//+------------------------------------------------------------------+ //| Create signal text function | //+------------------------------------------------------------------+ void createSignalText(datetime time,double price,int arrowcode, int direction,color clr,double angle,string txt ){ //--- Generate a unique name for the signal object string objName = " "; StringConcatenate(objName, "Signal @ ",time," at Price ",DoubleToString(price,_Digits)); //--- Create the arrow object at the specified time and price if (ObjectCreate(0,objName,OBJ_ARROW,0,time,price)){ //--- Set arrow properties ObjectSetInteger(0,objName,OBJPROP_ARROWCODE,arrowcode); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); if (direction > 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_TOP); if (direction < 0) ObjectSetInteger(0,objName,OBJPROP_ANCHOR,ANCHOR_BOTTOM); } //--- Generate a unique name for the description text object string objNameDesc = objName+txt; //--- Create the text object at the specified time and price if (ObjectCreate(0,objNameDesc,OBJ_TEXT,0,time,price)){ //--- Set text properties ObjectSetInteger(0,objNameDesc,OBJPROP_COLOR,clr); ObjectSetDouble(0,objNameDesc,OBJPROP_ANGLE,angle); if (direction > 0){ ObjectSetInteger(0,objNameDesc,OBJPROP_ANCHOR,ANCHOR_LEFT); ObjectSetString(0,objNameDesc,OBJPROP_TEXT," "+txt); } if (direction < 0){ ObjectSetInteger(0,objNameDesc,OBJPROP_ANCHOR,ANCHOR_BOTTOM); ObjectSetString(0,objNameDesc,OBJPROP_TEXT," "+txt); } } }
Aquí definimos la función "createSignalText" para representar visualmente las señales de negociación en el gráfico con flechas y texto descriptivo. Esta función mejora la claridad del gráfico al marcar eventos significativos como señales de compra o venta. En primer lugar, generamos un nombre único para el objeto flecha utilizando la función StringConcatenate. El nombre incluye la palabra "Signal", el tiempo especificado y el precio de la señal. Esta nomenclatura única garantiza que no haya superposición con otros objetos en el gráfico.
A continuación, creamos un objeto flecha en el gráfico en el momento y precio especificados utilizando la función ObjectCreate. Si la creación se realiza correctamente, procedemos a personalizar sus propiedades. El parámetro «arrowcode» determina el tipo de flecha que se mostrará, mientras que el parámetro «clr» especifica el color de la flecha. Según la dirección de la señal, el punto de anclaje de la flecha se establece en la parte superior (ANCHOR_TOP) para las señales ascendentes o en la parte inferior (ANCHOR_BOTTOM) para las señales descendentes. Esto garantiza que la posición de la flecha se alinee correctamente con el contexto de la señal.
A continuación, creamos un objeto de texto descriptivo para acompañar a la flecha. Se genera un nombre único para el objeto de texto añadiendo la descripción «txt» al nombre de la flecha. El objeto de texto se coloca en las mismas coordenadas de tiempo y precio que la flecha. Las propiedades del objeto de texto se configuran para mejorar su apariencia y alineación. El parámetro «clr» define el color del texto, y el parámetro «angle» determina su rotación. Para las señales ascendentes, el ancla se alinea a la izquierda (ANCHOR_LEFT) y se añaden espacios al principio del texto para ajustar el espaciado. Del mismo modo, para las señales descendentes, el ancla se alinea con la parte inferior (ANCHOR_BOTTOM) con el mismo ajuste de espaciado.
Ahora podemos usar esta función para crear las flechas con las anotaciones correspondientes.
//--- FOR A BUY SIGNAL //--- Retrieve the time of the signal datetime textTime = iTime(_Symbol,_Period,1); //--- Retrieve the price of the signal double textPrice = iLow(_Symbol,_Period,1); //--- Create a visual signal on the chart for a buy createSignalText(textTime,textPrice,221,1,clrBlue,-90,"Buy Signal"); //... //--- FOR A SELL SIGNAL //--- Retrieve the time of the signal datetime textTime = iTime(_Symbol,_Period,1); //--- Retrieve the price of the signal double textPrice = iHigh(_Symbol,_Period,1); //--- Create a visual signal on the chart for a sell createSignalText(textTime,textPrice,222,-1,clrRed,90,"Sell Signal");
Aquí, creamos marcadores visuales en el gráfico para representar señales de compra y venta. Estos marcadores consisten en flechas y texto descriptivo que las acompaña para mejorar la claridad del gráfico y ayudar en la toma de decisiones.
Para una señal de compra:
- Recuperar la hora de la señal:
Utilizando la función iTime, obtenemos la fecha y hora de la penúltima barra completada (índice 1) en el gráfico para el símbolo y el intervalo de tiempo actuales (_Symbol y _Period). Esto garantiza que la señal corresponda a una barra confirmada.
- Recuperar el precio de la señal:
Utilizamos la función iLow para obtener el precio más bajo de la misma barra (1). Esta es la posición donde queremos colocar el marcador.
- Crear una señal visual:
La función "createSignalText" se llama con los valores recuperados de "textTime" y "textPrice", junto con parámetros adicionales:
- "221": El código de flecha para un tipo de flecha específico que representa una señal de compra.
- "1": Dirección de la señal, que indica movimiento ascendente.
- "clrBlue": Color de la flecha y del texto, que representa una señal positiva.
- "-90": Ángulo del texto para una correcta alineación.
- "Buy Signal": El texto descriptivo se muestra cerca de la flecha. Esto marca visualmente la señal de compra en el gráfico.
Para una señal de venta:
- Recuperar la hora de la señal:
Al igual que con la señal de compra, utilizamos iTime para obtener la fecha y hora de la barra en el índice 1.
- Recuperar el precio de la señal:
La función iHigh se utiliza para obtener el precio más alto de la misma barra. Esto representa la posición de colocación del marcador de señal de venta.
- Crear una señal visual:
La función «createSignalText» se invoca con:
- "222": El código de flecha que representa una señal de venta.
- "-1": Dirección de la señal, que indica movimiento hacia abajo.
- "clrRed": Color de la flecha y del texto, que indica una señal negativa.
- "90": Ángulo de texto para alineación.
- "Sell Signal": El texto descriptivo que se muestra cerca de la flecha. Esto añade un marcador claro para la señal de venta en el gráfico.
Al ejecutar el programa, obtenemos el siguiente resultado.

En la imagen podemos ver que, una vez que tenemos una señal confirmada, aparece la flecha y su anotación correspondiente en el gráfico para mayor claridad. Esto añade un toque profesional al gráfico, facilitando la interpretación de las señales de negociación a través de claras indicaciones visuales. Ahora podemos añadir una función de trailing stop al código para asegurar beneficios una vez alcanzados ciertos niveles predefinidos. Para simplificar, utilizaremos una función.
//+------------------------------------------------------------------+ //| Trailing stop function | //+------------------------------------------------------------------+ void applyTrailingStop(int slpoints, CTrade &trade_object,ulong magicno=0,int minProfitPts=0){ //--- Calculate the stop loss price for buy positions double buySl = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID) - slpoints*_Point,_Digits); //--- Calculate the stop loss price for sell positions double sellSl = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK) + slpoints*_Point,_Digits); //--- Loop through all positions in the account for (int i=PositionsTotal()-1; i>=0; i--){ //--- Get the ticket of the position ulong ticket = PositionGetTicket(i); //--- Ensure the ticket is valid if (ticket > 0){ //--- Select the position by ticket if (PositionSelectByTicket(ticket)){ //--- Check if the position matches the symbol and magic number (if provided) if (PositionGetSymbol(POSITION_SYMBOL) == _Symbol && (magicno == 0 || PositionGetInteger(POSITION_MAGIC) == magicno) ){ //--- Retrieve the open price and current stop loss of the position double positionOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); double positionSl = PositionGetDouble(POSITION_SL); //--- Handle trailing stop for buy positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ //--- Calculate the minimum profit price for the trailing stop double minProfitPrice = NormalizeDouble((positionOpenPrice+minProfitPts*_Point),_Digits); //--- Apply trailing stop only if conditions are met if (buySl > minProfitPrice && buySl > positionOpenPrice && (positionSl == 0 || buySl > positionSl) ){ //--- Modify the position's stop loss trade_object.PositionModify(ticket,buySl,PositionGetDouble(POSITION_TP)); } } //--- Handle trailing stop for sell positions else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ //--- Calculate the minimum profit price for the trailing stop double minProfitPrice = NormalizeDouble((positionOpenPrice-minProfitPts*_Point),_Digits); //--- Apply trailing stop only if conditions are met if (sellSl < minProfitPrice && sellSl < positionOpenPrice && (positionSl == 0 || sellSl < positionSl) ){ //--- Modify the position's stop loss trade_object.PositionModify(ticket,sellSl,PositionGetDouble(POSITION_TP)); } } } } } } }
Aquí, implementamos un mecanismo de trailing stop en la función "applyTrailingStop" para ajustar dinámicamente los niveles de stop-loss para las posiciones de negociación activas. Esto garantiza que, a medida que el mercado evoluciona favorablemente, obtengamos beneficios minimizando los riesgos. La función opera utilizando la siguiente lógica. Primero, calculamos los niveles de stop-loss tanto para las posiciones de compra como para las de venta. Utilizando la función SymbolInfoDouble, recuperamos el precio SYMBOL_BID para determinar el nivel «buySl», restando los «slpoints» especificados (distancia de stop-loss en puntos) y normalizándolo al número correcto de decimales utilizando la función NormalizeDouble. Del mismo modo, calculamos el nivel «sellSl» sumando los «slpoints» al precio SYMBOL_ASK y normalizándolo.
A continuación, iteramos a través de todas las posiciones activas en la cuenta de trading utilizando un bucle for inverso («for (int i=PositionsTotal()-1; i>=0; i--)»). Para cada posición, recuperamos su «ticket» utilizando la función PositionGetTicket. Si el «ticket» es válido, seleccionamos la posición correspondiente utilizando la función PositionSelectByTicket. Dentro del bucle, comprobamos si la posición coincide con el «símbolo» actual y el «magicno» (número mágico) proporcionado. Si «magicno» es 0, incluimos todas las posiciones independientemente de su número mágico. Para las posiciones elegibles, recuperamos su POSITION_PRICE_OPEN (precio de apertura) y POSITION_SL (nivel actual de stop-loss).
Para las posiciones de compra, calculamos el «minProfitPrice» sumando el «minProfitPts» (beneficio mínimo en puntos) al precio de apertura y normalizándolo. Solo aplicamos el trailing stop si el nivel «buySl» cumple todas las condiciones:
- «buySl» supera el «minProfitPrice».
- «buySl» es superior al precio de apertura.
- «buySl» es mayor que el stop-loss actual o no hay ningún stop-loss establecido («positionSl == 0»).
Si se cumplen estas condiciones, modificamos el stop-loss de la posición utilizando el método «PositionModify» del objeto «CTrade». Para las posiciones de venta, calculamos el «minProfitPrice» restando el «minProfitPts» del precio de apertura y normalizándolo. Del mismo modo, aplicamos el trailing stop si el nivel «sellSl» cumple las siguientes condiciones:
- «sellSl» está por debajo de «minProfitPrice».
- «sellSl» es inferior al precio de apertura.
- «sellSl» es inferior al stop-loss actual o no hay ningún stop-loss establecido.
Si se cumplen estas condiciones, también modificamos el stop-loss de la posición utilizando el método «PositionModify». A continuación, podemos llamar a estas funciones en cada tick para aplicar la lógica de trailing stop a las posiciones abiertas de la siguiente manera.
//--- Apply trailing stop if allowed in the input parameters if (inpisAllowTrailingStop){ applyTrailingStop(inpTrailPts,obj_Trade,inpMagicNo,inpMinTrailPts); }
Aquí llamamos a la función trailing stop y, al ejecutar el programa, obtenemos el siguiente resultado.

En la imagen podemos ver que el objetivo del trailing stop se ha alcanzado con éxito. Ahora necesitamos visualizar los datos en el gráfico. Para ello, necesitaremos un panel de control con una base principal y etiquetas. Para la base, necesitaremos una etiqueta rectangular. Aquí está la implementación de la función.
//+------------------------------------------------------------------+ //| Create Rectangle label function | //+------------------------------------------------------------------+ bool createRecLabel(string objNAME,int xD,int yD,int xS,int yS, color clrBg,int widthBorder,color clrBorder = clrNONE, ENUM_BORDER_TYPE borderType = BORDER_FLAT,ENUM_LINE_STYLE borderStyle = STYLE_SOLID ){ //--- Reset the last error code ResetLastError(); //--- Attempt to create the rectangle label object if (!ObjectCreate(0,objNAME,OBJ_RECTANGLE_LABEL,0,0,0)){ //--- Log the error if creation fails Print(__FUNCTION__,": Failed to create the REC LABEL. Error Code = ",_LastError); return (false); } //--- Set rectangle label properties ObjectSetInteger(0, objNAME,OBJPROP_XDISTANCE, xD); ObjectSetInteger(0, objNAME,OBJPROP_YDISTANCE, yD); ObjectSetInteger(0, objNAME,OBJPROP_XSIZE, xS); ObjectSetInteger(0, objNAME,OBJPROP_YSIZE, yS); ObjectSetInteger(0, objNAME,OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, objNAME,OBJPROP_BGCOLOR, clrBg); ObjectSetInteger(0, objNAME,OBJPROP_BORDER_TYPE, borderType); ObjectSetInteger(0, objNAME,OBJPROP_STYLE, borderStyle); ObjectSetInteger(0, objNAME,OBJPROP_WIDTH, widthBorder); ObjectSetInteger(0, objNAME,OBJPROP_COLOR, clrBorder); ObjectSetInteger(0, objNAME,OBJPROP_BACK, false); ObjectSetInteger(0, objNAME,OBJPROP_STATE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTABLE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTED, false); //--- Redraw the chart to reflect changes ChartRedraw(0); return (true); }
En la función booleana "createRecLabel" que definimos, creamos una etiqueta rectangular personalizable en el gráfico siguiendo una serie de pasos. En primer lugar, restablecemos cualquier código de error anterior utilizando la función ResetLastError. A continuación, intentamos crear el objeto de etiqueta rectangular utilizando la función ObjectCreate. Si esta creación falla, imprimimos un mensaje de error con el motivo del fallo y devolvemos "false". Si la creación se realiza correctamente, procedemos a establecer varias propiedades para la etiqueta del rectángulo utilizando la función ObjectSetInteger.
Estas propiedades nos permiten definir la posición, el tamaño, el color de fondo, el estilo del borde y otros aspectos visuales del rectángulo. Asignamos los parámetros «xD», «yD», «xS» y «yS» para determinar la posición y el tamaño de la etiqueta rectangular, utilizando OBJPROP_XDISTANCE, «OBJPROP_YDISTANCE», «OBJPROP_XSIZE» y «OBJPROP_YSIZE». Además, establecemos el color de fondo, el tipo de borde y el estilo de borde a través de OBJPROP_BGCOLOR, «OBJPROP_BORDER_TYPE» y «OBJPROP_STYLE», respectivamente.
Por último, para garantizar que la representación visual de la etiqueta se actualice, llamamos a la función ChartRedraw para actualizar el gráfico. Si la etiqueta rectangular se crea correctamente y todas las propiedades se configuran correctamente, la función devuelve «true». De esta manera, podemos anotar visualmente el gráfico con etiquetas rectangulares personalizadas basadas en los parámetros proporcionados. Hacemos lo mismo para una función de etiqueta.
//+------------------------------------------------------------------+ //| Create label function | //+------------------------------------------------------------------+ bool createLabel(string objNAME,int xD,int yD,string txt, color clrTxt = clrBlack,int fontSize = 12, string font = "Arial Rounded MT Bold" ){ //--- Reset the last error code ResetLastError(); //--- Attempt to create the label object if (!ObjectCreate(0,objNAME,OBJ_LABEL,0,0,0)){ //--- Log the error if creation fails Print(__FUNCTION__,": Failed to create the LABEL. Error Code = ",_LastError); return (false); } //--- Set label properties ObjectSetInteger(0, objNAME,OBJPROP_XDISTANCE, xD); ObjectSetInteger(0, objNAME,OBJPROP_YDISTANCE, yD); ObjectSetInteger(0, objNAME,OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetString(0, objNAME,OBJPROP_TEXT, txt); ObjectSetInteger(0, objNAME,OBJPROP_COLOR, clrTxt); ObjectSetString(0, objNAME,OBJPROP_FONT, font); ObjectSetInteger(0, objNAME,OBJPROP_FONTSIZE, fontSize); ObjectSetInteger(0, objNAME,OBJPROP_BACK, false); ObjectSetInteger(0, objNAME,OBJPROP_STATE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTABLE, false); ObjectSetInteger(0, objNAME,OBJPROP_SELECTED, false); //--- Redraw the chart to reflect changes ChartRedraw(0); return (true); }
Armados con estas funciones, ahora podemos crear una función para gestionar la creación del panel de control cuando sea necesario.
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void createDashboard(){ //--- }
Aquí creamos la función vacía llamada "createDashboard", y podemos usarla para alojar la lógica para la creación del panel de control. Para realizar un seguimiento eficaz de los cambios, podemos llamar a la función en el controlador de eventos OnInit antes de definir su cuerpo, como se muestra a continuación.
//--- createDashboard(); //---
Después de llamar a la función, podemos definir su cuerpo. Lo primero que hacemos es definir el cuerpo del panel de control, y tendremos que definir su nombre como una constante global.
//+------------------------------------------------------------------+ //| Global constants for dashboard object names | //+------------------------------------------------------------------+ const string DASH_MAIN = "MAIN";
Aquí definimos una cadena constante, const, lo que significa que no se modificará a lo largo del programa. Ahora utilizamos la constante para la creación de la etiqueta de la siguiente manera.
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void createDashboard(){ //--- Create the main dashboard rectangle createRecLabel(DASH_MAIN,10,50+30,200,120,clrBlack,2,clrBlue,BORDER_FLAT,STYLE_SOLID); //--- }
En la función «createDashboard», iniciamos el proceso de creación de un panel visual en el gráfico. Para lograrlo, llamamos a la función «createRecLabel», que se encarga de dibujar un rectángulo en el gráfico que servirá como base del panel de control. La función cuenta con parámetros específicos para definir la apariencia y la posición de este rectángulo. En primer lugar, especificamos el nombre del rectángulo como «DASH_MAIN», lo que nos permitirá identificar este objeto más adelante. A continuación, definimos la posición del rectángulo estableciendo su esquina superior izquierda en las coordenadas (10, 50+30) del gráfico, utilizando los parámetros «xD» e «yD». El ancho y el alto del rectángulo se establecen en 200 y 120 píxeles, respectivamente, mediante los parámetros «xS» e «yS», pero se pueden ajustar posteriormente.
A continuación, definimos la apariencia visual del rectángulo. El color de fondo del rectángulo está configurado en "clrBlack", y elegimos un color azul ("clrBlue") para el borde. El borde tiene un ancho de 2 píxeles y un estilo de línea continua (STYLE_SOLID), y el tipo de borde está establecido en plano (BORDER_FLAT). Estos ajustes garantizan que el rectángulo tenga una apariencia clara y definida. Este rectángulo sirve como elemento fundamental del panel de control, y se le pueden agregar elementos adicionales, como texto o componentes interactivos, en pasos posteriores. Sin embargo, ejecutemos el hito actual y obtengamos el resultado.

Como se puede apreciar en la imagen, la base del panel de control es tal y como la habíamos previsto. Luego podemos crear los demás elementos del panel de control utilizando la función de etiqueta y siguiendo el mismo procedimiento. Así pues, definimos el resto de los objetos de la siguiente manera.
//+------------------------------------------------------------------+ //| Global constants for dashboard object names | //+------------------------------------------------------------------+ const string DASH_MAIN = "MAIN"; const string DASH_HEAD = "HEAD"; const string DASH_ICON1 = "ICON 1"; const string DASH_ICON2 = "ICON 2"; const string DASH_NAME = "NAME"; const string DASH_OS = "OS"; const string DASH_COMPANY = "COMPANY"; const string DASH_PERIOD = "PERIOD"; const string DASH_POSITIONS = "POSITIONS"; const string DASH_PROFIT = "PROFIT";
Aquí, simplemente definimos el resto de los objetos. Nuevamente, utilizamos la función label para crear la etiqueta del encabezado como se muestra a continuación.
//+------------------------------------------------------------------+ //| Create dashboard function | //+------------------------------------------------------------------+ void createDashboard(){ //--- Create the main dashboard rectangle createRecLabel(DASH_MAIN,10,50+30,200,120+30,clrBlack,2,clrBlue,BORDER_FLAT,STYLE_SOLID); //--- Add icons and text labels to the dashboard createLabel(DASH_ICON1,13,53+30,CharToString(40),clrRed,17,"Wingdings"); createLabel(DASH_ICON2,180,53+30,"@",clrWhite,17,"Webdings"); createLabel(DASH_HEAD,65,53+30,"Dashboard",clrWhite,14,"Impact"); }
Aquí, mejoramos el panel de control añadiendo iconos y un encabezado mediante la función "createLabel". Esta función se llama varias veces para colocar elementos de texto en posiciones específicas del gráfico, lo que nos permite crear una interfaz visualmente atractiva e informativa. Primero, creamos un icono etiquetado como "DASH_ICON1", que se coloca en las coordenadas (13, 53+30) relativas al gráfico. El icono está representado por el código de carácter 40, convertido a una cadena utilizando la función "CharToString(40)". Este icono se muestra en rojo ("clrRed") con un tamaño de fuente de 17 y el estilo de fuente está configurado en "Wingdings" para representar el carácter como un símbolo gráfico.
A continuación, agregamos otro icono etiquetado como "DASH_ICON2", ubicado en las coordenadas (180, 53+30). Este icono utiliza el carácter "@", mostrado en blanco ("clrWhite") con un tamaño de fuente de 17. El estilo de fuente es "Webdings", lo que garantiza que el carácter "@" aparezca de forma decorativa y estilizada. Aquí está la representación.

Finalmente, incluimos un encabezado de texto etiquetado como "DASH_HEAD" en la posición (65, 53+30). El encabezado muestra el texto "Dashboard" en blanco ("clrWhite") con un tamaño de fuente de 14. El estilo de fuente está configurado en "Impact", lo que le da al título una apariencia llamativa y distintiva. A continuación, podemos definir el resto de las etiquetas.
createLabel(DASH_NAME,20,90+30,"EA Name: Crossover RSI Suite",clrWhite,10,"Calibri"); createLabel(DASH_COMPANY,20,90+30+15,"LTD: "+AccountInfoString(ACCOUNT_COMPANY),clrWhite,10,"Calibri"); createLabel(DASH_OS,20,90+30+15+15,"OS: "+TerminalInfoString(TERMINAL_OS_VERSION),clrWhite,10,"Calibri"); createLabel(DASH_PERIOD,20,90+30+15+15+15,"Period: "+EnumToString(Period()),clrWhite,10,"Calibri"); createLabel(DASH_POSITIONS,20,90+30+15+15+15+30,"Positions: "+IntegerToString(PositionsTotal()),clrWhite,10,"Calibri"); createLabel(DASH_PROFIT,20,90+30+15+15+15+30+15,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY),clrWhite,10,"Calibri");
Aquí, rellenamos el panel de control con etiquetas informativas importantes utilizando la función "createLabel". Primero, creamos una etiqueta "DASH_NAME", ubicada en (20, 90+30). Esta etiqueta muestra el texto "EA Name: Crossover RSI Suite" en blanco ("clrWhite") con un tamaño de fuente de 10 y el estilo de fuente es "Calibri". Esta etiqueta sirve como nombre del Asesor Experto, lo que proporciona al usuario una identificación clara.
A continuación, agregamos la etiqueta "DASH_COMPANY" en (20, 90+30+15). Muestra el texto «LTD: », seguido de la información de la empresa de la cuenta, que se recupera mediante la función AccountInfoString con el parámetro ACCOUNT_COMPANY. La etiqueta está diseñada en blanco con un tamaño de fuente de 10 y utiliza la fuente «Calibri». A continuación, la etiqueta «DASH_OS» se coloca en (20, 90+30+15+15). Muestra la versión del sistema operativo, con el texto «OS: », combinado con el resultado de la función TerminalInfoString con el parámetro TERMINAL_OS_VERSION. Esta etiqueta ayuda al usuario a conocer el sistema operativo del terminal, también en blanco con un tamaño de fuente de 10 y la fuente «Calibri».
A continuación, incluimos la etiqueta «DASH_PERIOD» en (20, 90+30+15+15+15). Esta etiqueta muestra el intervalo de tiempo actual del gráfico con el texto «Periodo: », seguido del resultado de la función EnumToString con el periodo. El texto blanco, el tamaño pequeño de la fuente y el tipo de letra «Calibri» mantienen la coherencia con el diseño general del panel de control. Además, añadimos la etiqueta «DASH_POSITIONS» en (20, 90+30+15+15+15+30). Esta etiqueta muestra el número total de posiciones abiertas actualmente en la cuenta, con el texto «Posiciones: », seguido del total de posiciones. Esta información es crucial para realizar un seguimiento de las operaciones activas.
Por último, la etiqueta «DASH_PROFIT» se coloca en (20, 90+30+15+15+15+30+15). Muestra las ganancias actuales de la cuenta con el texto «Ganancias: », seguido del resultado de la función de ganancias de la cuenta, que representa las ganancias con dos decimales, junto con la divisa de la cuenta recuperada mediante la función AccountInfoString.
Por último, debemos eliminar el panel de control una vez que se haya desinstalado el programa. Por lo tanto, necesitamos una función para eliminar el panel de control.
//+------------------------------------------------------------------+ //| Delete dashboard function | //+------------------------------------------------------------------+ void deleteDashboard(){ //--- Delete all objects related to the dashboard ObjectDelete(0,DASH_MAIN); ObjectDelete(0,DASH_ICON1); ObjectDelete(0,DASH_ICON2); ObjectDelete(0,DASH_HEAD); ObjectDelete(0,DASH_NAME); ObjectDelete(0,DASH_COMPANY); ObjectDelete(0,DASH_OS); ObjectDelete(0,DASH_PERIOD); ObjectDelete(0,DASH_POSITIONS); ObjectDelete(0,DASH_PROFIT); //--- Redraw the chart to reflect changes ChartRedraw(); }
Aquí, creamos una función vacía «deleteDashboard», llamamos a la función ObjectDelete con todos los nombres de los objetos y, por último, volvemos a dibujar el gráfico utilizando la función ChartRedraw para que los cambios surtan efecto. A continuación, llamamos a esta función en la función de desinicialización. Una vez más, debemos actualizar el panel de control cada vez que tengamos posiciones para mostrar las posiciones y los beneficios correctos. Esta es la lógica que empleamos.
if (PositionsTotal() > 0){ ObjectSetString(0,DASH_POSITIONS,OBJPROP_TEXT,"Positions: "+IntegerToString(PositionsTotal())); ObjectSetString(0,DASH_PROFIT,OBJPROP_TEXT,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); }
Aquí, comprobamos que si las posiciones están por encima de 0, tenemos una posición y podemos actualizar sus propiedades. Aquí está el resultado.

A partir de la visualización, podemos ver que el panel de control no se actualiza una vez que se cierran las posiciones. Por lo tanto, necesitaremos realizar un seguimiento de cuándo se cierran las posiciones y, cuando no haya posiciones, estableceremos los valores predeterminados del panel de control. Para ello, necesitaremos el controlador de eventos OnTradeTransaction.
//+------------------------------------------------------------------+ //| OnTradeTransaction function | //+------------------------------------------------------------------+ void OnTradeTransaction( const MqlTradeTransaction& trans, // trade transaction structure const MqlTradeRequest& request, // request structure const MqlTradeResult& result // response structure ){ if (trans.type == TRADE_TRANSACTION_DEAL_ADD){ Print("A deal was added. Make updates."); if (PositionsTotal() <= 0){ ObjectSetString(0,DASH_POSITIONS,OBJPROP_TEXT,"Positions: "+IntegerToString(PositionsTotal())); ObjectSetString(0,DASH_PROFIT,OBJPROP_TEXT,"Profit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); } } }
Aquí, configuramos la función OnTradeTransaction, que se activa cada vez que se produce una transacción relacionada con el comercio. Esta función procesa eventos comerciales y actualiza la información relevante en el panel de control en respuesta a acciones específicas. Comenzamos comprobando si el «type» de la transacción comercial, proporcionado por el parámetro «trans» de tipo MqlTradeTransaction, es igual a TRADE_TRANSACTION_DEAL_ADD. Esta condición determina si se ha añadido una nueva operación a la cuenta. Cuando se detecta una transacción de este tipo, imprimimos el mensaje "A deal was added. Make updates." al registro con fines de depuración o informativos.
A continuación, comprobamos si el número total de posiciones abiertas, obtenido mediante la función PositionsTotal, es menor o igual a 0. Esto garantiza que las actualizaciones del panel de control se realicen solo cuando no queden posiciones activas en la cuenta. Si se cumple la condición, utilizamos la función ObjectSetString para actualizar dos etiquetas en el panel de control. Aquí está el resultado.

En la imagen podemos ver que las actualizaciones se aplican a todas las operaciones realizadas, con lo que se alcanza nuestro objetivo. Ahora solo queda realizar una prueba retrospectiva del programa y analizar su rendimiento. Esto se aborda en la siguiente sección.
Pruebas retrospectivas
Tras realizar exhaustivas pruebas retrospectivas, hemos obtenido los siguientes resultados.
Gráfico de las pruebas retrospectivas:

Informe de pruebas retrospectivas:

Aquí también se incluye un formato de vídeo que muestra la prueba retrospectiva completa de la estrategia durante un período de 1 año, hasta 2024.
Conclusión
En conclusión, hemos mostrado cómo desarrollar un Asesor Experto (EA) MQL5 robusto que integra indicadores técnicos, gestión automatizada de operaciones y un panel interactivo. Al combinar herramientas como los cruces de media móvil, el índice de fuerza relativa (RSI) y los trailing stops con características como actualizaciones dinámicas de operaciones, trailing stops y una interfaz fácil de usar, hemos creado un EA capaz de generar señales, gestionar operaciones y proporcionar información en tiempo real para una toma de decisiones eficaz.
Descargo de responsabilidad: Este artículo tiene fines educativos únicamente. Operar en los mercados financieros implica riesgos financieros significativos, y las condiciones del mercado pueden ser impredecibles. Si bien las estrategias analizadas proporcionan un marco estructurado, el desempeño pasado no garantiza resultados futuros. Las pruebas exhaustivas y una gestión de riesgos adecuada son esenciales antes de la puesta en marcha en producción.
Al aplicar estos conceptos, podrá construir sistemas de negociación más adaptativos y mejorar sus estrategias de negociación algorítmica. ¡Feliz programación y exitosas operaciones!
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17040
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.
Características del Wizard MQL5 que debe conocer (Parte 53): Market Facilitation Index (MFI)
Aprendizaje automático y Data Science (Parte 33): Pandas Dataframe en MQL5, recopilación de datos para facilitar el uso de ML
Predicción de tendencias con LSTM para estrategias de seguimiento de tendencias
Desarrollo de asesores expertos autooptimizables en MQL5 (Parte 5): Reglas de negociación autoadaptativas
- 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
Hola. En el vídeo se muestra todo, desde la compilación, el período de prueba y los parámetros de entrada utilizados.