English Русский Deutsch 日本語
preview
Automatización de estrategias de trading en MQL5 (Parte 36): Trading basado en la oferta y la demanda con el modelo de retesteo e impulso

Automatización de estrategias de trading en MQL5 (Parte 36): Trading basado en la oferta y la demanda con el modelo de retesteo e impulso

MetaTrader 5 — Trading |
38 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Introducción

En nuestro artículo anterior (Parte 35), desarrollamos un sistema de trading basado en breaker blocks en MetaQuotes Language 5 (MQL5) que identificaba rangos de consolidación, validaba los bloques mediante máximos y mínimos de oscilación (swing highs y swing lows) y operaba en los retesteos con parámetros de riesgo personalizables e información visual. En la parte 36, desarrollamos un sistema de trading basado en la oferta y la demanda que usa un modelo de retesteo e impulso. Este modelo detecta zonas de oferta y demanda a través de la consolidación, las valida con movimientos impulsivos y ejecuta operaciones en los retesteos con confirmación de la tendencia y visualizaciones dinámicas en los gráficos. Hoy hablaremos de los siguientes temas:

  1. Comprensión del marco de la estrategia de oferta y demanda
  2. Implementación en MQL5
  3. Backtesting
  4. Conclusión

Al final, usted tendrá una estrategia funcional en MQL5 para operar en los retesteos de las zonas de oferta y demanda, lista para personalizarla. ¡Manos a la obra!


Comprensión del marco de la estrategia de oferta y demanda

La estrategia de oferta y demanda identifica zonas de precios clave en las que se ha producido una actividad significativa de compra (demanda) o venta (oferta), normalmente tras periodos de consolidación. Una vez que un movimiento impulsivo del precio confirma la validez de una zona, los operadores buscan operar en su retesteo. Pueden abrir posiciones de compra cuando el precio vuelve a situarse en una zona de demanda en una tendencia bajista, o iniciar posiciones de venta en una zona de oferta en una tendencia alcista, a la espera de un rebote. Al definir los niveles de riesgo y de beneficio, los operadores sacan partido de las oportunidades con alta probabilidad de éxito. Eche un vistazo a continuación a las diferentes configuraciones que podríamos tener.

Configuración de la zona de oferta:

CONFIGURACIÓN DE LA ZONA DE OFERTA

Configuración de la zona de demanda:

CONFIGURACIÓN DE LA ZONA DE DEMANDA

Nuestro plan consiste en detectar rangos de consolidación a lo largo de un número determinado de barras, validar las zonas con movimientos impulsivos mediante un umbral basado en un multiplicador y confirmar las entradas en las operaciones con comprobaciones opcionales de la tendencia. Implementaremos una lógica para realizar un seguimiento del estado de las zonas, ejecutar operaciones en los retesteos con ajustes personalizables de stop-loss y take-profit, y visualizar las zonas con etiquetas y colores dinámicos, creando así un sistema para operar con precisión en función de la oferta y la demanda. En resumen, aquí tiene una representación gráfica de nuestros objetivos.

MARCO DE OFERTA Y DEMANDA


Implementación en MQL5

Para crear el programa en MQL5, abra el MetaEditor, vaya al Navegador, localice la carpeta «Experts», haga clic en la pestaña «Nuevo» y siga las instrucciones para crear el archivo. Una vez creado el archivo, tendremos que declarar en el entorno de programación algunos parámetros de entrada y variables globales que usaremos a lo largo de todo el programa.

//+------------------------------------------------------------------+
//|                                         Supply and Demand 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 mediante la directiva #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 programación. Esta configuración garantizará que la ejecución de las operaciones se lleve a cabo de forma eficiente sin necesidad de intervención manual. Luego podemos declarar algunas enumeraciones que permitirán clasificar ciertos parámetros de entrada del usuario.
//+------------------------------------------------------------------+
//| Enum for trading tested zones                                    |
//+------------------------------------------------------------------+
enum TradeTestedZonesMode {                        // Define modes for trading tested zones
   NoRetrade,                                      // Trade zones only once
   LimitedRetrade,                                 // Trade zones up to a maximum number of times
   UnlimitedRetrade                                // Trade zones as long as they are valid
};

//+------------------------------------------------------------------+
//| Enum for broken zones validation                                 |
//+------------------------------------------------------------------+
enum BrokenZonesMode {                             // Define modes for broken zones validation
   AllowBroken,                                    // Zones can be marked as broken
   NoBroken                                        // Zones remain testable regardless of price break
};

//+------------------------------------------------------------------+
//| Enum for zone size restriction                                   |
//+------------------------------------------------------------------+
enum ZoneSizeMode {                                // Define modes for zone size restrictions
   NoRestriction,                                  // No restriction on zone size
   EnforceLimits                                   // Enforce minimum and maximum zone points
};

//+------------------------------------------------------------------+
//| Enum for trend confirmation                                      |
//+------------------------------------------------------------------+
enum TrendConfirmationMode {                       // Define modes for trend confirmation
   NoConfirmation,                                 // No trend confirmation required
   ConfirmTrend                                    // Confirm trend before trading on tap
};

Declaramos por adelantado algunas enumeraciones clave para configurar el comportamiento de las operaciones y la validación de zonas. En primer lugar, creamos la enumeración «TradeTestedZonesMode» con las opciones «NoRetrade» (operar en las zonas una sola vez), «LimitedRetrade» (operar hasta un límite establecido) y «UnlimitedRetrade» (operar mientras sean válidas), que controlan cuántas veces se puede operar en las zonas. Después definimos la enumeración «BrokenZonesMode» con «AllowBroken» (marcar las zonas como rotas si el precio las traspasa) y «NoBroken» (mantener las zonas disponibles para retesteos), lo que determina la validez de las zonas tras las rupturas. A continuación, implementamos la enumeración «ZoneSizeMode» con «NoRestriction» (sin límites de tamaño) y «EnforceLimits» (restringir el tamaño de la zona dentro de unos límites), lo que garantiza que las zonas cumplan los criterios de tamaño.

