English Русский 中文 Deutsch 日本語 Português
preview
Creación de un asesor experto MQL5 basado en la estrategia de ruptura del rango diario (Daily Range Breakout)

Creación de un asesor experto MQL5 basado en la estrategia de ruptura del rango diario (Daily Range Breakout)

MetaTrader 5Trading |
490 3
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En este artículo, exploraremos cómo crear un Asesor Experto (Expert Advisor, EA) en MetaQuotes Language 5 (MQL5) basado en la estrategia de ruptura del rango diario (Daily Range Breakout). Dado que los operadores buscan continuamente soluciones de trading automatizadas eficaces, la estrategia Daily Range Breakout ofrece un enfoque sistemático que aprovecha los movimientos de precios más allá de un rango definido, lo que la convierte en una opción atractiva para los operadores de Forex en MetaTrader 5.

Comenzaremos describiendo los principios fundamentales de la estrategia Daily Range Breakout, proporcionando una base sólida para su implementación en el trading automatizado. A continuación, profundizaremos en los detalles específicos para identificar las condiciones de ruptura y establecer los puntos de entrada y salida. A continuación, le guiaremos a través del proceso de codificación en MQL5, destacando las funciones y la lógica esenciales que impulsan la estrategia. Además, analizaremos la importancia de realizar pruebas retrospectivas y optimizar el programa para garantizar su eficacia en condiciones de negociación. Los temas que trataremos en este artículo incluyen:

  1. Comprender la estrategia de ruptura del rango diario
  2. El plano del asesor experto
  3. Implementación de la ruptura del rango diario en MQL5
  4. Pruebas retrospectivas y optimización
  5. Conclusión

Al final de este artículo, dispondrá de los conocimientos necesarios para desarrollar un Asesor Experto MQL5 que utilice eficazmente la estrategia Daily Range Breakout, mejorando así su enfoque de trading. Empecemos.


Comprender la estrategia de ruptura del rango diario

La estrategia Daily Range Breakout es un método de trading muy conocido entre los operadores de Forex. Les permite aprovechar los grandes movimientos de precios que se producen después de que el mercado haya formado un rango diario. La estrategia utiliza la evolución de los precios del mercado para determinar dónde se encuentran los niveles importantes de soporte y resistencia. Una vez que estos operadores saben dónde se encuentran estos niveles, operan con las rupturas de los mismos, buscando los probables movimientos importantes que suelen producirse después de que el mercado rompa uno de estos niveles.

Esta estrategia se centra en el rango diario, que se establece como la diferencia entre los precios más altos y más bajos de un par de divisas dentro de un día de negociación. Los puntos de ruptura se deducen del rango del día de negociación anterior. Una ruptura se produce cuando el precio se mueve por encima de un nivel de resistencia establecido o por debajo de un nivel de soporte. En retrospectiva, los precios del último día parecen establecer niveles muy bien definidos que pueden utilizarse como posibles puntos de ruptura. Cuando el precio supera el nivel de resistencia, se toma una posición larga. Cuando el precio rompe a la baja el nivel de soporte, se toma una posición corta. Aquí hay un ejemplo de lo que queremos decir.

ILUSTRACIÓN DEL RANGO DIARIO

Para obtener la máxima eficacia, esta estrategia se emplea en gráficos de 1 hora o de 4 horas. Cuando los operadores utilizan esta plantilla en estos marcos temporales, a menudo pueden capturar los movimientos de precios más grandes y significativos. Esto se debe a que la estrategia está prácticamente libre del ruido presente en los marcos temporales inferiores. La estrategia de ruptura suele utilizar la evolución de los precios durante la sesión asiática para determinar el rango diario, antes de ejecutar operaciones durante las sesiones de Londres y Nueva York. Las estrategias de ruptura suelen tener el problema de dar señales falsas, y la ruptura del rango diario no es una excepción. Por lo tanto, al igual que con cualquier estrategia de trading, es fundamental gestionar el riesgo al utilizar el Daily Range Breakout. Coloque sus órdenes stop-loss justo por debajo del último mínimo para las posiciones largas y por encima del último máximo para las operaciones cortas, a fin de mantener un riesgo razonable. Esta será nuestra estrategia. Gestiona el riesgo mediante una orden stop-loss situada por encima o por debajo del último máximo o mínimo, según sea el caso. Aquí hay otra ilustración de la lógica del stop-loss.

ENTRADA DE STOP LOSS

La estrategia de ruptura del rango diario es beneficiosa en varios aspectos. En primer lugar, su simplicidad lo convierte en una opción adecuada tanto para traders principiantes como experimentados. En segundo lugar, utiliza niveles definidos, lo que evita que los operadores tomen demasiadas decisiones discrecionales. La forma en que este método de negociación desglosa el mercado permite obtener una imagen clara antes y después de cada sesión diaria. Por la mañana, la actividad bursátil del mercado puede delimitarse en un «rango» específico. Luego, tras el cierre de la sesión matinal, «romper» la línea superior o inferior del rango se convierte en una posible señal para entrar en una operación al día siguiente. En la siguiente sección definiremos con mayor precisión nuestros parámetros comerciales, proporcionando un plan claro con todos los detalles específicos.


El plano del asesor experto

Ruptura del rango superior: Condición de compra

Cuando el precio supera el rango superior establecido el día anterior, significa una ruptura alcista y sugiere que el mercado puede seguir subiendo. Esta ruptura indica un fuerte interés comprador y el potencial para un mayor movimiento alcista. Abrimos una posición de compra cuando el precio de cierre de la barra actual está por encima del nivel superior del rango, con el objetivo de obtener beneficios del impulso que suele seguir a tales rupturas.

ROMPIMIENTO DE LA BANDA SUPERIOR

Ruptura del rango inferior: Condición de venta

Por el contrario, cuando el precio rompe por debajo del rango inferior establecido el día anterior, significa una ruptura bajista y sugiere que el mercado puede seguir bajando. Esta ruptura indica una fuerte presión vendedora y la posibilidad de que se produzca un nuevo movimiento bajista. Abrimos una posición de venta cuando el precio de cierre de la barra actual está por debajo del nivel inferior del rango, anticipando que la debilidad del precio continuará tras la ruptura.

ROMPIMIENTO DE LA BANDA INFERIOR

Estas representaciones visuales del plan estratégico serán útiles cuando implementemos estas condiciones comerciales en MQL5, ya que servirán como referencia para codificar reglas precisas de entrada y salida.


Implementación de la ruptura del rango diario en MQL5

Después de aprender todas las teorías sobre la estrategia de trading Daily Range Breakout, 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. Como alternativa, puede hacer clic en el icono IDE (Entorno de desarrollo integrado) de la barra de herramientas. Esto abrirá MetaEditor, que permite escribir robots de trading, indicadores técnicos, scripts y bibliotecas de funciones.

METAEDITOR ABIERTO

Una vez abierto el MetaEditor, en la barra de herramientas, vaya a la pestaña Archivo y marque Nuevo archivo, o simplemente presione 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.

CREAR NUEVO EA

En el asistente que aparece, marque Asesor experto (plantilla) y haga clic en Siguiente.

Asistente MQL

En las propiedades generales del Asesor Experto, en la sección de nombre, proporcione el nombre de archivo de su experto. Tenga en cuenta que para especificar o crear una carpeta si no existe, utilice 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 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.

NUEVO NOMBRE DE EA

Después de proporcionar el nombre de archivo del Asesor Experto deseado, haga clic en Siguiente, luego en Siguiente y, por último, 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".

//+------------------------------------------------------------------+
//|                          Daily Range Breakout Expert Advisor.mq5 |
//|      Copyright 2024, ALLAN MUNENE MUTIIRIA. #@Forex Algo-Trader. |
//|                                     https://forexalg0-trader.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, ALLAN MUNENE MUTIIRIA. #@Forex Algo-Trader"
#property link      "https://forexalg0-trader.com"
#property description "Daily Range Breakout Expert Advisor"
#property version   "1.00"

Al cargar el programa se realiza una información como la que se muestra a continuación.

INFORMACIÓN DE METADATOS

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 de operación. 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 permitirá acceder fácilmente a los métodos contenidos en esa clase, gracias a los desarrolladores de MQL5.

CLASE CTRADE

Después, debemos declarar varias variables importantes para almacenar y realizar un seguimiento de los datos de ruptura del rango.

double maximum_price = -DBL_MAX;      //--- Initialize the maximum price with the smallest possible value
double minimum_price = DBL_MAX;       //--- Initialize the minimum price with the largest possible value
datetime maximum_time, minimum_time;  //--- Declare variables to store the time of the highest and lowest prices
bool isHaveDailyRange_Prices = false; //--- Boolean flag to check if daily range prices are extracted
bool isHaveRangeBreak = false;        //--- Boolean flag to check if a range breakout has occurred

Aquí declaramos varias variables importantes para realizar un seguimiento de los datos clave de precios y gestionar las rupturas de rango en la lógica de negociación. En primer lugar, inicializamos dos variables double, «maximum_price» y «minimum_price», que almacenarán los precios más altos y más bajos encontrados durante un período específico. El «maximum_price» se establece en -DBL_MAX, el valor doble más pequeño posible, lo que garantiza que cualquier precio encontrado será más alto y sustituirá a este valor inicial. Del mismo modo, establecemos «minimum_price» en DBL_MAX, el mayor valor doble posible, lo que garantiza que cualquier precio inferior lo sustituirá como mínimo.

También declaramos dos variables datetime, «maximum_time» y «minimum_time», para almacenar las horas exactas en las que se producen los precios máximo y mínimo. Esto nos ayudará más adelante si necesitamos consultar los momentos específicos en los que se alcanzaron estos niveles de precios.

