Building a Fair Value Gap (FVG) Indicator in MQL5 for MetaTrader 5

Fair Value Gap (FVG) is one of the most discussed concepts in modern price action trading, especially among traders who study institutional order flow, liquidity, and imbalance. In simple terms, an FVG represents an area on the chart where price moves so aggressively that little or no trading activity occurs between two candles, leaving a gap-like imbalance in the market. Traders often expect price to revisit these zones before continuing the original move.

FVG


In this article, I will explain how the provided MQL5 Fair Value Gap indicator works, what each part of the code does, and why it can be useful for discretionary and algorithmic traders. This indicator is designed for MetaTrader 5 (MT5) and automatically detects bullish and bearish fair value gaps, draws rectangles on the chart, and optionally extends those rectangles until the gap is mitigated.

What Is a Fair Value Gap?

Before discussing the code, it is important to understand the trading logic behind it.

A bullish fair value gap appears when price moves upward with strong momentum and creates a gap between the high of an earlier candle and the low of a later candle. A bearish fair value gap is the opposite: price moves downward so quickly that a gap forms between the low of an earlier candle and the high of a later candle.

Visually, the indicator uses a three-candle structure:

  • Left candle
  • Middle candle
  • Right candle

For a bullish FVG:

  • The high of the left candle is below the low of the right candle.
  • The middle candle acts as the displacement candle that creates the imbalance.

For a bearish FVG:

  • The low of the left candle is above the high of the right candle.

These gaps are not exchange gaps; they are price imbalances inside continuous forex or CFD data.

Indicator Overview

The indicator is declared as a chart-window indicator:

#property indicator_chart_window

It does not draw traditional lines or histograms. Instead, it uses graphical rectangle objects to highlight FVG zones directly on the chart.

The indicator contains:

  • 3 buffers for storing FVG data
  • Bullish and bearish detection logic
  • Automatic rectangle drawing
  • Optional extension until mitigation
  • Custom colors and styles
  • Debug logging support

User Inputs

Main Section

input bool InpContinueToMitigation = true;

When enabled, the FVG rectangle will continue extending to the right until price trades back into the gap (mitigation).

Style Section

input color InpDownTrendColor = clrPink;
input color InpUpTrendColor = clrGreen;
input bool InpFill = true;
input ENUM_BORDER_STYLE InpBoderStyle = BORDER_STYLE_SOLID;
input int InpBorderWidth = 1;

These inputs allow traders to customize:

  • Bullish FVG color
  • Bearish FVG color
  • Filled or transparent rectangles
  • Border style
  • Border width

This is very useful because traders often use dark or light chart themes.

Debug Section

input bool InpDebugEnabled = false;

When enabled, the indicator prints detailed information to the MT5 Experts log, which helps during development and troubleshooting.

Indicator Buffers

The indicator stores three types of data:

double FvgHighPriceBuffer[];
double FvgLowPriceBuffer[];
double FvgTrendBuffer[];

FvgHighPriceBuffer

Stores the upper boundary of the gap.

FvgLowPriceBuffer

Stores the lower boundary of the gap.

FvgTrendBuffer

Stores the direction:

  • 1 = bullish
  • -1 = bearish
  • 0 = no FVG

Although the indicator does not plot these buffers visually, they can be accessed by other indicators, Expert Advisors, or scripts.

Initialization Process

Inside OnInit(), the buffers are initialized and configured.

ArrayInitialize(FvgHighPriceBuffer, EMPTY_VALUE);
ArraySetAsSeries(FvgHighPriceBuffer, true);
SetIndexBuffer(0, FvgHighPriceBuffer, INDICATOR_DATA);

The same process is repeated for the other buffers.

Why Use ArraySetAsSeries(true)?

In MQL5, time-series arrays are usually indexed from the most recent candle:

  • 0 = current candle
  • 1 = previous candle
  • 2 = two candles ago

Using series indexing makes price-action calculations more intuitive.

Cleaning Up on Deinitialization

When the indicator is removed, OnDeinit() is executed.

ObjectsDeleteAll(0, OBJECT_PREFIX);

This removes all rectangle objects created by the indicator, preventing clutter on the chart.

Main Calculation Logic

The heart of the indicator is OnCalculate().

The first optimization is:

if (rates_total == prev_calculated)
   return rates_total;

This prevents unnecessary recalculation when no new candles are available.

Bullish FVG Detection

The bullish logic is:

bool upGap = leftHighPrice < rightLowPrice;

This is the core imbalance condition.

Additional filters:

bool upLeft = midLowPrice <= leftHighPrice && midLowPrice > leftLowPrice;
bool upRight = midHighPrice >= rightLowPrice && midHighPrice < rightHighPrice;

