English Русский Deutsch 日本語
preview
Automatización de estrategias de trading en MQL5 (Parte 35): Creación de un sistema de trading con bloques de ruptura

Automatización de estrategias de trading en MQL5 (Parte 35): Creación de un sistema de trading con bloques de ruptura

MetaTrader 5 — Trading |
11 2
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 34), desarrollamos un sistema de ruptura de líneas de tendencia en MetaQuotes Language 5 (MQL5) que identificaba líneas de tendencia de soporte y resistencia a partir de puntos de oscilación y las validaba mediante el coeficiente de determinación R², para ejecutar operaciones de ruptura con visualizaciones dinámicas en el gráfico. En la Parte 35, creamos un sistema de trading basado en bloques de ruptura que detecta rangos de consolidación, valida los bloques de ruptura mediante puntos de oscilación y opera en los retesteos con parámetros de riesgo personalizables e información visual. Hoy hablaremos de los siguientes temas:

  1. Comprender el marco de la estrategia de bloques de ruptura
  2. Implementación en MQL5
  3. Backtesting
  4. Conclusión

Al final, tendrás una estrategia funcional en MQL5 para operar en los retesteos de bloques de ruptura, lista para personalizarla. ¡Manos a la obra!


Comprender el marco de la estrategia de bloques de ruptura

La estrategia de bloques de ruptura es una estrategia de trading que identifica rangos de consolidación en los que el precio se mueve dentro de un rango estrecho, tras los cuales se producen una ruptura y un movimiento impulsivo que forman bloques de órdenes; cuando estos se invalidan, se convierten en bloques de ruptura que ofrecen posibles oportunidades para operar en los retesteos. Aprovecha el retorno del precio a estos bloques después de haberse alejado considerablemente de ellos, abriendo posiciones en la dirección de la ruptura con niveles de stop loss y de take profit definidos y con el apoyo de elementos visuales en el gráfico. Echa un vistazo a los distintos bloques de ruptura que podemos encontrar a continuación.

Bloque de ruptura bajista:

BLOQUE DE RUPTURA BAJISTA

Bloque de ruptura alcista:

BLOQUE DE RUPTURA ALCISTA

Nuestro plan consiste en detectar rangos de consolidación mediante el análisis de un número determinado de barras, identificar las rupturas cuando el precio sale del rango y confirmar los movimientos impulsivos utilizando un umbral basado en un multiplicador. Implementaremos una lógica para validar los bloques de ruptura mediante puntos de oscilación, ejecutar operaciones en los retesteos con parámetros personalizables y visualizar los bloques con etiquetas y flechas dinámicas, creando así un sistema para identificar y aprovechar oportunidades de trading con bloques de ruptura. En resumen, aquí tienes una representación gráfica de nuestros objetivos.

ESTRUCTURA DEL BLOQUE DE RUPTURA


Implementación en MQL5

Para crear el programa en MQL5, abre MetaEditor, ve al Navegador, busca la carpeta «Experts», haz clic en la pestaña «Nuevo» y sigue las instrucciones para crear el archivo. Una vez creado, en el entorno de programación, tendremos que declarar algunos parámetros de entrada y variables globales que utilizaremos a lo largo de todo el programa.

//+------------------------------------------------------------------+
//|                                    Breaker Block Strategy EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Forex Algo-Trader, Allan"
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

#include <Trade/Trade.mqh>                         //--- Include Trade library for position management
CTrade obj_Trade;                                  //--- Instantiate trade object for order operations

Comenzamos la implementación incluyendo la biblioteca de operaciones con «#include <Trade/Trade.mqh>», que proporciona funciones integradas para gestionar las operaciones de trading. A continuación, inicializamos el objeto de operaciones «obj_Trade» utilizando la clase CTrade, lo que permite al asesor experto ejecutar órdenes de compra y venta mediante código. Esta configuración garantizará que la ejecución de las operaciones se lleve a cabo de forma eficiente sin necesidad de intervención manual. A continuación, podemos definir algunos parámetros de entrada para que el usuario pueda modificar y controlar el comportamiento desde la interfaz de usuario (UI).

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input double tradeLotSize = 0.01;                  // Trade size for each position in lots
input bool   enableTrading = true;                 // Toggle to enable or disable automated trading
input bool   enableTrailingStop = true;            // Toggle to enable or disable trailing stop
input double trailingStopPoints = 30;              // Distance in points for trailing stop adjustment
input double minProfitToTrail = 50;                // Minimum profit in points before trailing starts
input int    uniqueMagicNumber = 12345;            // Unique identifier for EA trades
input int    consolidationBars = 7;                // Number of bars to check for consolidation range
input double maxConsolidationSpread = 50;          // Maximum allowed spread in points for consolidation
input int    barsToWaitAfterBreakout = 3;          // Bars to wait after breakout before impulse check
input double impulseMultiplier = 1.0;              // Multiplier for detecting impulsive price moves
input double stopLossDistance = 1500;              // Stop loss distance in points from entry
input double takeProfitDistance = 1500;            // Take profit distance in points from entry
input double moveAwayDistance = 50;                // Distance in points for price to move away post-invalidation
input color  bullishColor = clrGreen;              // Base color for bullish order/breaker blocks
input color  bearishColor = clrRed;                // Base color for bearish order/breaker blocks
input color  labelTextColor = clrBlack;            // Color for text labels on blocks
input bool   enableSwingValidation = true;         // Enable validation of swing points for block invalidation
input bool   showSwingPoints = true;               // Show swing point labels if validation enabled
input color  swingLabelColor = clrWhite;           // Color for swing point labels
input int    swingFontSize = 10;                   // Font size for swing point labels

Aquí definimos los parámetros de entrada para configurar el comportamiento del programa. «tradeLotSize» establece el tamaño de la posición, mientras que «enableTrading» y «enableTrailingStop» controlan la ejecución y los stops dinámicos, y «trailingStopPoints» y «minProfitToTrail» ajustan la lógica de los stops. «uniqueMagicNumber» identifica las operaciones, y la consolidación se detecta mediante «consolidationBars» y «maxConsolidationSpread». El resto de los parámetros de entrada se explican por sí mismos. Hemos añadido comentarios para facilitar su comprensión. Por último, debemos definir algunas variables globales que utilizaremos para el control general del sistema.

//+------------------------------------------------------------------+
//| Structure for price and index                                    |
//+------------------------------------------------------------------+
struct PriceAndIndex {                             //--- Define structure for price and index data
   double price;                                   //--- Store price value (high or low)
   int    index;                                   //--- Store bar index of price
};

//+------------------------------------------------------------------+
//| Global variables for market state tracking                       |
//+------------------------------------------------------------------+
PriceAndIndex rangeHighestHigh = {0, 0};           //--- Track highest high in consolidation range
PriceAndIndex rangeLowestLow = {0, 0};             //--- Track lowest low in consolidation range
bool   isBreakoutDetected = false;                 //--- Flag for breakout detection
double lastImpulseLow = 0.0;                       //--- Store low price after breakout for impulse
double lastImpulseHigh = 0.0;                      //--- Store high price after breakout for impulse
int    breakoutBarNumber = -1;                     //--- Store bar index of breakout
datetime breakoutTimestamp = 0;                    //--- Store timestamp of breakout
string   blockNames[];                             //--- Store names of block objects
datetime blockEndTimes[];                          //--- Store end times of blocks
bool     invalidatedStatus[];                      //--- Track invalidation status of blocks
string   blockTypes[];                             //--- Track block types (OB/BB, bullish/bearish)
bool     movedAwayStatus[];                        //--- Track if price moved away after invalidation
bool     retestedStatus[];                         //--- Track if block was retested
string   blockLabels[];                            //--- Store label object names for blocks
datetime creationTimes[];                          //--- Store creation times of blocks
datetime invalidationTimes[];                      //--- Store invalidation times of blocks
double   invalidationSwings[];                     //--- Store swing high/low at invalidation
bool     isBullishImpulse = false;                 //--- Flag for bullish impulsive move
bool     isBearishImpulse = false;                 //--- Flag for bearish impulsive move
#define OB_Prefix "OB REC "                        //--- Define prefix for order block names