Por último, añadimos la enumeración «TrendConfirmationMode» con las opciones «NoConfirmation» (sin comprobación de tendencia) y «ConfirmTrend» (se requiere validación de tendencia), lo que permite filtrar opcionalmente las operaciones en función de la tendencia. De este modo, el sistema contará con una configuración flexible para la operativa por zonas y las reglas de validación. Podemos utilizar estas enumeraciones para crear nuestros parámetros de entrada.

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input double tradeLotSize = 0.01;                   // Trade size in lots
input bool   enableTrading = true;                  // Enable automated trading
input bool   enableTrailingStop = true;             // Enable trailing stop
input double trailingStopPoints = 30;               // Trailing stop points
input double minProfitToTrail = 50;                 // Minimum trailing points
input int    uniqueMagicNumber = 12345;             // Magic Number
input int    consolidationBars = 5;                 // Consolidation range bars
input double maxConsolidationSpread = 30;           // Maximum allowed spread in points for consolidation
input double stopLossDistance = 200;                // Stop loss in points
input double takeProfitDistance = 400;              // Take profit in points
input double minMoveAwayPoints = 50;                // Minimum points price must move away before zone is ready
input bool   deleteBrokenZonesFromChart = false;    // Delete broken zones from chart
input bool   deleteExpiredZonesFromChart = false;   // Delete expired zones from chart
input int    zoneExtensionBars = 150;               // Number of bars to extend zones to the right
input bool   enableImpulseValidation = true;        // Enable impulse move validation
input int    impulseCheckBars = 3;                  // Number of bars to check for impulsive move
input double impulseMultiplier = 1.0;               // Multiplier for impulsive threshold
input TradeTestedZonesMode tradeTestedMode = NoRetrade; // Mode for trading tested zones
input int    maxTradesPerZone = 2;                  // Maximum trades per zone for LimitedRetrade
input BrokenZonesMode brokenZoneMode = AllowBroken; // Mode for broken zones validation
input color  demandZoneColor = clrBlue;             // Color for untested demand zones
input color  supplyZoneColor = clrRed;              // Color for untested supply zones
input color  testedDemandZoneColor = clrBlueViolet; // Color for tested demand zones
input color  testedSupplyZoneColor = clrOrange;     // Color for tested supply zones
input color  brokenZoneColor = clrDarkGray;         // Color for broken zones
input color  labelTextColor = clrBlack;             // Color for text labels
input ZoneSizeMode zoneSizeRestriction = NoRestriction; // Zone size restriction mode
input double minZonePoints = 50;                    // Minimum zone size in points
input double maxZonePoints = 300;                   // Maximum zone size in points
input TrendConfirmationMode trendConfirmation = NoConfirmation; // Trend confirmation mode
input int    trendLookbackBars = 10;                // Number of bars for trend confirmation
input double minTrendPoints = 1;                    // Minimum points for trend confirmation

Aquí establecemos los parámetros de entrada de nuestro sistema para definir su comportamiento operativo y de visualización. Hemos añadido comentarios que se explican por sí mismos para que todo resulte fácil y claro. Por último, como vamos a gestionar varias zonas de oferta y demanda, debemos declarar una estructura en la que almacenaremos la información de dichas zonas para facilitar su gestión.

//+------------------------------------------------------------------+
//| Structure for zone information                                   |
//+------------------------------------------------------------------+
struct SDZone {                                    //--- Define structure for supply/demand zones
   double   high;                                  //--- Store zone high price
   double   low;                                   //--- Store zone low price
   datetime startTime;                             //--- Store zone start time
   datetime endTime;                               //--- Store zone end time
   datetime breakoutTime;                          //--- Store breakout time
   bool     isDemand;                              //--- Indicate demand (true) or supply (false)
   bool     tested;                                //--- Track if zone was tested
   bool     broken;                                //--- Track if zone was broken
   bool     readyForTest;                          //--- Track if zone is ready for testing
   int      tradeCount;                            //--- Track number of trades on zone
   string   name;                                  //--- Store zone object name
};

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
SDZone zones[];                                    //--- Store active supply/demand zones
SDZone potentialZones[];                           //--- Store potential zones awaiting validation
int    maxZones = 50;                              //--- Set maximum number of zones to track

//+------------------------------------------------------------------+
//| 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, "SDZone_");                //--- Remove all zone objects from chart
   ChartRedraw(0);                                //--- Redraw chart to clear objects
}

En primer lugar, creamos la estructura «SDZone» para almacenar los detalles de la zona, incluidos los precios máximo y mínimo, las horas de inicio, fin y ruptura, los indicadores de si la zona es de demanda o de oferta («isDemand»), si se ha testeado («tested»), si se ha roto («broken») y si está lista para el testeo («readyForTest»), el recuento de operaciones («tradeCount») y el nombre del objeto («name»). A continuación, inicializamos las variables globales: el array «zones», que contiene las zonas activas de oferta y demanda; el array «potentialZones», para las zonas pendientes de validación; y la variable «maxZones», fijada en 50 para limitar el número de zonas de las que se realiza un seguimiento. Usted puede aumentar o reducir este valor en función de su marco temporal y sus ajustes; nosotros simplemente hemos elegido un valor estándar arbitrario.

En el controlador de eventos OnInit, llamamos a «SetExpertMagicNumber» en «obj_Trade» con «uniqueMagicNumber» para etiquetar las operaciones y devolvemos INIT_SUCCEEDED si la inicialización se ha realizado correctamente. En la función OnDeinit, usamos ObjectsDeleteAll para eliminar todos los objetos del gráfico que tengan el prefijo «SDZone_», ya que vamos a nombrar todos nuestros objetos con este prefijo, y llamamos a ChartRedraw para actualizar el gráfico, lo que garantiza una limpieza adecuada de los recursos. Ahora podemos definir algunas funciones auxiliares que nos ayudarán a detectar y gestionar las zonas. Empezaremos con la lógica que detectará las zonas, pero antes, creemos una función auxiliar que nos ayude a depurar las zonas.

//+------------------------------------------------------------------+
//| Print zones for debugging                                        |
//+------------------------------------------------------------------+
void PrintZones(SDZone &arr[]) {
   Print("Current zones count: ", ArraySize(arr)); //--- Log total number of zones
   for (int i = 0; i < ArraySize(arr); i++) {     //--- Iterate through zones
      Print("Zone ", i, ": ", arr[i].name, " endTime: ", TimeToString(arr[i].endTime)); //--- Log zone details
   }
}

Para supervisar el estado de las zonas, desarrollamos la función «PrintZones», que recibe un array de tipo «SDZone», registra el número total de zonas mediante Print con ArraySize y recorre el array para registrar el índice, el nombre y la hora de finalización de cada zona con «Print» y TimeToString, lo que permite un seguimiento claro. Ahora podemos desarrollar la lógica principal para detectar las zonas.

