Automatización de estrategias de trading en MQL5 (Parte 6): Dominar la detección de bloques de órdenes para el comercio inteligente con dinero
Introducción
En el artículo anterior (parte 5 de la serie), desarrollamos la estrategia Adaptive Crossover RSI Trading Suite, que combina cruces de medias móviles con filtrado RSI para identificar oportunidades de negociación de alta probabilidad. Ahora, en la Parte 6, nos centramos en el análisis puro de la acción del precio con un Sistema de detección de bloques de órdenes automatizado en MetaQuotes Language 5 (MQL5), una potente herramienta utilizada en el trading con dinero inteligente. Esta estrategia identifica bloques de órdenes institucionales clave (zonas donde los grandes actores acumulan o distribuyen posiciones), lo que ayuda a los operadores a anticipar posibles reversiones y continuaciones de tendencias.
A diferencia de los indicadores tradicionales, este enfoque se basa completamente en la estructura de precios y detecta bloques de órdenes alcistas y bajistas de forma dinámica en función del comportamiento histórico de los precios. El sistema visualiza estas zonas directamente en el gráfico, brindando a los operadores un contexto de mercado claro y posibles configuraciones comerciales. En este artículo, cubriremos el desarrollo paso a paso de esta estrategia, desde la definición de bloques de órdenes hasta su implementación en MQL5, realizando pruebas retrospectivas de su efectividad y analizando el rendimiento. Estructuraremos esta discusión a través de las siguientes secciones:
- Plan de estrategia
- Implementación en MQL5
- Pruebas retrospectivas
- Conclusión
Al finalizar, tendrá una base sólida en la automatización de la detección de bloques de órdenes, lo que le permitirá integrar conceptos de dinero inteligente en sus algoritmos comerciales. Empecemos.
Plan de estrategia
Comenzaremos identificando los rangos de consolidación, que ocurren cuando el precio se mueve dentro de un rango limitado sin una dirección de tendencia clara. Para ello, escanearemos el mercado en busca de áreas donde la acción del precio carece de rupturas significativas. Una vez que detectemos una ruptura de este rango, evaluaremos si se puede formar un bloque de órdenes. Nuestro proceso de validación implicará verificar las tres velas anteriores antes de la ruptura. Si estas velas exhiben un movimiento impulsivo, clasificaremos el bloque de orden como alcista o bajista según la dirección de ruptura. Un bloque de orden alcista se identificará cuando la ruptura sea hacia arriba, mientras que un bloque de orden bajista se marcará cuando la ruptura sea hacia abajo. Una vez validado, trazaremos el bloque de orden en el gráfico para referencia futura. He aquí un ejemplo.