Aquí definimos las estructuras de datos y los mecanismos de seguimiento del estado del sistema. En primer lugar, definimos la estructura «PriceAndIndex» para almacenar un valor de precio (máximo o mínimo) y su índice de barra correspondiente, lo que permite un seguimiento preciso de los límites del rango de consolidación. A continuación, inicializamos las variables globales: «rangeHighestHigh» y «rangeLowestLow» como instancias de «PriceAndIndex» para almacenar el máximo más alto y el mínimo más bajo del rango de consolidación; «isBreakoutDetected» con el valor «false» para señalar los eventos de ruptura; «lastImpulseLow» y «lastImpulseHigh» con el valor 0.0 para registrar los precios posteriores a la ruptura y comprobar los impulsos; «breakoutBarNumber» con el valor -1 y «breakoutTimestamp» con el valor 0 para realizar un seguimiento del momento de la ruptura; y las matrices «blockNames», «blockEndTimes», «invalidatedStatus», «blockTypes», «movedAwayStatus», «retestedStatus», «blockLabels», «creationTimes», «invalidationTimes» e «invalidationSwings» para gestionar los objetos de bloque, su caducidad, invalidación, tipos (bloque de órdenes o bloque de ruptura, alcista o bajista), estado de retesteo y puntos de oscilación asociados.

Por último, definimos «OB_Prefix» como «OB REC » para garantizar la coherencia en la denominación de los objetos de bloque de órdenes. Definamos ahora algunas funciones auxiliares que necesitaremos para oscurecer el color de los bloques de órdenes invalidados y gestionar los manejadores de eventos.

//+------------------------------------------------------------------+
//| Darken color by factor                                           |
//+------------------------------------------------------------------+
color DarkenColor(color colorValue, double factor = 0.8) {
   int red = int((colorValue & 0xFF) * factor);          //--- Extract and darken red component
   int green = int(((colorValue >> 8) & 0xFF) * factor); //--- Extract and darken green component
   int blue = int(((colorValue >> 16) & 0xFF) * factor); //--- Extract and darken blue component
   return (color)(red | (green << 8) | (blue << 16));    //--- Combine components into color
}

//+------------------------------------------------------------------+
//| Price data accessors                                             |
//+------------------------------------------------------------------+
double high(int index) {
   return iHigh(_Symbol, _Period, index);         //--- Return high price for specified index
}
double low(int index) {
   return iLow(_Symbol, _Period, index);          //--- Return low price for specified index
}
double close(int index) {
   return iClose(_Symbol, _Period, index);        //--- Return close price for specified index
}
datetime time(int index) {
   return iTime(_Symbol, _Period, index);         //--- Return time for specified index
}


//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(uniqueMagicNumber); //--- Set magic number for trade identification
   return(INIT_SUCCEEDED);                            //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, OB_Prefix);                //--- Remove all objects with OB prefix
   ChartRedraw(0);                                //--- Redraw chart to clear objects
}

Seguimos implementando funciones de utilidad y de gestión del ciclo de vida de nuestro sistema. En primer lugar, desarrollamos la función «DarkenColor», que toma un valor de color y un factor opcional (por defecto, 0.8), extrae los componentes de rojo, verde y azul mediante operaciones bit a bit («colorValue & 0xFF», «(colorValue >> 8) & 0xFF», «(colorValue >> 16) & 0xFF»), los oscurece multiplicándolos por el factor y los combina mediante desplazamientos bit a bit («red | (green << 8) | (blue << 16)») para devolver un color más oscuro que permita distinguir visualmente los bloques invalidados.

A continuación, creamos las funciones de acceso «high», «low», «close» y «time», que devuelven el precio máximo (iHigh), el precio mínimo («iLow»), el precio de cierre («iClose») y la marca de tiempo (iTime) para un índice de barra determinado del símbolo y el periodo actuales, lo que simplifica la obtención de los datos de precios. A continuación, en la función OnInit, llamamos a «SetExpertMagicNumber» en «obj_Trade» con «uniqueMagicNumber» para etiquetar las operaciones a efectos de identificación y devolvemos INIT_SUCCEEDED si la inicialización se ha realizado correctamente. Por último, en la función OnDeinit, utilizamos ObjectsDeleteAll para eliminar todos los objetos del gráfico cuyo nombre comience por el prefijo indicado en «OB_Prefix» y llamamos a ChartRedraw para actualizar el gráfico, lo que garantiza una limpieza adecuada de los recursos. Ahora podemos pasar al manejador de eventos principal OnTick para implementar nuestra lógica de control principal.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   static bool isNewBar = false;                  //--- Track new bar status
   int currentBarCount = iBars(_Symbol, _Period); //--- Get current bar count
   static int previousBarCount = currentBarCount; //--- Store previous bar count
   if (previousBarCount == currentBarCount) {     //--- Check if no new bar
      isNewBar = false;                           //--- Set no new bar
   } else {                                       //--- New bar detected
      isNewBar = true;                            //--- Set new bar flag
      previousBarCount = currentBarCount;         //--- Update previous bar count
   }
   if (!isNewBar) return;                         //--- Exit if not new bar
   int startBarIndex = 1;                         //--- Set start index for analysis
   int chartScale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get chart zoom scale
   int dynamicFontSize = 8 + (chartScale * 2);    //--- Calculate dynamic font size
   if (!isBreakoutDetected) {                     //--- Check if no breakout detected
      if (rangeHighestHigh.price == 0 && rangeLowestLow.price == 0) { //--- Check if range not set
         bool isConsolidated = true;              //--- Assume consolidation
         for (int i = startBarIndex; i < startBarIndex + consolidationBars - 1; i++) { //--- Iterate consolidation bars
            if (MathAbs(high(i) - high(i + 1)) > maxConsolidationSpread * Point() || MathAbs(low(i) - low(i + 1)) > maxConsolidationSpread * Point()) { //--- Check spread
               isConsolidated = false;            //--- Mark as not consolidated
               break;                             //--- Exit loop
            }
         }
         if (isConsolidated) {                    //--- Confirm consolidation
            rangeHighestHigh.price = high(startBarIndex); //--- Set initial high
            rangeHighestHigh.index = startBarIndex; //--- Set high index
            for (int i = startBarIndex + 1; i < startBarIndex + consolidationBars; i++) { //--- Find highest high
               if (high(i) > rangeHighestHigh.price) { //--- Check higher high
                  rangeHighestHigh.price = high(i); //--- Update highest high
                  rangeHighestHigh.index = i;    //--- Update high index
               }
            }
            rangeLowestLow.price = low(startBarIndex); //--- Set initial low
            rangeLowestLow.index = startBarIndex; //--- Set low index
            for (int i = startBarIndex + 1; i < startBarIndex + consolidationBars; i++) { //--- Find lowest low
               if (low(i) < rangeLowestLow.price) { //--- Check lower low
                  rangeLowestLow.price = low(i);  //--- Update lowest low
                  rangeLowestLow.index = i;       //--- Update low index
               }
            }
            Print("Consolidation range established: Highest High = ", rangeHighestHigh.price, " at index ", rangeHighestHigh.index, " and Lowest Low = ", rangeLowestLow.price, " at index ", rangeLowestLow.index); //--- Log range
         }
      } else {                                    //--- Range already set
         double currentHigh = high(1);            //--- Get current high
         double currentLow = low(1);              //--- Get current low
         if (currentHigh <= rangeHighestHigh.price && currentLow >= rangeLowestLow.price) { //--- Check within range
            Print("Range extended: High = ", currentHigh, ", Low = ", currentLow); //--- Log range extension
         } else {                                 //--- Outside range
            Print("No extension: Bar outside range."); //--- Log no extension
         }
      }
   }
}

