What it does
A fair value gap (FVG), sometimes called an imbalance, is a three-candle pattern where price moved so quickly that the first and third candles do not overlap. This indicator finds those gaps on closed candles, draws each one as a box and keeps extending it to the right until price comes back and fills it. You can filter out small gaps with an ATR-based minimum size.
How it works
For each closed candle the script compares it with the candle two bars earlier:
- Bullish gap: the current low is above the high of two candles ago. The box runs from that earlier high (bottom) to the current low (top).
- Bearish gap: the current high is below the low of two candles ago. The box runs from the current high (bottom) to that earlier low (top).
If a minimum size is set, the gap height must be at least that multiple of ATR (14). Open gaps are then checked on every closed candle. With "Full fill", a bullish gap is filled when a low reaches the bottom of the box. "Midpoint" uses the 50% line and "First touch" uses the near edge. With shrinking on, a partial fill trims the box so it only shows the part of the gap price has not yet traded through.
Filled gaps are deleted or kept in grey. Both lists are capped, so the script stays within TradingView's drawing limits.
How to use it
- Gaps show where price moved one-sidedly. Traders often watch for price to revisit them, and some treat a partly filled gap in the direction of the trend as a possible area for a pullback to pause.
- Gaps work best with context. A bullish gap formed after a break of structure, or near a higher-timeframe support level, carries more weight than a random gap inside a range.
- On M1 and M5 charts, raise the minimum size to 0.2 to 0.5 ATR to cut out noise. On H1 and above, the default of 0 is usually fine.
- Use "First touch" if you want an alert when price first returns, and "Full fill" if you only want to retire gaps that are completely closed.
Alerts: new bullish FVG, new bearish FVG, bullish FVG filled, bearish FVG filled.
Settings
| Input | Default | What it changes |
|---|---|---|
| Minimum gap size (x ATR) | 0 | Filters small gaps |
| ATR length | 14 | ATR used for the size filter |
| Show bullish / bearish gaps | On / On | Which side is drawn |
| Gap counts as filled on | Full fill | Full fill, midpoint or first touch |
| Shrink box as price fills it | On | Trims partly filled gaps |
| Keep filled gaps (greyed) | Off | Leaves filled gaps visible |
| Max open gaps per side | 20 | Cap on live gaps |
| Max filled gaps kept | 20 | Cap on grey gaps |
Limitations
Not every gap gets filled, and many fill only after a long time, so an open gap is not a target or a prediction. In trending markets gaps can pile up without price returning to them. Forex prices trade nearly around the clock, but weekend gaps and very thin sessions create gaps that mean little. Spread and different data feeds can change which candles form a gap, so two brokers may show slightly different results.
Pine Script source
In TradingView open Pine Editor, paste the code, click Save and then Add to chart.
FXP_FairValueGaps.pine
119 lines
// © FXPIPS ONLINE — https://fxpips.online/tradingview-indicators/fair-value-gaps/
// License: MIT. Educational tool — not financial advice. Trading involves substantial risk.
//@version=6
// Draws three-candle fair value gaps (imbalances) as boxes that extend to the right
// until price fills them. Gaps are detected on closed candles only.
indicator("FXP Fair Value Gaps", shorttitle = "FXP FVG", overlay = true, max_boxes_count = 500)
// ── Inputs ─────────────────────────────────────────────────────────────
grpDet = "Detection"
minAtr = input.float(0.0, "Minimum gap size (x ATR)", minval = 0.0, step = 0.05, group = grpDet, tooltip = "Ignore gaps smaller than this many ATRs. 0 = keep every gap.")
atrLen = input.int(14, "ATR length", minval = 1, group = grpDet)
showBull = input.bool(true, "Show bullish gaps", group = grpDet)
showBear = input.bool(true, "Show bearish gaps", group = grpDet)
grpFill = "Filling"
fillMode = input.string("Full fill", "Gap counts as filled on", options = ["Full fill", "Midpoint", "First touch"], group = grpFill)
shrink = input.bool(true, "Shrink box as price fills it", group = grpFill)
keepFill = input.bool(false, "Keep filled gaps (greyed)", group = grpFill)
grpDisp = "Display"
maxActive = input.int(20, "Max open gaps per side", minval = 1, maxval = 100, group = grpDisp)
maxFilled = input.int(20, "Max filled gaps kept", minval = 0, maxval = 100, group = grpDisp)
colBull = input.color(color.new(#26a69a, 80), "Bullish gap", group = grpDisp)
colBear = input.color(color.new(#ef5350, 80), "Bearish gap", group = grpDisp)
colFilled = input.color(color.new(color.gray, 90), "Filled gap", group = grpDisp)
// ── Types and storage ──────────────────────────────────────────────────
type Gap
box bx
float top
float bottom
float mid
var array<Gap> bullGaps = array.new<Gap>()
var array<Gap> bearGaps = array.new<Gap>()
var array<Gap> filledGaps = array.new<Gap>()
closeGap(Gap g) =>
if keepFill and maxFilled > 0
g.bx.set_right(bar_index)
g.bx.set_bgcolor(colFilled)
g.bx.set_border_color(color.new(color.gray, 70))
filledGaps.push(g)
while filledGaps.size() > maxFilled
Gap old = filledGaps.shift()
old.bx.delete()
else
g.bx.delete()
true
// ── Update open gaps (closed bars) ─────────────────────────────────────
bool bullFilled = false
bool bearFilled = false
if bullGaps.size() > 0
for i = bullGaps.size() - 1 to 0
Gap g = bullGaps.get(i)
if barstate.isconfirmed
float lvl = fillMode == "First touch" ? g.top : fillMode == "Midpoint" ? g.mid : g.bottom
if low <= lvl
bullFilled := true
bullGaps.remove(i)
closeGap(g)
else
if shrink and low < g.top
g.top := math.max(low, g.bottom)
g.bx.set_top(g.top)
g.bx.set_right(bar_index)
else
g.bx.set_right(bar_index)
if bearGaps.size() > 0
for i = bearGaps.size() - 1 to 0
Gap g = bearGaps.get(i)
if barstate.isconfirmed
float lvl = fillMode == "First touch" ? g.bottom : fillMode == "Midpoint" ? g.mid : g.top
if high >= lvl
bearFilled := true
bearGaps.remove(i)
closeGap(g)
else
if shrink and high > g.bottom
g.bottom := math.min(high, g.top)
g.bx.set_bottom(g.bottom)
g.bx.set_right(bar_index)
else
g.bx.set_right(bar_index)
// ── Detect new gaps ────────────────────────────────────────────────────
float atr = ta.atr(atrLen)
float minSize = minAtr * nz(atr)
bool bullGap = barstate.isconfirmed and showBull and low > high[2] and (low - high[2]) >= minSize
bool bearGap = barstate.isconfirmed and showBear and high < low[2] and (low[2] - high) >= minSize
if bullGap
float t = low
float b = high[2]
box bx = box.new(bar_index - 2, t, bar_index, b, border_color = color.new(colBull, 50), bgcolor = colBull, text = "FVG", text_color = color.new(colBull, 20), text_size = size.tiny, text_halign = text.align_right)
bullGaps.push(Gap.new(bx, t, b, (t + b) / 2))
while bullGaps.size() > maxActive
Gap old = bullGaps.shift()
old.bx.delete()
if bearGap
float t = low[2]
float b = high
box bx = box.new(bar_index - 2, t, bar_index, b, border_color = color.new(colBear, 50), bgcolor = colBear, text = "FVG", text_color = color.new(colBear, 20), text_size = size.tiny, text_halign = text.align_right)
bearGaps.push(Gap.new(bx, t, b, (t + b) / 2))
while bearGaps.size() > maxActive
Gap old = bearGaps.shift()
old.bx.delete()
// ── Alerts ─────────────────────────────────────────────────────────────
alertcondition(bullGap, "New bullish FVG", "{{ticker}} {{interval}}: new bullish fair value gap")
alertcondition(bearGap, "New bearish FVG", "{{ticker}} {{interval}}: new bearish fair value gap")
alertcondition(bullFilled, "Bullish FVG filled", "{{ticker}} {{interval}}: bullish fair value gap filled at {{close}}")
alertcondition(bearFilled, "Bearish FVG filled", "{{ticker}} {{interval}}: bearish fair value gap filled at {{close}}")