Además, se declaran dos variables bool para gestionar la lógica relacionada con los rangos de precios y las rupturas. El primero, «isHaveDailyRange_Prices», se inicializa como falso y sirve como indicador para señalar si se han determinado correctamente los precios del rango diario (es decir, el máximo y el mínimo). El segundo, «isHaveRangeBreak», también inicializado en falso, actúa como un indicador para señalar si se ha producido una ruptura, lo que significa que el precio se ha movido fuera del rango diario. Además, presentaremos visualmente los rangos en el gráfico. Por lo tanto, necesitaremos nombres para los rangos y también podemos declararlos aquí.

#define RECTANGLE_PREFIX "RANGE RECTANGLE " //--- Prefix for naming range rectangles
#define UPPER_LINE_PREFIX "UPPER LINE "     //--- Prefix for naming upper range line
#define LOWER_LINE_PREFIX "LOWER LINE "     //--- Prefix for naming lower range line

Aquí definimos tres directivas del preprocesador que crean prefijos para nombrar diversos objetos gráficos asociados con un rango de negociación. Utilizamos la directiva #define RECTANGLE_PREFIX «RANGE RECTANGLE» para establecer una convención de nomenclatura coherente para los rectángulos que representan el rango de negociación, lo que facilita la identificación y gestión de esos objetos dentro del gráfico. Del mismo modo, #define UPPER_LINE_PREFIX «UPPER LINE » crea un prefijo específico para la línea límite superior del rango, mientras que LOWER_LINE_PREFIX «LOWER LINE » cumple la misma función para la línea límite inferior. Al utilizar estos prefijos, nos aseguramos de que todos los objetos gráficos relacionados con el rango se nombren de forma sistemática, lo que ayuda a mantener la claridad y la organización del código, especialmente cuando pueden existir varios objetos en el gráfico.

A partir de ahí, podemos pasar ahora a la lógica real de procesamiento del código. Ejecutaremos nuestra lógica en los procesos de tick y, por lo tanto, nos sumergiremos directamente en el controlador de eventos OnTick, que se llama y se ejecuta en cada tick que se procesa en el gráfico.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
//---

}

Este es simplemente el controlador de eventos predeterminado que utilizaremos como base para nuestra lógica de control. A continuación, debemos declarar algunas variables para almacenar nuestra lógica de intervalo de tiempo.

   static datetime midnight = iTime(_Symbol,PERIOD_D1,0);  //--- Get the time of midnight (start of the day) for daily chart
   static datetime sixAM = midnight + 6 * 3600;            //--- Calculate 6 AM based on midnight time
   static datetime scanBarTime = sixAM + 1 * PeriodSeconds(_Period); //--- Set scan time for the next bar after 6 AM

Declaramos tres variables estáticas (static) para gestionar las funciones relacionadas con el tiempo. A la primera variable, «midnight», se le asigna el valor devuelto por la función iTime, que recupera la hora de medianoche para el gráfico diario del símbolo actual, indicado por _Symbol, y el período establecido en PERIOD_D1, para indicar que queremos trabajar con velas diarias. 0 indica la barra actual. Esto establece un punto de referencia básico para los cálculos diarios.

A continuación, calculamos la hora correspondiente a «sixAM» sumando seis horas, representadas como 6 * 3600, donde 3600 es el número de segundos que hay en una hora (es decir, 1 hora multiplicada por 60 minutos multiplicada por 60 segundos) a la variable «midnight». Esto nos permite especificar una hora para el análisis diario después de la apertura del mercado, lo que facilita nuestro análisis de la evolución de los precios desde las primeras horas de la jornada bursátil.

Por último, establecemos la variable «scanBarTime» para indicar el tiempo de exploración de la siguiente barra después de «sixAM». Lo conseguimos añadiendo dinámicamente una barra adicional a la hora de escaneo actual de las 6 de la mañana, de modo que también tengamos en cuenta la barra de las 6 de la mañana para el escaneo. El 1 representa el número de barras a las que saltar el escaneo y la función PeriodSeconds convierte automáticamente el período actual del gráfico en segundos. Por ejemplo, podríamos tener un gráfico de 1 hora, lo que significa que convertimos la hora en segundos y multiplicamos los segundos por 1 barra, lo que normalmente daría como resultado 3600 segundos, y luego los sumamos a las 6 de la mañana, lo que nos lleva a la barra de las 7 de la mañana. En general, estas variables estáticas son cruciales para implementar nuestra lógica basada en el tiempo dentro de la estrategia de trading.

A continuación, también podemos declarar variables para definir nuestros rangos de ruptura válidos en el tiempo. Si se produce una ruptura después de 7 horas o a una hora específica, como la 1 p. m., no consideramos válida ninguna señal y, por lo tanto, esperamos a la configuración del día siguiente.

   static datetime validBreakTime_start = scanBarTime;     //--- Set the start of valid breakout time
   static datetime validBreakTime_end = midnight + (6+5) * 3600; //--- Set the end of valid breakout time to 11 AM

Aquí, declaramos dos variables estáticas (static) adicionales para definir la ventana de tiempo para las condiciones de ruptura válidas dentro de nuestra estrategia de trading. La primera variable, «validBreakTime_start», se inicializa con el valor de «scanBarTime», que hemos establecido anteriormente. Esto establece el inicio de nuestro tiempo de ruptura válido, lo que nos permite centrarnos en la acción del precio a partir de la siguiente barra después de las 6 de la mañana.

La segunda variable, «validBreakTime_end», se calcula sumando (6 + 5) * 3600 a la variable «midnight». Esta expresión especifica el final de nuestro periodo válido de ruptura, que corresponde a las 11:00 a. m. Al establecer este marco temporal, creamos una ventana clara durante la cual evaluaremos las condiciones de ruptura, asegurándonos de que nuestras decisiones comerciales se basen en los movimientos de precios que se produzcan dentro de este rango definido. Después de todo eso, estamos listos para comenzar nuestra lógica. Lo primero que debemos tener en cuenta es que queremos comprobar las configuraciones todos los días, por lo que necesitaremos una lógica que identifique un nuevo día.

   if (isNewDay()){
        //---

   }

Utilizamos una instrucción if para comprobar si hay un nuevo día y, si lo hay, ejecutamos el fragmento de código que hay dentro. Utilizamos una función booleana personalizada «isNewDay» para comprobar si es un nuevo día. Su lógica es la siguiente:

bool isNewDay() {
   //--- Flag to indicate if a new day has started
   bool newDay = false;
   
   //--- Structure to hold the current date and time
   MqlDateTime Str_DateTime;
   
   //--- Convert the current time to a structured format
   TimeToStruct(TimeCurrent(), Str_DateTime);
   
   //--- Static variable to store the previous day
   static int prevDay = 0;
   
   //--- Get the current day from the structured time
   int currDay = Str_DateTime.day;
   
   //--- If the previous day is the same as the current day, we're still on the same day
   if (prevDay == currDay) {
      newDay = false;
   }
   //--- If the current day differs from the previous one, we have a new day
   else if (prevDay != currDay) {
      //--- Print a message indicating the new day
      Print("WE HAVE A NEW DAY WITH DATE ", currDay);
      
      //--- Update the previous day to the current day
      prevDay = currDay;
      
      //--- Set the flag to true, indicating a new day has started
      newDay = true;
   }
   
   //--- Return whether a new day has started
   return (newDay);
}

Aquí definimos la función booleana «isNewDay», que se encarga de determinar si ha comenzado un nuevo día en nuestra estrategia comercial. Inicializamos una variable booleana «newDay» con el valor «false», que sirve como indicador para señalar si ha comenzado un nuevo día. Para realizar un seguimiento de la fecha y hora actuales, creamos una estructura de tipo MqlDateTime denominada «Str_DateTime». Utilizamos la función TimeToStruct para convertir la hora actual obtenida de la hora actual en un formato estructurado, rellenando la estructura «Str_DateTime» con la información relevante sobre la fecha y la hora.

A continuación, declaramos una variable entera estática (static) «prevDay», inicializada a cero, que almacena el día de la última fecha registrada. A continuación, recuperamos el día actual de la estructura «Str_DateTime» y lo asignamos a la variable entera «currDay».

Comparamos «prevDay» con «currDay». Si son iguales, significa que todavía estamos en el mismo día, y establecemos «newDay» en «false». Por el contrario, si «prevDay» difiere de «currDay», reconocemos que ha comenzado un nuevo día. En este caso, imprimimos un mensaje que indica la transición a un nuevo día utilizando la función Print, actualizando la variable «prevDay» con el valor de «currDay». A continuación, establecemos el indicador «newDay» en «true», lo que confirma que ha comenzado un nuevo día. Por último, la función devuelve el valor del indicador «newDay», lo que nos permite utilizar esta información en nuestra lógica de negociación para determinar si es necesario tomar alguna medida en función del inicio de un nuevo día.

Ahora, en esta función, restablecemos todo cuando comienza un nuevo día para los cálculos diarios y el mapeo de la lógica de control, tal y como se indica a continuación.

      //--- Reset values for the new day
      midnight = iTime(_Symbol,PERIOD_D1,0);    //--- Get the new midnight time
      sixAM = midnight + 6 * 3600;              //--- Recalculate 6 AM
      scanBarTime = sixAM + 1 * PeriodSeconds(_Period); //--- Recalculate the scan bar time

      validBreakTime_start = scanBarTime;       //--- Update valid breakout start time
      validBreakTime_end = midnight + (6+5) * 3600; //--- Update valid breakout end time to 11 AM

      maximum_price = -DBL_MAX;                 //--- Reset the maximum price for the new day
      minimum_price = DBL_MAX;                  //--- Reset the minimum price for the new day
      
      isHaveDailyRange_Prices = false;          //--- Reset the daily range flag for the new day
      isHaveRangeBreak = false;                 //--- Reset the breakout flag for the new day