Comenzamos definiendo una lógica para detectar, en primer lugar, los rangos de consolidación. En la función OnTick, detectamos nuevas barras comparando el número actual de barras obtenido con iBars con la variable estática «previousBarCount». Si se detecta una nueva barra, establecemos «isNewBar» en «true» y actualizamos «previousBarCount»; de lo contrario, establecemos «isNewBar» en «false» y salimos de la función. A continuación, recuperamos la escala de zoom del gráfico con ChartGetInteger utilizando CHART_SCALE y calculamos un «dynamicFontSize» igual a 8 más el doble de la escala para ajustar el tamaño de las etiquetas de forma adaptativa. Si no se detecta ninguna ruptura («isBreakoutDetected» es falso) y no hay ningún rango establecido («rangeHighestHigh.price» y «rangeLowestLow.price» son 0), comprobamos si hay consolidación recorriendo «consolidationBars» barras a partir de «startBarIndex», cuyo valor es 1. Utilizamos Point() en el límite «maxConsolidationSpread * Point()» y MathAbs para comprobar que las diferencias entre los máximos de barras adyacentes y entre sus mínimos sean inferiores a dicho límite; si se supera, establecemos «isConsolidated» en falso.

Si se confirma la consolidación, asignamos a «rangeHighestHigh.price» y «rangeLowestLow.price» el máximo y el mínimo de la barra con índice «startBarIndex», respectivamente, utilizando «high» y «low». A continuación, recorremos «consolidationBars» barras para actualizar esos valores con el máximo más alto y el mínimo más bajo, junto con sus índices, y registramos el rango con la función Print. Si existe un rango, comprobamos si el máximo y el mínimo de la última barra cerrada («high(1)», «low(1)») se encuentran entre los límites «rangeLowestLow.price» y «rangeHighestHigh.price»; si es así, registramos la prolongación del rango y, si quedan fuera, registramos que no se prolonga. Ahora podemos utilizar los precios para detectar, visualizar y gestionar los bloques de órdenes antes de utilizarlos para un análisis más detallado, ya que primero debemos invalidarlos antes de que se conviertan en bloques de ruptura.