//+------------------------------------------------------------------+
//| Detect supply and demand zones                                   |
//+------------------------------------------------------------------+
void DetectZones() {
   int startIndex = consolidationBars + 1;                 //--- Set start index for consolidation check
   if (iBars(_Symbol, _Period) < startIndex + 1) return;   //--- Exit if insufficient bars
   bool isConsolidated = true;                             //--- Assume consolidation
   double highPrice = iHigh(_Symbol, _Period, startIndex); //--- Initialize high price
   double lowPrice = iLow(_Symbol, _Period, startIndex);   //--- Initialize low price
   for (int i = startIndex - 1; i >= 2; i--) {             //--- Iterate through consolidation bars
      highPrice = MathMax(highPrice, iHigh(_Symbol, _Period, i)); //--- Update highest high
      lowPrice = MathMin(lowPrice, iLow(_Symbol, _Period, i)); //--- Update lowest low
      if (highPrice - lowPrice > maxConsolidationSpread * _Point) { //--- Check spread limit
         isConsolidated = false;                           //--- Mark as not consolidated
         break;                                            //--- Exit loop
      }
   }
   if (isConsolidated) {                                   //--- Confirm consolidation
      double closePrice = iClose(_Symbol, _Period, 1);     //--- Get last closed bar price
      double breakoutLow = iLow(_Symbol, _Period, 1);      //--- Get breakout bar low
      double breakoutHigh = iHigh(_Symbol, _Period, 1);    //--- Get breakout bar high
      bool isDemandZone = closePrice > highPrice && breakoutLow >= lowPrice; //--- Check demand zone
      bool isSupplyZone = closePrice < lowPrice && breakoutHigh <= highPrice; //--- Check supply zone
      if (isDemandZone || isSupplyZone) {                   //--- Validate zone type
         double zoneSize = (highPrice - lowPrice) / _Point; //--- Calculate zone size
         if (zoneSizeRestriction == EnforceLimits && (zoneSize < minZonePoints || zoneSize > maxZonePoints)) return; //--- Check size restrictions
         datetime lastClosedBarTime = iTime(_Symbol, _Period, 1); //--- Get last bar time
         bool overlaps = false;                             //--- Initialize overlap flag
         for (int j = 0; j < ArraySize(zones); j++) {       //--- Check existing zones
            if (lastClosedBarTime < zones[j].endTime) {     //--- Check time overlap
               double maxLow = MathMax(lowPrice, zones[j].low); //--- Find max low
               double minHigh = MathMin(highPrice, zones[j].high); //--- Find min high
               if (maxLow <= minHigh) {                     //--- Check price overlap
                  overlaps = true;                          //--- Mark as overlapping
                  break;                                    //--- Exit loop
               }
            }
         }
         bool duplicate = false;                        //--- Initialize duplicate flag
         for (int j = 0; j < ArraySize(zones); j++) {   //--- Check for duplicates
            if (lastClosedBarTime < zones[j].endTime) { //--- Check time
               if (MathAbs(zones[j].high - highPrice) < _Point && MathAbs(zones[j].low - lowPrice) < _Point) { //--- Check price match
                  duplicate = true;                     //--- Mark as duplicate
                  break;                                //--- Exit loop
               }
            }
         }
         if (overlaps || duplicate) return;             //--- Skip overlapping or duplicate zones
         if (enableImpulseValidation) {                 //--- Check impulse validation
            bool pot_overlaps = false;                  //--- Initialize potential overlap flag
            for (int j = 0; j < ArraySize(potentialZones); j++) { //--- Check potential zones
               if (lastClosedBarTime < potentialZones[j].endTime) { //--- Check time overlap
                  double maxLow = MathMax(lowPrice, potentialZones[j].low); //--- Find max low
                  double minHigh = MathMin(highPrice, potentialZones[j].high); //--- Find min high
                  if (maxLow <= minHigh) {              //--- Check price overlap
                     pot_overlaps = true;               //--- Mark as overlapping
                     break;                             //--- Exit loop
                  }
               }
            }
            bool pot_duplicate = false;           //--- Initialize potential duplicate flag
            for (int j = 0; j < ArraySize(potentialZones); j++) { //--- Check potential duplicates
               if (lastClosedBarTime < potentialZones[j].endTime) { //--- Check time
                  if (MathAbs(potentialZones[j].high - highPrice) < _Point && MathAbs(potentialZones[j].low - lowPrice) < _Point) { //--- Check price match
                     pot_duplicate = true;      //--- Mark as duplicate
                     break;                     //--- Exit loop
                  }
               }
            }
            if (pot_overlaps || pot_duplicate) return; //--- Skip overlapping or duplicate potential zones
            int potCount = ArraySize(potentialZones); //--- Get potential zones count
            ArrayResize(potentialZones, potCount + 1); //--- Resize potential zones array
            potentialZones[potCount].high = highPrice; //--- Set zone high
            potentialZones[potCount].low = lowPrice; //--- Set zone low
            potentialZones[potCount].startTime = iTime(_Symbol, _Period, startIndex); //--- Set start time
            potentialZones[potCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Set end time
            potentialZones[potCount].breakoutTime = iTime(_Symbol, _Period, 1); //--- Set breakout time
            potentialZones[potCount].isDemand = isDemandZone; //--- Set zone type
            potentialZones[potCount].tested = false; //--- Set untested
            potentialZones[potCount].broken = false; //--- Set not broken
            potentialZones[potCount].readyForTest = false; //--- Set not ready
            potentialZones[potCount].tradeCount = 0; //--- Initialize trade count
            potentialZones[potCount].name = "PotentialZone_" + TimeToString(potentialZones[potCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            Print("Potential zone created: ", (isDemandZone ? "Demand" : "Supply"), " at ", lowPrice, " - ", highPrice, " endTime: ", TimeToString(potentialZones[potCount].endTime)); //--- Log potential zone
         } else {                                 //--- No impulse validation
            int zoneCount = ArraySize(zones);     //--- Get zones count
            if (zoneCount >= maxZones) {          //--- Check max zones limit
               ArrayRemove(zones, 0, 1);          //--- Remove oldest zone
               zoneCount--;                       //--- Decrease count
            }
            ArrayResize(zones, zoneCount + 1);    //--- Resize zones array
            zones[zoneCount].high = highPrice;    //--- Set zone high
            zones[zoneCount].low = lowPrice;      //--- Set zone low
            zones[zoneCount].startTime = iTime(_Symbol, _Period, startIndex); //--- Set start time
            zones[zoneCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Set end time
            zones[zoneCount].breakoutTime = iTime(_Symbol, _Period, 1); //--- Set breakout time
            zones[zoneCount].isDemand = isDemandZone; //--- Set zone type
            zones[zoneCount].tested = false;      //--- Set untested
            zones[zoneCount].broken = false;      //--- Set not broken
            zones[zoneCount].readyForTest = false; //--- Set not ready
            zones[zoneCount].tradeCount = 0;      //--- Initialize trade count
            zones[zoneCount].name = "SDZone_" + TimeToString(zones[zoneCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            Print("Zone created: ", (isDemandZone ? "Demand" : "Supply"), " zone: ", zones[zoneCount].name, " at ", lowPrice, " - ", highPrice, " endTime: ", TimeToString(zones[zoneCount].endTime)); //--- Log zone creation
            PrintZones(zones);                    //--- Print zones for debugging
         }
      }
   }
}

Aquí implementamos la lógica de detección de zonas para nuestro sistema. En la función «DetectZones», establecemos «startIndex» en «consolidationBars + 1» y salimos si no hay suficientes barras, según lo que indique la función iBars. Suponemos que hay consolidación («isConsolidated» es verdadero), inicializamos «highPrice» y «lowPrice» con iHigh e iLow en «startIndex» y recorremos las barras hacia atrás, actualizando esos precios con MathMax y MathMin. Si el rango supera «maxConsolidationSpread * Point», establecemos «isConsolidated» en falso. Si hay consolidación, comprobamos el precio de cierre («iClose»), el mínimo («iLow») y el máximo («iHigh») de la última barra para identificar zonas de demanda («closePrice > highPrice» y «breakoutLow >= lowPrice») o de oferta («closePrice < lowPrice» y «breakoutHigh <= highPrice»).

Para las zonas válidas, verificamos las restricciones de tamaño con «zoneSizeRestriction» y «minZonePoints»/«maxZonePoints» y comprobamos si hay solapamientos o duplicados en «zones» y «potentialZones» utilizando «MathMax» y «MathMin». Si «enableImpulseValidation» es verdadero, añadimos la zona a «potentialZones» mediante ArrayResize, establecemos campos como «high», «low», «startTime» (iTime), «endTime» («TimeCurrent + zoneExtensionBars») y «name» («PotentialZone») y registramos la información con «Print». En caso contrario, añadimos la zona directamente a «zones», eliminando la más antigua si se alcanza «maxZones», y registramos la información con «Print» y «PrintZones» para facilitar la depuración y el seguimiento de nuestras zonas. Así creamos la lógica principal para detectar y almacenar las zonas de oferta y demanda. Podemos ejecutar esto en el controlador de eventos OnInit para detectar las zonas.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   static datetime lastBarTime = 0;                      //--- Store last processed bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   bool isNewBar = (currentBarTime != lastBarTime);      //--- Check for new bar
   if (isNewBar) {                                       //--- Process new bar
      lastBarTime = currentBarTime;                      //--- Update last bar time
      DetectZones();                                     //--- Detect new zones
   }
}

En el controlador de eventos OnTick, detectamos las nuevas barras comparando la hora de la barra actual, obtenida mediante iTime (para el símbolo y el periodo con desplazamiento 0), con la variable estática «lastBarTime». Si las horas difieren, establecemos «isNewBar» en verdadero y actualizamos «lastBarTime». Si se detecta una nueva barra, llamamos a nuestra función «DetectZones» para identificar nuevas zonas de oferta y demanda basándonos en los patrones de consolidación. Ahora podemos detectar las zonas, como se muestra a continuación.

ZONAS POTENCIALES DETECTADAS

Ahora que podemos detectar posibles zonas de oferta y demanda, solo tenemos que validarlas mediante los movimientos alcistas o bajistas, a los que nos referiremos como movimientos de impulso. Para facilitar la modularización, podemos incluir toda la lógica en una función.

//+------------------------------------------------------------------+
//| Validate potential zones based on impulsive move                 |
//+------------------------------------------------------------------+
void ValidatePotentialZones() {
   datetime lastClosedBarTime = iTime(_Symbol, _Period, 1);   //--- Get last closed bar time
   for (int p = ArraySize(potentialZones) - 1; p >= 0; p--) { //--- Iterate potential zones backward
      if (lastClosedBarTime >= potentialZones[p].endTime) {   //--- Check for expired zone
         Print("Potential zone expired and removed from array: ", potentialZones[p].name, " endTime: ", TimeToString(potentialZones[p].endTime)); //--- Log expiration
         ArrayRemove(potentialZones, p, 1);                   //--- Remove expired zone
         continue;                                            //--- Skip to next
      }
      if (TimeCurrent() > potentialZones[p].breakoutTime + impulseCheckBars * PeriodSeconds(_Period)) { //--- Check impulse window
         bool isImpulsive = false;                            //--- Initialize impulsive flag
         int breakoutShift = iBarShift(_Symbol, _Period, potentialZones[p].breakoutTime, false); //--- Get breakout bar shift
         double range = potentialZones[p].high - potentialZones[p].low; //--- Calculate zone range
         double threshold = range * impulseMultiplier;        //--- Calculate impulse threshold
         for (int shift = 1; shift <= impulseCheckBars; shift++) { //--- Check bars after breakout
            if (shift + breakoutShift >= iBars(_Symbol, _Period)) continue; //--- Skip out-of-bounds
            double cl = iClose(_Symbol, _Period, shift);      //--- Get close price
            if (potentialZones[p].isDemand) {                 //--- Check demand zone
               if (cl >= potentialZones[p].high + threshold) { //--- Check bullish impulse
                  isImpulsive = true;                         //--- Set impulsive flag
                  break;                                      //--- Exit loop
               }
            } else {                                          //--- Check supply zone
               if (cl <= potentialZones[p].low - threshold) { //--- Check bearish impulse
                  isImpulsive = true;                         //--- Set impulsive flag
                  break;                                      //--- Exit loop
               }
            }
         }
         if (isImpulsive) {                                  //--- Process impulsive zone
            double zoneSize = (potentialZones[p].high - potentialZones[p].low) / _Point; //--- Calculate zone size
            if (zoneSizeRestriction == EnforceLimits && (zoneSize < minZonePoints || zoneSize > maxZonePoints)) { //--- Check size limits
               ArrayRemove(potentialZones, p, 1);            //--- Remove invalid zone
               continue;                                     //--- Skip to next
            }
            bool overlaps = false;                           //--- Initialize overlap flag
            for (int j = 0; j < ArraySize(zones); j++) {     //--- Check existing zones
               if (lastClosedBarTime < zones[j].endTime) {   //--- Check time overlap
                  double maxLow = MathMax(potentialZones[p].low, zones[j].low); //--- Find max low
                  double minHigh = MathMin(potentialZones[p].high, zones[j].high); //--- Find min high
                  if (maxLow <= minHigh) {                   //--- Check price overlap
                     overlaps = true;                        //--- Mark as overlapping
                     break;                                  //--- Exit loop
                  }
               }
            }
            bool duplicate = false;                          //--- Initialize duplicate flag
            for (int j = 0; j < ArraySize(zones); j++) {     //--- Check for duplicates
               if (lastClosedBarTime < zones[j].endTime) {   //--- Check time
                  if (MathAbs(zones[j].high - potentialZones[p].high) < _Point && MathAbs(zones[j].low - potentialZones[p].low) < _Point) { //--- Check price match
                     duplicate = true;                       //--- Mark as duplicate
                     break;                                  //--- Exit loop
                  }
               }
            }
            if (overlaps || duplicate) {                     //--- Check overlap or duplicate
               Print("Validated zone overlaps or duplicates, discarded: ", potentialZones[p].low, " - ", potentialZones[p].high); //--- Log discard
               ArrayRemove(potentialZones, p, 1);            //--- Remove zone
               continue;                                     //--- Skip to next
            }
            int zoneCount = ArraySize(zones);                //--- Get zones count
            if (zoneCount >= maxZones) {                     //--- Check max zones limit
               ArrayRemove(zones, 0, 1);                     //--- Remove oldest zone
               zoneCount--;                                  //--- Decrease count
            }
            ArrayResize(zones, zoneCount + 1);               //--- Resize zones array
            zones[zoneCount] = potentialZones[p];            //--- Copy potential zone
            zones[zoneCount].name = "SDZone_" + TimeToString(zones[zoneCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            zones[zoneCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Update end time
            Print("Zone validated: ", (zones[zoneCount].isDemand ? "Demand" : "Supply"), " zone: ", zones[zoneCount].name, " at ", zones[zoneCount].low, " - ", zones[zoneCount].high, " endTime: ", TimeToString(zones[zoneCount].endTime)); //--- Log validation
            ArrayRemove(potentialZones, p, 1);               //--- Remove validated zone
            PrintZones(zones);                               //--- Print zones for debugging
         } else {                                            //--- Zone not impulsive
            Print("Potential zone not impulsive, discarded: ", potentialZones[p].low, " - ", potentialZones[p].high); //--- Log discard
            ArrayRemove(potentialZones, p, 1);               //--- Remove non-impulsive zone
         }
      }
   }
}

Aquí creamos una función para implementar la lógica de validación de las posibles zonas de oferta y demanda. En la función «ValidatePotentialZones», recorremos «potentialZones» hacia atrás y comprobamos si la hora de la última barra cerrada (iTime con desplazamiento 1) supera el valor de «endTime» de una zona; si es así, eliminamos la zona caducada con ArrayRemove y registramos la acción. Para las zonas situadas dentro de la ventana de impulso («TimeCurrent > breakoutTime + impulseCheckBars * PeriodSeconds»), calculamos el rango de la zona («high - low») y el umbral de impulso («range * impulseMultiplier»). A continuación, comprobamos las barras posteriores a la ruptura (iBarShift) para ver si el precio de cierre (iClose) supera el máximo más el umbral en las zonas de demanda o es inferior al mínimo menos el umbral en las zonas de oferta; si se cumple la condición, establecemos «isImpulsive» en verdadero.

Si el movimiento es impulsivo, verificamos el tamaño de la zona comparándolo con «minZonePoints» y «maxZonePoints» cuando «zoneSizeRestriction» es «EnforceLimits». Comprobamos también si hay solapamientos o duplicados en «zones» utilizando MathMax y MathMin y, si la zona es válida, la trasladamos a «zones» con ArrayResize, actualizamos su nombre a «SDZone_» y su hora de finalización, registramos la acción con las funciones «Print» y «PrintZones» y, por último, la eliminamos de «potentialZones». Las zonas sin un movimiento impulsivo se descartan con ArrayRemove y se registra la acción. Así se crea un sistema que valida las zonas en función de los movimientos impulsivos y garantiza que sean únicas y válidas. Cuando usted llame a la función en el controlador de eventos «tick», debería obtener algo similar a lo que se muestra a continuación.

VALIDACIÓN DE LAS ZONAS DE OFERTA Y DEMANDA

Ahora que podemos validar las zonas, gestionémoslas y visualicémoslas en el gráfico para facilitar su seguimiento.

//+------------------------------------------------------------------+
//| Update and draw zones                                            |
//+------------------------------------------------------------------+
void UpdateZones() {
   datetime lastClosedBarTime = iTime(_Symbol, _Period, 1); //--- Get last closed bar time
   for (int i = ArraySize(zones) - 1; i >= 0; i--) { //--- Iterate zones backward
      if (lastClosedBarTime >= zones[i].endTime) { //--- Check for expired zone
         Print("Zone expired and removed from array: ", zones[i].name, " endTime: ", TimeToString(zones[i].endTime)); //--- Log expiration
         if (deleteExpiredZonesFromChart) {    //--- Check if deleting expired
            ObjectDelete(0, zones[i].name);    //--- Delete zone rectangle
            ObjectDelete(0, zones[i].name + "Label"); //--- Delete zone label
         }
         ArrayRemove(zones, i, 1);             //--- Remove expired zone
         continue;                             //--- Skip to next
      }
      bool wasReady = zones[i].readyForTest;   //--- Store previous ready status
      if (!zones[i].readyForTest) {            //--- Check if not ready
         double currentClose = iClose(_Symbol, _Period, 1); //--- Get current close
         double zoneLevel = zones[i].isDemand ? zones[i].high : zones[i].low; //--- Get zone level
         double distance = zones[i].isDemand ? (currentClose - zoneLevel) : (zoneLevel - currentClose); //--- Calculate distance
         if (distance > minMoveAwayPoints * _Point) { //--- Check move away distance
            zones[i].readyForTest = true;      //--- Set ready for test
         }
      }
      if (!wasReady && zones[i].readyForTest) { //--- Check if newly ready
         Print("Zone ready for test: ", zones[i].name); //--- Log ready status
      }
      if (brokenZoneMode == AllowBroken && !zones[i].tested) { //--- Check if breakable
         double currentClose = iClose(_Symbol, _Period, 1); //--- Get current close
         bool wasBroken = zones[i].broken;     //--- Store previous broken status
         if (zones[i].isDemand) {              //--- Check demand zone
            if (currentClose < zones[i].low) { //--- Check if broken
               zones[i].broken = true;         //--- Mark as broken
            }
         } else {                              //--- Check supply zone
            if (currentClose > zones[i].high) { //--- Check if broken
               zones[i].broken = true;         //--- Mark as broken
            }
         }
         if (!wasBroken && zones[i].broken) {  //--- Check if newly broken
            Print("Zone broken in UpdateZones: ", zones[i].name); //--- Log broken zone
            ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, brokenZoneColor); //--- Update zone color
            string labelName = zones[i].name + "Label"; //--- Get label name
            string labelText = zones[i].isDemand ? "Demand Zone (Broken)" : "Supply Zone (Broken)"; //--- Set broken label
            ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Update label text
            if (deleteBrokenZonesFromChart) {  //--- Check if deleting broken
               ObjectDelete(0, zones[i].name); //--- Delete zone rectangle
               ObjectDelete(0, labelName);     //--- Delete zone label
            }
         }
      }
      if (ObjectFind(0, zones[i].name) >= 0 || (!zones[i].broken || !deleteBrokenZonesFromChart)) { //--- Check if drawable
         color zoneColor;                        //--- Initialize zone color
         if (zones[i].tested) {                  //--- Check if tested
            zoneColor = zones[i].isDemand ? testedDemandZoneColor : testedSupplyZoneColor; //--- Set tested color
         } else if (zones[i].broken) {           //--- Check if broken
            zoneColor = brokenZoneColor;         //--- Set broken color
         } else {                                //--- Untested zone
            zoneColor = zones[i].isDemand ? demandZoneColor : supplyZoneColor; //--- Set untested color
         }
         ObjectCreate(0, zones[i].name, OBJ_RECTANGLE, 0, zones[i].startTime, zones[i].high, zones[i].endTime, zones[i].low); //--- Create zone rectangle
         ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, zoneColor); //--- Set zone color
         ObjectSetInteger(0, zones[i].name, OBJPROP_FILL, true); //--- Enable fill
         ObjectSetInteger(0, zones[i].name, OBJPROP_BACK, true); //--- Set to background
         string labelName = zones[i].name + "Label"; //--- Generate label name
         string labelText = zones[i].isDemand ? "Demand Zone" : "Supply Zone"; //--- Set base label
         if (zones[i].tested) labelText += " (Tested)"; //--- Append tested status
         else if (zones[i].broken) labelText += " (Broken)"; //--- Append broken status
         datetime labelTime = zones[i].startTime + (zones[i].endTime - zones[i].startTime) / 2; //--- Calculate label time
         double labelPrice = (zones[i].high + zones[i].low) / 2; //--- Calculate label price
         ObjectCreate(0, labelName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Create label
         ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Set label text
         ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelTextColor); //--- Set label color
         ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Set label anchor
      }
   }
   ChartRedraw(0);                                //--- Redraw chart
}

Procederemos a implementar la lógica de gestión y visualización de zonas del sistema. En la función «UpdateZones», recorremos «zones» hacia atrás y comprobamos si la hora de la última barra cerrada (iTime con desplazamiento 1) supera el valor de «endTime» de una zona. Eliminamos las zonas caducadas con ArrayRemove, borramos sus objetos del gráfico (OBJ_RECTANGLE y «Label») si «deleteExpiredZonesFromChart» es verdadero y registramos la acción, de modo que las zonas caducadas dejen de tenerse en cuenta. En el caso de las zonas que aún no están listas, calculamos la distancia entre el cierre actual (iClose) y el máximo (demanda) o el mínimo (oferta) de la zona. Si esta distancia supera «minMoveAwayPoints * _Point», establecemos «readyForTest» en verdadero y registramos la acción si la zona acaba de pasar a estar lista.

Si «brokenZoneMode» es «AllowBroken» y la zona aún no se ha probado, la marcamos como rota si el cierre se sitúa por debajo del mínimo (demanda) o por encima del máximo (oferta). Actualizamos el color a «brokenZoneColor» con ObjectSetInteger y la etiqueta a «Zona de demanda/oferta (broken)» con ObjectSetString, eliminamos los objetos si «deleteBrokenZonesFromChart» es verdadero y registramos el evento. Para las zonas que se pueden dibujar (existentes o no rotas, con «deleteBrokenZonesFromChart» en falso), establecemos los colores («demandZoneColor», «supplyZoneColor», «testedDemandZoneColor», «testedSupplyZoneColor» o «brokenZoneColor») según el estado, dibujamos rectángulos con ObjectCreate («OBJ_RECTANGLE») utilizando «startTime», «high», «endTime» y «low», y añadimos etiquetas centradas con «ObjectCreate» (OBJ_TEXT) utilizando «labelTextColor». A continuación, volvemos a dibujar el gráfico con la función ChartRedraw, actualizando así los estados de las zonas y representándolas dinámicamente. Ahora podemos llamar a esta función en el controlador de eventos «tick» y, al hacerlo, obtenemos el siguiente resultado.

ZONAS GESTIONADAS Y VISUALIZADAS

Ahora que podemos gestionar las zonas y visualizarlas en el gráfico, solo tenemos que hacer un seguimiento de ellas y operar en función de las condiciones de trading que se cumplan. Crearemos una función que recorra las zonas válidas y compruebe las condiciones de negociación.

//+------------------------------------------------------------------+
//| Trade on zones                                                   |
//+------------------------------------------------------------------+
void TradeOnZones(bool isNewBar) {
   static datetime lastTradeCheck = 0;                   //--- Store last trade check time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   if (!isNewBar || lastTradeCheck == currentBarTime) return; //--- Exit if not new bar or checked
   lastTradeCheck = currentBarTime;                      //--- Update last trade check
   double currentBid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get current bid
   double currentAsk = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get current ask
   for (int i = 0; i < ArraySize(zones); i++) {          //--- Iterate through zones
      if (zones[i].broken) continue;                     //--- Skip broken zones
      if (tradeTestedMode == NoRetrade && zones[i].tested) continue; //--- Skip tested zones
      if (tradeTestedMode == LimitedRetrade && zones[i].tested && zones[i].tradeCount >= maxTradesPerZone) continue; //--- Skip max trades
      if (!zones[i].readyForTest) continue;              //--- Skip not ready zones
      double prevHigh = iHigh(_Symbol, _Period, 1);      //--- Get previous high
      double prevLow = iLow(_Symbol, _Period, 1);        //--- Get previous low
      double prevClose = iClose(_Symbol, _Period, 1);    //--- Get previous close
      bool tapped = false;                               //--- Initialize tap flag
      bool overlap = (prevLow <= zones[i].high && prevHigh >= zones[i].low); //--- Check candle overlap
      if (zones[i].isDemand) {                           //--- Check demand zone
         if (overlap && prevClose > zones[i].high) {     //--- Confirm demand tap
            tapped = true;                               //--- Set tapped flag
         }
      } else {                                           //--- Check supply zone
         if (overlap && prevClose < zones[i].low) {      //--- Confirm supply tap
            tapped = true;                               //--- Set tapped flag
         }
      }
      if (tapped) {                                      //--- Process tapped zone
         bool trendConfirmed = (trendConfirmation == NoConfirmation); //--- Assume no trend confirmation
         if (trendConfirmation == ConfirmTrend) {        //--- Check trend confirmation
            int oldShift = 2 + trendLookbackBars - 1;    //--- Calculate lookback shift
            if (oldShift >= iBars(_Symbol, _Period)) continue; //--- Skip if insufficient bars
            double oldClose = iClose(_Symbol, _Period, oldShift); //--- Get old close
            double recentClose = iClose(_Symbol, _Period, 2); //--- Get recent close
            double minChange = minTrendPoints * _Point; //--- Calculate min trend change
            if (zones[i].isDemand) {                    //--- Check demand trend
               trendConfirmed = (oldClose > recentClose + minChange); //--- Confirm downtrend
            } else {                                    //--- Check supply trend
               trendConfirmed = (oldClose < recentClose - minChange); //--- Confirm uptrend
            }
         }
         if (!trendConfirmed) continue;                 //--- Skip if trend not confirmed
         bool wasTested = zones[i].tested;              //--- Store previous tested status
         if (zones[i].isDemand) {                       //--- Handle demand trade
            double entryPrice = currentAsk;             //--- Set entry at ask
            double stopLossPrice = NormalizeDouble(zones[i].low - stopLossDistance * _Point, _Digits); //--- Set stop loss
            double takeProfitPrice = NormalizeDouble(entryPrice + takeProfitDistance * _Point, _Digits); //--- Set take profit
            obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice, "Buy at Demand Zone"); //--- Execute buy trade
            Print("Buy trade entered at Demand Zone: ", zones[i].name); //--- Log buy trade
         } else {                                       //--- Handle supply trade
            double entryPrice = currentBid;             //--- Set entry at bid
            double stopLossPrice = NormalizeDouble(zones[i].high + stopLossDistance * _Point, _Digits); //--- Set stop loss
            double takeProfitPrice = NormalizeDouble(entryPrice - takeProfitDistance * _Point, _Digits); //--- Set take profit
            obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice, "Sell at Supply Zone"); //--- Execute sell trade
            Print("Sell trade entered at Supply Zone: ", zones[i].name); //--- Log sell trade
         }
         zones[i].tested = true;                        //--- Mark zone as tested
         zones[i].tradeCount++;                         //--- Increment trade count
         if (!wasTested && zones[i].tested) {           //--- Check if newly tested
            Print("Zone tested: ", zones[i].name, ", Trade count: ", zones[i].tradeCount); //--- Log tested zone
         }
         color zoneColor = zones[i].isDemand ? testedDemandZoneColor : testedSupplyZoneColor; //--- Set tested color
         ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, zoneColor); //--- Update zone color
         string labelName = zones[i].name + "Label";                   //--- Get label name
         string labelText = zones[i].isDemand ? "Demand Zone (Tested)" : "Supply Zone (Tested)"; //--- Set tested label
         ObjectSetString(0, labelName, OBJPROP_TEXT, labelText);       //--- Update label text
      }
   }
   ChartRedraw(0);                                       //--- Redraw chart
}

Para implementar la lógica de negociación en caso de retesteos o toques de zonas, creamos la función «TradeOnZones». En primer lugar, detectamos las nuevas barras mediante una variable static llamada «lastTradeCheck» y salimos si no hay una barra nueva o si ya se ha comprobado. Si hay una barra nueva pendiente de comprobar, actualizamos «lastTradeCheck» con iTime y obtenemos los precios bid y ask, normalizados mediante las funciones SymbolInfoDouble y NormalizeDouble. Recorremos «zones», omitiendo las zonas rotas, las que se hayan testeado demasiadas veces (según «tradeTestedMode» y «maxTradesPerZone») o las que no estén listas. A continuación, comprobamos el máximo (iHigh), el mínimo («iLow») y el cierre (iClose) de la barra anterior para determinar si hay solapamiento con la zona; para las zonas de demanda («isDemand»), confirmamos un toque si «overlap» es verdadero y «prevClose > high»; para las zonas de oferta, si «overlap» es verdadero y «prevClose < low», y asignamos el valor correspondiente a «tapped».

Si el precio ha tocado la zona y «trendConfirmation» es «ConfirmTrend», confirmamos la tendencia comparando la diferencia entre los cierres antiguos y recientes («iClose») a lo largo de «trendLookbackBars» barras con «minTrendPoints * _Point»; si no se confirma, omitimos esa zona. Para los toques válidos, ejecutamos operaciones: en las zonas de demanda, compramos al precio ask con un stop loss situado a una distancia de «stopLossDistance * _Point» por debajo de «low» y un take profit situado a una distancia de «takeProfitDistance * _Point» por encima del precio de entrada, utilizando «obj_Trade.Buy» y registrando la operación con Print; en las zonas de oferta, vendemos al precio bid con un stop loss por encima de «high» y un take profit por debajo del precio de entrada, utilizando «obj_Trade.Sell». Marcamos la zona como testeada («tested»), incrementamos «tradeCount», registramos el evento si se acaba de testear por primera vez, actualizamos el color de la zona a «testedDemandZoneColor» o «testedSupplyZoneColor» con ObjectSetInteger y actualizamos el texto de la etiqueta a «Demand/Supply Zone (Tested)» mediante la función ObjectSetString. Por último, volvemos a trazar el gráfico. Al llamar a la función, obtenemos el siguiente resultado.

SEÑALES CONFIRMADAS

En la imagen se puede observar que detectamos los toques de la zona y almacenamos el número de operaciones o toques correspondientes a esa zona, de modo que podamos operar en toques posteriores si fuera necesario. Ahora solo queda añadir un stop dinámico para maximizar las ganancias. También lo implementaremos en una función.

//+------------------------------------------------------------------+
//| Apply trailing stop to open positions                            |
//+------------------------------------------------------------------+
void ApplyTrailingStop() {     
   double point = _Point;                               //--- Get point value
   for (int i = PositionsTotal() - 1; i >= 0; i--) {    //--- Iterate through positions
      if (PositionGetTicket(i) > 0) {                   //--- Check valid ticket
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == uniqueMagicNumber) { //--- Verify symbol and magic
            double sl = PositionGetDouble(POSITION_SL); //--- Get current stop loss
            double tp = PositionGetDouble(POSITION_TP); //--- Get current take profit
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingStopPoints * point, _Digits); //--- Calculate new stop loss
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > minProfitToTrail * point) { //--- Check trailing condition
                  obj_Trade.PositionModify(PositionGetInteger(POSITION_TICKET), newSL, tp); //--- Update stop loss
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingStopPoints * point, _Digits); //--- Calculate new stop loss
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > minProfitToTrail * point) { //--- Check trailing condition
                  obj_Trade.PositionModify(PositionGetInteger(POSITION_TICKET), newSL, tp); //--- Update stop loss
               }
            }
         }
      }
   }
}