When a bullish FVG is detected:

SetBuffers(i + 1, rightLowPrice, leftHighPrice, 1);

Then the rectangle is drawn.

Bearish FVG Detection

The bearish condition is:

bool downGap = leftLowPrice > rightHighPrice;

This means the market moved downward so aggressively that the earlier low remains above the later high.

The indicator then stores:

SetBuffers(i + 1, leftLowPrice, rightHighPrice, -1);

And draws a bearish rectangle.

Drawing the Rectangle

The DrawBox() function creates the visual object.

ObjectCreate(0, objName, OBJ_RECTANGLE, 0,
             leftDt, leftPrice,
             rightDt, rightPrice);

Color Selection

ObjectSetInteger(0, objName, OBJPROP_COLOR,
                 leftPrice < rightPrice ? InpUpTrendColor : InpDownTrendColor);
  • Green for bullish
  • Pink for bearish

Fill Option

ObjectSetInteger(0, objName, OBJPROP_FILL, InpFill);

Background Drawing

ObjectSetInteger(0, objName, OBJPROP_BACK, true);

This keeps candles visible above the rectangle.

Practical Trading Applications

Trend Continuation

Bullish FVGs in an uptrend can act as discount zones where traders look for continuation entries.

Pullback Entries

Instead of chasing momentum, traders can wait for price to return into the imbalance.

Liquidity Mapping

Unmitigated gaps often highlight areas where institutional participation may still be incomplete.

Confluence

The indicator becomes much more powerful when combined with:

  • Market structure
  • Break of structure (BOS)
  • Order blocks
  • Session highs and lows
  • Fibonacci retracements
  • Higher-timeframe bias

Multi-Timeframe Analysis

A common approach is:

  • Higher timeframe (H4 / D1): Determine the main bias and identify major FVG zones.
  • Execution timeframe (M15 / M5): Use smaller FVGs for precise entries and confirmations.

For example, a trader may identify a daily bullish FVG and then wait for a 15-minute bullish FVG inside that daily zone before entering a long trade.

Performance Considerations

The code is relatively efficient because:

  • It avoids full recalculation on every tick.
  • It uses object-based drawing instead of heavy plotting.
  • It processes only newly added bars.

Possible Improvements

Advanced developers could extend this indicator by adding:

  • Minimum gap size in points or ATR
  • Alerts when a new FVG appears
  • Alerts when mitigation occurs
  • Multi-timeframe FVG projection
  • Partial mitigation detection
  • Midpoint (50%) line inside the gap
  • Statistics such as fill rate and average fill time

Final Thoughts

This MQL5 Fair Value Gap indicator is a clean and well-structured implementation of a popular institutional price-action concept. The code demonstrates several important MQL5 techniques:

  • Buffer management
  • Time-series processing
  • Object-oriented chart drawing
  • Dynamic object updating
  • Efficient recalculation logic

More importantly, it transforms the abstract idea of market imbalance into a practical visual tool that can be used in both manual and automated trading workflows.



The Full Code is Here.


//+------------------------------------------------------------------+
//|                                                          Fvg.mq5 |
//|                                         Copyright 2024, rpanchyk |
//|                                      https://github.com/rpanchyk |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, rpanchyk"
#property link "https://github.com/rpanchyk"
#property version "1.03"
#property description "Indicator shows fair value gaps"

#property indicator_chart_window
#property indicator_plots 3
#property indicator_buffers 3

// types
enum ENUM_BORDER_STYLE {
   BORDER_STYLE_SOLID = STYLE_SOLID, // Solid
   BORDER_STYLE_DASH = STYLE_DASH // Dash
};

// buffers
double FvgHighPriceBuffer[]; // Higher price of FVG
double FvgLowPriceBuffer[]; // Lower price of FVG
double FvgTrendBuffer[]; // Trend of FVG [0: NO, -1: DOWN, 1: UP]

// config
input group "Section :: Main";
input bool InpContinueToMitigation = true; // Continue to mitigation

input group "Section :: Style";
input color InpDownTrendColor = clrPink; // Down trend color
input color InpUpTrendColor = clrGreen; // Up trend color
input bool InpFill = true; // Fill solid (true) or transparent (false)
input ENUM_BORDER_STYLE InpBoderStyle = BORDER_STYLE_SOLID; // Border line style
input int InpBorderWidth = 1; // Border line width

input group "Section :: Dev";
input bool InpDebugEnabled = false; // Enable debug (verbose logging)