if (rangeHighestHigh.price > 0 && rangeLowestLow.price > 0) { //--- Check if range defined
   double currentClosePrice = close(1);        //--- Get current close price
   if (currentClosePrice > rangeHighestHigh.price) { //--- Check upward breakout
      Print("Upward breakout at ", currentClosePrice, " > ", rangeHighestHigh.price); //--- Log breakout
      isBreakoutDetected = true;               //--- Set breakout flag
   } else if (currentClosePrice < rangeLowestLow.price) { //--- Check downward breakout
      Print("Downward breakout at ", currentClosePrice, " < ", rangeLowestLow.price); //--- Log breakout
      isBreakoutDetected = true;               //--- Set breakout flag
   }
}
if (isBreakoutDetected) {                      //--- Process breakout
   Print("Breakout detected. Resetting for the next range."); //--- Log reset
   breakoutBarNumber = 1;                      //--- Set breakout bar index
   breakoutTimestamp = TimeCurrent();          //--- Set breakout timestamp
   lastImpulseHigh = rangeHighestHigh.price;   //--- Store high for impulse check
   lastImpulseLow = rangeLowestLow.price;      //--- Store low for impulse check
   isBreakoutDetected = false;                 //--- Reset breakout flag
   rangeHighestHigh.price = 0;                 //--- Clear highest high
   rangeHighestHigh.index = 0;                 //--- Clear high index
   rangeLowestLow.price = 0;                   //--- Clear lowest low
   rangeLowestLow.index = 0;                   //--- Clear low index
}
if (breakoutBarNumber >= 0 && TimeCurrent() > breakoutTimestamp + barsToWaitAfterBreakout * PeriodSeconds()) { //--- Check impulse window
   double impulseRange = lastImpulseHigh - lastImpulseLow; //--- Calculate impulse range
   double impulseThresholdPrice = impulseRange * impulseMultiplier; //--- Calculate impulse threshold
   isBullishImpulse = false;                   //--- Reset bullish impulse flag
   isBearishImpulse = false;                   //--- Reset bearish impulse flag
   for (int i = 1; i <= barsToWaitAfterBreakout; i++) { //--- Check bars for impulse
      double closePrice = close(i);            //--- Get close price
      if (closePrice >= lastImpulseHigh + impulseThresholdPrice) { //--- Check bullish impulse
         isBullishImpulse = true;              //--- Set bullish impulse flag
         Print("Impulsive upward move: ", closePrice, " >= ", lastImpulseHigh + impulseThresholdPrice); //--- Log bullish impulse
         break;                                //--- Exit loop
      } else if (closePrice <= lastImpulseLow - impulseThresholdPrice) { //--- Check bearish impulse
         isBearishImpulse = true;              //--- Set bearish impulse flag
         Print("Impulsive downward move: ", closePrice, " <= ", lastImpulseLow - impulseThresholdPrice); //--- Log bearish impulse
         break;                                //--- Exit loop
      }
   }
   if (!isBullishImpulse && !isBearishImpulse) { //--- Check no impulse
      Print("No impulsive movement detected."); //--- Log no impulse
   }
   bool isOrderBlockValid = isBearishImpulse || isBullishImpulse; //--- Validate order block
   if (isOrderBlockValid) {                    //--- Process valid order block
      datetime blockStartTime = iTime(_Symbol, _Period, consolidationBars + barsToWaitAfterBreakout + 1); //--- Set block start time
      double blockTopPrice = lastImpulseHigh;  //--- Set block top price
      int visibleBarsOnChart = (int)ChartGetInteger(0, CHART_VISIBLE_BARS); //--- Get visible bars
      datetime blockEndTime = blockStartTime + (visibleBarsOnChart / 1) * PeriodSeconds(); //--- Set block end time
      double blockBottomPrice = lastImpulseLow; //--- Set block bottom price
      string blockName = OB_Prefix + "(" + TimeToString(blockStartTime) + ")"; //--- Generate block name
      color blockColor = isBullishImpulse ? bullishColor : bearishColor; //--- Set block color
      string blockLabel = isBullishImpulse ? "Bullish Order Block" : "Bearish Order Block"; //--- Set block label
      string blockType = isBullishImpulse ? "OB-bullish" : "OB-bearish"; //--- Set block type
      if (ObjectFind(0, blockName) < 0) {      //--- Check if block exists
         ObjectCreate(0, blockName, OBJ_RECTANGLE, 0, blockStartTime, blockTopPrice, blockEndTime, blockBottomPrice); //--- Create block rectangle
         ObjectSetInteger(0, blockName, OBJPROP_TIME, 0, blockStartTime); //--- Set start time
         ObjectSetDouble(0, blockName, OBJPROP_PRICE, 0, blockTopPrice); //--- Set top price
         ObjectSetInteger(0, blockName, OBJPROP_TIME, 1, blockEndTime); //--- Set end time
         ObjectSetDouble(0, blockName, OBJPROP_PRICE, 1, blockBottomPrice); //--- Set bottom price
         ObjectSetInteger(0, blockName, OBJPROP_FILL, true); //--- Enable fill
         ObjectSetInteger(0, blockName, OBJPROP_COLOR, blockColor); //--- Set block color
         ObjectSetInteger(0, blockName, OBJPROP_BACK, false); //--- Set to foreground
         datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; //--- Calculate label time
         double labelPrice = (blockTopPrice + blockBottomPrice) / 2; //--- Calculate label price
         string labelObjectName = blockName + " Label"; //--- Generate label name
         ObjectCreate(0, labelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Create label
         ObjectSetString(0, labelObjectName, OBJPROP_TEXT, blockLabel); //--- Set label text
         ObjectSetInteger(0, labelObjectName, OBJPROP_COLOR, labelTextColor); //--- Set label color
         ObjectSetInteger(0, labelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set label font size
         ObjectSetInteger(0, labelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Set label anchor
         ChartRedraw(0);                       //--- Redraw chart
         ArrayResize(blockNames, ArraySize(blockNames) + 1); //--- Resize block names array
         blockNames[ArraySize(blockNames) - 1] = blockName; //--- Add block name
         ArrayResize(blockEndTimes, ArraySize(blockEndTimes) + 1); //--- Resize end times array
         blockEndTimes[ArraySize(blockEndTimes) - 1] = blockEndTime; //--- Add end time
         ArrayResize(invalidatedStatus, ArraySize(invalidatedStatus) + 1); //--- Resize invalidated status
         invalidatedStatus[ArraySize(invalidatedStatus) - 1] = false; //--- Set invalidated status
         ArrayResize(blockTypes, ArraySize(blockTypes) + 1); //--- Resize block types array
         blockTypes[ArraySize(blockTypes) - 1] = blockType; //--- Add block type
         ArrayResize(movedAwayStatus, ArraySize(movedAwayStatus) + 1); //--- Resize moved away status
         movedAwayStatus[ArraySize(movedAwayStatus) - 1] = false; //--- Set moved away status
         ArrayResize(retestedStatus, ArraySize(retestedStatus) + 1); //--- Resize retested status
         retestedStatus[ArraySize(retestedStatus) - 1] = false; //--- Set retested status
         ArrayResize(blockLabels, ArraySize(blockLabels) + 1); //--- Resize block labels array
         blockLabels[ArraySize(blockLabels) - 1] = labelObjectName; //--- Add label name
         ArrayResize(creationTimes, ArraySize(creationTimes) + 1); //--- Resize creation times
         creationTimes[ArraySize(creationTimes) - 1] = time(1); //--- Set creation time
         ArrayResize(invalidationTimes, ArraySize(invalidationTimes) + 1); //--- Resize invalidation times
         invalidationTimes[ArraySize(invalidationTimes) - 1] = 0; //--- Set invalidation time
         ArrayResize(invalidationSwings, ArraySize(invalidationSwings) + 1); //--- Resize invalidation swings
         invalidationSwings[ArraySize(invalidationSwings) - 1] = 0.0; //--- Set invalidation swing
         Print("Order Block created: ", blockName); //--- Log block creation
      }
   }
   breakoutBarNumber = -1;                     //--- Reset breakout bar
   breakoutTimestamp = 0;                      //--- Reset breakout timestamp
   lastImpulseHigh = 0;                        //--- Reset impulse high
   lastImpulseLow = 0;                         //--- Reset impulse low
   isBullishImpulse = false;                   //--- Reset bullish impulse
   isBearishImpulse = false;                   //--- Reset bearish impulse
}

Aquí implementamos la detección de rupturas y la lógica de creación de bloques de órdenes. En primer lugar, comprobamos si hay definido un rango de consolidación («rangeHighestHigh.price > 0» y «rangeLowestLow.price > 0») y obtenemos el precio de cierre de la última barra cerrada con «close(1)». Si supera «rangeHighestHigh.price», registramos una ruptura al alza y establecemos «isBreakoutDetected» en «true»; si está por debajo de «rangeLowestLow.price», registramos una ruptura a la baja y también establecemos el indicador en «true». A continuación, si «isBreakoutDetected» es verdadero, registramos la ruptura, establecemos «breakoutBarNumber» en 1 y «breakoutTimestamp» en TimeCurrent, almacenamos «lastImpulseHigh» y «lastImpulseLow», y restablecemos las variables de rango y el indicador de ruptura.

A continuación, si «breakoutBarNumber» es un número no negativo y la hora actual supera «breakoutTimestamp + barsToWaitAfterBreakout * PeriodSeconds», calculamos «impulseRange» («lastImpulseHigh - lastImpulseLow») y el umbral («impulseRange * impulseMultiplier»). Después, comprobamos si alguna de las «barsToWaitAfterBreakout» barras tiene un precio de cierre superior a «lastImpulseHigh + impulseThresholdPrice» (en cuyo caso activamos «isBullishImpulse») o inferior a «lastImpulseLow - impulseThresholdPrice» (en cuyo caso activamos «isBearishImpulse»).

Si no se detecta ningún impulso, lo registramos; si el impulso es válido («isBearishImpulse» o «isBullishImpulse»), creamos un bloque de órdenes utilizando ObjectCreate (OBJ_RECTANGLE), con «blockStartTime» obtenido de iTime, los límites superior e inferior tomados de «lastImpulseHigh» y «lastImpulseLow», respectivamente, y la hora de finalización basada en «ChartGetInteger(CHART_VISIBLE_BARS)». Configuramos propiedades como «OBJPROP_FILL» y «OBJPROP_COLOR» («bullishColor» o «bearishColor»), añadimos una etiqueta centrada con «blockLabel» mediante «OBJ_TEXT» y actualizamos las matrices «blockNames», «blockEndTimes», «invalidatedStatus», «blockTypes», «movedAwayStatus», «retestedStatus», «blockLabels», «creationTimes» e «invalidationSwings». Por último, restablecemos las variables de ruptura. Esto permite crear un sistema para detectar rupturas y visualizar bloques de órdenes. Tras la compilación, obtenemos el siguiente resultado.

DETECCIÓN DE BLOQUES DE ÓRDENES INICIALES

Ahora que podemos detectar los bloques de órdenes iniciales, definamos una lógica para que, cuando se produzca la correspondiente ruptura del precio, los marquemos como invalidados y confirmemos dicha invalidación mediante los puntos de oscilación de la acción del precio. Oscureceremos su color para diferenciarlos.

for (int j = ArraySize(blockNames) - 1; j >= 0; j--) { //--- Iterate through blocks
   string currentBlockName = blockNames[j];    //--- Get current block name
   bool doesBlockExist = false;                //--- Initialize block existence flag
   double blockHigh = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 0); //--- Get block high
   double blockLow = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 1); //--- Get block low
   datetime blockStartTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 0); //--- Get block start
   datetime blockEndTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 1); //--- Get block end
   color blockCurrentColor = (color)ObjectGetInteger(0, currentBlockName, OBJPROP_COLOR); //--- Get block color
   if (time(1) < blockEndTime) {               //--- Check if block still valid
      doesBlockExist = true;                   //--- Set block exists
   }
   if (StringFind(blockTypes[j], "OB-") == 0 && !invalidatedStatus[j]) { //--- Check valid order block
      bool invalidated = false;                //--- Initialize invalidation flag
      string newBlockType = "";                //--- Initialize new block type
      color invalidatedColor = clrNONE;        //--- Initialize invalidated color
      string newLabel = "";                    //--- Initialize new label
      bool isForBullishBB = false;             //--- Initialize bullish breaker block flag
      double breakPrice = 0.0;                 //--- Initialize break price
      int arrowCode = 0;                       //--- Initialize arrow code
      int anchor = 0;                          //--- Initialize anchor
      if (blockTypes[j] == "OB-bearish" && close(1) > blockHigh) { //--- Check bearish block invalidation
         isForBullishBB = true;                //--- Set bullish breaker block
         breakPrice = blockHigh;               //--- Set break price
         arrowCode = 233;                      //--- Set upward arrow
         anchor = ANCHOR_BOTTOM;               //--- Set bottom anchor
         newBlockType = "Invalidated-bearish"; //--- Set invalidated type
         invalidatedColor = DarkenColor(bearishColor); //--- Darken bearish color
         newLabel = "Invalidated Bearish Order Block"; //--- Set invalidated label
      } else if (blockTypes[j] == "OB-bullish" && close(1) < blockLow) { //--- Check bullish block invalidation
         isForBullishBB = false;               //--- Set bearish breaker block
         breakPrice = blockLow;                //--- Set break price
         arrowCode = 234;                      //--- Set downward arrow
         anchor = ANCHOR_TOP;                  //--- Set top anchor
         newBlockType = "Invalidated-bullish"; //--- Set invalidated type
         invalidatedColor = DarkenColor(bullishColor); //--- Darken bullish color
         newLabel = "Invalidated Bullish Order Block"; //--- Set invalidated label
      } else {                                 //--- No invalidation
         continue;                             //--- Skip to next block
      }
      bool validSwingForInvalidation = true;   //--- Assume valid swing
      int swingShift = -1;                     //--- Initialize swing shift
      double swingPrice = 0.0;                 //--- Initialize swing price
      if (enableSwingValidation) {             //--- Check swing validation
         int creationShift = iBarShift(_Symbol, _Period, creationTimes[j], false); //--- Get creation bar shift
         if (creationShift > 1) {              //--- Ensure enough bars
            double extreme = isForBullishBB ? blockLow : blockHigh; //--- Set extreme price
            bool isBearishOB = isForBullishBB; //--- Set bearish OB flag
            if (isBearishOB) {                 //--- Handle bearish OB
               double minLow = extreme;        //--- Initialize minimum low
               for (int k = creationShift - 1; k > 1; k--) { //--- Find lower low
                  if (low(k) < minLow) {       //--- Check lower low
                     minLow = low(k);          //--- Update minimum low
                     swingShift = k;           //--- Update swing shift
                  }
               }
               validSwingForInvalidation = minLow < extreme; //--- Validate swing
               swingPrice = minLow;            //--- Set swing price
            } else {                           //--- Handle bullish OB
               double maxHigh = extreme;       //--- Initialize maximum high
               for (int k = creationShift - 1; k > 1; k--) { //--- Find higher high
                  if (high(k) > maxHigh) {     //--- Check higher high
                     maxHigh = high(k);        //--- Update maximum high
                     swingShift = k;           //--- Update swing shift
                  }
               }
               validSwingForInvalidation = maxHigh > extreme; //--- Validate swing
               swingPrice = maxHigh;           //--- Set swing price
            }
         } else {                              //--- Insufficient bars
            validSwingForInvalidation = false; //--- Invalidate swing
         }
      }
      if (validSwingForInvalidation) {        //--- Confirm swing validation
         invalidated = true;                  //--- Set invalidated flag
      }
      if (invalidated) {                      //--- Process invalidation
         ObjectSetInteger(0, currentBlockName, OBJPROP_COLOR, invalidatedColor); //--- Update block color
         ObjectDelete(0, blockLabels[j]);     //--- Delete old label
         datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; //--- Calculate new label time
         double labelPrice = (blockHigh + blockLow) / 2; //--- Calculate new label price
         string newLabelObjectName = currentBlockName + " Label"; //--- Generate new label name
         ObjectCreate(0, newLabelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Create new label
         ObjectSetString(0, newLabelObjectName, OBJPROP_TEXT, newLabel); //--- Set label text
         ObjectSetInteger(0, newLabelObjectName, OBJPROP_COLOR, labelTextColor); //--- Set label color
         ObjectSetInteger(0, newLabelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set label font size
         ObjectSetInteger(0, newLabelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Set label anchor
         string arrowName = currentBlockName + "_break_arrow"; //--- Generate arrow name
         if (ObjectFind(0, arrowName) < 0) {  //--- Check if arrow exists
            ObjectCreate(0, arrowName, OBJ_ARROW, 0, time(1), breakPrice); //--- Create break arrow
            ObjectSetInteger(0, arrowName, OBJPROP_ARROWCODE, arrowCode); //--- Set arrow code
            ObjectSetInteger(0, arrowName, OBJPROP_ANCHOR, anchor); //--- Set arrow anchor
            ObjectSetInteger(0, arrowName, OBJPROP_COLOR, invalidatedColor); //--- Set arrow color
         }
         if (enableSwingValidation && showSwingPoints && swingShift > 0) { //--- Check swing point display
            string swingLabelName = currentBlockName + "_invalid_swing"; //--- Generate swing label name
            if (ObjectFind(0, swingLabelName) < 0) { //--- Check if swing label exists
               datetime swingTime = time(swingShift); //--- Get swing time
               ObjectCreate(0, swingLabelName, OBJ_TEXT, 0, swingTime, swingPrice); //--- Create swing label
               string swingText = isForBullishBB ? "LL" : "HH"; //--- Set swing text
               ObjectSetString(0, swingLabelName, OBJPROP_TEXT, swingText); //--- Set swing label text
               ObjectSetInteger(0, swingLabelName, OBJPROP_COLOR, swingLabelColor); //--- Set swing label color
               ObjectSetInteger(0, swingLabelName, OBJPROP_FONTSIZE, swingFontSize); //--- Set swing label font size
               ObjectSetInteger(0, swingLabelName, OBJPROP_ANCHOR, isForBullishBB ? ANCHOR_LEFT_UPPER : ANCHOR_LEFT_LOWER); //--- Set swing label anchor
            }
         }
         ChartRedraw(0);                       //--- Redraw chart
         invalidatedStatus[j] = true;          //--- Set invalidated status
         blockTypes[j] = newBlockType;         //--- Update block type
         movedAwayStatus[j] = false;           //--- Reset moved away status
         retestedStatus[j] = false;            //--- Reset retested status
         blockLabels[j] = newLabelObjectName;  //--- Update label name
         invalidationTimes[j] = time(1);       //--- Set invalidation time
         invalidationSwings[j] = isForBullishBB ? high(1) : low(1); //--- Set invalidation swing
         Print("Order Block invalidated: ", currentBlockName); //--- Log invalidation
      }
   }
   if (!doesBlockExist) {                     //--- Check if block expired
      ArrayRemove(blockNames, j, 1);          //--- Remove block name
      ArrayRemove(blockEndTimes, j, 1);       //--- Remove end time
      ArrayRemove(invalidatedStatus, j, 1);   //--- Remove invalidated status
      ArrayRemove(blockTypes, j, 1);          //--- Remove block type
      ArrayRemove(movedAwayStatus, j, 1);     //--- Remove moved away status
      ArrayRemove(retestedStatus, j, 1);      //--- Remove retested status
      ArrayRemove(blockLabels, j, 1);         //--- Remove label name
      ArrayRemove(creationTimes, j, 1);       //--- Remove creation time
      ArrayRemove(invalidationTimes, j, 1);   //--- Remove invalidation time
      ArrayRemove(invalidationSwings, j, 1);  //--- Remove invalidation swing
      Print("Removed expired block at index ", j); //--- Log block removal
   }
}

Para implementar la lógica de gestión e invalidación de bloques de órdenes, recorremos «blockNames» hacia atrás para procesar cada bloque, recuperando sus precios máximo y mínimo con ObjectGetDouble y las horas de inicio y fin con ObjectGetInteger, y marcándolo como existente si la hora de apertura de la última barra cerrada («time(1)») es anterior a su hora de fin. Para los bloques de órdenes válidos (cuyo tipo comienza por «OB-» y que no están invalidados), comprobamos si se cumplen las condiciones de invalidación: si el tipo es «OB-bearish» y el precio de cierre («close(1)») supera el máximo del bloque, creamos un bloque de ruptura alcista con una flecha hacia arriba (código 233) utilizando «ObjectCreate» (OBJ_ARROW) y un color oscurecido mediante «DarkenColor»; si el tipo es «OB-bullish» y el precio de cierre está por debajo del mínimo del bloque, creamos un bloque de ruptura bajista con una flecha hacia abajo (código 234). MQL5 ofrece una amplia lista de códigos Wingdings, tal y como se muestra a continuación, para que puedas utilizarlos como prefieras.

MQL5 WINGDINGS

Si «enableSwingValidation» es «true», validamos el bloque comprobando si se ha registrado un mínimo más bajo (para un bloque bajista) o un máximo más alto (para un bloque alcista) desde su creación mediante iBarShift y «low»/«high»; si es válido, actualizamos su color y su etiqueta con ObjectSetInteger y ObjectCreate (OBJ_TEXT). Si la opción «showSwingPoints» está activada, añadimos una etiqueta («LL» o «HH») con «ObjectCreate» en la hora y el precio del punto de oscilación. En caso de invalidación, actualizamos los estados de los bloques con «invalidatedStatus», «blockTypes» e «invalidationTimes», registramos la invalidación y restablecemos el estado del retesteo. Si un bloque no existe, utilizamos ArrayRemove para eliminar sus estados, como «invalidatedStatus», de las matrices de almacenamiento y registramos la eliminación; a continuación, volvemos a dibujar el gráfico con la función ChartRedraw. Al compilar, deberías obtener algo parecido a lo siguiente.

BLOQUES DE ÓRDENES INVALIDADOS

Ahora que hemos completado el segundo paso, el de la invalidación, podemos pasar al siguiente, en el que hacemos un seguimiento y esperamos a que el precio realice un retesteo de nuestros bloques de órdenes invalidados para marcarlos como bloques de ruptura.

for (int j = ArraySize(blockNames) - 1; j >= 0; j--) { //--- Iterate invalidated blocks
   if (StringFind(blockTypes[j], "Invalidated-") != 0) continue; //--- Skip non-invalidated
   string currentBlockName = blockNames[j];    //--- Get current block name
   double blockHigh = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 0); //--- Get block high
   double blockLow = ObjectGetDouble(0, currentBlockName, OBJPROP_PRICE, 1); //--- Get block low
   datetime blockStartTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 0); //--- Get block start
   datetime blockEndTime = (datetime)ObjectGetInteger(0, currentBlockName, OBJPROP_TIME, 1); //--- Get block end
   bool isForBullishBB = (blockTypes[j] == "Invalidated-bearish"); //--- Check for bullish breaker block
   datetime currentBarTime = time(1);          //--- Get current bar time
   if (currentBarTime <= invalidationTimes[j]) continue; //--- Skip if same or earlier bar
   if (!movedAwayStatus[j]) {                  //--- Check if not moved away
      if (isForBullishBB && close(1) > blockHigh + moveAwayDistance * _Point) { //--- Check bullish move away
         movedAwayStatus[j] = true;           //--- Set moved away
         Print("Moved away for bullish BB setup: ", currentBlockName); //--- Log move away
      } else if (!isForBullishBB && close(1) < blockLow - moveAwayDistance * _Point) { //--- Check bearish move away
         movedAwayStatus[j] = true;           //--- Set moved away
         Print("Moved away for bearish BB setup: ", currentBlockName); //--- Log move away
      }
   }
   if (movedAwayStatus[j] && !retestedStatus[j]) { //--- Check for retest
      bool retestCondition = false;            //--- Initialize retest condition
      if (isForBullishBB && low(1) <= blockHigh && close(1) > blockHigh) { //--- Check bullish retest
         retestCondition = true;               //--- Set retest condition
      } else if (!isForBullishBB && high(1) >= blockLow && close(1) < blockLow) { //--- Check bearish retest
         retestCondition = true;               //--- Set retest condition
      }
      bool validSwingForRetest = true;         //--- Assume valid swing
      int swingShift = -1;                     //--- Initialize swing shift
      double swingPrice = 0.0;                 //--- Initialize swing price
      if (enableSwingValidation && retestCondition) { //--- Check swing validation
         int invalidShift = iBarShift(_Symbol, _Period, invalidationTimes[j], false); //--- Get invalidation shift
         if (invalidShift > 1) {               //--- Ensure enough bars
            double extreme = invalidationSwings[j]; //--- Get invalidation swing
            if (isForBullishBB) {              //--- Handle bullish breaker block
               double maxHigh = extreme;       //--- Initialize maximum high
               for (int k = invalidShift - 1; k > 1; k--) { //--- Find higher high
                  if (high(k) > maxHigh) {     //--- Check higher high
                     maxHigh = high(k);        //--- Update maximum high
                     swingShift = k;           //--- Update swing shift
                  }
               }
               validSwingForRetest = maxHigh > extreme; //--- Validate swing
               swingPrice = maxHigh;           //--- Set swing price
            } else {                           //--- Handle bearish breaker block
               double minLow = extreme;        //--- Initialize minimum low
               for (int k = invalidShift - 1; k > 1; k--) { //--- Find lower low
                  if (low(k) < minLow) {       //--- Check lower low
                     minLow = low(k);          //--- Update minimum low
                     swingShift = k;           //--- Update swing shift
                  }
               }
               validSwingForRetest = minLow < extreme; //--- Validate swing
               swingPrice = minLow;            //--- Set swing price
            }
         } else {                              //--- Insufficient bars
            validSwingForRetest = false;       //--- Invalidate swing
         }
      }
   }
}