Aquí implementamos la lógica del stop dinámico para gestionar las posiciones abiertas de forma dinámica. En la función «ApplyTrailingStop», obtenemos el tamaño del punto con _Point y recorremos las posiciones abiertas en orden inverso utilizando PositionsTotal. Comprobamos el ticket de cada posición con PositionGetTicket, su símbolo con PositionGetString y su número mágico con PositionGetInteger, comparando este último con el número mágico configurado.

Para las posiciones de compra (POSITION_TYPE_BUY), calculamos un nuevo stop loss como el precio bid (SymbolInfoDouble con SYMBOL_BID) menos «trailingStopPoints * point», normalizado con NormalizeDouble, y lo actualizamos con «obj_Trade.PositionModify» si es superior al stop loss actual («PositionGetDouble(POSITION_SL)») y el beneficio supera «minProfitToTrail * point». En el caso de las posiciones de venta, calculamos el nuevo stop loss como el precio ask («SYMBOL_ASK») más «trailingStopPoints * point» y lo actualizamos si es inferior al stop loss actual y el beneficio supera el umbral; así ajustamos los stop loss para asegurar las ganancias durante los movimientos favorables del precio. Ahora podemos llamarla en cada tick para gestionar el proceso de la siguiente manera.

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (enableTrailingStop) {                      //--- Check if trailing stop enabled
      ApplyTrailingStop();                        //--- Apply trailing stop to positions
   }
   static datetime lastBarTime = 0;               //--- Store last processed bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   bool isNewBar = (currentBarTime != lastBarTime); //--- Check for new bar
   if (isNewBar) {                                //--- Process new bar
      lastBarTime = currentBarTime;               //--- Update last bar time
      DetectZones();                              //--- Detect new zones
      ValidatePotentialZones();                   //--- Validate potential zones
      UpdateZones();                              //--- Update existing zones
   }
   if (enableTrading) {                           //--- Check if trading enabled
      TradeOnZones(isNewBar);                     //--- Execute trades on zones
   }
}