En la función, restablecemos varias variables y parámetros al comienzo de un nuevo día de negociación para prepararnos para nuevos cálculos y seguimiento de datos. Comenzamos obteniendo la nueva hora de medianoche del día actual utilizando la función iTime, que proporciona la marca de tiempo del inicio de la barra diaria actual. A continuación, actualizamos la variable «midnight» con este nuevo valor.

A continuación, recalculamos la hora de las 6 de la mañana añadiendo 6 horas, representadas como «6 * 3600», a la variable «medianoche» recién establecida. Esto nos da un punto de referencia para el inicio de la sesión bursátil por la mañana. A continuación, establecemos «scanBarTime» en una barra después de las 6 a. m. añadiendo la duración de un periodo, obtenida mediante la función PeriodSeconds, lo que garantiza que nuestros cálculos se alineen con el periodo actual del gráfico.

A continuación, actualizamos las ventanas de tiempo de ruptura válidas estableciendo «validBreakTime_start» en el nuevo valor calculado «scanBarTime». Este ajuste indica el punto de partida para considerar posibles rupturas durante la jornada bursátil. También establecemos «validBreakTime_end» a las 11:00 a. m. calculándolo como «medianoche + (6 + 5) * 3600», lo que garantiza que tengamos un punto final claro para nuestra evaluación de ruptura. Además, restablecemos los valores de «maximum_price» y «minimum_price» para realizar un seguimiento de los movimientos de precios del nuevo día, inicializando «maximum_price» a -DBL_MAX (el valor más bajo posible) y «minimum_price» a DBL_MAX (el valor más alto posible). Este reinicio nos permite capturar con precisión los precios más altos y más bajos a lo largo del día.

Por último, establecemos los indicadores booleanos «isHaveDailyRange_Prices» e «isHaveRangeBreak» en «false», lo que indica que aún no hemos establecido un rango diario ni identificado una ruptura del rango para el nuevo día. Este reinicio completo prepara nuestro sistema para nuevos cálculos, lo que garantiza que podamos supervisar con precisión la evolución de los precios a medida que avanza el día. Ahora podemos pasar a la lógica de escaneo de barras. No es necesario realizar el escaneo en cada tick, sino solo cuando se genera una nueva barra. Por lo tanto, necesitaremos otra lógica de control para gestionar la identificación de nuevas barras. 

   if (isNewBar()){
        //---

   }

Aquí, seguimos utilizando una instrucción if junto con la función «isNewBar» para aplicar la lógica de generación de nuevas barras. El código de función adaptado se muestra a continuación en un fragmento de código.

bool isNewBar() {
   //--- Static variable to hold the previous number of bars
   static int prevBars = 0;
   
   //--- Get the current number of bars on the chart
   int currBars = iBars(_Symbol, _Period);
   
   //--- If the number of bars hasn't changed, return false
   if (prevBars == currBars) return (false);
   
   //--- Update the previous bar count with the current one
   prevBars = currBars;
   
   //--- Return true if a new bar has been formed
   return (true);
}

Comenzamos declarando una variable estática (static) llamada «prevBars», que almacena el recuento anterior de barras mostradas en el gráfico. La palabra clave static garantiza que la variable conserve su valor entre llamadas a la función, lo que nos permite realizar un seguimiento eficaz de los cambios en el recuento de barras. A continuación, obtenemos el número actual de barras en el gráfico utilizando la función iBars, donde _Symbol representa el instrumento de negociación y _Period se refiere al marco temporal del gráfico. Esta función devuelve el número total de barras disponibles actualmente para el símbolo y el período especificados.

A continuación, comparamos el recuento de barras actual, almacenado en la variable «currBars», con el recuento de barras anterior, «prevBars». Si estos dos valores son iguales, significa que no se ha formado ninguna barra nueva desde la última comprobación, por lo que devolvemos «false» para indicar que seguimos en la misma barra. Si los recuentos difieren, significa que se ha creado una nueva barra, lo que nos lleva a actualizar «prevBars» con el valor de «currBars». Por último, devolvemos «true» para indicar que efectivamente se ha formado una nueva barra. A continuación, dentro de la función, debemos procesar los datos cuando se forma una nueva barra, centrándonos especialmente en una condición temporal específica para extraer los datos de precios.

      //--- If a new bar has been formed, process the data
      datetime currentBarTime = iTime(_Symbol,_Period,0); //--- Get the time of the current bar
      
      if (currentBarTime == scanBarTime && !isHaveDailyRange_Prices){
         //--- If it's time to scan and the daily range is not yet extracted
         Print("WE HAVE ENOUGH BARS DATA FOR DOCUMENTATION. MAKE THE EXTRACTION"); //--- Log the extraction process
         int total_bars = int((sixAM - midnight)/PeriodSeconds(_Period)) + 1; //--- Calculate total bars between midnight and 6 AM
         Print("Total Bars for scan = ",total_bars); //--- Log the total number of bars for scanning
         int highest_price_bar_index = -1;   //--- Variable to store the bar index of the highest price
         int lowest_price_bar_index = -1;    //--- Variable to store the bar index of the lowest price

         //--- 

      }

En primer lugar, declaramos una variable «currentBarTime» utilizando la función iTime, que recupera la hora de la barra actual en el gráfico. Esto nos ayuda a determinar si nos encontramos en un momento específico del día en el que queremos procesar determinados datos sobre precios. A continuación, comprobamos dos condiciones en la sentencia if. En primer lugar, verificamos si la hora de la barra actual coincide con la hora de la barra de exploración, que es la hora designada que planeamos analizar (en este caso, establecida para las 6 a. m.). En segundo lugar, comprobamos si los precios del rango diario aún no se han extraído verificando que el indicador «isHaveDailyRange_Prices» sea falso. Si ambas condiciones se cumplen, significa que estamos en el momento adecuado y hay que extraer los datos del rango de precios.

A continuación, registramos un mensaje utilizando la función Print para indicar que hay suficientes datos de barras disponibles y que el proceso de extracción va a comenzar. Esto ayuda a rastrear cuándo y por qué se activa el proceso durante la ejecución. Procedemos a calcular el número total de barras entre la medianoche y las 6 de la mañana, lo cual es crucial para determinar el rango de precios durante ese periodo. La función PeriodSeconds proporciona la duración de cada barra, y dividimos la diferencia de tiempo entre «sixAM» y «midnight» por esta duración para calcular el número total de barras. Añadimos 1 para asegurarnos de que se incluyen todas las barras de este rango.

Por último, imprimimos el número total de barras para escanear utilizando otra función Print y, a continuación, declaramos dos variables: «highest_price_bar_index» y «lowest_price_bar_index». Inicializamos estas variables con el valor -1 y las utilizaremos para almacenar el índice de las barras que contienen los precios más altos y más bajos, respectivamente, dentro del rango observado. Esta configuración nos prepara para extraer los datos de precios de estas barras específicas. Cuando ejecutamos el programa, obtenemos los siguientes resultados.

CONFIRMACIÓN DEL ESCÁNER DE BARRAS

Podemos ver que, una vez establecido el número de barras necesario para la consideración del rango, informamos del estado de finalización y del número de barras dentro del rango para su consideración. En este punto, podemos proceder a extraer datos del rango diario identificado y establecer los límites del rango.

         for (int i=1; i<=total_bars ; i++){ //--- Loop through all bars within the defined time range
            double open_i = open(i);         //--- Get the opening price of the i-th bar
            double close_i = close(i);       //--- Get the closing price of the i-th bar
            
            double highest_price_i = (open_i > close_i) ? open_i : close_i; //--- Determine the highest price between open and close
            double lowest_price_i = (open_i < close_i) ? open_i : close_i;  //--- Determine the lowest price between open and close
            
            if (highest_price_i > maximum_price){
               //--- If the current highest price is greater than the recorded maximum price
               maximum_price = highest_price_i; //--- Update the maximum price
               highest_price_bar_index = i;     //--- Update the index of the highest price bar
               maximum_time = time(i);          //--- Update the time of the highest price
            }
            if (lowest_price_i < minimum_price){
               //--- If the current lowest price is lower than the recorded minimum price
               minimum_price = lowest_price_i;  //--- Update the minimum price
               lowest_price_bar_index = i;      //--- Update the index of the lowest price bar
               minimum_time = time(i);          //--- Update the time of the lowest price
            }
         }

Para realizar la extracción de datos, recorremos todas las barras dentro del intervalo de tiempo específico (desde la medianoche hasta las 6 de la mañana) para determinar los precios más altos y más bajos. El objetivo es encontrar los precios máximos y mínimos que se produjeron dentro de este rango y registrar el momento en que ocurrieron. Comenzamos configurando un bucle for con la instrucción «for (int i=1; i<=total_bars ; i++)». Esta instrucción significa que el bucle recorre cada barra, desde la primera (index 1) hasta «total_bars», que se calculó previamente para representar el número de barras entre la medianoche y las 6 de la mañana. La variable «i» representa el índice de cada barra en el bucle.

Dentro del bucle, recuperamos los precios de apertura y cierre de cada barra utilizando las funciones personalizadas «open» y «close», respectivamente. Estas dos variables —«open_i» para el precio de apertura y «close_i» para el precio de cierre— nos ayudan a analizar el movimiento del precio de cada barra. Antes de continuar, cabe destacar que estas funciones personalizadas son solo funciones de utilidad que definimos en otro lugar del ámbito global y utilizamos directamente. A continuación se muestra un fragmento de su código.