Para implementar la lógica de detección de retesteos de los bloques de órdenes invalidados, recorremos hacia atrás la lista «blockNames» en busca de bloques que contengan «Invalidated-» en «blockTypes», recuperando los precios máximo y mínimo con ObjectGetDouble y las horas de inicio y fin con ObjectGetInteger, y comprobando si el bloque es un bloque de ruptura alcista («Invalidated-bearish»). Si la hora de apertura de la última barra cerrada («time(1)») no es posterior a «invalidationTimes», pasamos al siguiente bloque. Para los bloques de los que el precio aún no se ha alejado («movedAwayStatus» en «false»), comprobamos si el precio de cierre («close(1)») supera «blockHigh + moveAwayDistance * _Point» en el caso alcista o cae por debajo de «blockLow - moveAwayDistance * _Point» en el caso bajista; si se cumple la condición, establecemos «movedAwayStatus» en «true».

Para los bloques de los que el precio ya se ha alejado pero que aún no ha retesteado («retestedStatus» en «false»), identificamos un retesteo alcista si «low(1)» alcanza «blockHigh» y «close(1)» está por encima de ese nivel, o un retesteo bajista si «high(1)» alcanza «blockLow» y «close(1)» está por debajo de ese nivel. Si «enableSwingValidation» está activado y se cumple una condición de retesteo, validamos los puntos de oscilación utilizando iBarShift para obtener la barra de invalidación. Comprobamos si, desde la invalidación, se ha producido un máximo más alto («high») en el caso alcista o un mínimo más bajo («low») en el caso bajista, y establecemos «validSwingForRetest» y «swingPrice» en consecuencia. Así creamos un sistema para identificar oportunidades válidas de retesteo tras un movimiento del precio. Podemos realizar un seguimiento de los retesteos, marcar los bloques como bloques de ruptura, cambiar su color para distinguirlos y abrir posiciones. Esta es la lógica que usamos para lograrlo.

