English Русский Deutsch 日本語
preview
Dominar los Fair Value Gaps: formación, lógica y automatización del trading con Breakers y Market Structure Shifts (MSS)

Dominar los Fair Value Gaps: formación, lógica y automatización del trading con Breakers y Market Structure Shifts (MSS)

MetaTrader 5Trading |
65 2
Eugene Mmene
Eugene Mmene

Introducción

Los Fair Value Gaps son un fenómeno muy común en la actualidad, a menudo considerados parte del repertorio del concepto de Smart Money (SMC) o, más concretamente, originados en los conceptos de Smart Money. También es habitual encontrar este concepto en redes sociales, conversaciones entre traders, plataformas de trading y salas de análisis gráfico online. No obstante, la mayoría lo interpreta simplemente como un punto de referencia, en lugar de verlo como una herramienta capaz de mejorar su operativa, definir su sesgo direccional y estructurar sus entradas en función del momento y el contexto en que se producen estos Fair Value Gaps.

Mi objetivo no es limitarme a analizar los Fair Value Gaps, sino mostrar cómo pueden utilizarse a favor del trader para estructurar el sesgo direccional, desarrollar una narrativa de mercado y, en última instancia, ejecutar operaciones. También analizaré las zonas en las que es más probable que se formen estos Fair Value Gaps, cuándo y cómo se producen, y cómo validarlos como puntos de entrada, zonas de toma de beneficios o, en última instancia, puntos de reentrada para ejecutar operaciones tras un retroceso posterior a una fuerte expansión del precio.

Una vez comprendidos los Fair Value Gaps y la lógica operativa empleada para identificar, determinar y validar el sesgo direccional y la narrativa subyacente, explicaré la formación de los factores de ruptura y los cambios en la estructura del mercado que normalmente confirman las operaciones alineadas con ese sesgo y esa narrativa. Estos factores de ruptura y cambios en la estructura del mercado resultan especialmente sólidos y, con frecuencia, llegan incluso a validar operaciones de largo plazo. Este artículo describirá y explicará de forma clara la eficacia de combinar Fair Value Gaps con factores de ruptura, especialmente considerando que ambos pueden producirse en todos los marcos temporales, tanto en operativas de corto plazo como de largo plazo.


Introducción a los Fair Value Gaps

Los Fair Value Gaps se forman cuando el precio experimenta movimientos rápidos en una u otra dirección, reflejando una fuerte presión de compra o venta, generalmente impulsada por participantes institucionales. En términos simples, esta presión del mercado suele generar un escenario en el que no todos los participantes disponen de las mismas oportunidades para colocar o ejecutar sus órdenes. En un Fair Value Gap (FVG) alcista, el precio abandona una zona con fuerza y velocidad hacia niveles superiores, dejando a los vendedores sin tiempo u oportunidad suficiente para participar de forma eficiente. En un Fair Value Gap (FVG) bajista, ocurre lo contrario: el precio abandona una zona con fuerza y velocidad hacia niveles inferiores, dejando a los compradores sin tiempo u oportunidad suficiente para intervenir. Esta zona se conoce como rango de precio desequilibrado, ya que no se ha producido una negociación significativa en la dirección opuesta al movimiento expansivo.

Esto puede entenderse mejor mediante la analogía del pintor: cuando un pintor pinta una pared, debe aplicar la pintura con trazos uniformes o similares en direcciones opuestas para que la cobertura sea homogénea, equilibrada y de buena calidad. Esta misma lógica se aplica al precio. Cuando una vela alcista se expande y atraviesa un determinado nivel de precio, por ejemplo, de 1.3010 a 1.3030, una vela bajista también debería atravesar ese mismo nivel para equilibrar el precio. Si esto no ocurre, se forma un Fair Value Gap (FVG). A menudo, si no siempre, el precio acabará volviendo a este nivel. Es en este punto y en este contexto donde debemos buscar aprovechar estos Fair Value Gaps, ya que ofrecen oportunidades amplias y potencialmente rentables.

La siguiente ilustración muestra ejemplos de Fair Value Gaps y cómo se utilizaron.

Fair Value Gap bajista

Cómo, cuándo y por qué se forman

Una vez definido el concepto de Fair Value Gap, resulta mucho más sencillo comprender su proceso de formación. Esto ocurre en situaciones muy bien definidas. El primer escenario se produce cuando tiene lugar la publicación de una noticia o comunicado económico. Puede tratarse de uno de los ejemplos de noticias de alto impacto marcadas en rojo en el calendario económico, como el CPI, el FOMC o el NFP, o incluso de otros tipos de comunicados; todo depende del impacto que tengan en el mercado.

La mayoría de las veces, estas publicaciones se producen cuando los grandes participantes del mercado ya han colocado órdenes pendientes y, por tanto, esperan esa volatilidad, velocidad y fuerza. En consecuencia, el mercado se desplaza rápidamente a su favor y deja tras de sí Fair Value Gaps de los que buscamos obtener beneficio. Como práctica estándar del sector, no es ideal operar antes de una noticia o comunicado económico de alto impacto. Suele ser más prudente esperar a que se publique la noticia y a que aparezca la volatilidad asociada antes de ejecutar operaciones. Por ello, lo ideal sería evitar estas entradas prematuras, ya que la mayoría ni siquiera ofrecen configuraciones operativas claras.

Otro escenario en el que se forman es cuando grandes participantes del mercado, como bancos y hedge funds, colocan sus órdenes. Estas órdenes influyen en los movimientos del mercado, ya que pueden representar miles de millones de dólares.

Estas grandes órdenes pueden detectarse fácilmente mediante velas de gran tamaño y movimientos bruscos y repentinos en una sola dirección, incluso sin la influencia de publicaciones económicas o noticias de alto impacto. En este artículo, intento ilustrar cómo, tras estos grandes movimientos, debemos entrenar nuestra atención para detectar los Fair Value Gaps que se producen durante dichas expansiones del precio.

La siguiente ilustración muestra cómo el precio rompió rápidamente una zona con velocidad y fuerza, y cómo la vela siguiente no retrocedió para cubrir la vela alcista, dejando una sección sin cubrir a la que se denomina Fair Value Gap.

Fair Value Gap alcista


Cómo analizar los Fair Value Gaps

En este capítulo, veremos cómo analizar los Fair Value Gaps, una vez explicado en los capítulos anteriores qué son, cuándo se producen y cómo se forman. Ahora nos centraremos en cómo analizar estos mismos gaps para determinar qué operaciones debemos esperar y ejecutar.

La primera regla básica es que, en la mayoría de los casos, no deberíamos buscar operaciones dentro de los Fair Value Gaps, sino utilizarlos como zonas desde las que el precio puede impulsarse. Cuando se emplean correctamente en la dirección de la tendencia, el sesgo direccional y la narrativa de mercado, pueden actuar como puntos de propulsión para la continuación del movimiento. Entonces, ¿por qué operar en contra de un bloque propulsivo que, en cuestión de momentos, puede actuar contra nosotros con toda su fuerza? Y esto es completamente cierto y preciso tanto en marcos temporales cortos como largos; todos actúan de la misma manera. La única diferencia radica en el tiempo que tarda esta dinámica en materializarse en una operación. No obstante, aproximadamente en el 90 % de los casos, los Fair Value Gaps tienden a comportarse de esta manera.

Lo ideal es que el primer paso consista en realizar un análisis de arriba abajo, desde los marcos temporales superiores hasta los inferiores, observando y marcando todos los Fair Value Gaps. A continuación, el paso más importante es analizar la dirección de la tendencia, el sesgo direccional y la narrativa de mercado que están en juego. Por ejemplo, si la tendencia, el sesgo direccional y la narrativa del oro en MN, W1 y D1 apuntan hacia compras, deberíamos priorizar escenarios operativos alineados con esa dirección. Esto puede confirmarse de tres formas principales: el precio cotiza por encima de las medias móviles, tanto simples como exponenciales; la estructura del mercado muestra claramente impulso comprador; y, además, las velas verdes o alcistas son mucho más numerosas que las velas rojas o bajistas.

