Deutsch 日本語
preview
Mastering Fair Value Gaps: Formation, Logic, and Automated Trading with Breakers and Market Structure Shifts

Mastering Fair Value Gaps: Formation, Logic, and Automated Trading with Breakers and Market Structure Shifts

MetaTrader 5Trading |
5 978 2
Eugene Mmene
Eugene Mmene

Introduction

Fair Value Gaps are a very common phenomenon today, often referred to as a repertoire of the smart money concept (SMC) or, rather, originating from smart money concepts. It's also very common to find it on social media when traders chat on trading platforms and online charting rooms, but most people consider it a reference point rather than something that would improve their trading or even offer them tools to define their bias and trade based on the time and place where these Fair Value Gaps occur.

My goal is not only to address Fair Value Gaps but also how they can be used to the trader's advantage to shape bias, narrative, and even execute trades. I will also analyze where these Fair Value Gaps are most likely to occur, when and how they occur, and how to validate them as trade entries or profit-taking points and ultimately as re-entry points for trade execution following a pullback, which occurs after a strong expansion.

After understanding Fair Value Gaps and the trading logic used to validate, determine, and identify underlying bias and narrative, I will explain the formation of breakout factors and the changes in market structure that typically validate trades regarding underlying bias and narrative. These breakout factors and changes in market structure are compelling and often even validate long-term trades. This article will clearly describe and explain the effectiveness of a combination of fair value gaps and breakout factors, especially considering that they occur on all time frames, both short and long-term.


Introduction to the Fair Value Gap

Fair Value Gaps are created when there are rapid price movements in either direction, indicating strong buying or selling pressure, primarily from institutional players. In simple terms, this trading pressure often leads to a scenario where traders do not have equal opportunities to place or execute trades; therefore, in a bullish fair value gap (FVG), where price has left a point with force and speed toward higher prices and sellers have not had enough time or opportunity to execute them, while in a bearish fair value gap (FVG), where price has left a point with force and speed toward lower prices and buyers have not had enough time or opportunity to execute them, this area is called an unbalanced price range, as there has been no price movement in the opposite direction from where the expansion came from.

This can be explained most clearly through painter's logic: when a painter paints a wall, they must paint it with equal, or even similar, brushstrokes in opposite directions so that the paint is applied evenly and is balanced, or rather, of high quality. This same logic applies to price. When a bullish candle expands and breaks through a price point, for example, from 1.3010 to 1.3030, a bearish candle must also break through that price point to balance the price, and if it doesn't, it becomes a fair value gap (FVG). Often, if not always, the price will return to this point eventually. This is where and when we should look to take advantage of these fair value gaps, as they offer vast and rewarding opportunities.

The following illustration shows examples of Fair Value Gaps and how they were used.

Bearish fair value gap

How, when, and why they are formed

Now that we have defined what Fair Value Gaps are, it is much easier to understand how they form. This occurs in very well-defined situations. The first scenario is when a press release occurs; it can be one of the examples of red fodder news (CPI, FOMC, or NFP) or even other types of press releases; it all depends on the impact it has.

Most of the time, this press release often occurs after the fact that large players in the market had already entered pending orders and therefore expected this volatility, speed, and strength; therefore, the market quickly moves in its favor and leaves behind Fair Value Gaps that we intend to profit from, I believe. As industry standards, it is not ideal to trade before a big press release, but it is often prudent to wait for the press release and its accompanying volatility before placing trade executions, so ideally we would avoid these early trade executions since most of them do not even offer trade setups.

Another scenario in which they form is when large players, such as banks and hedge funds, place orders; they therefore influence market movements because their orders are worth billions of dollars.

These large orders can be easily detected with gigantic candlesticks and sharp, sudden movements in one direction, even without the influence of news releases. In this article, I attempt to illustrate how, following these large movements, we must train our attention to detect the Fair Value Gaps that occur during these large movements.

The following illustration shows how price quickly broke out of an area with speed and force, and the next candle did not retrace to cover the bullish candle, leaving an uncovered section referred to as the fair value gap.

Bullish fair value gap


How to Analyze Fair Value Gaps

In this chapter, we'll look at how to analyze Fair Value Gaps, having explained in the previous chapters what they are, when they occur, and how they form. Now we'll focus on how to analyze these same gaps to determine which trades to wait for and execute.