if (retestCondition && validSwingForRetest) { //--- Confirm retest and swing
   if (enableTrading) {                  //--- Check trading enabled
      double entryPrice = 0.0;           //--- Initialize entry price
      double stopLossPrice = 0.0;        //--- Initialize stop loss
      double takeProfitPrice = 0.0;      //--- Initialize take profit
      if (isForBullishBB) {              //--- Handle bullish trade
         entryPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Set entry at ask
         stopLossPrice = entryPrice - stopLossDistance * _Point; //--- Set stop loss
         takeProfitPrice = entryPrice + takeProfitDistance * _Point; //--- Set take profit
         obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice); //--- Execute buy trade
         Print("Buy trade on bullish BB retest: ", currentBlockName); //--- Log buy trade
      } else {                           //--- Handle bearish trade
         entryPrice = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Set entry at bid
         stopLossPrice = entryPrice + stopLossDistance * _Point; //--- Set stop loss
         takeProfitPrice = entryPrice - takeProfitDistance * _Point; //--- Set take profit
         obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice); //--- Execute sell trade
         Print("Sell trade on bearish BB retest: ", currentBlockName); //--- Log sell trade
      }
   }
   color bbColor = isForBullishBB ? clrBlueViolet : clrOrange; //--- Set breaker block color
   ObjectSetInteger(0, currentBlockName, OBJPROP_COLOR, bbColor); //--- Update block color
   ObjectDelete(0, blockLabels[j]);     //--- Delete old label
   string newLabel = isForBullishBB ? "Bullish Breaker Block" : "Bearish Breaker Block"; //--- Set new label
   datetime labelTime = blockStartTime + (blockEndTime - blockStartTime) / 2; //--- Calculate label time
   double labelPrice = (blockHigh + blockLow) / 2; //--- Calculate label price
   string newLabelObjectName = currentBlockName + " Label"; //--- Generate new label name
   ObjectCreate(0, newLabelObjectName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Create new label
   ObjectSetString(0, newLabelObjectName, OBJPROP_TEXT, newLabel); //--- Set label text
   ObjectSetInteger(0, newLabelObjectName, OBJPROP_COLOR, labelTextColor); //--- Set label color
   ObjectSetInteger(0, newLabelObjectName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set label font size
   ObjectSetInteger(0, newLabelObjectName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Set label anchor
   if (enableSwingValidation && showSwingPoints && swingShift > 0) { //--- Check swing point display
      string swingLabelName = currentBlockName + "_retest_swing"; //--- Generate swing label name
      if (ObjectFind(0, swingLabelName) < 0) { //--- Check if swing label exists
         datetime swingTime = time(swingShift); //--- Get swing time
         ObjectCreate(0, swingLabelName, OBJ_TEXT, 0, swingTime, swingPrice); //--- Create swing label
         string swingText = isForBullishBB ? "HH" : "LL"; //--- Set swing text
         ObjectSetString(0, swingLabelName, OBJPROP_TEXT, swingText); //--- Set swing label text
         ObjectSetInteger(0, swingLabelName, OBJPROP_COLOR, swingLabelColor); //--- Set swing label color
         ObjectSetInteger(0, swingLabelName, OBJPROP_FONTSIZE, swingFontSize); //--- Set swing label font size
         ObjectSetInteger(0, swingLabelName, OBJPROP_ANCHOR, isForBullishBB ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER); //--- Set swing label anchor
      }
   }
   ChartRedraw(0);                       //--- Redraw chart
   blockTypes[j] = isForBullishBB ? "BB-bullish" : "BB-bearish"; //--- Update block type
   retestedStatus[j] = true;             //--- Set retested status
   blockLabels[j] = newLabelObjectName;  //--- Update label name
   Print("Converted to ", newLabel, ": ", currentBlockName); //--- Log conversion
}

Por último, implementamos la lógica de negociación y visualización para los bloques de ruptura sometidos a retesteo. Si se confirma un retesteo («retestCondition» y «validSwingForRetest» son verdaderos) y la negociación está habilitada («enableTrading»), ejecutamos operaciones: para un bloque de ruptura alcista («isForBullishBB»), establecemos la entrada al precio ask utilizando SymbolInfoDouble, calculamos el nivel de stop loss («stopLossDistance * _Point» por debajo de la entrada) y el nivel de take profit («takeProfitDistance * _Point» por encima de la entrada), y ejecutamos una compra con «obj_Trade.Buy»; para un bloque bajista, utilizamos el precio bid, fijamos el nivel de stop loss por encima de la entrada y el nivel de take profit por debajo, y ejecutamos una venta con «obj_Trade.Sell», registrando lo que corresponda.

A continuación, actualizamos el aspecto del bloque estableciendo su color en clrBlueViolet si es alcista o en «clrOrange» si es bajista mediante ObjectSetInteger, eliminamos la etiqueta anterior con ObjectDelete y creamos una nueva etiqueta OBJ_TEXT («Bullish Breaker Block» o «Bearish Breaker Block») en el punto medio del bloque utilizando «ObjectCreate» con «labelTextColor» y «dynamicFontSize». Si «enableSwingValidation» y «showSwingPoints» son verdaderos y se dispone de un «swingShift» válido, añadimos una etiqueta al punto de oscilación («HH» en el caso alcista y «LL» en el bajista), en el instante y al precio de dicho punto, utilizando ObjectCreate con «swingLabelColor» y «swingFontSize». Por último, actualizamos «blockTypes» a «BB-bullish» o «BB-bearish», establecemos «retestedStatus» en «true», actualizamos «blockLabels», registramos la conversión y volvemos a dibujar el gráfico. Tras la compilación, obtenemos el siguiente resultado.

BLOQUES DE RUPTURA CONFIRMADOS

En la imagen se puede observar que podemos detectar, visualizar y operar con los bloques de ruptura en consecuencia. Lo que tenemos que hacer ahora es añadir una lógica de stop dinámico para maximizar los beneficios, y con eso ya estará todo listo. Para ello, definimos una función que permite modularizar el código.

//+------------------------------------------------------------------+
//| Apply trailing stop to open positions                            |
//+------------------------------------------------------------------+
void applyTrailingStop(double trailingPoints, CTrade &trade_object, int magicNo = 0) {
   double buyStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingPoints * _Point, _Digits); //--- Calculate buy stop loss
   double sellStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingPoints * _Point, _Digits); //--- Calculate sell stop loss
   for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through open positions
      ulong ticket = PositionGetTicket(i);         //--- Get position ticket
      if (ticket > 0 && PositionGetString(POSITION_SYMBOL) == _Symbol && (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)) { //--- Verify position
         if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && buyStopLoss > PositionGetDouble(POSITION_PRICE_OPEN) && (buyStopLoss > PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)) { //--- Check buy trailing
            trade_object.PositionModify(ticket, buyStopLoss, PositionGetDouble(POSITION_TP)); //--- Update buy stop loss
         } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && sellStopLoss < PositionGetDouble(POSITION_PRICE_OPEN) && (sellStopLoss < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)) { //--- Check sell trailing
            trade_object.PositionModify(ticket, sellStopLoss, PositionGetDouble(POSITION_TP)); //--- Update sell stop loss
         }
      }
   }
}