Una vez definidas la tendencia y la narrativa en juego, debemos tener en cuenta otro aspecto importante. El precio está en constante movimiento, buscando liquidez para barrerla, y la tendencia, la dirección y el sesgo se determinan en marcos temporales más amplios. La liquidez se concentra principalmente en los mínimos y máximos diarios, semanales y mensuales anteriores, y es ahí donde cabe esperar reacciones significativas. Por lo tanto, una vez que tengamos una idea de la liquidez y la tendencia, podremos ejecutar nuestras operaciones con facilidad y acierto.

Un ejemplo de esta situación es que hemos constatado que el precio del oro se está acercando al máximo de la semana anterior, lo cual se ve confirmado por la estructura del mercado y las medias móviles. Durante la semana o en sesiones consecutivas, el precio continúa desplazándose hacia el máximo de la semana anterior, mientras siguen formándose Fair Value Gaps. Estos Fair Value Gaps se etiquetarán como alcistas y, si se producen en marcos temporales superiores —por ejemplo, H1, H4 o D1—, aún mejor; deberán marcarse como Fair Value Gaps alcistas. Los traders siempre deberían anticipar operaciones de compra cuando el precio regresa a estos gaps; de este modo, pueden buscar oportunidades de ejecución.

Es importante señalar que, en la mayoría de los casos, el precio tiende a reequilibrar los Fair Value Gaps. Sin embargo, si el precio se encuentra cerca de su objetivo, como se mencionó anteriormente —por ejemplo, máximos semanales, mensuales o diarios—, una vez alcanzado dicho objetivo y purgada la liquidez, la probabilidad de que ese Fair Value Gap se utilice para nuevas operaciones se reduce significativamente, pudiendo incluso convertirse en una compra fallida. Por tanto, deberíamos evitar este tipo de operaciones.


Cómo influyen en las operaciones y en su dirección

Básicamente, la idea es que, cuando existe una tendencia y una dirección predeterminadas —por ejemplo, bajista—, se buscan y marcan los Fair Value Gaps mediante un análisis de arriba abajo. Cabe señalar que el precio volverá a estos Fair Value Gaps para ofrecer oportunidades de operación y entrada, siempre que se mantengan los objetivos semanales, diarios o mensuales. Por lo general, el precio debe recuperar el equilibrio y rellenar los Fair Value Gaps. Por lo tanto, actúa como un impulsor de fuerza para un par o activo cuando el precio regresa a esa zona y aparecen compradores secundarios, ya que más traders abren nuevas posiciones en ese punto, lo que explica la velocidad y la fuerza del mercado. Esto provoca movimientos alcistas fuertes en el precio. Además, refuerza la dirección, el sesgo direccional y la narrativa de la tendencia.


Cómo utilizar correctamente los Fair Value Gaps para ejecutar entradas operativas

Este es uno de los apartados más importantes y valiosos del artículo, porque, una vez validados y analizados todos nuestros criterios, podemos entender cómo y dónde validar o invalidar los Fair Value Gaps, cómo se forman y en qué zonas tienen mayor probabilidad de aparecer. Esta parte es sencilla, ya que todo el trabajo duro ya está hecho.

Ahora solo hace falta paciencia, disciplina y concentración. Analizaremos principalmente desde D1, ya que es el gráfico de referencia que nos permite buscar Fair Value Gaps. Si detectamos un Fair Value Gap y el precio regresa a él, una vez que entra en la zona, bajamos rápidamente al marco temporal H4 y buscamos una vela envolvente alcista —si la tendencia, el sesgo direccional y la narrativa son compradores— o una vela envolvente bajista —si el sesgo, la tendencia y la narrativa son vendedores—. Esto puede ocurrir en cualquier punto dentro del Fair Value Gap, aunque lo ideal es que se produzca cerca del inicio de las sesiones de trading, por ejemplo, durante la apertura de Londres o Nueva York. Tras detectar la vela envolvente, volvemos al gráfico H1; aquí, buscamos una ruptura en la estructura del mercado.

Una ruptura en la estructura del mercado se refiere a un cambio en el estado de entrega del precio; si antes vendías y ahora pasas a ser comprador, eso significa que se ha producido un cambio en el estado de entrega y, por lo tanto, un cambio en la estructura del mercado. Por lo tanto, en H1, esto es lo que estás buscando. Los stop loss deben colocarse en los extremos opuestos de las rupturas, y las entradas pueden ejecutarse en rupturas con brechas pequeñas y a precios razonables que puedan producirse en la ruptura del H1 o en el nivel de retroceso del 60 %, si este se da.

Se produce una ruptura cuando se supera el último máximo o mínimo anterior a un cambio en la estructura del mercado. Una ruptura rápida y contundente del último máximo en un mercado bajista constituye una ruptura alcista, mientras que una ruptura rápida y contundente del último mínimo en un mercado alcista constituye una ruptura bajista.

El mismo fenómeno se produce en otros intervalos de tiempo. Si detectamos una brecha en el valor razonable en el gráfico H4, buscamos una vela envolvente en el gráfico H1 y, a continuación, un cambio en la estructura del mercado en el gráfico M15. Podemos aplicar esta estrategia a los operadores que utilizan marcos temporales más cortos. Se repite el mismo fenómeno: se produce una brecha de valor razonable en H1 y una vela envolvente en M15; por lo tanto, se produce un cambio en la estructura del mercado en el marco temporal M5. Además, en lo que respecta al scalping, se produjeron brechas de valor razonable en el gráfico M15. Podemos buscar velas envolvente en el gráfico M5 y cambios en la estructura del mercado en el gráfico M1.


Cómo y cuándo evitar operar con Fair Value Gaps

La mayoría de los Fair Value Gaps suelen ofrecer oportunidades operativas, pero, como se explicó anteriormente, algunos no son ideales para operar.

1. Los Fair Value Gaps que se forman en contra de la tendencia, el sesgo direccional y la narrativa del mercado no suelen ser respetados. Por ejemplo, si la tendencia o el sesgo son alcistas y el precio retrocede dejando un Fair Value Gap, posiblemente como parte de un pullback, no deberíamos esperar que el precio respete ese Fair Value Gap situado por encima cuando retome su movimiento alcista. Lo más probable es que sea ignorado rápidamente y atravesado por el precio.

2. El segundo escenario en el que estos Fair Value Gaps tienden a fallar se da cuando los objetivos principales del marco semanal, diario o mensual ya han sido alcanzados. Los Fair Value Gaps pueden fallar porque el precio ya no tiene motivos para continuar la tendencia en esa dirección, dado que la liquidez ya ha sido purgada. Por lo tanto, ya no queda la misma fuerza ni velocidad disponible, puesto que los grandes participantes del mercado ya han asegurado sus beneficios. Por lo tanto, los Fair Value Gaps pueden fallar porque el precio puede buscar un retroceso o incluso un cambio de dirección.

3. El tercer y último escenario en el que este fenómeno puede fallar se produce cuando, por ejemplo, no aparecen velas envolventes ni cambios en la estructura del mercado en marcos temporales inferiores, como se explicó anteriormente. Además, deben producirse durante sesiones de trading muy activas y en ese orden.

Primero se forman los Fair Value Gaps, después aparecen las velas envolventes y, finalmente, se produce el cambio en la estructura del mercado. Por lo tanto, solo cuando se den en este orden podremos ejecutar operaciones con configuraciones óptimas y de gran calidad. Esto indica claramente que un trader no debería utilizar este método ni operar un Fair Value Gap si no se cumplen todos los factores mencionados anteriormente, ya que existe una alta probabilidad de fallo.


