LearnSmart Money Concepts

How to Trade Fair Value Gaps (FVG): Spot and Trade the Fill

A step-by-step guide to fair value gaps: the exact three-candle rule, how to spot FVGs, entry and stop placement on the fill, plus Pine Script v6 code.

Agentic TradersJul 24, 202614 min readIntermediate
How to Trade Fair Value Gaps (FVG): Spot and Trade the Fill

Price rarely travels in a straight line without leaving evidence behind. When one candle rips through three levels of resting orders, it prints a range that nobody traded back into, and that untouched range is a fair value gap.

Most traders find FVGs by eye, mark forty of them on a chart, and then wonder why half of them never fill. The detection rule is mechanical enough to code in six lines. The filter that separates a tradeable gap from decoration has almost nothing to do with the gap itself.

Short answer: a fair value gap is a three-candle imbalance. In a bullish FVG, candle 1's high sits below candle 3's low, and the empty space between those two wicks is the gap, created by a large candle 2 that moved too fast for two-sided trade. You trade it by waiting for price to return into that range, entering on the reaction, and placing your stop beyond the far edge of the gap.

The full recipe, before any of the detail:

  1. Detect. Bullish FVG: low[0] > high[2]. Bearish FVG: high[0] < low[2]. The gap runs from high[2] to low[0] (bullish) or from high[0] to low[2] (bearish).
  2. Filter by size. Discard any gap smaller than roughly 0.25x ATR(14) on that timeframe. Below that it's rounding noise, not displacement.
  3. Filter by context. Only take gaps pointing the same direction as the higher-timeframe trend, and only after a break of structure in that direction.
  4. Enter. Limit order at the 50% midpoint of the gap (its "consequent encroachment"), or a market entry on the first close that rejects out of the gap.
  5. Stop. Beyond the far edge of the gap, padded by 0.5x to 1x ATR(14) so a wick through the level doesn't take you out.
  6. Target. The swing high or low that the displacement candle created, which is usually 2R to 4R from a midpoint entry.

Everything below is the detail behind those six lines: the exact rule, the code to plot it, the structure filter that does the real work, and a Python script that measures how often your gaps on your symbol actually fill.

The three-candle rule, written out exactly

Forget the word "gap" for a second. Nothing here is a weekend or overnight gap in the traditional sense, where a market closes at one price and opens at another. An FVG happens inside continuous trading. It's an imbalance, not a hole in the tape.

Take any three consecutive candles and label them 1, 2, 3 in time order. Candle 2 is the one that moved.

Bullish FVG. Candle 2 closes strongly up. If candle 1's high is below candle 3's low, there's a price range that candle 2 traversed but that neither of its neighbours traded into. That range is the gap:

gap_bottom = high of candle 1
gap_top    = low  of candle 3
condition  = gap_top > gap_bottom

Bearish FVG. Mirror image. Candle 2 closes strongly down, and candle 1's low sits above candle 3's high:

gap_top    = low  of candle 1
gap_bottom = high of candle 3
condition  = gap_top > gap_bottom

Notice what the rule uses: wicks, not bodies. Candle 1's high and candle 3's low are extremes, so the gap is the range where no trade happened on the bars either side of the move. Body-based versions of this rule exist and they produce wider, sloppier zones. Stick to wicks.

The reason the concept has any teeth is microstructure rather than mysticism. A one-sided burst of market orders eats through resting liquidity and drags price, and the relationship between order-flow imbalance and price movement is linear for small signed order flows in empirical studies of trades and quotes. A gap is the visible fossil of that imbalance on a candle chart. It marks where price moved without anyone getting a fair two-sided auction.

That framing also tells you when the idea breaks. A gap formed by a thin, low-volume run at 3am Sunday is a different animal from one formed on the London open, even though both satisfy the same three-candle test.

Spotting an FVG on a live chart in five steps

Do this manually a few dozen times before you automate it. Pattern recognition earned by hand survives contact with a live chart; pattern recognition delegated to an indicator does not.

  1. Pick your timeframe and stay there. Start with the 4-hour on a liquid symbol. Lower timeframes print gaps constantly and most are meaningless.
  2. Find the displacement candle first, not the gap. Scan for a candle with a large body relative to its neighbours, ideally one that closed beyond a recent swing point. That candle is your candidate 2.
  3. Check the two neighbours. Drop a horizontal line from the high of the candle before it, and another from the low of the candle after it. If the second line is above the first, you have a bullish FVG. If you're checking a down move, invert.
  4. Measure it against ATR. Compare the gap height with ATR(14) on that same chart. Under about a quarter of ATR, ignore it. Over about one full ATR, treat it as significant.
  5. Mark the midpoint. Draw the 50% level inside the box. That line, not the edge, is where most of your entries will sit.