The first basic rule is that, in most cases, we should avoid trading Fair Value Gaps, but we must accept trading away from them, as they act as propulsion when used effectively in the direction of the trend, bias, and narrative. So, why trade against a propulsive block that will, in a few moments, operate against us with full force? And this is very true and accurate on both shorter and longer time frames; they all act the same way. The only difference is the time it takes to materialize into a trade, but they always, most of the time, act this way 90% of the time.

Ideally, the first step is to analyze from top to bottom, from the highest to the lowest timeframes, observing and highlighting all Fair Value Gaps. Then, the most important step is to analyze the trend direction, bias, and narrative at play. For example, the trend, bias, and narrative for gold, for example, on MN, W1, and D1, point to buy trades. This can be confirmed in three main ways: the price is trading above the moving averages (simple and exponential moving averages); another way is that the market structure clearly shows buying momentum; furthermore, the green/bullish candles are far more numerous than the red/bearish candles.

Once the trend and narrative in play have been defined, we must consider another important aspect. Price is constantly moving, seeking liquidity to clear, and the narrative, direction, and bias are determined on higher time frames. Liquidity primarily occurs at previous daily, weekly, and monthly lows and highs, and this is where significant reactions should be expected. Therefore, once we have an idea of the liquidity and trend, we can easily execute well.

An example of this scenario is that we have now identified that the gold price is approaching the previous week's high, which is confirmed by the market structure/moving averages. During the week or concurrent sessions, the price continues to move toward the previous week's high, and Fair Value Gaps continue to form. These Fair Value Gaps will be labeled as bullish, and if they occur on longer timeframes (e.g., H4, D1, H1), even better, they should be marked as bullish. Traders should always anticipate buying trades when the price returns to these gaps; this way, they can look for trade executions.

It's important to note that, in most cases, if not always, the price always rebalances Fair Value Gaps. However, if the price is close to its target, as mentioned above (e.g., weekly, monthly, or daily highs), once this target is reached and liquidity is purged, the chance that its fair value gap will not be used for new trades is significantly reduced, and it could turn into a failed purchase. Therefore, we should avoid these types of trades.


How they affect transactions and their address

Basically, the idea is that, when there is a predetermined trend and direction (e.g., bearish), Fair Value Gaps are sought through top-down analysis and marked. It should be noted that the price will return to these Fair Value Gaps (prices) to offer trading and entry options, as long as the weekly, daily, or monthly targets are maintained. Generally, the price must regain equilibrium and fill the Fair Value Gaps. Therefore, it acts as a driver of strength for a pair/asset when the price returns as a secondary buyer, as more traders open new positions at this point, which explains the speed and strength of the market. This causes strong, positive price swings. Furthermore, it reinforces the direction, bias, and narrative of the trend.


How to use Fair Value Gaps successfully to execute trade entries

This is the most important and valuable piece of information in this article, because after thoroughly validating and analyzing all our criteria, we understand how to find where and how to invalidate or validate Fair Value Gaps, as well as how they form and where they most likely occur. This part is simplified, since all the hard work has already been done.

Now all that's needed is patience, discipline, and focus. We'll analyze primarily from D1, as it's the reference chart, allowing us to look for Fair Value Gaps. If we detect a fair value gap and the price returns to it, once inside, we quickly drop to H4 and look for a bullish engulfing candle (if the trend, bias, and narrative are buyers) or a bearish engulfing candle (if the bias, trend, and narrative are sellers). This can occur at any point within the Fair Value Gaps, but ideally, it should occur near the start of trading sessions, for example, in London or New York. After detecting the engulfing candle, we drop back down to H1; here, we look for a break in the market structure.

A breakout in the market structure refers to a change in the price's delivery status; if you were selling and then become a buyer, it means there's a change in the delivery status and therefore a change in the market structure. Therefore, on H1, this is what you're looking for. Stop losses should be placed at opposite ends of the breakouts, and entries can be executed on breakouts in small, reasonably priced gaps that may occur at the H1 breakout or at the 60% retracement level, if that occurs.

A breakout occurs when the last high or low before a change in market structure is broken. A fast, strong break of the last high in a selling market is a bullish breakout, while a fast, strong break of the last low in a buying market is a bearish breakout.