Fair Value Gaps en marcos temporales superiores

Los gaps en marcos temporales superiores hacen referencia a Fair Value Gaps formados en H4, D1 y W1. Estos pueden utilizarse fácilmente para movimientos amplios o de largo plazo, incluso de 200 a 500 pips, ya que suelen ser más estables y lentos, con menor exposición a la volatilidad de corto plazo o a movimientos unidireccionales bruscos. Por ello, resultan más adecuados para traders de largo plazo que pueden mantener sus operaciones con paciencia, sin nerviosismo ni miedo.


Fair Value Gaps en marcos temporales inferiores

Los gaps en marcos temporales inferiores hacen referencia a los Fair Value Gaps que se forman en H1, M15 y M5. Estos gaps se utilizan principalmente, e idealmente, por traders de corto plazo, no por traders de largo plazo. Esto incluye desde scalpers hasta traders intradía que mantienen sus operaciones durante unas pocas horas antes de cerrarlas. Pueden producirse en entornos de mercado tendenciales, especialmente en M15, y cuando se aprovechan durante sesiones activas, suelen resultar muy fiables. Están diseñados para una ejecución rápida y pueden ofrecer fácilmente entre 100 y 150 pips. Dependiendo de la fuerza y la velocidad del movimiento del precio, estos gaps presentan fluctuaciones de corto plazo y una alta volatilidad. Requieren traders capaces de tomar decisiones operativas rápidas y desenvolverse bien en este tipo de entornos.


Automatización del trading con Fair Value Gaps en MQL5

Con el fin de automatizar esta estrategia, he desarrollado...

  • Un indicador FVG para detectar y marcar los gaps.
  • Un EA FVG + MSS para operar en función de las detecciones, incorporando MSS para las entradas.

Código fuente del indicador FVG

Este indicador detecta FVG buscando velas amplias que no se solapan, es decir, desequilibrios entre máximos y mínimos. Dibuja rectángulos para visualizar los gaps detectados.

#property copyright "Eugene Mmene"
#property link      "EMcapital"
#property version   "1.0"

input int minPts = 100; // Minimum points for FVG gap to be valid
input int FVG_Rec_Ext_Bars = 10; // Length of FVG rectangle in bars
input bool DrawFVG = true; // Draw FVG rectangles on chart

#define FVG_Prefix "FVG_REC_"
#define CLR_UP clrLime
#define CLR_DOWN clrRed

struct TimeframeData {
   ENUM_TIMEFRAMES tf;
   datetime lastBar;
};

TimeframeData tfs[];
string eaSymbol;

void CreateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr) {
   Print("CreateRec called: objName=", objName, ", time1=", TimeToString(time1), ", price1=", DoubleToString(price1, _Digits));
   if(DrawFVG && ObjectFind(0, objName) < 0) {
      if(ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2)) {
         ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
         ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
         ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
         ObjectSetInteger(0, objName, OBJPROP_FILL, true);
         ObjectSetInteger(0, objName, OBJPROP_BACK, false);
         Print("CreateRec: Rectangle created successfully: ", objName);
      } else {
         Print("CreateRec: Failed to create rectangle: ", objName, ", Error=", GetLastError());
      }
   } else {
      Print("CreateRec: Skipped - DrawFVG=", DrawFVG, ", Object exists=", ObjectFind(0, objName) >= 0);
   }
}

void DetectFVGs() {
   Print("DetectFVGs started");
   int maxObjects = 100; // Limit total objects to prevent overload
   int currentObjects = ObjectsTotal(0, 0, OBJ_RECTANGLE);
   if(currentObjects >= maxObjects) {
      Print("DetectFVGs: Object limit reached (", currentObjects, "/", maxObjects, "), skipping FVG creation");
      return;
   }

   for(int i = 0; i < ArraySize(tfs); i++) {
      if(!NewBar(tfs[i].tf)) continue;
      double low0 = iLow(eaSymbol, tfs[i].tf, 0);
      double high2 = iHigh(eaSymbol, tfs[i].tf, 2);
      double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, 0);
      
      double high0 = iHigh(eaSymbol, tfs[i].tf, 0);
      double low2 = iLow(eaSymbol, tfs[i].tf, 2);
      double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, 0);
      
      if(gap_L0_H2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bullish FVG detected: Low=%s, High[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(low0, _Digits), DoubleToString(high2, _Digits), DoubleToString(gap_L0_H2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), high2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, low0, CLR_UP);
      }
      if(gap_H0_L2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bearish FVG detected: High=%s, Low[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(high0, _Digits), DoubleToString(low2, _Digits), DoubleToString(gap_H0_L2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), low2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, high0, CLR_DOWN);
      }
   }
   if(DrawFVG) ChartRedraw(0); // Single redraw per tick
   Print("DetectFVGs completed");
}

bool NewBar(ENUM_TIMEFRAMES tf) {
   int idx = TimeframeIndex(tf);
   if(idx < 0) return false;
   datetime cur = iTime(eaSymbol, tf, 0);
   if(cur != tfs[idx].lastBar) {
      tfs[idx].lastBar = cur;
      return true;
   }
   return false;
}

int TimeframeIndex(ENUM_TIMEFRAMES tf) {
   for(int i = 0; i < ArraySize(tfs); i++) {
      if(tfs[i].tf == tf) return i;
   }
   return -1;
}

int OnInit() {
   eaSymbol = _Symbol;
   if(!SymbolSelect(eaSymbol, true)) {
      Print("Error: Failed to select ", eaSymbol, " in Market Watch");
      return(INIT_FAILED);
   }

   ArrayResize(tfs, 3);
   tfs[0].tf = PERIOD_M5;
   tfs[1].tf = PERIOD_M15;
   tfs[2].tf = PERIOD_H1;
   for(int i = 0; i < 3; i++) {
      tfs[i].lastBar = 0;
   }

   Print("FVG Detector initialized for ", eaSymbol, ". Timeframes: M5, M15, H1");
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   if(DrawFVG) ObjectsDeleteAll(0, FVG_Prefix);
   Print("FVG Detector stopped: ", reason);
}

void OnTick() {
   DetectFVGs();
}

Instalación: Compilar en MetaEditor y adjuntarlo al gráfico. Dibuja rectángulos verdes para los FVG alcistas y rectángulos rojos para los bajistas.

Ejemplo de uso: En el gráfico GOLD M15, detecta las lagunas para realizar un análisis visual.


Código fuente del asesor experto FVG + MSS

Este EA detecta los FVG, espera a que se produzca un retroceso hacia la brecha, comprueba si se produce una ruptura del último máximo o mínimo previo y abre posiciones. Incluye la gestión de riesgos (un 2 % de riesgo por operación).  

#property copyright "Eugene Mmene"
#property link      "EMcapital"
#property version   "2.27.2"

#include <Trade\Trade.mqh>