Si las tres velas anteriores no muestran un movimiento impulsivo, no validaremos un bloqueo de orden. En su lugar, solo dibujaremos el rango de consolidación, asegurándonos de no marcar zonas débiles o insignificantes. Después de marcar los bloques de órdenes válidos, monitorearemos continuamente la acción del precio. Si el precio retrocede a un bloque de orden previamente validado, ejecutaremos operaciones en alineación con la dirección de ruptura inicial, esperando que la tendencia continúe. Sin embargo, si un bloque de órdenes se extiende más allá del último punto de precio significativo, lo eliminaremos de nuestra matriz de bloques de órdenes válidos, garantizando así que solo operamos en zonas relevantes y nuevas. Este enfoque estructurado nos ayudará a centrarnos en configuraciones de alta probabilidad, filtrando rupturas débiles y garantizando que nuestras operaciones se alineen con los movimientos de dinero inteligente.
Implementación en MQL5
Para implementar la identificación de los bloques de órdenes en MQL5, necesitaremos definir algunas variables globales que serán necesarias a lo largo del proceso.
#include <Trade/Trade.mqh> CTrade obj_Trade; // Struct to hold both the price and the index of the high or low struct PriceIndex { double price; int index; }; // Global variables to track the range and breakout state PriceIndex highestHigh = {0, 0}; // Stores the highest high of the range PriceIndex lowestLow = {0, 0}; // Stores the lowest low of the range bool breakoutDetected = false; // Tracks if a breakout has occurred double impulseLow = 0.0; double impulseHigh = 0.0; int breakoutBarIndex = -1; // To track the bar at which breakout occurred datetime breakoutTime = 0; // To store the breakout time string totalOBs_names[]; datetime totalOBs_dates[]; bool totalOBs_is_signals[]; #define OB_Prefix "OB REC " #define CLR_UP clrLime #define CLR_DOWN clrRed bool is_OB_UP = false; bool is_OB_DOWN = false;
Comenzamos incluyendo la biblioteca «Trade.mqh» y creando un objeto «CTrade», «obj_Trade», para gestionar la ejecución de las operaciones. Definimos una estructura «PriceIndex» para almacenar tanto el nivel de precios como su índice correspondiente, lo que nos ayuda a realizar un seguimiento de los máximos y mínimos más altos dentro del rango de consolidación. Las variables globales «highestHigh» y «lowestLow» almacenan estos niveles clave, mientras que el indicador «breakoutDetected» indica si se ha producido una ruptura.
Para validar el movimiento impulsivo, introducimos «impulseLow» e «impulseHigh», que ayudarán a determinar la fuerza de la ruptura. La variable «breakoutBarIndex» rastrea la barra exacta en la que se produjo la ruptura, y «breakoutTime» almacena la marca de tiempo correspondiente. Para la gestión de bloques de órdenes, mantenemos tres matrices globales: «totalOBs_names», «totalOBs_dates» y «totalOBs_is_signals». Estas matrices almacenan los nombres de los bloques de órdenes, sus respectivas marcas de tiempo y si son señales comerciales válidas.
Definimos el prefijo del bloque de órdenes como «OB_Prefix» y asignamos códigos de color para los bloques de órdenes alcistas y bajistas utilizando «CLR_UP» para los alcistas (lima) y «CLR_DOWN» para los bajistas (rojo). Por último, los indicadores booleanos «is_OB_UP» e «is_OB_DOWN» nos ayudan a determinar si el último bloque de órdenes detectado es alcista o bajista. No necesitamos realizar un seguimiento de los bloques de órdenes en la inicialización del programa, ya que queremos comenzar desde cero. Por lo tanto, implementaremos la lógica de control directamente en el controlador de eventos OnTick.
//+------------------------------------------------------------------+ //| Expert ontick function | //+------------------------------------------------------------------+ void OnTick() { static bool isNewBar = false; int currBars = iBars(_Symbol, _Period); static int prevBars = currBars; // Detect a new bar if (prevBars == currBars) { isNewBar = false; } else if (prevBars != currBars) { isNewBar = true; prevBars = currBars; } if (!isNewBar) return; // Process only on a new bar int rangeCandles = 7; // Initial number of candles to check double maxDeviation = 50; // Max deviation between highs and lows in points int startingIndex = 1; // Starting index for the scan int waitBars = 3; //--- }
En el controlador de eventos OnTick, comenzamos detectando la formación de una nueva barra utilizando «currBars» y «prevBars». Establecemos «isNewBar» en «true» cuando aparece una nueva barra y regresamos antes de tiempo si no se detecta ninguna barra nueva. A continuación, definimos «rangeCandles» como «7», que representa el número mínimo de velas que analizamos para identificar la consolidación. La variable «maxDeviation» se establece en «50» puntos, lo que limita la diferencia aceptable entre los precios más altos y más bajos dentro del rango. El «startingIndex» se inicializa en «1», lo que garantiza que comencemos el escaneo desde la barra completada más reciente. Además, establecemos «waitBars» en «3» para definir cuántas barras deben pasar antes de validar un bloque de órdenes. A continuación, debemos verificar los rangos de consolidación y obtener los precios para determinar con mayor precisión los bloques de órdenes válidos.
// Check for consolidation or extend the range if (!breakoutDetected) { if (highestHigh.price == 0 && lowestLow.price == 0) { // If range is not yet established, look for consolidation if (IsConsolidationEqualHighsAndLows(rangeCandles, maxDeviation, startingIndex)) { GetHighestHigh(rangeCandles, startingIndex, highestHigh); GetLowestLow(rangeCandles, startingIndex, lowestLow); Print("Consolidation range established: Highest High = ", highestHigh.price, " at index ", highestHigh.index, " and Lowest Low = ", lowestLow.price, " at index ", lowestLow.index); } } else { // Extend the range if the current bar's prices remain within the range ExtendRangeIfWithinLimits(); } }
En cada nueva barra que se forma, verificamos la consolidación o ampliamos el rango existente si no se ha detectado ninguna ruptura. Si «highestHigh.price» y «lowestLow.price» son ambos cero, significa que aún no se ha establecido ningún rango de consolidación. A continuación, llamamos a la función «IsConsolidationEqualHighsAndLows» para comprobar si las últimas «rangeCandles» forman una consolidación dentro de la «maxDeviation» permitida. Si se confirma, utilizamos las funciones «GetHighestHigh» y «GetLowestLow» para determinar los precios máximos y mínimos exactos dentro del rango, almacenando sus valores junto con sus respectivos índices de barras.
Si ya se ha establecido un rango, nos aseguramos de que la barra actual permanezca dentro de los límites definidos llamando a la función «ExtendRangeIfWithinLimits». Esta función ayuda a ajustar dinámicamente el rango siempre y cuando no se produzca una ruptura. Aquí está la implementación de los fragmentos de código de las funciones personalizadas.
// Function to detect consolidation where both highs and lows are nearly equal bool IsConsolidationEqualHighsAndLows(int rangeCandles, double maxDeviation, int startingIndex) { // Loop through the last `rangeCandles` to check if highs and lows are nearly equal for (int i = startingIndex; i < startingIndex + rangeCandles - 1; i++) { // Compare the high of the current candle with the next one if (MathAbs(high(i) - high(i + 1)) > maxDeviation * Point()) { return false; // If the high difference is greater than allowed, it's not a consolidation } // Compare the low of the current candle with the next one if (MathAbs(low(i) - low(i + 1)) > maxDeviation * Point()) { return false; // If the low difference is greater than allowed, it's not a consolidation } } // If both highs and lows are nearly equal, it's a consolidation range return true; }
Definimos una función booleana «IsConsolidationEqualHighsAndLows» que se encarga de detectar la consolidación verificando si los máximos y mínimos de las últimas «rangeCandles» son casi iguales dentro de una «maxDeviation» especificada. Lo conseguimos iterando sobre cada barra, empezando por «startingIndex», y comparando los máximos y mínimos de las velas consecutivas.
Dentro del bucle for, utilizamos la función MathAbs para calcular la diferencia absoluta entre el máximo de la barra actual («high(i)») y el siguiente máximo. Si esta diferencia supera la desviación máxima convertida a forma de punto, Point, la función devuelve inmediatamente falso, lo que indica que los máximos no son lo suficientemente iguales como para considerarse una consolidación. Del mismo modo, volvemos a aplicar la función MathAbs para comparar los mínimos de barras consecutivas («low(i)» y «low(i + 1)»), asegurándonos de que los mínimos también se encuentran dentro de la desviación permitida. Si falla alguna comprobación, la función sale anticipadamente con falso. Si todos los máximos y mínimos permanecen dentro de la desviación aceptable, devolvemos el valor verdadero, lo que confirma un rango de consolidación válido. Las siguientes funciones que definimos son las encargadas de recuperar los precios de barra más altos y más bajos.
// Function to get the highest high and its index in the last `rangeCandles` candles, starting from `startingIndex` void GetHighestHigh(int rangeCandles, int startingIndex, PriceIndex &highestHighRef) { highestHighRef.price = high(startingIndex); // Start by assuming the first candle's high is the highest highestHighRef.index = startingIndex; // The index of the highest high (starting with the `startingIndex`) // Loop through the candles and find the highest high and its index for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) { if (high(i) > highestHighRef.price) { highestHighRef.price = high(i); // Update highest high highestHighRef.index = i; // Update index of highest high } } } // Function to get the lowest low and its index in the last `rangeCandles` candles, starting from `startingIndex` void GetLowestLow(int rangeCandles, int startingIndex, PriceIndex &lowestLowRef) { lowestLowRef.price = low(startingIndex); // Start by assuming the first candle's low is the lowest lowestLowRef.index = startingIndex; // The index of the lowest low (starting with the `startingIndex`) // Loop through the candles and find the lowest low and its index for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) { if (low(i) < lowestLowRef.price) { lowestLowRef.price = low(i); // Update lowest low lowestLowRef.index = i; // Update index of lowest low } } }
La función «GetHighestHigh» se encarga de identificar el máximo más alto y su índice correspondiente dentro de las últimas barras «rangeCandles», comenzando desde «startingIndex». Inicializamos «highestHighRef.price» con el máximo de la primera vela del rango («high(startingIndex)») y establecemos «highestHighRef.index» en «startingIndex». A continuación, iteramos a través de las velas restantes en el rango especificado, comprobando si alguna de ellas tiene un precio más alto que el actual «highestHighRef.price». Si se encuentra un nuevo máximo más alto, actualizamos tanto «highestHighRef.price» como «highestHighRef.index». Esta función nos ayuda a determinar el límite superior de un rango de consolidación.
De manera similar, la función "GetLowestLow" encuentra el mínimo más bajo y su índice dentro del mismo rango. Inicializamos «lowestLowRef.price» con «low(startingIndex)» y «lowestLowRef.index» con «startingIndex». A medida que recorremos las velas, comprobamos si alguna tiene un precio inferior al «lowestLowRef.price» actual. Si es así, actualizamos tanto «lowestLowRef.price» como «lowestLowRef.index». Esta función determina el límite inferior de un rango de consolidación. Por último, tenemos la función que ampliará el rango.
// Function to extend the range if the latest bar remains within the range limits void ExtendRangeIfWithinLimits() { double currentHigh = high(1); // Get the high of the latest closed bar double currentLow = low(1); // Get the low of the latest closed bar if (currentHigh <= highestHigh.price && currentLow >= lowestLow.price) { // Extend the range if the current bar is within the established range Print("Range extended: Including candle with High = ", currentHigh, " and Low = ", currentLow); } else { Print("No extension possible. The current bar is outside the range."); } }
Aquí, la función «ExtendRangeIfWithinLimits» garantiza que el rango de consolidación identificado previamente siga siendo válido si las nuevas barras continúan estando dentro de sus límites. Primero recuperamos el máximo y el mínimo de la vela cerrada más recientemente utilizando las funciones «high(1)» y «low(1)». A continuación, comprobamos si «currentHigh» es menor o igual que «highestHigh.price» y si «currentLow» es mayor o igual que «lowestLow.price». Si se cumplen ambas condiciones, se amplía el rango y se imprime un mensaje de confirmación indicando que la nueva vela se incluye dentro del rango existente.
De lo contrario, si la nueva vela se mueve fuera del rango establecido, no se produce ninguna extensión e imprimimos un mensaje que indica que el rango no se puede extender. Esta función juega un papel clave en el mantenimiento de zonas de consolidación válidas y evita la detección de rupturas innecesarias si el mercado permanece dentro del rango predefinido.
También utilizamos funciones predefinidas responsables de recuperar datos de precios de barras. Aquí están sus fragmentos de código.
//--- One-line functions to access price data double high(int index) { return iHigh(_Symbol, _Period, index); } double low(int index) { return iLow(_Symbol, _Period, index); } double open(int index) { return iOpen(_Symbol, _Period, index); } double close(int index) { return iClose(_Symbol, _Period, index); } datetime time(int index) { return iTime(_Symbol, _Period, index); }
Estas funciones de una sola línea «high», «low», «open», «close» y «time» sirven como envoltorios simples para recuperar datos de precios y tiempos de barras históricas. Cada función llama a la función integrada MQL5 correspondiente —iHigh, iLow, iOpen, iClose y iTime— para obtener el valor solicitado para un «index» determinado. La función «high» devuelve el precio máximo de una barra específica, mientras que la función «low» devuelve el precio mínimo. Del mismo modo, «open» recupera el precio de apertura y «close» recupera el precio de cierre. La función «time» devuelve la marca de tiempo de la barra. Los utilizamos para mejorar la legibilidad del código y permitir un acceso más limpio y estructurado a los datos históricos en todo nuestro programa.
Armados con estas funciones, ahora podemos comprobar si se producen rupturas si se establece un rango de consolidación mediante el siguiente fragmento de código.
// Check for breakout if a consolidation range is established if (highestHigh.price > 0 && lowestLow.price > 0) { breakoutDetected = CheckRangeBreak(highestHigh, lowestLow); }
Aquí, si se establece un rango de consolidación, comprobamos si se produce una ruptura del rango utilizando de nuevo una función personalizada llamada «CheckRangeBreak» y almacenamos el resultado en la variable «breakoutDetected». La implementación de la función es la siguiente.
// Function to check for range breaks bool CheckRangeBreak(PriceIndex &highestHighRef, PriceIndex &lowestLowRef) { double closingPrice = close(1); // Get the closing price of the current candle if (closingPrice > highestHighRef.price) { Print("Range break upwards detected. Closing price ", closingPrice, " is above the highest high: ", highestHighRef.price); return true; // Breakout detected } else if (closingPrice < lowestLowRef.price) { Print("Range break downwards detected. Closing price ", closingPrice, " is below the lowest low: ", lowestLowRef.price); return true; // Breakout detected } return false; // No breakout }
Para la función booleana «CheckRangeBreak», comparamos el «closingPrice» de la vela actual con el «highestHighRef.price» y el «lowestLowRef.price». Si el « closingPrice » es superior al « highestHighRef.price », detectamos una ruptura al alza. Si es inferior al «lowestLowRef.price», detectamos una ruptura a la baja. En ambos casos, devolvemos «true» e imprimimos la dirección de la ruptura. Si no se cumple ninguna de las dos condiciones, devolvemos «false».
Ahora podemos utilizar la variable para detectar una ruptura en la que necesitamos restablecer el estado del rango para prepararnos para un posible próximo rango de consolidación, como se indica a continuación.
// Reset state after breakout if (breakoutDetected) { Print("Breakout detected. Resetting for the next range."); breakoutBarIndex = 1; // Use the current bar's index (index 1 refers to the most recent completed bar) breakoutTime = TimeCurrent(); impulseHigh = highestHigh.price; impulseLow = lowestLow.price; breakoutDetected = false; highestHigh.price = 0; highestHigh.index = 0; lowestLow.price = 0; lowestLow.index = 0; }
Después de detectar una ruptura, restablecemos el estado para el siguiente rango. Establecemos "breakoutBarIndex" en 1, haciendo referencia a la barra completada más reciente. También actualizamos "breakoutTime" con la hora actual usando la función "TimeCurrent". "ImpulseHigh" e "ImpulseLow" se establecen en el "highestHigh.price" y "lowestLow.price" del rango anterior. Luego marcamos "breakoutDetected" como "falso" y restablecemos los precios e índices "highestHigh" y "loestLow" a 0, preparándonos para la siguiente detección de rango. Ahora podemos proceder a verificar bloques de órdenes válidos en función del movimiento impulsivo.
if (breakoutBarIndex >= 0 && TimeCurrent() > breakoutTime + waitBars * PeriodSeconds()) { DetectImpulsiveMovement(impulseHigh,impulseLow,waitBars,1); bool is_OB_Valid = is_OB_DOWN || is_OB_UP; datetime time1 = iTime(_Symbol,_Period,rangeCandles+waitBars+1); double price1 = impulseHigh; int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS); datetime time2 = is_OB_Valid ? time1 + (visibleBars/1)*PeriodSeconds() : time(waitBars+1); double price2 = impulseLow; string obNAME = OB_Prefix+"("+TimeToString(time1)+")"; color obClr = clrBlack; if (is_OB_Valid){obClr = is_OB_UP ? CLR_UP : CLR_DOWN;} else if (!is_OB_Valid){obClr = clrBlue;} string obText = ""; if (is_OB_Valid){obText = is_OB_UP ? "Bullish Order Block"+ShortToString(0x2BED) : "Bearish Order Block"+ShortToString(0x2BEF);} else if (!is_OB_Valid){obText = "Range";} //--- }
Aquí, primero comprobamos si «breakoutBarIndex» es mayor o igual a 0 y si la hora actual es mayor que «breakoutTime» más un periodo de espera, calculado multiplicando «waitBars» por el periodo en segundos (utilizando la función PeriodSeconds). Si se cumple esta condición, llamamos a la función «DetectImpulsiveMovement» para identificar movimientos impulsivos del mercado, pasando los valores de «impulseHigh», «impulseLow», «waitBars» y un parámetro fijo de 1.
A continuación, validamos el bloque de orden comprobando si «is_OB_DOWN» o «is_OB_UP» es verdadero, y almacenamos el resultado en «is_OB_Valid». Recuperamos la marca de tiempo de la barra con iTime, que proporciona la hora de una barra específica en el símbolo y el período, y la almacenamos en «time1». El precio de esta barra se almacena en «impulseHigh», que utilizamos para cálculos posteriores. A continuación, obtenemos el número de barras visibles en el gráfico utilizando la función ChartGetInteger con el parámetro CHART_VISIBLE_BARS, que devuelve el número de barras visibles en el gráfico. A continuación, calculamos «time2», que depende de si el bloque de pedidos es válido. Si «is_OB_Valid» es verdadero, ajustamos el tiempo añadiendo las barras visibles a «time1», multiplicadas por el periodo en segundos. De lo contrario, utilizamos el tiempo del siguiente compás, determinado por «time(waitBars+1)». Lo determinamos utilizando un operador ternario.
El «precio2» se establece en «impulso bajo». A continuación, generamos el nombre del bloque de pedido utilizando «OB_Prefix» junto con la hora formateada mediante la función TimeToString. El color del bloque de pedido se establece mediante la variable «obClr», cuyo valor predeterminado es negro. Si el bloque de orden es válido, establecemos el color como «CLR_UP» (para un bloque de orden ascendente) o «CLR_DOWN» (para un bloque de orden descendente). Si el bloque de orden no es válido, el color se establece en azul.
El texto del bloque de orden, almacenado en «obText», se establece en función de la dirección del bloque de orden. Si el bloque de órdenes es válido, mostramos «Bullish Order Block» o «Bearish Order Block» con códigos de caracteres Unicode únicos (0x2BED para alcistas, 0x2BEF para bajistas), que convertimos utilizando la función «ShortToString». Si no es así, lo etiquetamos como «Range». Estos símbolos Unicode son los siguientes.