//--- Utility functions to retrieve price and time data for a given bar index
double open(int index){return (iOpen(_Symbol,_Period,index));}   //--- Get the opening price
double high(int index){return (iHigh(_Symbol,_Period,index));}   //--- Get the highest price
double low(int index){return (iLow(_Symbol,_Period,index));}     //--- Get the lowest price
double close(int index){return (iClose(_Symbol,_Period,index));} //--- Get the closing price
datetime time(int index){return (iTime(_Symbol,_Period,index));} //--- Get the time of the bar

A continuación, utilizamos una operación ternaria para determinar los precios más altos y más bajos de cada barra. La instrucción «double highest_price_i = (open_i > close_i) ? open_i : close_i;" comprueba si el precio de apertura es mayor que el precio de cierre. Si es así, el precio de apertura se establece como el precio más alto de esa barra. De lo contrario, el precio de cierre se convierte en el más alto. Del mismo modo, "double lowest_price_i = (open_i < close_i) ? open_i : close_i;" compara los precios de apertura y cierre para determinar el precio más bajo de la barra.

Después de calcular los precios más altos y más bajos para la barra actual, los comparamos con el precio máximo y mínimo general para todo el período hasta este momento:

  • Si el precio más alto de la barra seleccionada es superior al precio máximo registrado, actualizamos el precio máximo con este nuevo valor. También almacenamos el índice de esta barra en «highest_price_bar_index» y registramos la hora de esta barra utilizando la función «time», que recupera la hora asociada a la barra i-ésima. Esto nos permite rastrear cuándo se produjo el precio más alto.
  • Si el "lowest_price_i" es menor que el "minimum_price" registrado, actualizamos el "minimum_price" a este nuevo valor. También almacenamos el índice de esta barra en «lowest_price_bar_index» y registramos la hora de esta barra en «minimum_time» utilizando la función «time».

Este proceso garantiza que, al final del bucle, hayamos identificado los precios más altos y más bajos dentro del intervalo de tiempo comprendido entre la medianoche y las 6 de la mañana, así como las horas en las que se produjeron. Utilizaremos estos valores más adelante para establecer los niveles de precios clave para el análisis de ruptura. Para asegurarnos de que obtenemos los niveles de precios, podemos registrarlos con fines de confirmación.

         //--- Log the maximum and minimum prices, along with their respective bar indices and times
         Print("Maximum Price = ",maximum_price,", Bar index = ",highest_price_bar_index,", Time = ",maximum_time);
         Print("Minimum Price = ",minimum_price,", Bar index = ",lowest_price_bar_index,", Time = ",minimum_time);

Aquí, solo imprimimos los precios máximos y mínimos identificados junto con sus índices de barra y la hora para fines de confirmación. Una vez ejecutado el programa, obtenemos los siguientes datos:

NIVELES DE PRECIOS

En la imagen podemos ver que nuestros precios máximos se encuentran en la séptima barra, cuyos datos del registro son 0,6548, lo que coincide con el precio de apertura en la ventana de datos. Es medianoche, tal y como se muestra en la escala de hora y fecha del retículo en el eje x. De este modo, podemos estar seguros de que disponemos de los precios diarios y podemos utilizarlos para realizar análisis más detallados. Sin embargo, ya no necesitamos realizar el análisis durante el día, ya que ya hemos obtenido los datos necesarios. Por lo tanto, podemos establecer nuestro indicador booleano para la variable de seguimiento de precios en verdadero y esperar al día siguiente para volver a obtener los precios.

         isHaveDailyRange_Prices = true; //--- Set the flag indicating daily range prices have been extracted

Una vez que hayamos marcado la bandera, estaremos listos. Sin embargo, no vemos visualmente la configuración del rango en el gráfico. De este modo, podemos desarrollar algún mecanismo que nos permita trazar los rangos en el gráfico. Para lograrlo, tendremos que crear funciones que podamos reutilizar. La primera función es la que se encargará de la creación de rectángulos.

//+------------------------------------------------------------------+
//|       FUNCTION TO CREATE A RECTANGLE                             |
//+------------------------------------------------------------------+
void create_Rectangle(string objName, datetime time1, double price1, datetime time2, double price2, color clr) {
   //--- Check if the object already exists by finding it on the chart
   if (ObjectFind(0, objName) < 0) {
      //--- Create a rectangle object using the defined parameters: name, type, and coordinates
      ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2);
      
      //--- Set the time for the first point of the rectangle (start point)
      ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
      
      //--- Set the price for the first point of the rectangle (start point)
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
      
      //--- Set the time for the second point of the rectangle (end point)
      ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
      
      //--- Set the price for the second point of the rectangle (end point)
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
      
      //--- Enable the fill property for the rectangle, making it filled
      ObjectSetInteger(0, objName, OBJPROP_FILL, true);
      
      //--- Set the color for the rectangle
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      
      //--- Set the rectangle to not appear behind other objects
      ObjectSetInteger(0, objName, OBJPROP_BACK, false);

      //--- Redraw the chart to reflect the new changes
      ChartRedraw(0);
   }
}

Aquí creamos una función void «create_Rectangle» que se encargará de crear un objeto rectángulo en un gráfico en MetaTrader. La función toma seis parámetros: «objName» (el nombre del objeto), «time1» y «price1» (coordenadas de la primera esquina del rectángulo), «time2» y «price2» (coordenadas de la esquina opuesta) y «clr» (el color del rectángulo). En la función, primero comprobamos si ya existe un objeto con el nombre dado en el gráfico utilizando la función ObjectFind. Si no se encuentra el objeto (es decir, devuelve un valor inferior a 0), procedemos a crear el rectángulo.

A continuación, llamamos a la función ObjectCreate para crear el objeto rectángulo, proporcionando los parámetros necesarios: el ID del gráfico (establecido en 0 para el gráfico actual), el nombre del objeto, el tipo de objeto (OBJ_RECTANGLE) y las coordenadas (definidas por «time1, price1» y «time2, price2»).

A continuación, utilizamos las funciones ObjectSetInteger y ObjectSetDouble para establecer las propiedades individuales del rectángulo:

  • «ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1)» establece la hora de la primera esquina (punto de inicio) del rectángulo.
  • «ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1)» establece el precio de la primera esquina (punto inicial) del rectángulo.
  • «ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2)» establece la hora para la segunda esquina (punto final) del rectángulo.
  • «ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2)» establece el precio de la segunda esquina (punto final) del rectángulo.

También habilitamos la propiedad de relleno para el rectángulo utilizando el método OBJPROP_FILL, lo que hace que el rectángulo se rellene visualmente en el gráfico, en lugar de ser solo un contorno. Después de esto, establecemos el color del rectángulo utilizando el método OBJPROP_COLOR, aplicando el color especificado («clr») pasado a la función. El rectángulo se configura además para que aparezca delante de otros objetos desactivando la propiedad OBJPROP_BACK. Por último, llamamos a la función ChartRedraw para actualizar el gráfico, asegurándonos de que el rectángulo recién creado se muestre inmediatamente en el gráfico. La siguiente función que debemos definir es la que permite crear líneas en el gráfico para mostrar el intervalo de tiempo entre el inicio y el final.

//+------------------------------------------------------------------+
//|      FUNCTION TO CREATE A TREND LINE                             |
//+------------------------------------------------------------------+
void create_Line(string objName, datetime time1, double price1, datetime time2, double price2, int width, color clr, string text) {
   //--- Check if the line object already exists by its name
   if (ObjectFind(0, objName) < 0) {
      //--- Create a trendline object with the specified parameters
      ObjectCreate(0, objName, OBJ_TREND, 0, time1, price1, time2, price2);
      
      //--- Set the time for the first point of the trendline
      ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
      
      //--- Set the price for the first point of the trendline
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
      
      //--- Set the time for the second point of the trendline
      ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
      
      //--- Set the price for the second point of the trendline
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
      
      //--- Set the width for the line
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, width);
      
      //--- Set the color of the trendline
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      
      //--- Set the trendline to not be behind other objects
      ObjectSetInteger(0, objName, OBJPROP_BACK, false);
      
      //--- Retrieve the current chart scale
      long scale = 0;
      if(!ChartGetInteger(0, CHART_SCALE, 0, scale)) {
         //--- Print an error message if unable to retrieve the chart scale
         Print("UNABLE TO GET THE CHART SCALE. DEFAULT OF ", scale, " IS CONSIDERED");
      }
      //--- Set a default font size based on the chart scale
      int fontsize = 11;
      if (scale == 0) { fontsize = 5; }
      else if (scale == 1) { fontsize = 6; }
      else if (scale == 2) { fontsize = 7; }
      else if (scale == 3) { fontsize = 9; }
      else if (scale == 4) { fontsize = 11; }
      else if (scale == 5) { fontsize = 13; }
      
      //--- Define the description text to appear near the right price
      string txt = " Right Price";
      string objNameDescr = objName + txt;
      
      //--- Create a text object next to the line to display the description
      ObjectCreate(0, objNameDescr, OBJ_TEXT, 0, time2, price2);
      
      //--- Set the color for the text
      ObjectSetInteger(0, objNameDescr, OBJPROP_COLOR, clr);
      
      //--- Set the font size for the text
      ObjectSetInteger(0, objNameDescr, OBJPROP_FONTSIZE, fontsize);
      
      //--- Anchor the text to the left of the line
      ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT);
      
      //--- Set the text content to display the specified string
      ObjectSetString(0, objNameDescr, OBJPROP_TEXT, " " + text);
      
      //--- Set the font of the text to "Calibri"
      ObjectSetString(0, objNameDescr, OBJPROP_FONT, "Calibri");
      
      //--- Redraw the chart to reflect the changes
      ChartRedraw(0);
   }
}