input double RiskPct = 2.0; // Base risk per trade %
input double MaxLossUSD = 110.0; // Maximum loss per trade in USD
input double RecTgt = 7000.0; // Equity recovery target
input int ATR_Prd = 14; // ATR period
input int Brk_Prd = 10; // Breakout period
input int EMA_Prd = 20; // EMA period
input string GS_Url = ""; // Google Sheets webhook URL
input bool NewsFilt = true; // News filter
input int NewsPause = 15; // Pause minutes
input double MinBrkStr = 0.1; // Min breakout strength (x ATR)
input int Vol_Prd = 1; // Volume period
input bool Bypass = true; // Bypass volume, breakout, HTF trend filters for testing
input bool useHTF = false; // Use D1 or H4 EMA trend filter
input string NewsAPI_Url = "https://www.alphavantage.co/query?function=NEWS_SENTIMENT&apikey="; // Alpha Vantage API URL
input string NewsAPI_Key = "pub_3f54bba977384ac19b6839a744444aba"; // Alpha Vantage API key
input double DailyDDLimit = 2.5; // Daily drawdown limit (%)
input double OverallDDLimit = 5.5; // Overall drawdown limit (%)
input double TargetBalanceOrEquity = 6600.0; // Target balance or equity to pass challenge ($)
input bool ResetProfitTarget = false; // Reset target to resume trading
input int minPts = 100; // Minimum points for FVG gap to be valid
input int FVG_Rec_Ext_Bars = 10; // Length of FVG rectangle in bars
input bool DrawFVG = true; // Draw FVG rectangles on chart

double CurRisk = RiskPct;
double OrigRisk = RiskPct;
double LastEqHigh = 0;
double StartingBalance = 0;
double DailyBalance = 0;
datetime LastDay = 0;
bool ProfitTargetReached = false;
bool DailyDDPaused = false;
CTrade trade;
int h_ema_d1 = INVALID_HANDLE;
int h_ema_h4 = INVALID_HANDLE;
int winStreak = 0;
int lossStreak = 0;
string eaSymbol = _Symbol;

struct TimeframeData {
   ENUM_TIMEFRAMES tf;
   int h_atr;
   int h_vol;
   int h_vol_ma;
   datetime lastSig;
   datetime lastBar;
};

TimeframeData tfs[];
struct NewsEvt { 
   datetime time; 
   string evt; 
   int impact; 
};
NewsEvt newsCal[];
int newsCnt = 0;

struct TradeLog {
   ulong ticket;
   bool isWin;
   double profit;
   double brkStr;
   double vol;
   double risk;
   ENUM_TIMEFRAMES tf;
};
TradeLog tradeHistory[];
int tradeCnt = 0;
double dynBrkStr = MinBrkStr;

#define FVG_Prefix "FVG_REC_"
#define CLR_UP clrLime
#define CLR_DOWN clrRed

void CreateRec(string objName, datetime time1, double price1, datetime time2, double price2, color clr) {
   Print("CreateRec called: objName=", objName, ", time1=", TimeToString(time1), ", price1=", DoubleToString(price1, _Digits));
   if(DrawFVG && ObjectFind(0, objName) < 0) {
      if(ObjectCreate(0, objName, OBJ_RECTANGLE, 0, time1, price1, time2, price2)) {
         ObjectSetInteger(0, objName, OBJPROP_TIME, 0, time1);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 0, price1);
         ObjectSetInteger(0, objName, OBJPROP_TIME, 1, time2);
         ObjectSetDouble(0, objName, OBJPROP_PRICE, 1, price2);
         ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
         ObjectSetInteger(0, objName, OBJPROP_FILL, true);
         ObjectSetInteger(0, objName, OBJPROP_BACK, false);
         Print("CreateRec: Rectangle created successfully: ", objName);
      } else {
         Print("CreateRec: Failed to create rectangle: ", objName, ", Error=", GetLastError());
      }
   } else {
      Print("CreateRec: Skipped - DrawFVG=", DrawFVG, ", Object exists=", ObjectFind(0, objName) >= 0);
   }
}

void DetectFVGs() {
   Print("DetectFVGs started");
   int maxObjects = 100; // Limit total objects to prevent overload
   int currentObjects = ObjectsTotal(0, 0, OBJ_RECTANGLE);
   if(currentObjects >= maxObjects) {
      Print("DetectFVGs: Object limit reached (", currentObjects, "/", maxObjects, "), skipping FVG creation");
      return;
   }

   for(int i = 0; i < ArraySize(tfs); i++) {
      if(!NewBar(tfs[i].tf)) continue;
      double low0 = iLow(eaSymbol, tfs[i].tf, 0);
      double high2 = iHigh(eaSymbol, tfs[i].tf, 2);
      double gap_L0_H2 = NormalizeDouble((low0 - high2) / _Point, 0);
      
      double high0 = iHigh(eaSymbol, tfs[i].tf, 0);
      double low2 = iLow(eaSymbol, tfs[i].tf, 2);
      double gap_H0_L2 = NormalizeDouble((low2 - high0) / _Point, 0);
      
      if(gap_L0_H2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bullish FVG detected: Low=%s, High[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(low0, _Digits), DoubleToString(high2, _Digits), DoubleToString(gap_L0_H2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), high2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, low0, CLR_UP);
      }
      if(gap_H0_L2 > minPts) {
         string fvgName = FVG_Prefix + EnumToString(tfs[i].tf) + "_" + TimeToString(iTime(eaSymbol, tfs[i].tf, 0), TIME_DATE|TIME_MINUTES);
         Print(StringFormat("%s Bearish FVG detected: High=%s, Low[2]=%s, Gap=%s points", 
               EnumToString(tfs[i].tf), DoubleToString(high0, _Digits), DoubleToString(low2, _Digits), DoubleToString(gap_H0_L2, 0)));
         CreateRec(fvgName, iTime(eaSymbol, tfs[i].tf, 0), low2, iTime(eaSymbol, tfs[i].tf, 0) + PeriodSeconds(tfs[i].tf) * FVG_Rec_Ext_Bars, high0, CLR_DOWN);
      }
   }
   if(DrawFVG) ChartRedraw(0); // Single redraw per tick
   Print("DetectFVGs completed");
}

int OnInit() {
   if(AccountInfoDouble(ACCOUNT_BALANCE) < 10.0) {
      Print("Low balance: ", DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2));
      return(INIT_FAILED);
   }
   string sym = Symbol();
   bool selected;
   if(!SymbolExist(sym, selected)) {
      Print("Error: Symbol ", sym, " not found in Market Watch. Available: ", _Symbol);
      eaSymbol = _Symbol;
   } else {
      eaSymbol = sym;
      Print("Symbol validated: ", eaSymbol, ", Selected in Market Watch: ", selected);
   }
   if(!SymbolSelect(eaSymbol, true)) {
      Print("Error: Failed to select ", eaSymbol, " in Market Watch");
      return(INIT_FAILED);
   }

   Print("Please ensure ", NewsAPI_Url, " is added to allowed WebRequest URLs in MT5 settings");

   StartingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
   LastEqHigh = AccountInfoDouble(ACCOUNT_EQUITY);
   DailyBalance = StartingBalance;
   LastDay = TimeCurrent() / 86400 * 86400;
   ProfitTargetReached = ResetProfitTarget ? false : ProfitTargetReached;
   DailyDDPaused = false;
   ArrayResize(newsCal, 100);
   ArrayResize(tradeHistory, 100);
   ArrayResize(tfs, 3);
   tfs[0].tf = PERIOD_M5;
   tfs[1].tf = PERIOD_M15;
   tfs[2].tf = PERIOD_H1;
   for(int i = 0; i < 3; i++) {
      tfs[i].h_atr = iATR(eaSymbol, tfs[i].tf, ATR_Prd);
      tfs[i].h_vol = iVolumes(eaSymbol, tfs[i].tf, VOLUME_TICK);
      tfs[i].h_vol_ma = iMA(eaSymbol, tfs[i].tf, Vol_Prd, 0, MODE_SMA, PRICE_CLOSE);
      tfs[i].lastSig = 0;
      tfs[i].lastBar = 0;
      if(tfs[i].h_atr == INVALID_HANDLE || tfs[i].h_vol == INVALID_HANDLE || tfs[i].h_vol_ma == INVALID_HANDLE) {
         Print("Indicator init failed for ", EnumToString(tfs[i].tf));
         return(INIT_FAILED);
      }
   }
   h_ema_d1 = iMA(eaSymbol, PERIOD_D1, EMA_Prd, 0, MODE_EMA, PRICE_CLOSE);
   h_ema_h4 = iMA(eaSymbol, PERIOD_H4, EMA_Prd, 0, MODE_EMA, PRICE_CLOSE);
   if(h_ema_d1 == INVALID_HANDLE || h_ema_h4 == INVALID_HANDLE) {
      Print("EMA init failed");
      return(INIT_FAILED);
   }
   if(NewsFilt) FetchNewsCalendar();
   Print("EA initialized. Timeframes: M5, M15, H1, News events: ", newsCnt, ", Bypass: ", Bypass, ", UseHTF: ", useHTF, 
         ", Time时间: EAT (UTC+3), Server: ", TerminalInfoString(TERMINAL_NAME), 
         ", Starting Balance: ", DoubleToString(StartingBalance, 2), ", Target Balance/Equity: ", DoubleToString(TargetBalanceOrEquity, 2));
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason) {
   if(h_ema_d1 != INVALID_HANDLE) IndicatorRelease(h_ema_d1);
   if(h_ema_h4 != INVALID_HANDLE) IndicatorRelease(h_ema_h4);
   for(int i = 0; i < ArraySize(tfs); i++) {
      if(tfs[i].h_atr != INVALID_HANDLE) IndicatorRelease(tfs[i].h_atr);
      if(tfs[i].h_vol != INVALID_HANDLE) IndicatorRelease(tfs[i].h_vol);
      if(tfs[i].h_vol_ma != INVALID_HANDLE) IndicatorRelease(tfs[i].h_vol_ma);
   }
   if(DrawFVG) ObjectsDeleteAll(0, FVG_Prefix);
   Print("EA stopped: ", reason);
}