Al ejecutar el programa, 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. Luego se muestra una prueba conjunta de ambas zonas correspondiente al mes anterior.

GIF DE LA PRUEBA UNIFICADA DE OFERTA Y DEMANDA

En la visualización podemos observar que el programa identifica y verifica todas las condiciones de entrada y, si se cumplen, abre la posición correspondiente con los parámetros de entrada correspondientes, logrando así nuestro objetivo. Solo queda realizar una prueba retrospectiva del programa, de la que nos ocuparemos en la siguiente sección.


Backtesting

Tras realizar una prueba retrospectiva exhaustiva, hemos obtenido los siguientes resultados.

Gráfico de la prueba retrospectiva:

GRÁFICO

Informe de la prueba retrospectiva:

INFORME


Conclusión

Como conclusión, hemos creado un sistema de trading basado en la oferta y la demanda en MQL5 para detectar zonas de oferta y demanda a partir de fases de consolidación, validarlas con movimientos impulsivos y operar los retesteos con confirmación de la tendencia y ajustes de riesgo personalizables. El sistema muestra las zonas mediante etiquetas y colores dinámicos, e incorpora stops dinámicos para una gestión eficaz del riesgo.

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 oferta y demanda, usted dispone de las herramientas necesarias para aprovechar las oportunidades de retesteo y seguir optimizando su operativa. ¡Le deseo un buen trading!

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

