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
- Each closed M30 candle is checked for a three-candle fair value gap, drawn as a zone.
- When a chart candle trades back into a live zone, the zone is armed.
- A close through the micro swing that formed at the extreme of the tap confirms the entry (a lower-timeframe change of character).
- 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
| Input | Default | What it changes |
|---|---|---|
| FVG timeframe | 30 minutes | Where gaps are found |
| Min gap size | 0.10 × ATR | Filters out small gaps |
| Zone lifetime | 96 FVG-timeframe bars | How long a zone stays live |
| Micro swing length | 3 bars | Sensitivity of the confirmation break |
| Risk:reward | 3 | Distance 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)

