Why Most Traders Lose Money on Fair Value Gaps (FVGs)
Fair Value Gaps and market gaps are some of the most powerful liquidity pools in price action trading. However, traditional FVG indicators have two massive flaws:
Chart Clutter: They leave massive boxes all over your screen long after the market has moved on.
Static Levels: They don’t account for partial mitigation—when price dips into a gap but doesn’t fill it completely.
The Gap Fill Breakouts indicator completely solves this. Built from the ground up in Pine Script v6, this intelligent script introduces an Adaptive Stair-Step Adjustment system that dynamically shrinks the gap box as price chips away at it.
How It Works: Pure, Clean Price Action
Instead of guessing when a gap is truly invalidated, this indicator dynamically tracks the unfilled portion of the market imbalance and pairs it with structural pivot levels:
Dynamic FVG Tracking: Automatically detects bullish and bearish FVGs.
Stair-Step Mitigation: As price fills the gap, the indicator dynamically resizes the box in real-time, showing you exactly how much imbalance remains.
Structural S/R Confirmation: The moment an FVG is formed, the script tracks the nearest structural pivot high/low.
Clean Breakout Signals: It triggers a sharp, actionable triangle signal (▲ or ▼) only when price breaks out past that validated structural level.
Key Features & Customization
ATR Volatility Filter: Adjust the ATR Multiplier to filter out micro-gaps and focus only on high-probability, institutional market imbalances.
Adaptive Memory Management: Set the maximum number of recent gaps to display (Number of Gaps Shown) to keep your execution chart perfectly clean and lightweight.
Real-Time Alerts: Fully equipped with optimized, once-per-bar-close alert triggers so you never miss a structural breakout.
//@version=6
indicator("Gap Fill Breakouts", overlay = true, max_boxes_count = 500, max_lines_count = 500)
// --- Inputs ---
atrMult = input.float(1.0, "ATR Multiplier for FVG Size", step = 0.1, tooltip = "Minimum size of the Fair Value Gap relative to ATR. A value of 1.0 means the gap must be at least the size of 1 ATR.")
pivotLength = input.int(5, "Pivot Length", tooltip = "Length used to detect structural support and resistance levels before the gap.")
maxBarsBack = input.int(200, "Max Bars Back for Signal", tooltip = "Maximum number of bars since the FVG was formed for a signal to be valid.")
maxGaps = input.int(5, "Number of Gaps Shown", minval = 1, tooltip = "Maximum number of recent gaps to display on the chart.")
// --- Style Inputs ---
bullFvgColor = input.color(color.new(#089981, 80), "Bullish FVG Color", group = "Style")
bearFvgColor = input.color(color.new(#f23645, 80), "Bearish FVG Color", group = "Style")
lineColor = input.color(color.new(#ffeb3b, 0), "Support/Resistance Line Color", group = "Style")
// --- Alert Inputs ---
alertBullish = input.bool(true, "Bullish Breakout Alerts", group = "Alerts")
alertBearish = input.bool(true, "Bearish Breakout Alerts", group = "Alerts")
// --- Types ---
type FVG
float top
float bottom
bool isBullish
box boxId
float srLevel
line srLineId
bool isActive
int creationBar
array boxHistory
// --- Variables ---
var fvgArray = array.new()
var atrLength = 14
// --- Functions ---
manageGaps(arr, max) =>
if arr.size() > max
oldFvg = arr.shift()
for bx in oldFvg.boxHistory
box.delete(bx)
if not na(oldFvg.srLineId)
line.delete(oldFvg.srLineId)
// --- Volatility Filter ---
atr = ta.atr(atrLength)
minGapSize = atr * atrMult
// --- FVG Detection ---
isBullishFvg = low[0] > high[2]
isBearishFvg = high[0] < low[2] bullFvgSize = isBullishFvg ? (low[0] - high[2]) : 0 bearFvgSize = isBearishFvg ? (low[2] - high[0]) : 0 if isBullishFvg and bullFvgSize >= minGapSize
b = box.new(bar_index - 2, low[0], bar_index, high[2], border_color = na, bgcolor = bullFvgColor, extend = extend.right)
bHist = array.new()
bHist.push(b)
fvgArray.push(FVG.new(low[0], high[2], true, b, na, na, true, bar_index, bHist))
manageGaps(fvgArray, maxGaps)
if isBearishFvg and bearFvgSize >= minGapSize
b = box.new(bar_index - 2, low[2], bar_index, high[0], border_color = na, bgcolor = bearFvgColor, extend = extend.right)
bHist = array.new()
bHist.push(b)
fvgArray.push(FVG.new(low[2], high[0], false, b, na, na, true, bar_index, bHist))
manageGaps(fvgArray, maxGaps)
// --- Pivot Detection (Based on Closes/Opens) ---
bodyHigh = math.max(close, open)
bodyLow = math.min(close, open)
ph = ta.pivothigh(bodyHigh, pivotLength, pivotLength)
pl = ta.pivotlow(bodyLow, pivotLength, pivotLength)
bool bullBreakout = false
bool bearBreakout = false
// --- Core Logic ---
if fvgArray.size() > 0
for i = fvgArray.size() - 1 to 0
fvg = fvgArray.get(i)
if not fvg.isActive
continue
if fvg.isBullish
// FVG Mitigation (Adaptive Stair-Step Adjustment)
if low < fvg.top
// Stop current box
box.set_right(fvg.boxId, bar_index)
box.set_extend(fvg.boxId, extend.none)
fvg.top := low
if fvg.top <= fvg.bottom fvg.isActive := false if not na(fvg.srLineId) line.set_x2(fvg.srLineId, bar_index) line.set_extend(fvg.srLineId, extend.none) continue else // Create new box for remaining gap fvg.boxId := box.new(bar_index, fvg.top, bar_index, fvg.bottom, border_color = na, bgcolor = bullFvgColor, extend = extend.right) fvg.boxHistory.push(fvg.boxId) // Structural Support Update if na(fvg.srLineId) and not na(pl) and pl > fvg.top
fvg.srLevel := pl
fvg.srLineId := line.new(bar_index - pivotLength, pl, bar_index, pl, color = lineColor, extend = extend.right)
// Breakout Signal
if not na(fvg.srLevel) and close < fvg.srLevel and close[1] >= fvg.srLevel
if (bar_index - fvg.creationBar) <= maxBarsBack bearBreakout := true fvg.srLevel := na // Prevent continuous firing if not na(fvg.srLineId) line.set_style(fvg.srLineId, line.style_dashed) else // Bearish // FVG Mitigation (Adaptive Stair-Step Adjustment) if high > fvg.bottom
// Stop current box
box.set_right(fvg.boxId, bar_index)
box.set_extend(fvg.boxId, extend.none)
fvg.bottom := high
if fvg.bottom >= fvg.top
fvg.isActive := false
if not na(fvg.srLineId)
line.set_x2(fvg.srLineId, bar_index)
line.set_extend(fvg.srLineId, extend.none)
continue
else
// Create new box for remaining gap
fvg.boxId := box.new(bar_index, fvg.top, bar_index, fvg.bottom, border_color = na, bgcolor = bearFvgColor, extend = extend.right)
fvg.boxHistory.push(fvg.boxId)
// Structural Resistance Update
if na(fvg.srLineId) and not na(ph) and ph < fvg.bottom fvg.srLevel := ph fvg.srLineId := line.new(bar_index - pivotLength, ph, bar_index, ph, color = lineColor, extend = extend.right) // Breakout Signal if not na(fvg.srLevel) and close > fvg.srLevel and close[1] <= fvg.srLevel
if (bar_index - fvg.creationBar) <= maxBarsBack
bullBreakout := true
fvg.srLevel := na // Prevent continuous firing
if not na(fvg.srLineId)
line.set_style(fvg.srLineId, line.style_dashed)
// --- Visuals ---
plotshape(bullBreakout, "Bullish Breakout", shape.triangleup, location.belowbar, color = color.new(#089981, 0), size = size.small)
plotshape(bearBreakout, "Bearish Breakout", shape.triangledown, location.abovebar, color = color.new(#f23645, 0), size = size.small)
// --- Alerts ---
if bullBreakout and alertBullish
alert("Bullish Adaptive FVG Breakout on " + syminfo.ticker, alert.freq_once_per_bar_close)
if bearBreakout and alertBearish
alert("Bearish Adaptive FVG Breakout on " + syminfo.ticker, alert.freq_once_per_bar_close)