Archivos adjuntos |
jiyanazad
jiyanazad | 27 nov 2025 en 20:59
Hola,
Lo he compilado sin ningún error. Por desgracia, no se ve nada ni en la prueba retrospectiva ni en el gráfico. No se trazan las zonas de oferta y demanda, ni se reconocen las señales ni se ejecutan operaciones. El registro de depuración y el diario también están vacíos. ¿Alguien ha conseguido que funcione? ¿Puede la herramienta de verificación automática comprobar si el código está completo y actualizado?
bzaranyika
bzaranyika | 4 dic 2025 en 19:04
Se ha trabajado con un marco temporal de 1 m
Allan Munene Mutiiria
Allan Munene Mutiiria | 5 dic 2025 en 07:52
bzaranyika #:
Se ha trabajado con un marco temporal de 1 m
Gracias por tus amables comentarios
Olowogbade Sunday
Olowogbade Sunday | 6 ene 2026 en 23:44
Gracias por esta gran ayuda

Pero no funciona con algunas cotizaciones, ¿es un error por mi parte?
Si es así, ¿cuáles son los criterios para que el código funcione con cualquier divisa o activo negociable?
Redes neuronales en el trading: Entrenamiento de modelos de picos profundos (Integración de picos) Redes neuronales en el trading: Entrenamiento de modelos de picos profundos (Integración de picos)
En este artículo se presenta la implementación práctica de los componentes clave del framework SEW-ResNet usando MQL5. El uso de arrays dinámicos y mecanismos de picos permite diseñar con flexibilidad la arquitectura del modelo y procesar de forma eficaz las series temporales financieras. Las soluciones propuestas muestran cómo SEW-ResNet puede optimizar los cálculos y mejorar la extracción de características relevantes.
Algoritmo de monos azules — Algoritmo Blue Monkey (BM) Algoritmo de monos azules — Algoritmo Blue Monkey (BM)
En este artículo se presenta la implementación del algoritmo metaheurístico Blue Monkey, basado en la simulación del comportamiento social de los monos azules. En este, se examinan los mecanismos clave del algoritmo —la estructura grupal de la población, el seguimiento de líderes locales y la renovación generacional mediante la sustitución de los individuos adultos con peor rendimiento por las crías con mejor rendimiento— y se analizan los resultados de las pruebas.
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.
Redes neuronales en el trading: Entrenamiento de modelos de picos profundos (SEW-ResNet) Redes neuronales en el trading: Entrenamiento de modelos de picos profundos (SEW-ResNet)
Le invitamos a conocer el framework SEW-ResNet, que permite construir modelos de picos profundos sin problemas de degradación y con una gestión eficaz de los gradientes. En este artículo mostramos cómo implementar una neurona de pico básica y sus algoritmos utilizando MQL5.