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
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:
- Comprensión del marco de la estrategia de oferta y demanda
- Implementación en MQL5
- Backtesting
- 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 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.

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 operationsComenzamos 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.

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.

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.

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.

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.

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.

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:

Informe de la prueba retrospectiva:

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
Advertencia: todos los derechos de estos materiales pertenecen a MetaQuotes Ltd. Queda totalmente prohibido el copiado total o parcial.
Este artículo ha sido escrito por un usuario del sitio web y refleja su punto de vista personal. MetaQuotes Ltd. no se responsabiliza de la exactitud de la información ofrecida, ni de las posibles consecuencias del uso de las soluciones, estrategias o recomendaciones descritas.
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?
Se ha trabajado con un marco temporal de 1 m