Aquí, creamos otra función vacía «create_Line» y le pasamos también los parámetros necesarios. La función toma ocho parámetros: «objName» (nombre del objeto línea), «time1» y «price1» (coordenadas del punto inicial), «time2» y «price2» (coordenadas del punto final), «width» (el grosor de la línea), «clr» (el color de la línea) y «text» (la descripción que se mostrará junto a la línea de tendencia). Empezamos por comprobar si la línea de tendencia ya existe en el gráfico utilizando ObjectFind. Si el objeto de línea de tendencia con el nombre especificado no existe (devuelve menos de 0), procedemos a crear la línea.

Para crear la línea de tendencia, utilizamos la función ObjectCreate, que define el tipo de objeto como OBJ_TREND y asigna las coordenadas iniciales («time1, price1») y finales («time2, price2») de la línea de tendencia.

A continuación, utilizamos ObjectSetInteger y ObjectSetDouble para asignar propiedades tanto al punto inicial como al punto final de la línea:

  • «ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1)» establece la hora del primer punto.
  • «ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1)» establece el precio del primer punto.
  • «ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2)» establece la hora del segundo punto.
  • «ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2)» establece el precio del segundo punto.

Procedemos estableciendo el ancho de la línea utilizando la propiedad OBJPROP_WIDTH, que controla el grosor de la línea, y a continuación establecemos el color de la línea. A continuación, nos aseguramos de que la línea se muestre delante de otros objetos estableciendo la propiedad OBJPROP_BACK en false, lo que significa que la línea de tendencia no aparecerá detrás de otros elementos del gráfico.

Para mejorar la visualización de la línea de tendencia, recuperamos la escala actual del gráfico utilizando ChartGetInteger. Si conseguimos obtener la escala, la utilizamos para establecer el tamaño de la fuente del texto descriptivo que se mostrará junto a la línea. Según la escala del gráfico, ajustamos el tamaño de la fuente en consecuencia, con un valor predeterminado de 11. A continuación, definimos una etiqueta descriptiva «Right Price» (Precio adecuado) que se colocará junto a la línea de tendencia, y generamos un nombre de objeto para esta etiqueta añadiendo «txt» al nombre del objeto original, formando «objNameDescr».

A continuación, creamos el objeto de texto utilizando la función ObjectCreate, colocándolo al final de la línea («time2, price2») y configurando varias propiedades:

  • «ObjectSetInteger(0, objNameDescr, OBJPROP_COLOR, clr)» establece el color del texto para que coincida con el color de la línea de tendencia.
  • «ObjectSetInteger(0, objNameDescr, OBJPROP_FONTSIZE, fontsize)» establece el tamaño de la fuente basándose en el valor calculado previamente.
  • «ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT)» ancla el texto a la izquierda de la línea.
  • «ObjectSetString(0, objNameDescr, OBJPROP_TEXT, “ ” + text)» establece el contenido de texto real en el parámetro «text» pasado a la función.
  • «ObjectSetString(0, objNameDescr, OBJPROP_FONT, “Calibri”)» establece la fuente del texto en «Calibri» para facilitar la lectura.

Por último, actualizamos el gráfico llamando a ChartRedraw para asegurarnos de que la línea de tendencia recién creada y el texto que la acompaña se muestren correctamente en el gráfico. A continuación, podemos llamar a estas funciones y utilizarlas para asignar los detalles del rango.

         //--- Create visual elements to represent the daily range
         create_Rectangle(RECTANGLE_PREFIX+TimeToString(maximum_time),maximum_time,maximum_price,minimum_time,minimum_price,clrBlue); //--- Create a rectangle for the daily range
         create_Line(UPPER_LINE_PREFIX+TimeToString(midnight),midnight,maximum_price,sixAM,maximum_price,3,clrBlack,DoubleToString(maximum_price,_Digits)); //--- Draw upper range line
         create_Line(LOWER_LINE_PREFIX+TimeToString(midnight),midnight,minimum_price,sixAM,minimum_price,3,clrRed,DoubleToString(minimum_price,_Digits));   //--- Draw lower range line

Una vez que compilamos el código y ejecutamos el programa, obtenemos los siguientes detalles.

PRIMERA TRAMA VISUAL

Ahora podemos ver que representamos visualmente y trazamos los detalles del rango en el gráfico, lo que resulta más atractivo y fácil de consultar y confirmar los precios. Ahora, lo siguiente que tenemos que hacer es comprobar si hay roturas. Este es el punto en el que debemos realizar comprobaciones en cada tick para determinar las rupturas de nivel y, si se cumplen las condiciones, iniciar la lógica de negociación correspondiente. Para la ruptura del nivel superior, tenemos la siguiente lógica.

   //--- Get the close price and time of the previous bar
   double barClose = close(1); 
   datetime barTime = time(1);
   
   //--- Check for upper range breakout condition
   if (barClose > maximum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
       && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the HIGH range. ",barClose," > ",maximum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,234,clrBlack,-1); //--- Draw a point to mark the breakout
   }

Aquí, comprobamos si el precio de cierre de la barra anterior supera el precio máximo diario, lo que indica una ruptura del rango superior. En primer lugar, recuperamos el precio de cierre y la hora de la barra anterior utilizando las funciones personalizadas «close» y «time», y almacenamos estos valores en «barClose» y «barTime», respectivamente. Esto nos permite hacer referencia al precio de cierre y la hora de la barra que estamos analizando.

A continuación, realizamos una serie de comprobaciones para confirmar si se ha producido una ruptura. Comprobamos si «barClose» es mayor que «maximum_price», asegurándonos de que el precio de cierre supera el precio más alto registrado durante el día. También verificamos que los precios del rango diario se hayan extraído utilizando el indicador «isHaveDailyRange_Prices» y confirmamos que no se haya detectado ninguna ruptura anteriormente con el indicador «!isHaveRangeBreak». Además, nos aseguramos de que la ruptura se produzca dentro de la ventana de ruptura válida comprobando si «barTime» se encuentra entre «validBreakTime_start» y «validBreakTime_end».

Si se cumplen todas las condiciones, registramos el evento de ruptura imprimiendo un mensaje que indica que el precio de cierre ha superado el rango superior. A continuación, establecemos «isHaveRangeBreak» en verdadero, lo que indica que se ha detectado una ruptura. Por último, llamamos a la función «drawBreakPoint» para marcar visualmente esta ruptura en el gráfico. La función utiliza la hora de la barra, el precio de cierre, el tamaño del marcador, el color y la prioridad para mostrar una representación visual de la ruptura. Aquí está la lógica de la función, que es similar a las funciones anteriores.

//+------------------------------------------------------------------+
//|       FUNCTION TO CREATE AN ARROW                                |
//+------------------------------------------------------------------+
void drawBreakPoint(string objName, datetime time, double price, int arrCode, color clr, int direction) {
   //--- Check if the arrow object already exists on the chart
   if (ObjectFind(0, objName) < 0) {
      //--- Create an arrow object with the specified time, price, and arrow code
      ObjectCreate(0, objName, OBJ_ARROW, 0, time, price);
      
      //--- Set the arrow's code (symbol)
      ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, arrCode);
      
      //--- Set the color for the arrow
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      
      //--- Set the font size for the arrow
      ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 12);
      
      //--- Set the anchor position for the arrow based on the direction
      if (direction > 0) ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_TOP);
      if (direction < 0) ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_BOTTOM);
      
      //--- Define a text label for the break point
      string txt = " Break";
      string objNameDescr = objName + txt;
      
      //--- Create a text object for the break point description
      ObjectCreate(0, objNameDescr, OBJ_TEXT, 0, time, price);
      
      //--- Set the color for the text description
      ObjectSetInteger(0, objNameDescr, OBJPROP_COLOR, clr);
      
      //--- Set the font size for the text
      ObjectSetInteger(0, objNameDescr, OBJPROP_FONTSIZE, 12);
      
      //--- Adjust the text anchor based on the direction of the arrow
      if (direction > 0) {
         ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
         ObjectSetString(0, objNameDescr, OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
         ObjectSetString(0, objNameDescr, OBJPROP_TEXT, " " + txt);
      }
   }
   //--- Redraw the chart to reflect the new objects
   ChartRedraw(0);
}

Para comprobar si se producen rupturas en el rango inferior, utilizamos una lógica similar a la que empleamos cuando buscamos rupturas en el rango superior.

   //--- Check for lower range breakout condition
   else if (barClose < minimum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
            && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the LOW range. ",barClose," < ",minimum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,233,clrBlue,1); //--- Draw a point to mark the breakout
   }

Tras la compilación, obtenemos el siguiente resultado.

ROTURA DEL NIVEL INFERIOR

Fue todo un éxito. Podemos ver que, una vez que se produce una ruptura del nivel inferior, aparece una flecha de punto de ruptura en el gráfico, que señala visualmente la vela en la que se produce la ruptura. Dejemos que el programa siga su curso y veamos también la ruptura opuesta.

RUPTURA DEL NIVEL SUPERIOR

Fue todo un éxito. También podemos ver que, tal y como se había previsto, se ha producido una ruptura en el nivel superior. Lo siguiente que debemos hacer es abrir posiciones una vez que se produzcan estas rupturas, y eso será todo.

   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
   
   //--- Check for upper range breakout condition
   if (barClose > maximum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
       && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the HIGH range. ",barClose," > ",maximum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,234,clrBlack,-1); //--- Draw a point to mark the breakout
      obj_Trade.Buy(0.01,_Symbol,Ask,minimum_price,Bid+(maximum_price-minimum_price)*2);
   }
   //--- Check for lower range breakout condition
   else if (barClose < minimum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
            && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the LOW range. ",barClose," < ",minimum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,233,clrBlue,1); //--- Draw a point to mark the breakout
      obj_Trade.Sell(0.01,_Symbol,Bid,maximum_price,Ask-(maximum_price-minimum_price)*2);
   }

