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.

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:
- Detect. Bullish FVG:
low[0] > high[2]. Bearish FVG:high[0] < low[2]. The gap runs fromhigh[2]tolow[0](bullish) or fromhigh[0]tolow[2](bearish). - Filter by size. Discard any gap smaller than roughly 0.25x ATR(14) on that timeframe. Below that it's rounding noise, not displacement.
- 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.
- 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.
- 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.
- 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.
- Pick your timeframe and stay there. Start with the 4-hour on a liquid symbol. Lower timeframes print gaps constantly and most are meaningless.
- 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.
- 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.
- 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.
- 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.
| Timeframe | Typical gaps per 500 bars | Min size filter | Realistic fill window | Best used for |
|---|---|---|---|---|
| 5m | 40 to 80 | 0.5x ATR(14) | 1 to 20 bars | Scalping entries inside an HTF zone |
| 1h | 15 to 35 | 0.3x ATR(14) | 5 to 60 bars | Intraday continuation |
| 4h | 8 to 20 | 0.25x ATR(14) | 10 to 80 bars | Swing entries, the default |
| 1D | 3 to 10 | 0.25x ATR(14) | 20 to 200 bars | Bias 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.
| Concept | What defines it | What it marks | Typical resolution | Main failure mode |
|---|---|---|---|---|
| Fair value gap | 3-candle wick imbalance (low[0] > high[2]) | Price range with one-sided trade | Often revisited within tens of bars | Fires constantly without a size and structure filter |
| Order block | Last opposing candle before displacement | Where the initiating orders likely sat | Revisited, or blown through if structure flipped | Everyone draws it differently; body vs wick ambiguity |
| Supply/demand zone | Consolidation base before a strong move | Area of accumulated resting orders | Wide zone, slow reaction, multiple tests | Zones get drawn so wide they're unfalsifiable |
| Breakaway gap | Session-to-session price discontinuity | A repricing event between auctions | Frequently does not fill | Assuming 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
- Supertrend Strategy: ATR Trend-Following You Can Automate for ATR-based stop placement in more depth.
- Bollinger Band Squeeze: Trading Volatility Breakouts With AI on the compression that precedes real displacement.
- RSI Divergence Strategy: Spot Reversals and Automate With AI for confirming the reaction at the fill.
- What Is Agentic Trading? How AI Agents Trade the Markets on how a reasoning agent differs from a rules bot.
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.
What Is Agentic Trading? How AI Agents Trade the Markets
Agentic trading is when an AI agent reasons through live market data and calls indicators as tools to trade on its own. Here's how it works, and its limits.
StrategiesIchimoku Cloud Strategy: Reading the Kumo and Automating Entries
Master all five Ichimoku lines, trade cloud bias and TK crosses, and use an AI agent that scores confluence before entering.
IndicatorsBollinger Band Squeeze: Trading Volatility Breakouts With AI
Learn to detect Bollinger Band squeezes, trade breakout and reversion playbooks, and automate with an AI agent that confirms expansion before acting.
StrategiesSupertrend Strategy: ATR Trend-Following You Can Automate
Master the Supertrend indicator with ATR settings, Pine Script strategy code, and AI agent automation that filters whipsaws 24/7.
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