// constants
const string OBJECT_PREFIX = "FVG";
const string OBJECT_PREFIX_CONTINUATED = OBJECT_PREFIX + "CNT";
const string OBJECT_SEP = "#";

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit() {
   if (InpDebugEnabled) {
      Print("Fvg indicator initialization started");
   }

   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);

   ArrayInitialize(FvgHighPriceBuffer, EMPTY_VALUE);
   ArraySetAsSeries(FvgHighPriceBuffer, true);
   SetIndexBuffer(0, FvgHighPriceBuffer, INDICATOR_DATA);
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetString(0, PLOT_LABEL, "Fvg High");
   PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_NONE);

   ArrayInitialize(FvgLowPriceBuffer, EMPTY_VALUE);
   ArraySetAsSeries(FvgLowPriceBuffer, true);
   SetIndexBuffer(1, FvgLowPriceBuffer, INDICATOR_DATA);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetString(1, PLOT_LABEL, "Fvg Low");
   PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_NONE);

   ArrayInitialize(FvgTrendBuffer, EMPTY_VALUE);
   ArraySetAsSeries(FvgTrendBuffer, true);
   SetIndexBuffer(2, FvgTrendBuffer, INDICATOR_DATA);
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetString(2, PLOT_LABEL, "Fvg Trend");
   PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_NONE);

   if (InpDebugEnabled) {
      Print("Fvg indicator initialization finished");
   }
   return (INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   if (InpDebugEnabled) {
      Print("Fvg indicator deinitialization started");
   }

   ArrayFill(FvgHighPriceBuffer, 0, ArraySize(FvgHighPriceBuffer), EMPTY_VALUE);
   ArrayResize(FvgHighPriceBuffer, 0);
   ArrayFree(FvgHighPriceBuffer);

   ArrayFill(FvgLowPriceBuffer, 0, ArraySize(FvgLowPriceBuffer), EMPTY_VALUE);
   ArrayResize(FvgLowPriceBuffer, 0);
   ArrayFree(FvgLowPriceBuffer);

   ArrayFill(FvgTrendBuffer, 0, ArraySize(FvgTrendBuffer), EMPTY_VALUE);
   ArrayResize(FvgTrendBuffer, 0);
   ArrayFree(FvgTrendBuffer);

   if (!MQLInfoInteger(MQL_TESTER)) {
      ObjectsDeleteAll(0, OBJECT_PREFIX);
   }

   if (InpDebugEnabled) {
      Print("Fvg indicator deinitialization finished");
   }
}

//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
   const int prev_calculated,
      const datetime & time[],
         const double & open[],
            const double & high[],
               const double & low[],
                  const double & close[],
                     const long & tick_volume[],
                        const long & volume[],
                           const int & spread[]) {
   if (rates_total == prev_calculated) {
      return rates_total;
   }

   ArraySetAsSeries(time, true);
   ArraySetAsSeries(open, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);

   if (InpContinueToMitigation) {
      int total = ObjectsTotal(0, 0, OBJ_RECTANGLE);
      for (int i = 0; i < total; i++) {
         string objName = ObjectName(0, i, 0, OBJ_RECTANGLE);
         if (StringFind(objName, OBJECT_PREFIX_CONTINUATED) == 0) {
            string result[];
            StringSplit(objName, StringGetCharacter(OBJECT_SEP, 0), result);

            datetime leftTime = StringToTime(result[1]);
            double leftPrice = StringToDouble(result[2]);
            datetime rightTime = time[0];
            double rightPrice = StringToDouble(result[4]);

            if (rightPrice < high[1] && rightPrice > low[1]) {
               rightTime = time[1];
               if (ObjectDelete(0, objName)) {
                  DrawBox(leftTime, leftPrice, rightTime, rightPrice, false);
               }
            } else {
               ObjectMove(0, objName, 1, rightTime, rightPrice);
               if (InpDebugEnabled) {
                  PrintFormat("Expand box %s", objName);
               }
            }
         }
      }
   }

   int limit = prev_calculated == 0 ? rates_total - 3 : rates_total - prev_calculated + 1;
   if (InpDebugEnabled) {
      PrintFormat("RatesTotal: %i, PrevCalculated: %i, Limit: %i", rates_total, prev_calculated, limit);
   }

   for (int i = 1; i < limit; i++) {
      double rightHighPrice = high[i];
      double rightLowPrice = low[i];
      double midHighPrice = high[i + 1];
      double midLowPrice = low[i + 1];
      double leftHighPrice = high[i + 2];
      double leftLowPrice = low[i + 2];

      datetime rightTime = time[i];
      datetime leftTime = time[i + 2];

      // Up trend
      bool upLeft = midLowPrice <= leftHighPrice && midLowPrice > leftLowPrice;
      bool upRight = midHighPrice >= rightLowPrice && midHighPrice < rightHighPrice;
      bool upGap = leftHighPrice < rightLowPrice;
      if (upLeft && upRight && upGap) {
         SetBuffers(i + 1, rightLowPrice, leftHighPrice, 1);

         if (InpContinueToMitigation) {
            rightTime = time[0];
            for (int j = i - 1; j > 0; j--) // Search mitigation bar
            {
               if ((rightLowPrice < high[j] && rightLowPrice >= low[j]) || (leftHighPrice > low[j] && leftHighPrice <= high[j])) {
                  rightTime = time[j];
                  break;
               }
            }
         }

         DrawBox(leftTime, leftHighPrice, rightTime, rightLowPrice, InpContinueToMitigation && rightTime == time[0]);

         continue;
      }

      // Down trend
      bool downLeft = midHighPrice >= leftLowPrice && midHighPrice < leftHighPrice;
      bool downRight = midLowPrice <= rightHighPrice && midLowPrice > rightLowPrice;
      bool downGap = leftLowPrice > rightHighPrice;
      if (downLeft && downRight && downGap) {
         SetBuffers(i + 1, leftLowPrice, rightHighPrice, -1);

         if (InpContinueToMitigation) {
            rightTime = time[0];
            for (int j = i - 1; j > 0; j--) // Search mitigation bar
            {
               if ((rightHighPrice <= high[j] && rightHighPrice > low[j]) || (leftLowPrice >= low[j] && leftLowPrice < high[j])) {
                  rightTime = time[j];
                  break;
               }
            }
         }

         DrawBox(leftTime, leftLowPrice, rightTime, rightHighPrice, InpContinueToMitigation && rightTime == time[0]);

         continue;
      }

      // Fvg not detected, set empty values to buffers
      SetBuffers(i + 1, 0, 0, 0);
   }

   return rates_total; // Set prev_calculated on next call
}