Con esta lógica, ahora podemos abrir posiciones. Una vez que ejecutamos el programa, obtenemos el siguiente resultado.

TRADE RUPTURA DE VENTA

A partir de la imagen, podemos determinar que abrimos las posiciones correctamente, por ejemplo, colocando el stop loss de una posición de venta en el nivel superior y el take profit 2 veces el tamaño del rango por debajo del punto de entrada. En la siguiente sección, nos centraremos en probar el programa para evaluar su rendimiento y ajustar los parámetros para obtener resultados óptimos.


Pruebas retrospectivas y optimización

Una vez completada la implementación, el siguiente paso fundamental es probar exhaustivamente el Asesor Experto (EA) para evaluar su rendimiento y optimizar sus parámetros. Las pruebas eficaces garantizan que la estrategia se comporte según lo previsto en diversas condiciones de mercado, minimizando el riesgo de que surjan problemas imprevistos durante la negociación. Aquí, utilizaremos el Probador de Estrategias de MetaTrader 5 para realizar pruebas retrospectivas y optimizaciones con el fin de encontrar los mejores valores de entrada posibles para nuestra estrategia.

Para realizar la optimización, necesitaremos información en la sección de configuración. La optimización que adoptaremos tendrá en cuenta la relación riesgo/recompensa, el tiempo de validez de la ruptura en horas y la dirección de la operación una vez que se produzca la ruptura. Es decir, se podría considerar que cuando se produce una ruptura a la baja, en lugar de vender en corto, se debería comprar en largo. Así de sencillo. Esta es la lógica que adoptamos.

enum trade_direction {Default_Trade_Directions,Invert_Trade_Directions};

input int r2r = 2;
input int hoursValidity = 5;
input trade_direction direction_of_trade = Default_Trade_Directions;

Aquí definimos una enumeración e inicializamos algunas variables de entrada que utilizaremos para controlar el comportamiento comercial y los parámetros de la estrategia. En primer lugar, declaramos una enum llamada «trade_direction», que define dos valores posibles: «Default_Trade_Directions» e «Invert_Trade_Directions». Una enum (enumeración) es un tipo de datos definido por el usuario en MQL5 que nos permite asignar nombres a constantes enteras, lo que hace que el código sea más legible y fácil de gestionar. En este caso, «trade_direction» ayudará a controlar si la operación sigue la dirección de negociación predeterminada o si la dirección se invierte en función de condiciones específicas.

A continuación, definimos tres variables de entrada que permiten al usuario modificar los valores directamente desde la configuración del Asesor Experto sin editar el código en sí, pero que serán más útiles cuando optimicemos el programa. La primera variable es «r2r», que está establecida en 2 por defecto y la utilizaremos para controlar el aspecto de riesgo-recompensa de la estrategia. La palabra clave input indica que esta variable puede ser ajustada externamente por el usuario. La segunda entrada es «hoursValidity», inicializada con un valor predeterminado de 5. Esta variable controlará cuánto tiempo permanecerán válidas las condiciones o señales de trading de ruptura en términos de horas.

Por último, la tercera entrada es «direction_of_trade», que es de tipo «trade_direction» (la enumeración que hemos definido anteriormente). Por defecto, lo configuramos en «Default_Trade_Directions», pero el usuario puede cambiarlo a «Invert_Trade_Directions» si desea que la operación se realice en la dirección opuesta. Esta entrada proporciona flexibilidad a la hora de decidir la dirección de la operación sin modificar la lógica central del Asesor Experto. Teniendo esto en cuenta, solo tenemos que sustituir los parámetros estáticos correspondientes en el código para añadir el aspecto dinámico.

   static datetime midnight = iTime(_Symbol,PERIOD_D1,0);  //--- Get the time of midnight (start of the day) for daily chart
   static datetime sixAM = midnight + 6 * 3600;            //--- Calculate 6 AM based on midnight time
   static datetime scanBarTime = sixAM + 1 * PeriodSeconds(_Period); //--- Set scan time for the next bar after 6 AM

   static datetime validBreakTime_start = scanBarTime;     //--- Set the start of valid breakout time
   static datetime validBreakTime_end = midnight + (6+hoursValidity) * 3600; //--- Set the end of valid breakout time to 11 AM
   
   if (isNewDay()){
      //--- Reset values for the new day
      midnight = iTime(_Symbol,PERIOD_D1,0);    //--- Get the new midnight time
      sixAM = midnight + 6 * 3600;              //--- Recalculate 6 AM
      scanBarTime = sixAM + 1 * PeriodSeconds(_Period); //--- Recalculate the scan bar time

      validBreakTime_start = scanBarTime;       //--- Update valid breakout start time
      validBreakTime_end = midnight + (6+hoursValidity) * 3600; //--- Update valid breakout end time to 11 AM

      maximum_price = -DBL_MAX;                 //--- Reset the maximum price for the new day
      minimum_price = DBL_MAX;                  //--- Reset the minimum price for the new day
      
      isHaveDailyRange_Prices = false;          //--- Reset the daily range flag for the new day
      isHaveRangeBreak = false;                 //--- Reset the breakout flag for the new day
   }

   //---

   //--- Check for upper range breakout condition
   if (barClose > maximum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
       && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the HIGH range. ",barClose," > ",maximum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,234,clrBlack,-1); //--- Draw a point to mark the breakout
      
      if (direction_of_trade == Default_Trade_Directions){
         obj_Trade.Buy(0.01,_Symbol,Ask,minimum_price,Bid+(maximum_price-minimum_price)*r2r);
      }
      else if (direction_of_trade == Invert_Trade_Directions){
         obj_Trade.Sell(0.01,_Symbol,Bid,Ask+(maximum_price-minimum_price),Ask-(maximum_price-minimum_price)*r2r);
      }
   }
   //--- Check for lower range breakout condition
   else if (barClose < minimum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
            && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the LOW range. ",barClose," < ",minimum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,233,clrBlue,1); //--- Draw a point to mark the breakout
      
      if (direction_of_trade == Default_Trade_Directions){
         obj_Trade.Sell(0.01,_Symbol,Bid,maximum_price,Ask-(maximum_price-minimum_price)*r2r);
      }
      else if (direction_of_trade == Invert_Trade_Directions){
         obj_Trade.Buy(0.01,_Symbol,Ask,Bid-(maximum_price-minimum_price),Bid+(maximum_price-minimum_price)*r2r);
      }
   }

Ahora hemos añadido los cambios y hemos resaltado las secciones específicas para mayor claridad. Si compilamos eso, ahora podemos encontrar las entradas en la sección de entradas como se muestra a continuación, y podemos elegir entre ellas para realizar la optimización y encontrar los mejores parámetros de negociación para el programa.

ENTRADAS DE OPTIMIZACIÓN

En la imagen podemos ver que tenemos los datos de optimización y solo tenemos que hacer clic en el botón de inicio para comenzar la optimización. En este caso, elegimos solo un mes para no optimizar en exceso el programa. Una vez completado, establecemos la configuración correcta para el programa y la utilizamos para realizar pruebas retrospectivas. Estos son los resultados obtenidos.

BACKTEST GIF

¡Fue todo un éxito! Podemos concluir que el programa funcionó según lo previsto. El fragmento de código fuente final responsable de la creación e implementación de la estrategia Daily Range Breakout es el siguiente:
//+------------------------------------------------------------------+
//|                          Daily Range Breakout Expert Advisor.mq5 |
//|      Copyright 2024, ALLAN MUNENE MUTIIRIA. #@Forex Algo-Trader. |
//|                                     https://forexalg0-trader.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, ALLAN MUNENE MUTIIRIA. #@Forex Algo-Trader"
#property link      "https://forexalg0-trader.com"
#property description "Daily Range Breakout Expert Advisor"
#property version   "1.00"

#include <Trade/Trade.mqh>
CTrade obj_Trade;

enum trade_direction {Default_Trade_Directions,Invert_Trade_Directions};

input int r2r = 2;
input int hoursValidity = 5;
input trade_direction direction_of_trade = Default_Trade_Directions;

double maximum_price = -DBL_MAX;  //--- Initialize the maximum price with the smallest possible value
double minimum_price = DBL_MAX;   //--- Initialize the minimum price with the largest possible value
datetime maximum_time, minimum_time; //--- Declare variables to store the time of the highest and lowest prices
bool isHaveDailyRange_Prices = false; //--- Boolean flag to check if daily range prices are extracted
bool isHaveRangeBreak = false;        //--- Boolean flag to check if a range breakout has occurred