On timeframe choice, higher is generally better, for a plain reason: a gap on the daily chart represents an imbalance built from hundreds of lower-timeframe bars, so it takes far more opposing volume to erase. Gaps on the 5-minute get resolved by ordinary noise within an hour.

TimeframeTypical gaps per 500 barsMin size filterRealistic fill windowBest used for
5m40 to 800.5x ATR(14)1 to 20 barsScalping entries inside an HTF zone
1h15 to 350.3x ATR(14)5 to 60 barsIntraday continuation
4h8 to 200.25x ATR(14)10 to 80 barsSwing entries, the default
1D3 to 100.25x ATR(14)20 to 200 barsBias and target selection

Those gap counts are what a size filter typically leaves you with on a liquid crypto or FX pair; run the Python script below on your own symbol rather than trusting a table, because tick size and session structure move these numbers around a lot.

Plotting fair value gaps automatically in Pine Script v6

This script detects both directions, filters by ATR, draws each gap as a box, extends live boxes to the right, and deletes a box once price has traded fully through it. It uses box.new(), whose signature takes left, top, right, bottom before the styling arguments, and arrays to keep a handle on every open gap.

//@version=6
indicator("Fair Value Gaps", overlay = true, max_boxes_count = 500)

minSize   = input.float(0.25, "Min gap size (x ATR)", minval = 0.0, step = 0.05)
extendBy  = input.int(40,     "Extend box (bars)",    minval = 1)
showBull  = input.bool(true,  "Bullish FVGs")
showBear  = input.bool(true,  "Bearish FVGs")

atr = ta.atr(14)

// Candle 1 = two bars back, candle 2 = the displacement bar, candle 3 = current.
bullGap  = low > high[2]
bearGap  = high < low[2]
bullSize = low - high[2]
bearSize = low[2] - high

var array<box> bullBoxes = array.new<box>()
var array<box> bearBoxes = array.new<box>()

if showBull and bullGap and bullSize >= minSize * atr
    array.push(bullBoxes, box.new(bar_index - 2, low, bar_index + extendBy, high[2],
      border_color = color.new(color.teal, 40), bgcolor = color.new(color.teal, 85)))

if showBear and bearGap and bearSize >= minSize * atr
    array.push(bearBoxes, box.new(bar_index - 2, low[2], bar_index + extendBy, high,
      border_color = color.new(color.red, 40), bgcolor = color.new(color.red, 85)))

// Retire a gap once price closes the whole thing; otherwise keep it extended.
if array.size(bullBoxes) > 0
    for i = array.size(bullBoxes) - 1 to 0
        b = array.get(bullBoxes, i)
        if low < box.get_bottom(b)
            box.delete(b)
            array.remove(bullBoxes, i)
        else
            box.set_right(b, bar_index + extendBy)

if array.size(bearBoxes) > 0
    for i = array.size(bearBoxes) - 1 to 0
        b = array.get(bearBoxes, i)
        if high > box.get_top(b)
            box.delete(b)
            array.remove(bearBoxes, i)
        else
            box.set_right(b, bar_index + extendBy)

// Alert when price first trades into the newest bullish gap's midpoint.
midTouch = array.size(bullBoxes) > 0 and
  low <= math.avg(box.get_top(array.get(bullBoxes, array.size(bullBoxes) - 1)),
                  box.get_bottom(array.get(bullBoxes, array.size(bullBoxes) - 1)))
alertcondition(midTouch, "FVG midpoint touched", "Price reached the FVG midpoint")

Two details worth understanding rather than copying. The detection fires on candle 3, which means the box appears two bars after the move started, and that's correct: you cannot know a gap exists until the third candle prints its low. And the deletion rule uses a full traversal, not a touch, so a box survives a wick into it and disappears only once the imbalance is genuinely gone.

If you want the gap to persist as a historical record instead, replace box.delete(b) with a colour change and drop the array.remove call.

Trading the fill: entry, stop, and targets

A gap is a zone, not a signal. Three entry styles, in order of how much they trade off fill rate against price:

Limit at the midpoint. Resting order at 50% of the gap. Best average price, and roughly half your orders never get hit because price reverses from the near edge. This is the default for swing traders working the 4-hour and daily.

Limit at the far edge. Order at the full extent of the gap. Fills less often still, but when it does, your stop can be tight because you're already at the boundary that invalidates the setup.

Confirmation entry. Wait for price to enter the gap and then close back out of it in the original direction. Worst price, highest hit rate, and the only version that gives you a candle to react to rather than a level to hope at.

Stop placement follows the invalidation logic, not a fixed percentage. For a bullish gap you're arguing that buyers step back in inside the imbalance. If price closes below the gap's lower edge, that argument is dead. So the stop sits below gap_bottom, padded by 0.5x to 1x ATR(14) to absorb a wick. The same ATR-based padding logic drives trailing stops in the Supertrend strategy, and the reasoning is identical: you want the stop outside normal noise for that instrument, not at a round number.

Position size then falls out of the stop distance:

risk_per_trade = account_equity * 0.01
stop_distance  = entry_price - (gap_bottom - 0.75 * atr14)
position_size  = risk_per_trade / stop_distance

Targets have two sensible anchors. The first is the swing point that the displacement candle broke, because that's the level the move was aiming at. The second is the next opposing FVG or unmitigated order block in the direction of travel, since that's where the opposite side has unfinished business. From a midpoint entry with an ATR-padded stop, the first target usually sits between 2R and 4R on the 4-hour.

Filtering with market structure so you don't trade every gap

This section is the one that matters. Detection is trivial and everyone has it. Selection is where the edge lives.

Break of structure comes first. A bullish FVG is only interesting if it formed during a move that broke a prior swing high. A gap that forms mid-range, breaking nothing, is a gap inside chop and price will slice back through it. Sequence: swing high breaks, displacement candle prints the gap, price retraces into the gap, you buy.

Higher-timeframe bias gates the lower timeframe. Trade 1-hour bullish gaps only while the 4-hour structure is making higher highs and higher lows. When the two disagree, the higher timeframe wins and your gap becomes a liquidity target rather than support.

Overlap with an order block is the strongest single confluence. An order block is the last opposing candle before the displacement; the FVG is the imbalance the displacement created. When the gap sits on top of that candle's range, you have two independent reasons to expect a reaction in the same few ticks, and you can put your stop below both.

Unmitigated beats revisited. Once price has traded into a gap and reacted, that gap has done its job. The second visit is materially weaker than the first. Track whether each zone is fresh; the Pine script above does this implicitly by deleting gaps that get fully traversed.

One more filter that costs nothing: skip gaps formed in the illiquid hours of your instrument. FX gaps printed during the Asian session on a EUR pair get retraced far more often than the same pattern at the London open, because the displacement wasn't real participation.

FVG vs order block vs supply and demand vs breakaway gap

These four get used interchangeably in retail content and they are not the same object.

ConceptWhat defines itWhat it marksTypical resolutionMain failure mode
Fair value gap3-candle wick imbalance (low[0] > high[2])Price range with one-sided tradeOften revisited within tens of barsFires constantly without a size and structure filter
Order blockLast opposing candle before displacementWhere the initiating orders likely satRevisited, or blown through if structure flippedEveryone draws it differently; body vs wick ambiguity
Supply/demand zoneConsolidation base before a strong moveArea of accumulated resting ordersWide zone, slow reaction, multiple testsZones get drawn so wide they're unfalsifiable
Breakaway gapSession-to-session price discontinuityA repricing event between auctionsFrequently does not fillAssuming it will fill because "gaps always fill"

That last row deserves a footnote, since retail folklore says every gap fills. Research on classic overnight price gaps across the DJI, S&P 500 and NASDAQ back to 1928 contradicts the myth that price gaps tend to get filled, and finds instead that prices tend to keep moving in the direction of the gap during the gap day. FVGs are a different construct and behave differently, but the lesson carries: "it has to fill" is a belief, not a rule, and it's exactly the belief that gets traders run over shorting into strength.

Measuring your own fill rate in Python

Here's how to stop guessing. This pulls candles, detects bullish FVGs with an ATR filter, and reports how often they were touched and fully filled inside a fixed forward window. It uses ccxt against the Binance klines endpoint, so swap the exchange or feed for whatever you actually trade.

import ccxt
import numpy as np
import pandas as pd