//--- Call the function on every tick

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (enableTrailingStop) {                      //--- Check if trailing stop enabled
      applyTrailingStop(trailingStopPoints, obj_Trade, uniqueMagicNumber); //--- Apply trailing stop
   }

   //---

}

Aquí implementamos la funcionalidad de stop dinámico y la integramos en la lógica basada en eventos. En primer lugar, desarrollamos la función «applyTrailingStop», que calcula el nivel de stop loss de una posición de compra como el precio bid actual (SymbolInfoDouble con SYMBOL_BID) menos «trailingPoints * _Point», y el nivel de stop loss de una posición de venta como el precio ask («SYMBOL_ASK») más «trailingPoints * _Point», ambos normalizados con NormalizeDouble al número de dígitos del símbolo. Recorremos en orden inverso las posiciones abiertas utilizando PositionsTotal, obtenemos el ticket de cada posición con PositionGetTicket y comprobamos que su símbolo y su número mágico coincidan con el símbolo utilizado y con «magicNo» (si es distinto de cero), respectivamente, mediante las funciones PositionGetString y «PositionGetInteger».

Para las posiciones de compra (POSITION_TYPE_BUY), comprobamos que «buyStopLoss» esté por encima del precio de apertura («PositionGetDouble(POSITION_PRICE_OPEN)») y que supere el nivel de stop loss actual o que este aún no esté definido; si se cumplen estas condiciones, actualizamos el nivel con «trade_object.PositionModify». Para las posiciones de venta, comprobamos que «sellStopLoss» esté por debajo del precio de apertura y que sea inferior al nivel de stop loss actual o que este aún no esté definido, y lo actualizamos de forma similar. A continuación, en la función OnTick, comprobamos si «enableTrailingStop» es verdadero y llamamos a «applyTrailingStop» con «trailingStopPoints», «obj_Trade» y «uniqueMagicNumber» para gestionar las posiciones abiertas en cada tick. Tras la compilación, obtenemos el siguiente resultado.