#define RECTANGLE_PREFIX "RANGE RECTANGLE " //--- Prefix for naming range rectangles
#define UPPER_LINE_PREFIX "UPPER LINE "     //--- Prefix for naming upper range line
#define LOWER_LINE_PREFIX "LOWER LINE "     //--- Prefix for naming lower range line

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){
   //--- Initialization code can be placed here if needed
   
   //---
   return(INIT_SUCCEEDED); //--- Return successful initialization
}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason){
   //--- Deinitialization code can be placed here if needed
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){
   //--- 
   static datetime midnight = iTime(_Symbol,PERIOD_D1,0);  //--- Get the time of midnight (start of the day) for daily chart
   static datetime sixAM = midnight + 6 * 3600;            //--- Calculate 6 AM based on midnight time
   static datetime scanBarTime = sixAM + 1 * PeriodSeconds(_Period); //--- Set scan time for the next bar after 6 AM

   static datetime validBreakTime_start = scanBarTime;     //--- Set the start of valid breakout time
   static datetime validBreakTime_end = midnight + (6+hoursValidity) * 3600; //--- Set the end of valid breakout time to 11 AM
   
   if (isNewDay()){
      //--- Reset values for the new day
      midnight = iTime(_Symbol,PERIOD_D1,0);    //--- Get the new midnight time
      sixAM = midnight + 6 * 3600;              //--- Recalculate 6 AM
      scanBarTime = sixAM + 1 * PeriodSeconds(_Period); //--- Recalculate the scan bar time

      validBreakTime_start = scanBarTime;       //--- Update valid breakout start time
      validBreakTime_end = midnight + (6+hoursValidity) * 3600; //--- Update valid breakout end time to 11 AM

      maximum_price = -DBL_MAX;                 //--- Reset the maximum price for the new day
      minimum_price = DBL_MAX;                  //--- Reset the minimum price for the new day
      
      isHaveDailyRange_Prices = false;          //--- Reset the daily range flag for the new day
      isHaveRangeBreak = false;                 //--- Reset the breakout flag for the new day
   }
   
   if (isNewBar()){
      //--- If a new bar has been formed, process the data
      datetime currentBarTime = iTime(_Symbol,_Period,0); //--- Get the time of the current bar
      
      if (currentBarTime == scanBarTime && !isHaveDailyRange_Prices){
         //--- If it's time to scan and the daily range is not yet extracted
         Print("WE HAVE ENOUGH BARS DATA FOR DOCUMENTATION. MAKE THE EXTRACTION"); //--- Log the extraction process
         int total_bars = int((sixAM - midnight)/PeriodSeconds(_Period)) + 1; //--- Calculate total bars between midnight and 6 AM
         Print("Total Bars for scan = ",total_bars); //--- Log the total number of bars for scanning
         int highest_price_bar_index = -1;   //--- Variable to store the bar index of the highest price
         int lowest_price_bar_index = -1;    //--- Variable to store the bar index of the lowest price
   
         for (int i=1; i<=total_bars ; i++){ //--- Loop through all bars within the defined time range
            double open_i = open(i);         //--- Get the opening price of the i-th bar
            double close_i = close(i);       //--- Get the closing price of the i-th bar
            
            double highest_price_i = (open_i > close_i) ? open_i : close_i; //--- Determine the highest price between open and close
            double lowest_price_i = (open_i < close_i) ? open_i : close_i;  //--- Determine the lowest price between open and close
            
            if (highest_price_i > maximum_price){
               //--- If the current highest price is greater than the recorded maximum price
               maximum_price = highest_price_i; //--- Update the maximum price
               highest_price_bar_index = i;     //--- Update the index of the highest price bar
               maximum_time = time(i);          //--- Update the time of the highest price
            }
            if (lowest_price_i < minimum_price){
               //--- If the current lowest price is lower than the recorded minimum price
               minimum_price = lowest_price_i;  //--- Update the minimum price
               lowest_price_bar_index = i;      //--- Update the index of the lowest price bar
               minimum_time = time(i);          //--- Update the time of the lowest price
            }
         }
         //--- Log the maximum and minimum prices, along with their respective bar indices and times
         Print("Maximum Price = ",maximum_price,", Bar index = ",highest_price_bar_index,", Time = ",maximum_time);
         Print("Minimum Price = ",minimum_price,", Bar index = ",lowest_price_bar_index,", Time = ",minimum_time);
         
         //--- Create visual elements to represent the daily range
         create_Rectangle(RECTANGLE_PREFIX+TimeToString(maximum_time),maximum_time,maximum_price,minimum_time,minimum_price,clrBlue); //--- Create a rectangle for the daily range
         create_Line(UPPER_LINE_PREFIX+TimeToString(midnight),midnight,maximum_price,sixAM,maximum_price,3,clrBlack,DoubleToString(maximum_price,_Digits)); //--- Draw upper range line
         create_Line(LOWER_LINE_PREFIX+TimeToString(midnight),midnight,minimum_price,sixAM,minimum_price,3,clrRed,DoubleToString(minimum_price,_Digits));   //--- Draw lower range line
         
         isHaveDailyRange_Prices = true; //--- Set the flag indicating daily range prices have been extracted
      }
   }
   
   //--- Get the close price and time of the previous bar
   double barClose = close(1); 
   datetime barTime = time(1);
   
   double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits);
   double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
   
   //--- Check for upper range breakout condition
   if (barClose > maximum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
       && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the HIGH range. ",barClose," > ",maximum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,234,clrBlack,-1); //--- Draw a point to mark the breakout
      
      if (direction_of_trade == Default_Trade_Directions){
         obj_Trade.Buy(0.01,_Symbol,Ask,minimum_price,Bid+(maximum_price-minimum_price)*r2r);
      }
      else if (direction_of_trade == Invert_Trade_Directions){
         obj_Trade.Sell(0.01,_Symbol,Bid,Ask+(maximum_price-minimum_price),Ask-(maximum_price-minimum_price)*r2r);
      }
   }
   //--- Check for lower range breakout condition
   else if (barClose < minimum_price && isHaveDailyRange_Prices && !isHaveRangeBreak
            && barTime >= validBreakTime_start && barTime <= validBreakTime_end){
      Print("CLOSE Price broke the LOW range. ",barClose," < ",minimum_price); //--- Log the breakout event
      isHaveRangeBreak = true; //--- Set the flag indicating a breakout occurred
      drawBreakPoint(TimeToString(barTime),barTime,barClose,233,clrBlue,1); //--- Draw a point to mark the breakout
      
      if (direction_of_trade == Default_Trade_Directions){
         obj_Trade.Sell(0.01,_Symbol,Bid,maximum_price,Ask-(maximum_price-minimum_price)*r2r);
      }
      else if (direction_of_trade == Invert_Trade_Directions){
         obj_Trade.Buy(0.01,_Symbol,Ask,Bid-(maximum_price-minimum_price),Bid+(maximum_price-minimum_price)*r2r);
      }
   }
}

//--- Utility functions to retrieve price and time data for a given bar index
double open(int index){return (iOpen(_Symbol,_Period,index));}   //--- Get the opening price
double high(int index){return (iHigh(_Symbol,_Period,index));}   //--- Get the highest price
double low(int index){return (iLow(_Symbol,_Period,index));}     //--- Get the lowest price
double close(int index){return (iClose(_Symbol,_Period,index));} //--- Get the closing price
datetime time(int index){return (iTime(_Symbol,_Period,index));} //--- Get the time of the bar

//+------------------------------------------------------------------+
//|       FUNCTION TO CREATE A RECTANGLE                             |
//+------------------------------------------------------------------+
void create_Rectangle(string objName, datetime time1, double price1, datetime time2, double price2, color clr) {
   //--- Check if the object already exists by finding it on the chart
   if (ObjectFind(0, objName) < 0) {
      //--- Create a rectangle object using the defined parameters: name, type, and coordinates
      ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2);
      
      //--- Set the time for the first point of the rectangle (start point)
      ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
      
      //--- Set the price for the first point of the rectangle (start point)
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
      
      //--- Set the time for the second point of the rectangle (end point)
      ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
      
      //--- Set the price for the second point of the rectangle (end point)
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
      
      //--- Enable the fill property for the rectangle, making it filled
      ObjectSetInteger(0, objName, OBJPROP_FILL, true);
      
      //--- Set the color for the rectangle
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      
      //--- Set the rectangle to not appear behind other objects
      ObjectSetInteger(0, objName, OBJPROP_BACK, false);

      //--- Redraw the chart to reflect the new changes
      ChartRedraw(0);
   }
}
//+------------------------------------------------------------------+
//|      FUNCTION TO CREATE A TREND LINE                             |
//+------------------------------------------------------------------+
void create_Line(string objName, datetime time1, double price1, datetime time2, double price2, int width, color clr, string text) {
   //--- Check if the line object already exists by its name
   if (ObjectFind(0, objName) < 0) {
      //--- Create a trendline object with the specified parameters
      ObjectCreate(0, objName, OBJ_TREND, 0, time1, price1, time2, price2);
      
      //--- Set the time for the first point of the trendline
      ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
      
      //--- Set the price for the first point of the trendline
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
      
      //--- Set the time for the second point of the trendline
      ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
      
      //--- Set the price for the second point of the trendline
      ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
      
      //--- Set the width for the line
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, width);
      
      //--- Set the color of the trendline
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      
      //--- Set the trendline to not be behind other objects
      ObjectSetInteger(0, objName, OBJPROP_BACK, false);
      
      //--- Retrieve the current chart scale
      long scale = 0;
      if(!ChartGetInteger(0, CHART_SCALE, 0, scale)) {
         //--- Print an error message if unable to retrieve the chart scale
         Print("UNABLE TO GET THE CHART SCALE. DEFAULT OF ", scale, " IS CONSIDERED");
      }
      //--- Set a default font size based on the chart scale
      int fontsize = 11;
      if (scale == 0) { fontsize = 5; }
      else if (scale == 1) { fontsize = 6; }
      else if (scale == 2) { fontsize = 7; }
      else if (scale == 3) { fontsize = 9; }
      else if (scale == 4) { fontsize = 11; }
      else if (scale == 5) { fontsize = 13; }
      
      //--- Define the description text to appear near the right price
      string txt = " Right Price";
      string objNameDescr = objName + txt;
      
      //--- Create a text object next to the line to display the description
      ObjectCreate(0, objNameDescr, OBJ_TEXT, 0, time2, price2);
      
      //--- Set the color for the text
      ObjectSetInteger(0, objNameDescr, OBJPROP_COLOR, clr);
      
      //--- Set the font size for the text
      ObjectSetInteger(0, objNameDescr, OBJPROP_FONTSIZE, fontsize);
      
      //--- Anchor the text to the left of the line
      ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT);
      
      //--- Set the text content to display the specified string
      ObjectSetString(0, objNameDescr, OBJPROP_TEXT, " " + text);
      
      //--- Set the font of the text to "Calibri"
      ObjectSetString(0, objNameDescr, OBJPROP_FONT, "Calibri");
      
      //--- Redraw the chart to reflect the changes
      ChartRedraw(0);
   }
}

