Skip to content

Premium collection — free download

M30 FVG Setup

Higher-timeframe fair value gap zones with lower-timeframe structure-break confirmation, stop and 1:3 target for each zone.

PremiumSmart moneyv1.0TradingView (Pine Script v6) Non-repainting signals Alerts

What it does

M30 FVG Setup draws fair value gaps from a higher timeframe (30 minutes by default) and waits for price to return to them on a lower timeframe. When a lower-timeframe candle confirms with a break of the micro swing formed at the tap, it marks the entry, stop and target.

How it works

  1. Each closed M30 candle is checked for a three-candle fair value gap, drawn as a zone.
  2. When a chart candle trades back into a live zone, the zone is armed.
  3. A close through the micro swing that formed at the extreme of the tap confirms the entry (a lower-timeframe change of character).
  4. The stop goes beyond the tap extreme or the far edge of the gap plus a buffer; the target is the risk multiplied by the risk:reward setting. Each zone produces at most one signal.

How to use it

Run it on M1 or M5 with M30 gaps (for example gold on M1). Favour zones that align with the higher-timeframe trend and skip zones that have already been traded through. A matching MT5 Expert Advisor uses the same rules.

Settings

InputDefaultWhat it changes
FVG timeframe30 minutesWhere gaps are found
Min gap size0.10 × ATRFilters out small gaps
Zone lifetime96 FVG-timeframe barsHow long a zone stays live
Micro swing length3 barsSensitivity of the confirmation break
Risk:reward3Distance of the target

Limitations

In strong trends price can slice through gaps without reacting, and on very low timeframes spread becomes a large share of the stop. The 1:3 target is fixed and ignores nearby liquidity.

Pine Script source

In TradingView open Pine Editor, paste the code, click Save and then Add to chart.

M30_FVG_Setup.pine

176 lines

//@version=6
// M30 FVG Setup — HTF fair value gap zones + LTF structure-break entries, 1:3 RR.
// Use on M1 (or any timeframe below the FVG timeframe), e.g. XAUUSD M1 with M30 FVGs.
// 1. Each closed M30 candle is checked for a 3-candle FVG and drawn as a zone.
// 2. A chart candle trading back into a live zone arms it.
// 3. A close through the micro swing formed at the tap extreme confirms the entry.
// 4. SL beyond the tap extreme / far edge of the FVG + buffer, TP = RR x risk. One signal per zone.
indicator("M30 FVG Setup (HTF FVG + LTF Confirmation)", "M30 FVG Setup", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500)

grpZ = "Fair value gaps"
htfTf      = input.timeframe("30", "FVG timeframe", group = grpZ)
minGapAtr  = input.float(0.10, "Min gap size (x ATR14 of FVG TF, 0 = off)", minval = 0, step = 0.05, group = grpZ)
expiryBars = input.int(96, "Zone lifetime (FVG-TF bars)", minval = 1, group = grpZ)
maxZones   = input.int(20, "Max live zones", minval = 1, group = grpZ)

grpC = "LTF confirmation"
swingLen   = input.int(3, "Micro swing length (bars)", minval = 1, group = grpC)
armTimeout = input.int(60, "Bars allowed between tap and confirmation", minval = 1, group = grpC)

grpR = "Risk"
rr         = input.float(3.0, "Reward : risk", minval = 0.1, step = 0.5, group = grpR)
slBufTicks = input.int(50, "SL buffer (ticks)", minval = 0, group = grpR)
showLevels = input.bool(true, "Draw entry / SL / TP", group = grpR)
levelBars  = input.int(40, "Level length (bars)", minval = 5, group = grpR)

grpV = "Style"
bullCol    = input.color(color.new(color.teal, 80), "Bullish zone", group = grpV)
bearCol    = input.color(color.new(color.red, 80), "Bearish zone", group = grpV)
deadCol    = input.color(color.new(color.gray, 90), "Used / invalid zone", group = grpV)
showStats  = input.bool(true, "Show stats panel", group = grpV)

if timeframe.in_seconds() >= timeframe.in_seconds(htfTf)
    runtime.error("Chart timeframe must be lower than the FVG timeframe (" + htfTf + ")")

// Closed-candle FVG on the HTF: candle 3 = [1], candle 1 = [3]. lookahead_on + [1] offsets does not repaint.
[bullFvg, bullTop, bullBot, bearFvg, bearTop, bearBot, htfAtr] = request.security(syminfo.tickerid, htfTf,
     [low[1] > high[3], low[1], high[3], high[1] < low[3], low[3], high[1], ta.atr(14)[1]], lookahead = barmerge.lookahead_on)

newHtf = timeframe.change(htfTf)
htfMs  = timeframe.in_seconds(htfTf) * 1000

type Zone
    bool  bull
    float top
    float bot
    int   born
    int   expires
    box   bx
    bool  armed = false
    int   armedBar = 0
    float ext = na
    float ref = na

type Trade
    bool  bull
    float entry
    float sl
    float tp

var zones  = array.new<Zone>()
var trades = array.new<Trade>()
var int wins = 0
var int losses = 0

addZone(bool bull, float top, float bot) =>
    if minGapAtr == 0 or top - bot >= htfAtr * minGapAtr
        bx = box.new(time - 3 * htfMs, top, time + expiryBars * htfMs, bot, xloc = xloc.bar_time,
             border_color = na, bgcolor = bull ? bullCol : bearCol)
        zones.push(Zone.new(bull, top, bot, time, time + expiryBars * htfMs, bx))
        if zones.size() > maxZones
            old = zones.shift()
            old.bx.set_bgcolor(deadCol)

if newHtf
    if bullFvg
        addZone(true, bullTop, bullBot)
    if bearFvg
        addZone(false, bearTop, bearBot)