void CloseAllPositions() {
   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket) || PositionGetString(POSITION_SYMBOL) != eaSymbol) continue;
      long magic = PositionGetInteger(POSITION_MAGIC);
      if(magic == MagicNumber(PERIOD_M5) || magic == MagicNumber(PERIOD_M15) || magic == MagicNumber(PERIOD_H1)) {
         trade.PositionClose(ticket);
         Print("Closed position: Ticket=", ticket, ", Symbol=", eaSymbol, ", Magic=", magic);
      }
   }
}

void OnTick() {
   datetime currentDay = TimeCurrent() / 86400 * 86400;
   if(currentDay > LastDay) {
      DailyBalance = AccountInfoDouble(ACCOUNT_BALANCE);
      LastDay = currentDay;
      DailyDDPaused = false;
      Print("Daily balance reset: ", DoubleToString(DailyBalance, 2), " at ", TimeToString(currentDay, TIME_DATE));
   }

   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   if(balance >= TargetBalanceOrEquity || equity >= TargetBalanceOrEquity) {
      CloseAllPositions();
      ProfitTargetReached = true;
      Print("Trading paused: Target balance or equity reached. Balance=", DoubleToString(balance, 2), ", Equity=", DoubleToString(equity, 2), 
            ", Target=", DoubleToString(TargetBalanceOrEquity, 2), ". All positions closed. Set ResetProfitTarget=true or restart EA to resume trading.");
      return;
   }

   double dailyDD = (DailyBalance - equity) / DailyBalance * 100;
   double overallDD = (StartingBalance - equity) / StartingBalance * 100;

   if(dailyDD >= DailyDDLimit) {
      CloseAllPositions();
      DailyDDPaused = true;
      Print("Trading paused until next trading day: Daily DD=", StringFormat("%.2f", dailyDD), "% reached (Limit: ", DoubleToString(DailyDDLimit, 2), 
            "%), Equity=", DoubleToString(equity, 2), ", Daily Balance=", DoubleToString(DailyBalance, 2), ". All positions closed.");
      return;
   }

   if(overallDD >= OverallDDLimit) {
      CloseAllPositions();
      ProfitTargetReached = true;
      Print("Trading paused: Overall DD=", StringFormat("%.2f", overallDD), "% reached (Limit: ", DoubleToString(OverallDDLimit, 2), 
            "%), Equity=", DoubleToString(equity, 2), ", Starting Balance=", DoubleToString(StartingBalance, 2), ". All positions closed. Set ResetProfitTarget=true or restart EA to resume trading.");
      return;
   }

   if(ProfitTargetReached) {
      Print("Trading paused: Target or overall drawdown previously reached. Balance=", DoubleToString(balance, 2), ", Equity=", DoubleToString(equity, 2), ", Target=", DoubleToString(TargetBalanceOrEquity, 2));
      return;
   }

   if(DailyDDPaused) {
      Print("Trading paused: Daily drawdown limit previously reached. Waiting for next trading day. Equity=", DoubleToString(equity, 2), ", Daily Balance=", DoubleToString(DailyBalance, 2));
      return;
   }

   static datetime lastNewsFetch = 0;
   if(NewsFilt && TimeCurrent() >= lastNewsFetch + 4 * 3600) {
      FetchNewsCalendar();
      lastNewsFetch = TimeCurrent();
   }

   for(int i = 0; i < ArraySize(tfs); i++) {
      if(!NewBar(tfs[i].tf)) continue;

      bool hasPosition = false;
      for(int j = PositionsTotal() - 1; j >= 0; j--) {
         ulong ticket = PositionGetTicket(j);
         if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == eaSymbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber(tfs[i].tf)) {
            hasPosition = true;
            break;
         }
      }
      if(hasPosition) {
         ManageTrades(tfs[i].tf);
         continue;
      }

      if(TimeCurrent() <= tfs[i].lastSig + PeriodSeconds(PERIOD_H1)) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Within 1-hour cooldown");
         continue;
      }

      if(NewsFilt && IsNews()) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: News pause active");
         continue;
      }

      double eq = AccountInfoDouble(ACCOUNT_EQUITY);
      if(eq > LastEqHigh) LastEqHigh = eq;
      if(eq < LastEqHigh && lossStreak >= 2) CurRisk = MathMax(OrigRisk * 0.25, 0.1);
      else if(winStreak >= 3) CurRisk = MathMin(OrigRisk * 1.25, 5.0);
      else CurRisk = OrigRisk;
      if(eq >= RecTgt) {
         CurRisk = OrigRisk;
         winStreak = 0;
         lossStreak = 0;
      }

      bool bullHTF = !useHTF || (BullTrend(PERIOD_D1) || BullTrend(PERIOD_H4));
      bool bearHTF = !useHTF || (BearTrend(PERIOD_D1) || BearTrend(PERIOD_H4));
      bool buyBrk = BuyBrk(tfs[i].tf);
      bool sellBrk = SellBrk(tfs[i].tf);
      Print(EnumToString(tfs[i].tf), " Signal check: BullHTF=", bullHTF, ", BearHTF=", bearHTF, ", BuyBrk=", buyBrk, ", SellBrk=", sellBrk, 
            ", Bid=", DoubleToString(SymbolInfoDouble(eaSymbol, SYMBOL_BID), _Digits), ", Ask=", DoubleToString(SymbolInfoDouble(eaSymbol, SYMBOL_ASK), _Digits));

      double atr[1], vol[2], vol_ma[1];
      if(CopyBuffer(tfs[i].h_atr, 0, 0, 1, atr) < 1 || 
         CopyBuffer(tfs[i].h_vol, 0, 0, 2, vol) < 2 || 
         CopyBuffer(tfs[i].h_vol_ma, 0, 1, 1, vol_ma) < 1) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Indicator copy failed");
         continue;
      }

      double slPips = atr[0] * 2 / _Point;
      double lots = CalcLots(eq, CurRisk, slPips);
      double margReq = SymbolInfoDouble(eaSymbol, SYMBOL_MARGIN_INITIAL) * lots;
      double freeMarg = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
      Print(EnumToString(tfs[i].tf), " Lot size: ", DoubleToString(lots, 2), ", Margin required: ", DoubleToString(margReq, 2), 
            ", Free margin: ", DoubleToString(freeMarg, 2), ", SL Pips: ", DoubleToString(slPips, 2), 
            ", Contract size: ", DoubleToString(SymbolInfoDouble(eaSymbol, SYMBOL_TRADE_CONTRACT_SIZE), 0));

      if(freeMarg < margReq * 1.2) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Margin low (", DoubleToString(freeMarg, 2), " < ", DoubleToString(margReq * 1.2, 2), ")");
         continue;
      }

      double brkStr = MathAbs(buyBrk ? SymbolInfoDouble(eaSymbol, SYMBOL_ASK) - iHigh(eaSymbol, tfs[i].tf, iHighest(eaSymbol, tfs[i].tf, MODE_HIGH, Brk_Prd, 1)) :
                                     iLow(eaSymbol, tfs[i].tf, iLowest(eaSymbol, tfs[i].tf, MODE_LOW, Brk_Prd, 1)) - SymbolInfoDouble(eaSymbol, SYMBOL_BID)) / atr[0];
      if(!Bypass && brkStr < dynBrkStr) {
         Print(EnumToString(tfs[i].tf), " Trade skipped: Breakout strength too low (", DoubleToString(brkStr, 2), " < ", DoubleToString(dynBrkStr, 2), ")");
         continue;
      }

      if(bullHTF && buyBrk) {
         double price = SymbolInfoDouble(eaSymbol, SYMBOL_ASK);
         double sl = price - slPips * _Point;
         double tp = price + slPips * 2 * _Point;
         Print(EnumToString(tfs[i].tf), " Attempting Buy: Price=", DoubleToString(price, _Digits), ", SL=", DoubleToString(sl, _Digits), 
               ", TP=", DoubleToString(tp, _Digits), ", Lots=", DoubleToString(lots, 2));
         trade.SetExpertMagicNumber(MagicNumber(tfs[i].tf));
         if(trade.Buy(lots, eaSymbol, price, sl, tp)) {
            tfs[i].lastSig = TimeCurrent();
            LogTrd(trade.ResultOrder(), eaSymbol, price, sl, tp, "Open", brkStr, vol[1], CurRisk, tfs[i].tf);
            Print(EnumToString(tfs[i].tf), " Buy opened: Ticket=", trade.ResultOrder());
         } else {
            Print(EnumToString(tfs[i].tf), " Buy failed: Retcode=", trade.ResultRetcode(), ", Error=", GetLastError(), ", Comment=", trade.ResultComment());
         }
      } else if(bearHTF && sellBrk) {
         double price = SymbolInfoDouble(eaSymbol, SYMBOL_BID);
         double sl = price + slPips * _Point;
         double tp = price - slPips * 2 * _Point;
         Print(EnumToString(tfs[i].tf), " Attempting Sell: Price=", DoubleToString(price, _Digits), ", SL=", DoubleToString(sl, _Digits), 
               ", TP=", DoubleToString(tp, _Digits), ", Lots=", DoubleToString(lots, 2));
         trade.SetExpertMagicNumber(MagicNumber(tfs[i].tf));
         if(trade.Sell(lots, eaSymbol, price, sl, tp)) {
            tfs[i].lastSig = TimeCurrent();
            LogTrd(trade.ResultOrder(), eaSymbol, price, sl, tp, "Open", brkStr, vol[1], CurRisk, tfs[i].tf);
            Print(EnumToString(tfs[i].tf), " Sell opened: Ticket=", trade.ResultOrder());
         } else {
            Print(EnumToString(tfs[i].tf), " Sell failed: Retcode=", trade.ResultRetcode(), ", Error=", GetLastError(), ", Comment=", trade.ResultComment());
         }
      } else {
         Print(EnumToString(tfs[i].tf), " Trade skipped: No valid signal");
      }
   }

   // Run FVG detection after trade logic to ensure non-interference
   DetectFVGs();
}