//+------------------------------------------------------------------+
//| Updates buffers with indicator data                              |
//+------------------------------------------------------------------+
void SetBuffers(int index, double highPrice, double lowPrice, double trend) {
   FvgHighPriceBuffer[index] = highPrice;
   FvgLowPriceBuffer[index] = lowPrice;
   FvgTrendBuffer[index] = trend;

   if (InpDebugEnabled && trend != 0) {
      PrintFormat("Time: %s, FvgTrendBuffer: %f, FvgHighPriceBuffer: %f, FvgLowPriceBuffer: %f",
         TimeToString(iTime(_Symbol, PERIOD_CURRENT, index)), FvgTrendBuffer[index],
         FvgHighPriceBuffer[index], FvgLowPriceBuffer[index]);
   }
}

//+------------------------------------------------------------------+
//| Draws FVG box                                                    |
//+------------------------------------------------------------------+
void DrawBox(datetime leftDt, double leftPrice, datetime rightDt, double rightPrice, bool continuated) {
   string objName = (continuated ? OBJECT_PREFIX_CONTINUATED : OBJECT_PREFIX) +
      OBJECT_SEP +
      TimeToString(leftDt) +
      OBJECT_SEP +
      DoubleToString(leftPrice) +
      OBJECT_SEP +
      TimeToString(rightDt) +
      OBJECT_SEP +
      DoubleToString(rightPrice);

   if (ObjectFind(0, objName) < 0) {
      ObjectCreate(0, objName, OBJ_RECTANGLE, 0, leftDt, leftPrice, rightDt, rightPrice);

      ObjectSetInteger(0, objName, OBJPROP_COLOR, leftPrice < rightPrice ? InpUpTrendColor : InpDownTrendColor);
      ObjectSetInteger(0, objName, OBJPROP_FILL, InpFill);
      ObjectSetInteger(0, objName, OBJPROP_STYLE, InpBoderStyle);
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, InpBorderWidth);
      ObjectSetInteger(0, objName, OBJPROP_BACK, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, false);
      ObjectSetInteger(0, objName, OBJPROP_ZORDER, 0);

      if (InpDebugEnabled) {
         PrintFormat("Draw box: %s", objName);
      }
   }
}
//+------------------------------------------------------------------+

Remember that an FVG is not a standalone trading signal. The highest-probability setups usually occur when the gap aligns with higher-timeframe structure, liquidity, and directional bias. Use this indicator as a framework for identifying areas of interest rather than as a guaranteed entry system.

For traders learning MQL5, this project is also an excellent educational example because it combines price-action logic with real-time graphical object management, which is one of the most valuable skills when building professional MetaTrader 5 indicators.