bool isNewBar() {
   //--- Static variable to hold the previous number of bars
   static int prevBars = 0;
   
   //--- Get the current number of bars on the chart
   int currBars = iBars(_Symbol, _Period);
   
   //--- If the number of bars hasn't changed, return false
   if (prevBars == currBars) return (false);
   
   //--- Update the previous bar count with the current one
   prevBars = currBars;
   
   //--- Return true if a new bar has been formed
   return (true);
}

bool isNewDay() {
   //--- Flag to indicate if a new day has started
   bool newDay = false;
   
   //--- Structure to hold the current date and time
   MqlDateTime Str_DateTime;
   
   //--- Convert the current time to a structured format
   TimeToStruct(TimeCurrent(), Str_DateTime);
   
   //--- Static variable to store the previous day
   static int prevDay = 0;
   
   //--- Get the current day from the structured time
   int currDay = Str_DateTime.day;
   
   //--- If the previous day is the same as the current day, we're still on the same day
   if (prevDay == currDay) {
      newDay = false;
   }
   //--- If the current day differs from the previous one, we have a new day
   else if (prevDay != currDay) {
      //--- Print a message indicating the new day
      Print("WE HAVE A NEW DAY WITH DATE ", currDay);
      
      //--- Update the previous day to the current day
      prevDay = currDay;
      
      //--- Set the flag to true, indicating a new day has started
      newDay = true;
   }
   
   //--- Return whether a new day has started
   return (newDay);
}
//+------------------------------------------------------------------+
//|       FUNCTION TO CREATE AN ARROW                                |
//+------------------------------------------------------------------+
void drawBreakPoint(string objName, datetime time, double price, int arrCode, color clr, int direction) {
   //--- Check if the arrow object already exists on the chart
   if (ObjectFind(0, objName) < 0) {
      //--- Create an arrow object with the specified time, price, and arrow code
      ObjectCreate(0, objName, OBJ_ARROW, 0, time, price);
      
      //--- Set the arrow's code (symbol)
      ObjectSetInteger(0, objName, OBJPROP_ARROWCODE, arrCode);
      
      //--- Set the color for the arrow
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      
      //--- Set the font size for the arrow
      ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 12);
      
      //--- Set the anchor position for the arrow based on the direction
      if (direction > 0) ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_TOP);
      if (direction < 0) ObjectSetInteger(0, objName, OBJPROP_ANCHOR, ANCHOR_BOTTOM);
      
      //--- Define a text label for the break point
      string txt = " Break";
      string objNameDescr = objName + txt;
      
      //--- Create a text object for the break point description
      ObjectCreate(0, objNameDescr, OBJ_TEXT, 0, time, price);
      
      //--- Set the color for the text description
      ObjectSetInteger(0, objNameDescr, OBJPROP_COLOR, clr);
      
      //--- Set the font size for the text
      ObjectSetInteger(0, objNameDescr, OBJPROP_FONTSIZE, 12);
      
      //--- Adjust the text anchor based on the direction of the arrow
      if (direction > 0) {
         ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
         ObjectSetString(0, objNameDescr, OBJPROP_TEXT, " " + txt);
      }
      if (direction < 0) {
         ObjectSetInteger(0, objNameDescr, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
         ObjectSetString(0, objNameDescr, OBJPROP_TEXT, " " + txt);
      }
   }
   //--- Redraw the chart to reflect the new objects
   ChartRedraw(0);
}

Resultados de la prueba retrospectiva:

RESULTADOS DE LAS PRUEBAS RETROSPECTIVAS

Gráfico de la prueba retrospectiva:

GRÁFICO DE LAS PRUEBAS RETROSPECTIVAS

Durante esta fase de pruebas, optimizamos los parámetros de entrada y verificamos el rendimiento de la estrategia con el probador de estrategias. Los ajustes que realizamos en los parámetros de entrada dotaron a la estrategia comercial de mayor flexibilidad. Hemos confirmado que la estrategia funciona según lo previsto y logra resultados favorables cuando la sometemos a pruebas retrospectivas y la optimizamos.


Conclusión

En resumen, este artículo ha expuesto un enfoque detallado y paso a paso para crear un Asesor Experto en MQL5 que opera con la ruptura del rango diario. Comenzamos con las partes esenciales que le dan al EA la capacidad de calcular los rangos de precios diarios y establecer los niveles de ruptura. Estos son clave para determinar los momentos justo antes y justo después en los que se puede esperar que el precio salga de su rango diario.

Además, examinamos cómo implementar diversas funciones MQL5 que vigilan las condiciones del mercado, realizan las comparaciones de precios necesarias y, a continuación, toman las medidas requeridas para ejecutar una operación justo en el momento en que se produce una ruptura. Para ayudar a los operadores a ver rápidamente los niveles clave necesarios para esta estrategia, hemos añadido algunas herramientas visuales (rectángulos y líneas de tendencia) al gráfico. Nos aseguramos de que, al programar las herramientas, estas fueran razonablemente flexibles, ya que la estrategia requiere una gran capacidad de ajuste de los parámetros de entrada.

Descargo de responsabilidad: La información presentada en este artículo tiene fines exclusivamente educativos. Su objetivo es proporcionar información sobre cómo crear un Asesor Experto (EA) basado en la estrategia Daily Range Breakout y servir de base para desarrollar sistemas más avanzados con una mayor optimización y pruebas. Las estrategias y métodos descritos no garantizan ningún resultado comercial, y el uso de este contenido es bajo su propia responsabilidad. Asegúrese siempre de realizar pruebas exhaustivas y tenga en cuenta las posibles condiciones del mercado antes de aplicar cualquier solución de trading automatizado.

La estrategia se probó utilizando el Probador de Estrategias de MetaTrader 5, lo que nos permitió evaluar su rendimiento y realizar ajustes para que funcionara aún mejor en nuestras condiciones de trading. ¡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/16135

Stanislav Korotky
Stanislav Korotky | 21 oct 2024 en 14:59

Parece un conocido "Morning Flat Breakout", no un "Daily Range Breakout" (que es diferente), por lo que el título es engañoso.

También no vi un código para el manejo de la zona horaria, porque dependiendo de su servidor GMT offset, la "mañana plana" que define el rango de ruptura, debe ser rastreado a partir no de una medianoche 00:00 necesariamente, pero con el desplazamiento.

Allan Munene Mutiiria
Allan Munene Mutiiria | 22 oct 2024 en 11:03
Stanislav Korotky #:

Parece un conocido "Morning Flat Breakout", no un "Daily Range Breakout" (que es diferente), por lo que el título es engañoso.

También no vi un código para el manejo de la zona horaria, porque dependiendo de su servidor GMT offset, la "mañana plana" que define el rango de ruptura, debe ser rastreado a partir no de una medianoche 00:00 necesariamente, pero con el desplazamiento.

Gracias por la respuesta. Depende de la estrategia que se utilice. En nuestro caso, es el Daily Range Breakout, esencialmente desde medianoche hasta las 6 AM, que por supuesto se puede ajustar. Gracias.

Aa123456789
Aa123456789 | 13 jun 2025 en 17:16
¿Puedes modificar el código para que el ea pueda diversificar y ajustar el take profit y stop loss y el tiempo de operación, y el % de riesgo?
Media móvil en MQL5 desde cero: Sencilla y accesible Media móvil en MQL5 desde cero: Sencilla y accesible
Usando ejemplos sencillos, hoy analizaremos los principios del cálculo de las medias móviles y nos familiarizaremos con las formas de optimizar el cálculo de los indicadores y, en consecuencia, de las medias móviles.
Redes neuronales en el trading: Modelo adaptativo multiagente (Final) Redes neuronales en el trading: Modelo adaptativo multiagente (Final)
En el artículo anterior, nos familiarizamos con el framework MASA, un framework adaptativo multiagente que combina enfoques de aprendizaje por refuerzo y estrategias adaptativas para ofrecer un equilibrio armonioso entre rentabilidad y riesgo en condiciones de mercado turbulentas. Asimismo, construimos la funcionalidad de los agentes individuales de este framework. En este artículo continuaremos el trabajo empezado, llevándolo a su conclusión lógica.
Selección de características y reducción de dimensionalidad mediante componentes principales Selección de características y reducción de dimensionalidad mediante componentes principales
El artículo profundiza en la implementación de un algoritmo modificado de análisis de componentes por selección ascendente, inspirándose en la investigación presentada en «Forward Selection Component Analysis: Algorithms and Applications» (Análisis de componentes por selección ascendente: algoritmos y aplicaciones), de Luca Puggini y Sean McLoone.
Reimaginando las estrategias clásicas en MQL5 (Parte X): ¿Puede la IA impulsar el MACD? Reimaginando las estrategias clásicas en MQL5 (Parte X): ¿Puede la IA impulsar el MACD?
Únase a nosotros mientras analizamos empíricamente el indicador MACD para comprobar si la aplicación de la IA a una estrategia, incluyendo el indicador, produciría alguna mejora en nuestra precisión a la hora de pronosticar el EURUSD. Evaluamos simultáneamente si el indicador en sí mismo es más fácil de predecir que el precio, así como si el valor del indicador es predictivo de los niveles de precios futuros. Le proporcionaremos la información que necesita para decidir si debe considerar invertir su tiempo en integrar el MACD en sus estrategias de trading con IA.