void ManageTrades(ENUM_TIMEFRAMES tf) {
   for(int i = PositionsTotal() - 1; i >= 0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(!PositionSelectByTicket(ticket) || PositionGetString(POSITION_SYMBOL) != eaSymbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber(tf)) continue;
      double openPrice, sl, tp, currPrice, lots, profit;
      if(!PositionGetDouble(POSITION_PRICE_OPEN, openPrice) ||
         !PositionGetDouble(POSITION_SL, sl) ||
         !PositionGetDouble(POSITION_TP, tp) ||
         !PositionGetDouble(POSITION_VOLUME, lots) ||
         !PositionGetDouble(POSITION_PROFIT, profit)) continue;
      currPrice = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY ? SymbolInfoDouble(eaSymbol, SYMBOL_BID) : SymbolInfoDouble(eaSymbol, SYMBOL_ASK);
      int idx = TimeframeIndex(tf);
      if(idx < 0) continue;
      double atr[1];
      if(CopyBuffer(tfs[idx].h_atr, 0, 0, 1, atr) < 1) continue;

      if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && currPrice >= openPrice + (openPrice - sl) * 3) ||
         (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && currPrice <= openPrice - (sl - openPrice) * 3)) {
         if(lots > SymbolInfoDouble(eaSymbol, SYMBOL_VOLUME_MIN) * 2) {
            trade.PositionClosePartial(ticket, lots / 2);
            trade.PositionModify(ticket, openPrice, tp);
            Print(EnumToString(tf), " Partial close at 1:3 RR: Ticket=", ticket, ", Lots=", DoubleToString(lots / 2, 2));
         }
      }

      double trail = atr[0] * 1.5 / _Point;
      if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && currPrice > openPrice + trail * _Point && sl < currPrice - trail * _Point) {
         trade.PositionModify(ticket, currPrice - trail * _Point, tp);
         Print(EnumToString(tf), " Trailing stop updated: Ticket=", ticket, ", New SL=", DoubleToString(currPrice - trail * _Point, _Digits));
      } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && currPrice < openPrice - trail * _Point && sl > currPrice + trail * _Point) {
         trade.PositionModify(ticket, currPrice + trail * _Point, tp);
         Print(EnumToString(tf), " Trailing stop updated: Ticket=", ticket, ", New SL=", DoubleToString(currPrice + trail * _Point, _Digits));
      }

      if(profit != 0 && !PositionSelectByTicket(ticket)) {
         LogTrd(ticket, eaSymbol, openPrice, sl, tp, "Close", 0, 0, CurRisk, tf);
         Print(EnumToString(tf), " Position closed: Ticket=", ticket, ", Profit=", DoubleToString(profit, 2));
      }
   }
}