swingHigh = ta.highest(high, swingLen)
swingLow  = ta.lowest(low, swingLen)
buf       = slBufTicks * syminfo.mintick

drawTrade(bool bull, float entry, float sl, float tp) =>
    if showLevels
        line.new(bar_index, entry, bar_index + levelBars, entry, color = color.gray, width = 1)
        line.new(bar_index, sl, bar_index + levelBars, sl, color = color.red, style = line.style_dashed)
        line.new(bar_index, tp, bar_index + levelBars, tp, color = color.green, style = line.style_dashed)
        box.new(bar_index, entry, bar_index + levelBars, sl, border_color = na, bgcolor = color.new(color.red, 88))
        box.new(bar_index, entry, bar_index + levelBars, tp, border_color = na, bgcolor = color.new(color.green, 88))
    label.new(bar_index, bull ? low : high, (bull ? "BUY" : "SELL") + "\nSL " + str.tostring(sl, format.mintick) + "\nTP " + str.tostring(tp, format.mintick),
         style = bull ? label.style_label_up : label.style_label_down, color = bull ? color.teal : color.red, textcolor = color.white, size = size.small)

buySig  = false
sellSig = false
sigSl   = float(na)
sigTp   = float(na)

// Resolve open signals (SL checked first on a bar that hits both: conservative).
if barstate.isconfirmed and trades.size() > 0
    for i = trades.size() - 1 to 0
        t = trades.get(i)
        hitSl = t.bull ? low <= t.sl : high >= t.sl
        hitTp = t.bull ? high >= t.tp : low <= t.tp
        if hitSl or hitTp
            if hitSl
                losses += 1
            else
                wins += 1
            trades.remove(i)

if barstate.isconfirmed and zones.size() > 0
    for i = zones.size() - 1 to 0
        z = zones.get(i)
        if time < z.born
            continue
        invalid = z.bull ? close < z.bot : close > z.top
        if time >= z.expires or invalid
            z.bx.set_right(time)
            z.bx.set_bgcolor(deadCol)
            zones.remove(i)
            continue
        if not z.armed
            if z.bull ? low <= z.top : high >= z.bot
                z.armed    := true
                z.armedBar := bar_index
                z.ext      := z.bull ? low : high
                z.ref      := z.bull ? swingHigh : swingLow
            continue
        if z.bull ? low < z.ext : high > z.ext
            z.ext      := z.bull ? low : high
            z.ref      := z.bull ? swingHigh : swingLow
            z.armedBar := bar_index
            continue
        if z.bull ? close > z.ref : close < z.ref
            entry = close
            sl    = z.bull ? math.min(z.ext, z.bot) - buf : math.max(z.ext, z.top) + buf
            risk  = math.abs(entry - sl)
            tp    = z.bull ? entry + rr * risk : entry - rr * risk
            if risk > 0
                drawTrade(z.bull, entry, sl, tp)
                trades.push(Trade.new(z.bull, entry, sl, tp))
                if z.bull
                    buySig := true
                else
                    sellSig := true
                sigSl := sl
                sigTp := tp
                alert(syminfo.ticker + " M30 FVG " + (z.bull ? "BUY" : "SELL") + " @ " + str.tostring(entry, format.mintick) +
                     " SL " + str.tostring(sl, format.mintick) + " TP " + str.tostring(tp, format.mintick), alert.freq_once_per_bar_close)
            z.bx.set_right(time)
            z.bx.set_bgcolor(deadCol)
            zones.remove(i)
            continue
        if bar_index - z.armedBar >= armTimeout
            z.armed := false

alertcondition(buySig, "M30 FVG Buy", "{{ticker}} M30 FVG BUY @ {{close}}")
alertcondition(sellSig, "M30 FVG Sell", "{{ticker}} M30 FVG SELL @ {{close}}")

plot(sigSl, "Signal SL", color = na, display = display.data_window)
plot(sigTp, "Signal TP", color = na, display = display.data_window)

var table stats = table.new(position.top_right, 2, 4, bgcolor = color.new(color.black, 20), border_width = 1)
if showStats and barstate.islast
    closed = wins + losses
    netR   = wins * rr - losses
    stats.cell(0, 0, "M30 FVG 1:" + str.tostring(rr), text_color = color.white, text_size = size.small)
    stats.cell(1, 0, str.tostring(closed) + " closed", text_color = color.white, text_size = size.small)
    stats.cell(0, 1, "Win rate", text_color = color.white, text_size = size.small)
    stats.cell(1, 1, closed > 0 ? str.tostring(100.0 * wins / closed, "#.0") + "%" : "-", text_color = color.white, text_size = size.small)
    stats.cell(0, 2, "W / L", text_color = color.white, text_size = size.small)
    stats.cell(1, 2, str.tostring(wins) + " / " + str.tostring(losses), text_color = color.white, text_size = size.small)
    stats.cell(0, 3, "Net R", text_color = color.white, text_size = size.small)
    stats.cell(1, 3, str.tostring(netR, "#.0"), text_color = netR >= 0 ? color.lime : color.red, text_size = size.small)

Key inputs

Key inputs of M30 FVG Setup
InputDefaultDescription
FVG timeframe30 minutesTimeframe scanned for three-candle fair value gaps.
Min gap size0.10 × ATR(14)Ignores tiny gaps (0 = off).
Risk:reward3Target as a multiple of the stop distance.

Educational tools, not advice. Indicators and Expert Advisors describe price; they do not predict it, and past behaviour does not guarantee future results. Test everything on a demo account first and never risk money you cannot afford to lose. See our disclaimer.

Keep exploring

Related tradingview indicators