La función para detectar movimientos impulsivos es la siguiente.
// Function to detect impulsive movement after breakout void DetectImpulsiveMovement(double breakoutHigh, double breakoutLow, int impulseBars, double impulseThreshold) { double range = breakoutHigh - breakoutLow; // Calculate the breakout range double impulseThresholdPrice = range * impulseThreshold; // Threshold for impulsive move // Check for the price movement in the next `impulseBars` bars after breakout for (int i = 1; i <= impulseBars; i++) { double closePrice = close(i); // Get the close price of the bar // Check if the price moves significantly beyond the breakout high if (closePrice >= breakoutHigh + impulseThresholdPrice) { is_OB_UP = true; Print("Impulsive upward movement detected: Close Price = ", closePrice, ", Threshold = ", breakoutHigh + impulseThresholdPrice); return; } // Check if the price moves significantly below the breakout low else if (closePrice <= breakoutLow - impulseThresholdPrice) { is_OB_DOWN = true; Print("Impulsive downward movement detected: Close Price = ", closePrice, ", Threshold = ", breakoutLow - impulseThresholdPrice); return; } } // If no impulsive movement is detected is_OB_UP = false; is_OB_DOWN = false; Print("No impulsive movement detected after breakout."); }
En la función, para detectar si el precio se mueve impulsivamente después de una ruptura, primero calculamos el «rango» restando el «breakoutLow» del «breakoutHigh». El «impulseThresholdPrice» se determina multiplicando el rango por el valor «impulseThreshold», que define cuánto debe moverse el precio para considerarse impulsivo. A continuación, comprobamos el movimiento del precio en las siguientes barras «impulseBars» utilizando un bucle for.
Para cada barra, obtenemos el «closePrice» utilizando la función «close(i)», que recupera el precio de cierre de la barra i-ésima. Si el precio de cierre supera el «breakoutHigh» en al menos el «impulseThresholdPrice», consideramos que se trata de un movimiento alcista impulsivo, establecemos «is_OB_UP» en verdadero e imprimimos el movimiento detectado. Del mismo modo, si el precio de cierre cae por debajo del «breakoutLow» al menos en el «impulseThresholdPrice», detectamos un movimiento impulsivo a la baja, establecemos «is_OB_DOWN» en verdadero e imprimimos el resultado.
Si no se detecta ningún movimiento significativo en el precio después de comprobar todas las barras, tanto «is_OB_UP» como «is_OB_DOWN» se establecen en falso, y se imprime que no se ha detectado ningún movimiento impulsivo. Ahora, podemos trazar los rangos en el gráfico así como los bloques de orden de la siguiente manera.
if (!is_OB_Valid){ if (ObjectFind(0,obNAME) < 0){ CreateRec(obNAME,time1,price1,time2,price2,obClr,obText); } } else if (is_OB_Valid){ if (ObjectFind(0,obNAME) < 0){ CreateRec(obNAME,time1,price1,time2,price2,obClr,obText); Print("Old ArraySize = ",ArraySize(totalOBs_names)); ArrayResize(totalOBs_names,ArraySize(totalOBs_names)+1); Print("New ArraySize = ",ArraySize(totalOBs_names)); totalOBs_names[ArraySize(totalOBs_names)-1] = obNAME; ArrayPrint(totalOBs_names); Print("Old ArraySize = ",ArraySize(totalOBs_dates)); ArrayResize(totalOBs_dates,ArraySize(totalOBs_dates)+1); Print("New ArraySize = ",ArraySize(totalOBs_dates)); totalOBs_dates[ArraySize(totalOBs_dates)-1] = time2; ArrayPrint(totalOBs_dates); Print("Old ArraySize = ",ArraySize(totalOBs_is_signals)); ArrayResize(totalOBs_is_signals,ArraySize(totalOBs_is_signals)+1); Print("New ArraySize = ",ArraySize(totalOBs_is_signals)); totalOBs_is_signals[ArraySize(totalOBs_is_signals)-1] = false; ArrayPrint(totalOBs_is_signals); } } breakoutBarIndex = -1; // Use the current bar's index (index 1 refers to the most recent completed bar) breakoutTime = 0; impulseHigh = 0; impulseLow = 0; is_OB_UP = false; is_OB_DOWN = false;
Aquí comprobamos si el bloque de pedido («is_OB_Valid») es válido. Si no es válido, utilizamos la función ObjectFind para determinar si ya existe un objeto con el nombre «obNAME» en el gráfico. Si no se encuentra el objeto (la función devuelve un valor negativo), llamamos a «CreateRec» para crear el bloque de órdenes en el gráfico utilizando los parámetros proporcionados, como la hora, el precio, el color y el texto.
Si el bloque de orden es válido, verificamos nuevamente si el objeto existe. Si no es así, lo creamos y luego gestionamos los datos del bloque de órdenes cambiando su tamaño mediante la función ArrayResize y actualizando nuestras tres matrices: «totalOBs_names» para almacenar los nombres de los bloques de órdenes, «totalOBs_dates» para las marcas de tiempo y «totalOBs_is_signals» para almacenar si cada bloque de órdenes es una señal válida (inicialmente establecida en falso). Después de cambiar el tamaño de las matrices, imprimimos los tamaños antiguos y nuevos de las matrices con ArraySize y mostramos el contenido de las matrices utilizando la función ArrayPrint. Por último, restablecemos el estado de ruptura estableciendo «breakoutBarIndex» en -1, restableciendo «breakoutTime», «impulseHigh» e «impulseLow» en 0, y estableciendo los indicadores de dirección del bloque de órdenes, «is_OB_UP» e «is_OB_DOWN», en falso.
Para crear los rectángulos con texto, utilizamos una función personalizada «CreateRec» de la siguiente manera.
void CreateRec(string objName,datetime time1,double price1, datetime time2,double price2,color clr,string txt){ if (ObjectFind(0,objName) < 0){ ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2); Print("SUCCESS CREATING OBJECT >",objName,"< WITH"," T1: ",time1,", P1: ",price1, ", T2: ",time2,", P2: ",price2); ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1); ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1); ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2); ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2); ObjectSetInteger(0,objName,OBJPROP_FILL,true); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); ObjectSetInteger(0,objName,OBJPROP_BACK,false); // Calculate the center position of the rectangle datetime midTime = time1 + (time2 - time1) / 2; double midPrice = (price1 + price2) / 2; // Create a descriptive text label centered in the rectangle string description = txt; string textObjName = objName + description; // Unique name for the text object if (ObjectFind(0, textObjName) < 0) { ObjectCreate(0, textObjName, OBJ_TEXT, 0, midTime, midPrice); ObjectSetString(0, textObjName, OBJPROP_TEXT, description); ObjectSetInteger(0, textObjName, OBJPROP_COLOR, clrBlack); ObjectSetInteger(0, textObjName, OBJPROP_FONTSIZE, 15); ObjectSetInteger(0, textObjName, OBJPROP_ANCHOR, ANCHOR_CENTER); Print("SUCCESS CREATING LABEL >", textObjName, "< WITH TEXT: ", description); } ChartRedraw(0); } }
En la función «CreateRec» que definimos, comprobamos si el objeto «objName» existe utilizando la función ObjectFind. Si no es así, creamos un rectángulo con los puntos de tiempo y precio dados utilizando la función ObjectCreate, definida por OBJ_RECTANGLE, y establecemos sus propiedades (por ejemplo, color, relleno, visibilidad) utilizando ObjectSetInteger y ObjectSetDouble. Calculamos la posición central del rectángulo y creamos una etiqueta en el centro utilizando ObjectCreate para el texto, definido por OBJ_TEXT, estableciendo sus propiedades (texto, color, tamaño, anclaje). Por último, llamamos a la función ChartRedraw para actualizar el gráfico. Si el objeto o la etiqueta ya existe, no se realiza ninguna acción.
Con los bloques de órdenes trazados, ahora podemos pasar a determinar si volvemos a probarlos y abrir posiciones cuando el precio entra y sale de sus rangos.
for (int j=ArraySize(totalOBs_names)-1; j>=0; j--){ string obNAME = totalOBs_names[j]; bool obExist = false; //Print("name = ",fvgNAME," >",ArraySize(totalFVGs)," >",j); //ArrayPrint(totalFVGs); //ArrayPrint(barTIMES); double obHigh = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,0); double obLow = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,1); datetime objTime1 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,0); datetime objTime2 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,1); color obColor = (color)ObjectGetInteger(0,obNAME,OBJPROP_COLOR); if (time(1) < objTime2){ //Print("FOUND: ",obNAME," @ bar ",j,", H: ",obHigh,", L: ",obLow); obExist = true; } double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); if (obColor == CLR_UP && Ask > obHigh && close(1) > obHigh && open(1) < obHigh && !totalOBs_is_signals[j]){ Print("BUY SIGNAL For (",obNAME,") Now @ ",Ask); double sl = Bid - 1500*_Point; double tp = Bid + 1500*_Point; obj_Trade.Buy(0.01,_Symbol,Ask,sl,tp); totalOBs_is_signals[j] = true; ArrayPrint(totalOBs_names,_Digits," [< >] "); ArrayPrint(totalOBs_is_signals,_Digits," [< >] "); } else if (obColor == CLR_DOWN && Bid < obLow && close(1) < obLow && open(1) > obLow && !totalOBs_is_signals[j]){ Print("SELL SIGNAL For (",obNAME,") Now @ ",Bid); double sl = Ask + 1500*_Point; double tp = Ask - 1500*_Point; obj_Trade.Sell(0.01,_Symbol,Bid,sl,tp); totalOBs_is_signals[j] = true; ArrayPrint(totalOBs_names,_Digits," [< >] "); ArrayPrint(totalOBs_is_signals,_Digits," [< >] "); } if (obExist == false){ bool removeName = ArrayRemove(totalOBs_names,0,1); bool removeTime = ArrayRemove(totalOBs_dates,0,1); bool remove_isSignal = ArrayRemove(totalOBs_is_signals,0,1); if (removeName && removeTime && remove_isSignal){ Print("Success removing the OB DATA from arrays. New Data as below:"); Print("Total Sizes => OBs: ",ArraySize(totalOBs_names),", TIMEs: ",ArraySize(totalOBs_dates),", SIGNALs: ",ArraySize(totalOBs_is_signals)); ArrayPrint(totalOBs_names); ArrayPrint(totalOBs_dates); ArrayPrint(totalOBs_is_signals); } } }
Aquí, recorremos la matriz «totalOBs_names» para procesar cada bloque de pedido («obNAME»). Recuperamos los precios máximos y mínimos, las marcas de tiempo y el color del bloque de órdenes utilizando las funciones ObjectGetDouble y ObjectGetInteger. Luego verificamos si la hora actual es anterior a la hora de finalización del bloque de pedido. Si se cumple la condición de tiempo, procedemos a verificar señales de compra o venta en función del color y las condiciones de precio del bloque de orden. Si se cumplen las condiciones, ejecutamos una operación de compra o venta utilizando las funciones «obj_Trade.Buy» u «obj_Trade.Sell», y actualizamos la matriz «totalOBs_is_signals» para marcar el bloque de órdenes como si hubiera activado una señal, de modo que no volvamos a operar en caso de que el precio retroceda.
Si un bloque de órdenes no cumple la condición de tiempo, lo eliminamos de las matrices «totalOBs_names», «totalOBs_dates» y «totalOBs_is_signals» utilizando la función ArrayRemove. Si la eliminación es exitosa, imprimimos tamaños y contenidos de matriz actualizados. Este es el hito actual que hemos alcanzado.

Desde la imagen podemos observar que los bloques de órdenes son detectados y negociados, lográndose nuestro objetivo, y lo que queda es realizar backtesting del programa y analizar su rendimiento. Esto se aborda en la siguiente sección.
Pruebas retrospectivas y optimización
Luego de realizar pruebas retrospectivas exhaustivas, tenemos los siguientes resultados.
Gráfico de prueba retrospectiva:

Informe de prueba retrospectiva:

Aquí también hay un formato de video que muestra toda la estrategia de prueba retrospectiva dentro de un período de 1 año, 2024.
Conclusión
En conclusión, hemos demostrado el proceso de desarrollo de un Asesor Experto (EA) MQL5 sofisticado que aprovecha la detección de bloques de órdenes para estrategias de negociación de dinero inteligente. Al incorporar herramientas como el análisis de rango dinámico, la acción del precio y la detección de rupturas en tiempo real, creamos un programa que puede identificar niveles clave de soporte y resistencia, generar señales comerciales procesables y gestionar órdenes con alta precisión.
Descargo de responsabilidad: este artículo está destinado únicamente a fines educativos. El trading conlleva un riesgo financiero sustancial y el comportamiento del mercado puede ser muy impredecible. Las estrategias descritas en este artículo ofrecen un enfoque estructurado pero no garantizan la rentabilidad futura. Es fundamental realizar pruebas adecuadas y gestionar los riesgos antes de operar en vivo.
Al aplicar estos métodos, puede crear sistemas de trading más efectivos, perfeccionar su enfoque del análisis de mercado y llevar su trading algorítmico al siguiente nivel. ¡Mucha suerte en tu viaje en el trading!
Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/17135
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.
Dominando JSON: Crea tu propio lector JSON desde cero en MQL5
Desarrollo de un kit de herramientas para el análisis de la acción del precio (Parte 12): Flujo externo (III) TrendMap
Características del Wizard MQL5 que debe conocer (Parte 54): Aprendizaje por refuerzo con SAC híbrido y tensores
Introducción a MQL5 (Parte 12): Guía para principiantes sobre cómo crear indicadores personalizados
- 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