void FetchNewsCalendar() {
   string url = NewsAPI_Url + NewsAPI_Key;
   string headers = "";
   char post[], result[];
   string result_headers;
   int timeout = 5000;
   int res = WebRequest("GET", url, headers, timeout, post, result, result_headers);
   if(res != 200) {
      Print("News API request failed: HTTP ", res, ", Error=", GetLastError());
      newsCal[0].time = StringToTime("2025.07.15 14:30");
      newsCal[0].evt = "CPI";
      newsCal[0].impact = 90;
      newsCal[1].time = StringToTime("2025.07.23 20:00");
      newsCal[1].evt = "FOMC";
      newsCal[1].impact = 90;
      newsCnt = 2;
      Print("Using fallback news calendar with ", newsCnt, " events");
      return;
   }

   string response = CharArrayToString(result);
   newsCnt = 0;
   ArrayResize(newsCal, 100);

   int pos = 0;
   while(pos >= 0 && newsCnt < 100) {
      pos = StringFind(response, "\"items\":", pos);
      if(pos < 0) break;
      pos = StringFind(response, "{", pos);
      if(pos < 0) break;

      int end = StringFind(response, "}", pos);
      if(end < 0) break;
      string item = StringSubstr(response, pos, end - pos + 1);

      string evtName = ExtractJsonField(item, "\"title\":");
      string evtTime = ExtractJsonField(item, "\"time_published\":");
      string relevance = ExtractJsonField(item, "\"relevance_score\":");

      if(evtName != "" && evtTime != "") {
         string dt = StringSubstr(evtTime, 0, 4) + "." + StringSubstr(evtTime, 4, 2) + "." + StringSubstr(evtTime, 6, 2) + " " + 
                     StringSubstr(evtTime, 9, 2) + ":" + StringSubstr(evtTime, 11, 2);
         newsCal[newsCnt].time = StringToTime(dt);
         newsCal[newsCnt].evt = evtName;
         newsCal[newsCnt].impact = (relevance != "") ? (int)(StringToDouble(relevance) * 100) : 80;
         if(newsCal[newsCnt].impact > 80 && newsCal[newsCnt].time > TimeCurrent() - 7 * 86400) {
            newsCnt++;
            Print("News event loaded: ", evtName, " at ", TimeToString(newsCal[newsCnt-1].time), ", Impact=", newsCal[newsCnt-1].impact);
         }
      }
      pos = end + 1;
   }
   Print("Loaded ", newsCnt, " high-impact news events from API");
}

string ExtractJsonField(string json, string field) {
   int pos = StringFind(json, field);
   if(pos < 0) return "";
   pos += StringLen(field);
   if(StringFind(json, "\"", pos) == pos) pos++;
   int end = StringFind(json, "\"", pos);
   if(end < 0) return "";
   return StringSubstr(json, pos, end - pos);
}

bool NewBar(ENUM_TIMEFRAMES tf) {
   int idx = TimeframeIndex(tf);
   if(idx < 0) return false;
   datetime cur = iTime(eaSymbol, tf, 0);
   if(cur != tfs[idx].lastBar) {
      tfs[idx].lastBar = cur;
      return true;
   }
   return false;
}

bool BullTrend(ENUM_TIMEFRAMES tf) {
   int handle = (tf == PERIOD_D1) ? h_ema_d1 : h_ema_h4;
   double ema[2];
   if(CopyBuffer(handle, 0, 1, 2, ema) < 2) {
      Print("EMA copy failed for ", EnumToString(tf));
      return false;
   }
   Print("EMA ", EnumToString(tf), ": ", DoubleToString(ema[1], _Digits), " vs ", DoubleToString(ema[0], _Digits));
   return ema[1] > ema[0];
}

bool BearTrend(ENUM_TIMEFRAMES tf) {
   int handle = (tf == PERIOD_D1) ? h_ema_d1 : h_ema_h4;
   double ema[2];
   if(CopyBuffer(handle, 0, 1, 2, ema) < 2) {
      Print("EMA copy failed for ", EnumToString(tf));
      return false;
   }
   Print("EMA ", EnumToString(tf), ": ", DoubleToString(ema[1], _Digits), " vs ", DoubleToString(ema[0], _Digits));
   return ema[1] < ema[0];
}

bool BuyBrk(ENUM_TIMEFRAMES tf) {
   double high = iHigh(eaSymbol, tf, iHighest(eaSymbol, tf, MODE_HIGH, Brk_Prd, 1));
   double price = SymbolInfoDouble(eaSymbol, SYMBOL_ASK);
   Print(EnumToString(tf), " BuyBrk check: Price=", DoubleToString(price, _Digits), ", High=", DoubleToString(high, _Digits));
   return price > high;
}

bool SellBrk(ENUM_TIMEFRAMES tf) {
   double low = iLow(eaSymbol, tf, iLowest(eaSymbol, tf, MODE_LOW, Brk_Prd, 1));
   double price = SymbolInfoDouble(eaSymbol, SYMBOL_BID);
   Print(EnumToString(tf), " SellBrk check: Price=", DoubleToString(price, _Digits), ", Low=", DoubleToString(low, _Digits));
   return price < low;
}

double CalcLots(double eq, double riskPct, double slPips) {
   double pipVal = SymbolInfoDouble(eaSymbol, SYMBOL_TRADE_TICK_VALUE);
   double riskAmt = MathMin(eq * (riskPct / 100), MaxLossUSD);
   double lots = riskAmt / (slPips * pipVal);
   double minLot = SymbolInfoDouble(eaSymbol, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(eaSymbol, SYMBOL_VOLUME_MAX);
   lots = NormalizeDouble(MathMax(minLot, MathMin(maxLot, lots)), 2);
   Print("CalcLots: Equity=", DoubleToString(eq, 2), ", Risk%=", DoubleToString(riskPct, 2), ", SL Pips=", DoubleToString(slPips, 2), 
         ", PipVal=", DoubleToString(pipVal, 2), ", Lots=", DoubleToString(lots, 2), ", MinLot=", DoubleToString(minLot, 2), ", MaxLot=", DoubleToString(maxLot, 2));
   return lots;
}

bool IsNews() {
   if(newsCnt == 0 && NewsFilt) {
      Print("No news events loaded, bypassing news filter");
      return false;
   }
   datetime now = TimeCurrent();
   Print("News check: Current time=", TimeToString(now, TIME_DATE|TIME_MINUTES));
   for(int i = 0; i < newsCnt; i++) {
      if(now >= newsCal[i].time - NewsPause * 60 && now <= newsCal[i].time + NewsPause * 60 && newsCal[i].impact > 80) {
         Print("News event active: ", newsCal[i].evt, " at ", TimeToString(newsCal[i].time, TIME_DATE|TIME_MINUTES));
         return true;
      }
   }
   Print("No active news events");
   return false;
}

void LogTrd(ulong ticket, string sym, double price, double sl, double tp, string stat, double brkStr, double vol, double risk, ENUM_TIMEFRAMES tf) {
   string data = StringFormat("T=%I64u,S=%s,Tm=%s,P=%f,SL=%f,TP=%f,St=%s,BrkStr=%f,Vol=%f,Risk=%f,TF=%s",
      ticket, sym, TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES), price, sl, tp, stat, brkStr, vol, risk, EnumToString(tf));
   if(StringLen(GS_Url) > 0) Print("Webhook pending: ", data);
   Print("Trd: ", data);

   if(stat == "Close" && tradeCnt < ArraySize(tradeHistory)) {
      double profit;
      if(PositionSelectByTicket(ticket)) {
         PositionGetDouble(POSITION_PROFIT, profit);
         tradeHistory[tradeCnt].ticket = ticket;
         tradeHistory[tradeCnt].isWin = profit > 0;
         tradeHistory[tradeCnt].profit = profit;
         tradeHistory[tradeCnt].brkStr = brkStr;
         tradeHistory[tradeCnt].vol = vol;
         tradeHistory[tradeCnt].risk = risk;
         tradeHistory[tradeCnt].tf = tf;
         tradeCnt++;
         UpdateWinLossStreak();
         AdjustBreakoutStrength();
      }
   }
}

void UpdateWinLossStreak() {
   if(tradeCnt > 0) {
      if(tradeHistory[tradeCnt-1].isWin) {
         winStreak++;
         lossStreak = 0;
      } else {
         lossStreak++;
         winStreak = 0;
      }
   }
}

void AdjustBreakoutStrength() {
   if(tradeCnt < 10) return;
   int lossCnt = 0;
   double avgBrkStr = 0;
   for(int i = tradeCnt - 10; i < tradeCnt; i++) {
      if(!tradeHistory[i].isWin) lossCnt++;
      avgBrkStr += tradeHistory[i].brkStr;
   }
   avgBrkStr /= 10;
   if(lossCnt >= 5 && avgBrkStr < MinBrkStr * 1.5) dynBrkStr = MinBrkStr * 1.5;
   else if(lossCnt <= 2) dynBrkStr = MinBrkStr;
   Print("Breakout strength adjusted: dynBrkStr=", DoubleToString(dynBrkStr, 2));
}