The same phenomenon occurs on other timeframes. If we detect a fair value gap on H4, we look for an engulfing candle on H1 and then for a change in market structure on M15. We can apply this strategy to traders with shorter timeframes. The same phenomenon is repeated: a fair value gap occurs on H1 and an engulfing candle on M15; therefore, a change in market structure occurs on the M5 timeframe. Furthermore, for scalping, Fair Value Gaps occurred on M15. We can look for engulfing candles on M5 and changes in market structure on M1.


How and when to avoid trading with Fair Value Gaps

Most Fair Value Gaps often provide trading opportunities, but as explained above, some Fair Value Gaps are not ideal for trading.

1. Fair Value Gaps that occur contrary to the trend, bias, and narrative direction will not be respected at all; for example, if the trend or bias is buy and the price turns down, leaving a fair value gap, possibly for pullbacks, don't expect the price to respect the fair value gap above it when it resumes its uptrend; it will be quickly ignored and passed over.

2. The second scenario in which these Fair Value Gaps are unsuccessful is when the overall weekly, daily, or monthly targets are met. Fair Value Gaps can fail because the price has no reason to continue the trend in that direction, given that liquidity has already been purged; therefore, there is no more strength or speed available, as the profits of large companies are already booked. Therefore, Fair Value Gaps can fail because the price might want to pull back or even reverse.

3. The third and final scenario in which this phenomenon will fail again is when, for example, changes in the engulfing candle and market structure do not occur on lower timeframes, as explained above. Furthermore, they must occur during very active trading sessions and in that order.

Fair Value Gaps form first, then engulfing candles, and then the market structure changes. Therefore, only after they occur in this order can we execute trades with top-notch, high-quality setups. This clearly indicates that a trader should not use this method or a fair value gap if they do not meet all the aforementioned factors, as there is a high probability of failure.


Fair Value Gaps over longer time frames

Higher time frame gaps refer to H4, D1, and W1 Fair Value Gaps, and these can be easily used for large or long-term price swings, even 200 to 500 pips, because they are more stable and slow, without short-term volatility or one-way movements, making them more suitable for long-term traders who can hold their trades patiently without nervousness or fear.


Fair Value Gaps in lower time frames

Lower-timeframe gaps refer to the Fair Value Gaps on the H1, M15, and M5 time frames. These gaps are primarily and ideally used by short-term, not long-term, traders, from scalpers to short-term traders who hold trades for a few hours and then exit them. They can occur in trending market environments, especially on the M15, and in sessions when leveraged, they are very reliable. They are designed for fast execution and easily offer 100-150 pips. Depending on the strength and speed of price movement, they feature short-term price fluctuations and high volatility. They require traders who make quick trading decisions and excel in these environments.


Automating Trading with Fair Value Gaps in MQL5

To automate this strategy, I have created

  • An FVG indicator to detect and map gaps.
  • An FVG + MSS EA to trade based on detections, incorporating MSS for entries.

Source code of the FVG indicator

This indicator detects FVGs by looking for large, non-overlapping candles (imbalance > lows). It draws rectangles to detect visual gaps.

#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();
}

Installation: Compile in MetaEditor and attach to the chart. Draws green rectangles for bullish FVGs and red rectangles for bearish ones.

Example of use: On GOLD M15, detect gaps for visual analysis.


Source code of the FVG + MSS Expert Advisor

This EA detects FVGs, waits for a pullback toward the gap, checks for a breakout of the last high/low (LH), and enters trades. It includes risk management (2% risk per trade).  

#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;
}

Installation and backtesting: Compile and attach to chart. Backtesting on GOLD M15 (2025) with 2% risk. 


Strategy Testing

The strategy works best on GOLD due to its relatively quick moves and high volatility, which are beneficial for retail intraday trading. We will test this strategy by trading GOLD from January 1, 2025, to July 29, 2025, on the 15-minute (M15) timeframe. Here are the parameters I have chosen for this strategy. 

Inputs 2



Strategy tester results

Upon testing on the strategy tester, here are the results.

  • Balance/Equity graph:

graph results

  • Backtest results:

test results


Summary

I wrote this article to try to explain a MetaTrader 5 Expert Advisor (EA) that combines the use of Fair Value Gaps (FVG) detection and Market Structure Shift (MSS) to identify high-probability trading setups on GOLD. Fair Value Gaps are one of the most valuable and standard Smart Money Concepts used to capture price inefficiencies and trend shifts.