SYMBOL, TIMEFRAME, HORIZON, MIN_ATR = "BTC/USDT", "4h", 60, 0.25

ex = ccxt.binance()
raw = ex.fetch_ohlcv(SYMBOL, timeframe=TIMEFRAME, limit=1000)
df = pd.DataFrame(raw, columns=["ts", "open", "high", "low", "close", "volume"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms")

prev_close = df["close"].shift(1)
tr = pd.concat([
    df["high"] - df["low"],
    (df["high"] - prev_close).abs(),
    (df["low"] - prev_close).abs(),
], axis=1).max(axis=1)
df["atr"] = tr.rolling(14).mean()

high, low, atr = df["high"].values, df["low"].values, df["atr"].values
rows = []

for i in range(15, len(df) - 1):
    if not low[i] > high[i - 2]:          # bullish FVG only
        continue
    top, bottom = low[i], high[i - 2]
    size = top - bottom
    if np.isnan(atr[i]) or size < MIN_ATR * atr[i]:
        continue
    fwd = low[i + 1 : i + 1 + HORIZON]
    rows.append({
        "bar": i,
        "size_atr": size / atr[i],
        "touched": bool((fwd <= top).any()),      # entered the zone at all
        "filled": bool((fwd <= bottom).any()),    # traversed it completely
    })

res = pd.DataFrame(rows)
print(f"{SYMBOL} {TIMEFRAME}: {len(res)} gaps, horizon {HORIZON} bars")
print(f"  touched: {res['touched'].mean():.1%}   fully filled: {res['filled'].mean():.1%}")

res["bucket"] = pd.cut(res["size_atr"], [0, 0.5, 1.0, 2.0, np.inf])
print(res.groupby("bucket", observed=True)[["touched", "filled"]].mean().round(3))

The size bucket breakdown is the interesting output. Run it and you get a direct answer to the question that decides your entry style: do larger gaps get revisited more or less often on your instrument, and does the answer change your minimum size filter? Note this measures fill behaviour only. It says nothing about whether trading the fill was profitable, because it ignores stops, targets and structure entirely.

Extend it in two directions before you trust anything: add a bearish branch, and add a structure filter so only gaps formed after a break of the prior swing count. The difference between the filtered and unfiltered fill rates is roughly the value of the filter.

Common mistakes that turn a good gap into a bad trade

Marking every gap on the chart. Without an ATR filter, a 4-hour chart of 500 bars gives you dozens of boxes, most of them a few ticks tall. The chart becomes unreadable and, worse, you'll always find a gap near price to justify a trade you already wanted to take. Filter by size first, then by structure, and you should be looking at a handful of live zones at most.

Buying a bullish gap while higher-timeframe structure is bearish. This is the single most expensive error in the whole method. In a downtrend, bullish gaps below price are not support; they're where trapped longs are sitting, and price runs them deliberately. Check the higher timeframe before you place the order, every time.

Treating the wick through as invalidation. Price routinely wicks past a gap's far edge and closes back inside. If your stop is one tick beyond the edge, you'll get taken out on the exact candle that starts the move. Pad it by 0.5x to 1x ATR(14), then size the position down to keep risk constant.

Trading a re-tested gap as if it were fresh. The second and third visits into the same zone perform noticeably worse than the first. Mark a gap as mitigated once price has traded meaningfully into it, and stop taking entries there.

Confusing the FVG with the candle that made it. The gap is the empty range between candles 1 and 3. It is not the body of the displacement candle, and it is not the whole three-candle range. Drawing it wrong makes your zone two or three times too tall, which quietly destroys your risk-to-reward maths.

Advanced setup: the parameters worth changing

Raise the minimum size filter on lower timeframes. 0.25x ATR is right for the 4-hour and daily. On the 5-minute, the same threshold leaves you with noise; push it to 0.5x or higher. Size filters should scale with how many bars per day the timeframe prints.

Use consequent encroachment as your default entry. The 50% level of the gap is a better resting point than either edge, because it splits the difference between fill probability and entry quality. It also keeps your stop distance predictable, which keeps position sizing consistent.

Watch for inversions. When a bullish gap gets fully traversed and price then rejects it from below, that zone often flips to resistance. Traders call this an inversion FVG. It's the cleanest way to trade a failed setup, and the Pine script's deletion rule is where you'd hook it: instead of deleting the box, recolour it and flip its direction.

Pair displacement with a volatility expansion check. The candle that creates a genuine gap almost always coincides with an expansion in range after a period of compression. If you already track squeezes, the setups that follow a Bollinger Band squeeze breakout produce the higher-quality gaps, because the displacement is a real regime change rather than a single large print.

Add a momentum divergence check at the fill. Price returning into a bullish gap while momentum makes a higher low is a meaningfully different tape from price grinding in on strengthening downside momentum. The read from an RSI divergence at the moment of the retrace is a cheap second opinion on whether the reaction is likely.

Cap the age of a zone. A gap from 300 bars ago on the 4-hour is archaeology. Expire boxes after some multiple of your holding period, 80 to 120 bars is reasonable on the 4-hour, so your chart shows only zones you'd actually trade.

Running this without watching charts all day

The awkward part of FVG trading is the waiting. The gap prints in ten minutes and then price takes two days to come back, usually at 3am, and the setup either needs structure confirmation at that moment or it doesn't.

That's the part you can describe in plain English and hand to an agent. In Agentic Traders you'd write something like:

"Watch ETH on the 4-hour. Track the most recent bullish fair value gap, where candle 1's high is below candle 3's low and the gap is at least 0.5x ATR(14) tall, and only count gaps that formed after price broke the previous swing high. When price trades back into that gap and a 4-hour candle closes above its midpoint, open a long sized at 1% account risk, with the stop 0.75x ATR(14) below the gap's lower edge and the first target at the swing high the displacement created."

The agent reasons through that rather than pattern-matching it: it reads the candles, calls ATR and the Smart Money Concepts tools for order blocks and market structure, and shows its full thinking on every decision, including the ones where it looks at the retrace and decides not to take it. Because it can wait on a condition, it stays dormant until price actually re-enters the zone instead of re-evaluating every hour. You can run the whole thing on the free $100K paper account at app.agentictraders.io and read a few weeks of its reasoning before any of it touches real money.

FAQ

Do fair value gaps always get filled?

No. A meaningful share never get revisited at all, especially gaps formed during strong trending expansion where price simply keeps going. The Python script above gives you the actual rate for your symbol and timeframe rather than a number from a YouTube thumbnail. Treat "unfilled" as normal and build the strategy around gaps that fill with structure on your side.

What's the difference between a fair value gap and an imbalance?

They're the same thing under two names. "Imbalance" is the broader microstructure term for one-sided trade; "fair value gap" is the specific three-candle chart pattern that makes an imbalance visible on candles. Some traders reserve "imbalance" for a smaller two-candle wick overlap, but the trading logic doesn't change.

Which timeframe is best for trading FVGs?

The 4-hour is the practical default for most retail traders: enough gaps to trade, few enough to filter properly, and a fill window measured in days rather than minutes. Use the daily to set direction and the 1-hour for entry refinement inside a 4-hour zone. Below 15 minutes the pattern still exists but the noise-to-signal ratio makes the size filter do all the work.

Can I trade fair value gaps without Smart Money Concepts?

Yes. Strip the terminology and what's left is a mean-reversion entry into a range of thin trade, taken only in the direction of the prevailing trend. That's a perfectly ordinary pullback strategy with a precise, codeable definition of where the pullback should stop. The SMC vocabulary adds structure filters that improve selection; it isn't load-bearing for the entry itself.

How do I avoid marking gaps that are just noise?

Two filters do almost all of the work: a minimum size expressed as a multiple of ATR on the same timeframe, and a requirement that the gap formed during a move that broke a prior swing point. Applied together on a 4-hour chart they typically cut a few dozen raw detections down to a handful of zones worth an order.

What to test next

Start manual. Mark 30 gaps on the 4-hour of one instrument, log which ones filled and what the structure looked like beforehand, and you'll have a better filter than any indicator will hand you. Then run the Python script on the same symbol to check whether your eye and the data agree.

After that, add the structure filter to the script and compare fill rates with and without it. If the filtered set doesn't behave differently from the unfiltered set, your filter isn't doing anything, and it's better to learn that from 1,000 bars of history than from twenty live trades.

Related articles

Pick one symbol, set the size and structure filters, and paper trade the next ten fills with an agent watching the zone so you never miss the 3am retrace: start here.

Stop reading. Start building.

Run this strategy with an AI Trader.

Tell the AI your strategy. It picks the indicators, reasons through every setup, and trades for you 24/7. Free to start. No code.

Build your AI Trader — free