STOP DINÁMICO ACTIVADO

En la imagen se puede observar que el stop dinámico se activa por completo cuando el precio evoluciona a nuestro favor. A continuación se muestra una prueba conjunta de los bloques de ruptura bajistas y alcistas.

GIF CONJUNTO DE BLOQUES DE RUPTURA

En la visualización podemos observar que el programa identifica y verifica todas las condiciones de entrada y, si se validan, abre la posición correspondiente con los parámetros de entrada adecuados, logrando así nuestro objetivo. Solo queda realizar las pruebas con datos históricos del programa, que se abordan en la siguiente sección.


Backtesting

Tras realizar pruebas exhaustivas con datos históricos, hemos obtenido los siguientes resultados.

Gráfico de las pruebas con datos históricos:

GRÁFICO

Informe de las pruebas con datos históricos:

INFORME


Conclusión

En conclusión, hemos creado un sistema de trading basado en bloques de ruptura en MQL5 para identificar rangos de consolidación, validar los bloques de ruptura mediante puntos de oscilación y ejecutar operaciones de retesteo con parámetros de riesgo personalizables y stops dinámicos. El sistema muestra los bloques de órdenes y los bloques de ruptura con etiquetas y flechas dinámicas, lo que facilita la toma de decisiones de trading.

Aviso legal: el presente artículo tiene fines únicamente educativos. Las operaciones de trading conllevan riesgos financieros importantes, y la volatilidad del mercado puede dar lugar a pérdidas. Resulta fundamental realizar un backtesting exhaustivo y aplicar una gestión del riesgo rigurosa antes de implementar este programa en mercados reales.

Con esta estrategia de bloques de ruptura, estarás preparado para aprovechar las oportunidades de retesteo del precio y seguir perfeccionando la estrategia en tu trayectoria como operador. ¡Te deseo un buen trading!

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/19638

Archivos adjuntos |
hiteshdaoya
hiteshdaoya | 27 nov 2025 en 08:06
Hola,

. He intentado añadir el archivo compartido, pero no se muestra en el gráfico; es decir, no ocurre nada en él ni se realizan operaciones. ¿Podrías ayudarme con el proceso de ejecución?

. Gracias de antemano.
Alexey Volchanskiy
Alexey Volchanskiy | 2 ago 2026 en 20:13
¿Para qué publicar aquí toda esta basura? Sobre la estrategia solo puedo decir una cosa: este pseudotrader se gana la vida escribiendo artículos del tipo:
— Si me preguntáis de dónde salen estos cuentos y leyendas, os diré, os responderé: ¡me los he sacado de la manga!
Un 1,6 % de beneficio totalmente aleatorio en 8 meses, ¡eso sí que es un resultado impresionante!
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Creación de un robot probabilístico y neutral respecto al mercado basado en la distribución de rentabilidades Creación de un robot probabilístico y neutral respecto al mercado basado en la distribución de rentabilidades
Una estrategia de negociación neutral respecto al mercado, basada en la distribución empírica de las rentabilidades, proporciona una alternativa a los métodos clásicos de análisis técnico, sustituyendo la previsión de la dirección del precio por la colocación estadística de órdenes en niveles que el precio probablemente alcanzará. El artículo analiza con detalle el marco matemático para el cálculo de los percentiles, los algoritmos de ponderación de los volúmenes de las posiciones en función de la probabilidad de ejecución y los mecanismos de adaptación a los cambios en las condiciones del mercado mediante la expiración de la red de órdenes. Así, presentamos una implementación completa en MQL5.
Particularidades del trabajo con números del tipo double en MQL4 Particularidades del trabajo con números del tipo double en MQL4
En estos apuntes hemos reunido consejos para resolver los errores más frecuentes al trabajar con números del tipo double en los programas en MQL4.
Características del Wizard MQL5 que debe conocer (Parte 82): Uso de patrones del TRIX y del WPR con aprendizaje por refuerzo mediante una red Q profunda (DQN) Características del Wizard MQL5 que debe conocer (Parte 82): Uso de patrones del TRIX y del WPR con aprendizaje por refuerzo mediante una red Q profunda (DQN)
En el último artículo, analizamos la combinación de Ichimoku y ADX en el framework del aprendizaje por inferencia. En este artículo retomamos el tema del aprendizaje por refuerzo cuando se utiliza junto con un par de indicadores que ya analizamos en la «Parte 68». El indicador TRIX y el rango porcentual de Williams. El algoritmo que usaremos en este análisis será la red Q profunda (DQN) con regresión por cuantiles. Como es habitual, presentamos este enfoque como una clase de señal personalizada diseñada para su implementación con el Asistente de MQL5.