I tested the EA on GOLD, and it revealed the EA’s ability to detect FVGs efficiently and aptly on M15 and H1 timeframes, but the Fair Value Gaps detection is only part of the equation because if market structure shift does not occur, then trades are not supposed to be executed even if Fair Value Gaps are valid. These market structure shifts are confirmations that help improve trade accuracy and quality during volatile sessions.

To implement this strategy, configure the input parameters on the EA as shown below to get desirable results. The EA is designed to scan for FVGs on M15 or H1 charts, ensuring market structure shifts align with higher timeframe trends (e.g., H4/D1). Interested users should backtest this EA on their demo accounts with GOLD. My main agenda and goal for this EA was to optimize it for selected and high-probability setups that incorporate risk management like 0.5-2% risk per trade and trailing stops.

I would also advise users to regularly review performance logs to refine settings and input parameters depending on one's goals or risk appetite. Disclaimer: Anybody using this EA should first test and start trading on his demo account to master this institutional approach for consistent profits before risking live funds.


Conclusion

The main takeaway and emphasis of this article is to try to clearly explain what Fair Value Gaps are, where they occur, how they occur, why they occur, and lastly, how they can be ideally used to analyze, understand, and even make trade executions utilizing them.

Most beginner traders and even some intermediate traders have no clue how to navigate this murky world of Fair Value Gaps and even get frustrated with them as they are unable to really grasp what is going on there or even don’t have the discerning eye to understand how price always comes back and how it utilizes these gaps to make classic trades and executions that can actually be profitable for them. Or even if they don’t execute trades on Fair Value Gaps, traders will be able to validate their own trades, their own setups and trades, and even the trend and direction regarding what I have shared in this article, and they will find it very interesting how these Fair Value Gaps play colossal and key roles.

By automating with MQL5, traders reduce emotional bias, enabling consistent execution of FVG + Breaker/MSS strategies.

All code referenced in the article is attached below. The following table describes all the source code files that accompany the article.
File Name Description
Fvg_detector.mq5 File containing code for fair value gap detection
fvg-mss.mq5
File containing code for the whole combined EA, fair value gap detection, and market structure shift 

Attached files |
fvg-mss.mq5 (54.64 KB)
Fvg_detector.mq5 (9.26 KB)
Last comments | Go to discussion (2)
Korrect Trades
Korrect Trades | 15 Sep 2025 at 12:09
Nice 👍.
I like this
Eugene Mmene
Eugene Mmene | 15 Sep 2025 at 12:12
Korrect Trades #:
Nice 👍.
I like this
Welcome
Neural Networks in Trading: An Ensemble of Agents with Attention Mechanisms (Final Part) Neural Networks in Trading: An Ensemble of Agents with Attention Mechanisms (Final Part)
In the previous article, we introduced the multi-agent adaptive framework MASAAT, which uses an ensemble of agents to perform cross-analysis of multimodal time series at different data scales. Today we will continue implementing the approaches of this framework in MQL5 and bring this work to a logical conclusion.
Dynamic mode decomposition applied to univariate time series in MQL5 Dynamic mode decomposition applied to univariate time series in MQL5
Dynamic mode decomposition (DMD) is a technique usually applied to high-dimensional datasets. In this article, we demonstrate the application of DMD on univariate time series, showing its ability to characterize a series as well as make forecasts. In doing so, we will investigate MQL5's built-in implementation of dynamic mode decomposition, paying particular attention to the new matrix method, DynamicModeDecomposition().
Developing a multi-currency Expert Advisor (Part 21): Preparing for an important experiment and optimizing the code Developing a multi-currency Expert Advisor (Part 21): Preparing for an important experiment and optimizing the code
For further progress it would be good to see if we can improve the results by periodically re-running the automatic optimization and generating a new EA. The stumbling block in many debates about the use of parameter optimization is the question of how long the obtained parameters can be used for trading in the future period while maintaining the profitability and drawdown at the specified levels. And is it even possible to do this?
Automating Trading Strategies in MQL5 (Part 31): Creating a Price Action 3 Drives Harmonic Pattern System Automating Trading Strategies in MQL5 (Part 31): Creating a Price Action 3 Drives Harmonic Pattern System
In this article, we develop a 3 Drives Pattern system in MQL5 that identifies bullish and bearish 3 Drives harmonic patterns using pivot points and Fibonacci ratios, executing trades with customizable entry, stop loss, and take-profit levels based on user-selected options. We enhance trader insight with visual feedback through chart objects.