long MagicNumber(ENUM_TIMEFRAMES tf) {
   if(tf == PERIOD_M5) return 1005;
   if(tf == PERIOD_M15) return 1015;
   if(tf == PERIOD_H1) return 1060;
   return 0;
}

int TimeframeIndex(ENUM_TIMEFRAMES tf) {
   for(int i = 0; i < ArraySize(tfs); i++) {
      if(tfs[i].tf == tf) return i;
   }
   return -1;
}

Instalación y backtesting: compilar e integrar en el gráfico. Backtesting en GOLD M15 (2025) con un riesgo del 2 %. 


Pruebas de estrategia

La estrategia funciona mejor con el oro debido a sus movimientos relativamente rápidos y a su elevada volatilidad, lo cual resulta beneficioso para el trading intradía de los inversores particulares. Probaremos esta estrategia operando en el oro desde el 1 de enero de 2025 hasta el 29 de julio de 2025, en un marco temporal de 15 minutos (M15). Estos son los parámetros que he elegido para esta estrategia. 

Entradas 2



Resultados del backtesting

Tras realizar las pruebas en el Probador de estrategias, estos son los resultados.

  • Gráfico de balance/patrimonio neto:

Gráfico de resultados

  • Resultados del backtest:

Resultados de las pruebas


Resumen

He escrito este artículo con el objetivo de explicar un Asesor Experto (EA) para MetaTrader 5 que combina la detección de Fair Value Gaps (FVG) y los cambios en la estructura del mercado (MSS) para identificar setups operativos de alta probabilidad en GOLD. Los Fair Value Gaps son uno de los conceptos más valiosos y habituales dentro de los Smart Money Concepts, utilizados para capturar ineficiencias del precio y cambios en la tendencia.

El EA fue probado en GOLD, demostrando su capacidad para detectar Fair Value Gaps de forma eficiente y precisa en los marcos temporales M15 y H1. No obstante, la detección de FVG representa solo una parte del proceso, ya que, si no se produce un Market Structure Shift (MSS), no deberían ejecutarse operaciones, incluso cuando los Fair Value Gaps detectados sean válidos. Estos cambios en la estructura del mercado actúan como confirmaciones que ayudan a mejorar la precisión y la calidad de las operaciones durante sesiones volátiles.

Para implementar esta estrategia, configura los parámetros de entrada del EA como se muestra a continuación para obtener los resultados deseados. El EA está diseñado para buscar FVG en gráficos M15 o H1, asegurando que los cambios en la estructura del mercado estén alineados con la tendencia de marcos temporales superiores, como H4 o D1. Los usuarios interesados deberían realizar backtests de este EA en sus cuentas demo con GOLD. Mi objetivo principal con este EA era optimizarlo para setups seleccionados y de alta probabilidad, incorporando elementos de gestión del riesgo como un riesgo del 0,5 % al 2 % por operación y trailing stops.

También recomendaría a los usuarios revisar periódicamente los registros de rendimiento para ajustar la configuración y los parámetros de entrada en función de sus objetivos o tolerancia al riesgo. Descargo de responsabilidad: Cualquier persona que utilice este Asesor Experto (EA) debe primero probarlo y comenzar a operar en su cuenta demo para dominar este enfoque institucional y obtener ganancias consistentes antes de arriesgar fondos reales.


Conclusión

La idea principal y el enfoque de este artículo es explicar con claridad qué son los Fair Value Gaps, dónde se producen, cómo se forman, por qué aparecen y, por último, cómo pueden utilizarse de forma adecuada para analizar, comprender e incluso ejecutar operaciones basadas en ellos.

La mayoría de los traders principiantes, e incluso algunos traders intermedios, no tienen claro cómo moverse en este mundo complejo de los Fair Value Gaps. Muchos incluso se frustran con ellos porque no logran comprender realmente qué está ocurriendo, ni desarrollan el criterio necesario para entender cómo el precio suele regresar a estas zonas y cómo utiliza estos gaps para generar operaciones y ejecuciones clásicas que pueden resultar rentables. Incluso si no ejecutan operaciones directamente sobre Fair Value Gaps, los traders podrán utilizarlos para validar sus propias operaciones, sus setups, así como la tendencia y la dirección del mercado, tomando como referencia lo explicado en este artículo. Además, podrán comprobar lo interesante que resulta el papel clave y determinante que desempeñan estos Fair Value Gaps.

Al automatizar con MQL5, los traders reducen el sesgo emocional y logran una ejecución más constante de estrategias basadas en FVG + Breaker/MSS.

Todo el código mencionado en el artículo se adjunta a continuación. La siguiente tabla describe todos los archivos de código fuente que acompañan al artículo.
Nombre del archivo Descripción
Fvg_detector.mq5 Archivo que contiene el código para la detección de Fair Value Gaps
fvg-mss.mq5
Código del EA combinado completo con detección de Fair Value Gaps y Market Structure Shift  

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

Archivos adjuntos |
fvg-mss.mq5 (54.64 KB)
Fvg_detector.mq5 (9.26 KB)
Korrect Trades
Korrect Trades | 15 sept 2025 en 12:09
Genial 👍.
Me gusta esto
Eugene Mmene
Eugene Mmene | 15 sept 2025 en 12:12
Korrect Trades #:
Genial 👍.
Me gusta esto
Bienvenido
Utilizando redes neuronales en MetaTrader Utilizando redes neuronales en MetaTrader
En el artículo se muestra la aplicación de las redes neuronales en los programas de MQL, usando la biblioteca de libre difusión FANN. Usando como ejemplo una estrategia que utiliza el indicador MACD se ha construido un experto que usa el filtrado con red neuronal de las operaciones. Dicho filtrado ha mejorado las características del sistema comercial.
Descomposición en modos dinámicos aplicada a series temporales univariadas en MQL5 Descomposición en modos dinámicos aplicada a series temporales univariadas en MQL5
La descomposición en modos dinámicos (DMD, por sus siglas en inglés) es una técnica que se suele aplicar a conjuntos de datos de alta dimensionalidad. En este artículo, demostramos la aplicación de DMD a series temporales univariadas, mostrando su capacidad para caracterizar una serie y realizar pronósticos. Para ello, investigaremos la implementación integrada de la descomposición en modos dinámicos en MQL5, prestando especial atención al nuevo método matricial, DynamicModeDecomposition().
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.
De novato a experto: Noticias animadas utilizando MQL5 (X) Vista multigráfico de múltiples símbolos para el trading de noticias De novato a experto: Noticias animadas utilizando MQL5 (X) Vista multigráfico de múltiples símbolos para el trading de noticias
Hoy vamos a desarrollar un sistema de visualización de varios gráficos utilizando objetos de gráfico. El objetivo es mejorar el trading basado en noticias mediante la aplicación de algoritmos MQL5 que ayudan a reducir el tiempo de reacción de los operadores en períodos de alta volatilidad, como los momentos en que se publican noticias importantes. En este caso, ofrecemos a los traders una forma integrada de supervisar múltiples símbolos principales a través de una única herramienta integral de trading basada en noticias. Nuestro trabajo sigue avanzando con News Headline EA, que ahora cuenta con un conjunto cada vez mayor de funciones que aportan un valor real tanto a los operadores que utilizan sistemas totalmente automatizados como a aquellos que prefieren el trading manual asistido por algoritmos. Descubre más conocimientos, ideas prácticas y observaciones continuando la lectura y sumándote a esta discusión.