Pine Script v6
//@version=6
indicator("Confluence FVG Finder", overlay=true, max_boxes_count=500, max_labels_count=500)
// Original code by @ProjectSyndicate on TradingView.com
// Refactored, visually updated, and alert-optimized by LuxAlgo Quant Agent
//---------------------------------------------------------------------------------------------------------------------}
// Types
//---------------------------------------------------------------------------------------------------------------------{
type FvgZone
box b
label l
label infoLabel
float top
float bot
bool bullish
string session
int formTime
float strength
int confluenceCount
//---------------------------------------------------------------------------------------------------------------------}
// Inputs
//---------------------------------------------------------------------------------------------------------------------{
useNormalizedZones = input.bool(true, "Normalize All Zone Heights", group="Universal Zone Settings")
zoneHeightMethod = input.string("ATR Based", "Zone Height Method", group="Universal Zone Settings", options=["ATR Based","Fixed Percentage"])
zoneHeightAtrMult = input.float(0.75, "Zone Height (ATR Multiplier)", group="Universal Zone Settings", minval=0.1, maxval=3.0, step=0.05)
zoneHeightPercent = input.float(0.3, "Zone Height (% of Price)", group="Universal Zone Settings", minval=0.05, maxval=2.0, step=0.05)
showBullishFvg = input.bool(true, "Show Bullish FVG", group="FVG Detection")
showBearishFvg = input.bool(true, "Show Bearish FVG", group="FVG Detection")
maxZones = input.int(8, "Max Zones Per Side", minval=1, maxval=50, group="FVG Detection")
tf1 = input.timeframe("60", "Timeframe 1 (anchor / lowest)", group="Multi-Timeframe Confluence")
tf2 = input.timeframe("120", "Timeframe 2", group="Multi-Timeframe Confluence")
tf3 = input.timeframe("240", "Timeframe 3", group="Multi-Timeframe Confluence")
minConfluence = input.int(2, "Minimum Timeframe Confluence", group="Multi-Timeframe Confluence", minval=2, maxval=3)
proximityAtrMult = input.float(5.0, "Proximity Tolerance (x ATR)", group="Multi-Timeframe Confluence", minval=0.5, maxval=10.0, step=0.5)
enableStrengthRating = input.bool(true, "Enable Strength Rating", group="Strength Rating")
minStrengthFilter = input.float(3.0, "Minimum Strength Filter", group="Strength Rating", minval=0.0, maxval=10.0, step=0.5)
confluenceBonus = input.float(0.5, "Strength Bonus Per Extra Timeframe", group="Strength Rating", minval=0.0, maxval=2.0, step=0.1)
bullishFvgColor = input.color(color.new(#089981, 85), "Bullish FVG Color", group="FVG Styling")
bearishFvgColor = input.color(color.new(#f23645, 85), "Bearish FVG Color", group="FVG Styling")
bullishBorderColor = input.color(color.new(#089981, 30), "Bullish Border", group="FVG Styling")
bearishBorderColor = input.color(color.new(#f23645, 30), "Bearish Border", group="FVG Styling")
fvgBorderWidth = input.int(2, "Border Width", minval=1, maxval=5, group="FVG Styling")
showLabels = input.bool(true, "Show Direction Labels", group="FVG Styling")
showInfo = input.bool(true, "Show Extended Info Labels", group="Extended Info Labels")
infoTextSize = input.string("Large", "Info Text Size", group="Extended Info Labels", options=["Tiny","Small","Normal","Large","Huge"])
mitigationType = input.string("50% Fill", "Mitigation Type", group="Mitigation Settings", options=["Touch","Full Fill","50% Fill"])
showMitigatedFvg = input.bool(false, "Show Mitigated FVG", group="Mitigation Settings")
mitigatedFvgColor = input.color(color.new(color.gray, 90), "Mitigated FVG Color", group="Mitigation Settings")
minGapSize = input.float(0.0, "Minimum Gap Size (0 = No Filter)", group="Filter Settings", minval=0.0, step=0.1)
useAtrFilter = input.bool(true, "Use ATR Filter", group="Filter Settings")
atrMultiplier = input.float(0.5, "Minimum Gap Size (ATR Multiplier)", group="Filter Settings", minval=0.1, step=0.1)
atrLength = input.int(14, "ATR Length", group="Filter Settings", minval=1)
//---------------------------------------------------------------------------------------------------------------------}
// Constants & Variables
//---------------------------------------------------------------------------------------------------------------------{
var array bullZones = array.new()
var array bearZones = array.new()
var int lastBullTime = na
var int lastBearTime = na
var label warnLbl = na
//---------------------------------------------------------------------------------------------------------------------}
// Functions / Methods
//---------------------------------------------------------------------------------------------------------------------{
getTextSize() =>
switch infoTextSize
"Tiny" => size.tiny
"Small" => size.small
"Normal" => size.normal
"Large" => size.large
"Huge" => size.huge
=> size.large
calcPips(priceDiff) =>
isGold = str.contains(syminfo.ticker, "XAU") or str.contains(syminfo.ticker, "GOLD")
pipValue = isGold ? 0.01 : syminfo.mintick * 10
math.round(priceDiff / pipValue)
getSession(barTime) =>
t = hour(barTime, "GMT") * 60 + minute(barTime, "GMT")
string s = "Other"
if t >= 480 and t < 1020
s := "London"
else if t >= 780 and t < 1320
s := "NY"
else if t >= 0 and t < 540
s := "Asian"
s
sessionInt(s) =>
s == "London" ? 1 : s == "NY" ? 2 : s == "Asian" ? 3 : 0
sessionStr(i) =>
i == 1 ? "London" : i == 2 ? "NY" : i == 3 ? "Asian" : "Other"
calculateFvgStrength(gapSize, atrValue) =>
if not enableStrengthRating
5.0
else
gapScore = 0.0
if atrValue > 0
gapRatio = gapSize / atrValue
if gapRatio >= 1.5
gapScore := 8.0
else if gapRatio >= 1.0
gapScore := 6.0
else if gapRatio >= 0.75
gapScore := 4.5
else if gapRatio >= 0.5
gapScore := 3.0
else
gapScore := 1.5
else
gapScore := 3.0
math.min(math.max(gapScore, 0.0), 10.0)
normalizeZone(originalTop, originalBottom, atrVal, priceVal) =>
if useNormalizedZones
targetHeight = zoneHeightMethod == "ATR Based" ? atrVal * zoneHeightAtrMult : priceVal * (zoneHeightPercent / 100.0)
originalMid = (originalTop + originalBottom) / 2.0
[originalMid + targetHeight / 2.0, originalMid - targetHeight / 2.0]
else
[originalTop, originalBottom]
detectTfBull() =>
var float aTop = na
var float aBot = na
var float aStr = 0.0
var int aSes = 0
var int aTime = na
atrv = ta.atr(atrLength)[1]
if low[1] > high[3]
gtop = low[1]
gbot = high[3]
gap = gtop - gbot
minGap = useAtrFilter ? atrv * atrMultiplier : minGapSize
if gap >= minGap
aTop := gtop
aBot := gbot
aStr := calculateFvgStrength(gap, atrv)
aSes := sessionInt(getSession(time[1]))
aTime := time[1]
if not na(aBot) and close[1] < aBot
aTop := na
aBot := na
aStr := 0.0
aSes := 0
aTime := na
[aTop, aBot, aStr, aSes, aTime]
detectTfBear() =>
var float aTop = na
var float aBot = na
var float aStr = 0.0
var int aSes = 0
var int aTime = na
atrv = ta.atr(atrLength)[1]
if high[1] < low[3]
gtop = low[3]
gbot = high[1]
gap = gtop - gbot
minGap = useAtrFilter ? atrv * atrMultiplier : minGapSize
if gap >= minGap
aTop := gtop
aBot := gbot
aStr := calculateFvgStrength(gap, atrv)
aSes := sessionInt(getSession(time[1]))
aTime := time[1]
if not na(aTop) and close[1] > aTop
aTop := na
aBot := na
aStr := 0.0
aSes := 0
aTime := na
[aTop, aBot, aStr, aSes, aTime]
zonesConfluent(topA, botA, topB, botB, atrVal) =>
valid = not na(topA) and not na(botA) and not na(topB) and not na(botB)
result = false
if valid
midA = (topA + botA) / 2.0
midB = (topB + botB) / 2.0
tolerance = atrVal * proximityAtrMult
result := math.abs(midA - midB) <= tolerance
result
isFvgMitigated(z, refHigh, refLow) =>
mid = (z.top + z.bot) / 2.0
z.bullish ?
(mitigationType == "Touch" ? refLow <= z.top : mitigationType == "Full Fill" ? refLow <= z.bot : refLow <= mid) :
(mitigationType == "Touch" ? refHigh >= z.bot : mitigationType == "Full Fill" ? refHigh >= z.top : refHigh >= mid)
//---------------------------------------------------------------------------------------------------------------------}
// Calculations
//---------------------------------------------------------------------------------------------------------------------{
[bt1, bb1, bs1, be1, bx1] = request.security(syminfo.tickerid, tf1, detectTfBull(), lookahead=barmerge.lookahead_off)
[bt2, bb2, bs2, be2, bx2] = request.security(syminfo.tickerid, tf2, detectTfBull(), lookahead=barmerge.lookahead_off)
[bt3, bb3, bs3, be3, bx3] = request.security(syminfo.tickerid, tf3, detectTfBull(), lookahead=barmerge.lookahead_off)
[rt1, rb1, rs1, re1, rx1] = request.security(syminfo.tickerid, tf1, detectTfBear(), lookahead=barmerge.lookahead_off)
[rt2, rb2, rs2, re2, rx2] = request.security(syminfo.tickerid, tf2, detectTfBear(), lookahead=barmerge.lookahead_off)
[rt3, rb3, rs3, re3, rx3] = request.security(syminfo.tickerid, tf3, detectTfBear(), lookahead=barmerge.lookahead_off)
[refAtr, refClose, refHigh, refLow] = request.security(syminfo.tickerid, tf1, [ta.atr(atrLength)[1], close[1], high[1], low[1]], lookahead=barmerge.lookahead_off)
tf1Secs = timeframe.in_seconds(tf1)
bool bullNewZone = false
bool bearNewZone = false
bool bullMitigated = false
bool bearMitigated = false
cleanZones(zones, refH, refL) =>
mitigatedFound = false
if array.size(zones) > 0
for i = array.size(zones) - 1 to 0
if i < array.size(zones)
z = array.get(zones, i)
if isFvgMitigated(z, refH, refL)
mitigatedFound := true
if showMitigatedFvg
box.set_bgcolor(z.b, mitigatedFvgColor)
box.set_extend(z.b, extend.none)
else
box.delete(z.b)
array.remove(zones, i)
if not na(z.l)
label.delete(z.l)
if not na(z.infoLabel)
label.delete(z.infoLabel)
mitigatedFound
limitSize(zones, maxSize) =>
while array.size(zones) > maxSize
old = array.shift(zones)
box.delete(old.b)
if not na(old.l)
label.delete(old.l)
if not na(old.infoLabel)
label.delete(old.infoLabel)
updateBoxes(zones) =>
if array.size(zones) > 0
for i = 0 to array.size(zones) - 1
z = array.get(zones, i)
if showInfo and not na(z.infoLabel)
ageBars = na(z.formTime) ? 0 : int((time - z.formTime) / (tf1Secs * 1000))
pips = calcPips(z.top - z.bot)
distancePips = calcPips(math.abs(close - (z.top + z.bot) / 2.0))
zoneType = z.bullish ? "Merged Bullish FVG" : "Merged Bearish FVG"
infoText = zoneType + " ||| " + str.tostring(z.confluenceCount) + " timeframes ||| Strength: " + str.tostring(math.round(z.strength * 10) / 10) + "/10 ||| " + z.session + " ||| Age: " + str.tostring(ageBars) + " bars ||| " + str.tostring(pips) + " pips ||| " + str.tostring(distancePips) + " pips away"
label.set_text(z.infoLabel, infoText)
label.set_xy(z.infoLabel, time, (z.top + z.bot) / 2.0)
label.set_style(z.infoLabel, label.style_label_left)
label.set_textalign(z.infoLabel, text.align_left)
if barstate.isconfirmed
if showBullishFvg
float mTop = na
float mBot = na
float mStr = 0.0
int mSes = 0
int mCnt = 0
int mTime = na
if not na(bt1)
mTop := bt1
mBot := bb1
mStr := bs1
mSes := be1
mCnt := 1
mTime := bx1
if not na(bt2)
if na(mTop)
mTop := bt2
mBot := bb2
mStr := bs2
mSes := be2
mCnt := 1
mTime := bx2
else if zonesConfluent(mTop, mBot, bt2, bb2, refAtr)
mTop := math.max(mTop, bt2)
mBot := math.min(mBot, bb2)
mStr := math.max(mStr, bs2)
mCnt += 1
mTime := math.max(mTime, bx2)
if not na(bt3)
if na(mTop)
mTop := bt3
mBot := bb3
mStr := bs3
mSes := be3
mCnt := 1
mTime := bx3
else if zonesConfluent(mTop, mBot, bt3, bb3, refAtr)
mTop := math.max(mTop, bt3)
mBot := math.min(mBot, bb3)
mStr := math.max(mStr, bs3)
mCnt += 1
mTime := math.max(mTime, bx3)
mergedStrength = math.min(mStr + math.max(mCnt - 1, 0) * confluenceBonus, 10.0)
if mCnt >= minConfluence and not na(mTop) and mergedStrength >= minStrengthFilter and (na(lastBullTime) or mTime > lastBullTime)
bool alreadyExists = false
if array.size(bullZones) > 0
for k = 0 to array.size(bullZones) - 1
existing = array.get(bullZones, k)
if math.abs((existing.top + existing.bot) / 2.0 - (mTop + mBot) / 2.0) < refAtr * 0.5
alreadyExists := true
if not alreadyExists
fvgSession = sessionStr(mSes)
[normTop, normBot] = normalizeZone(mTop, mBot, refAtr, refClose)
rightT = math.max(mTime, time)
b = box.new(mTime, normTop, rightT, normBot, xloc=xloc.bar_time, border_color=bullishBorderColor, bgcolor=bullishFvgColor, border_width=fvgBorderWidth, extend=extend.right)
label l = na
if showLabels
l := label.new(mTime, normBot, "Bull FVG", xloc=xloc.bar_time, color=color.new(#089981, 20), textcolor=chart.fg_color, style=label.style_label_up, size=size.small)
label infoL = na
if showInfo
pips = calcPips(normTop - normBot)
distancePips = calcPips(math.abs(close - (normTop + normBot) / 2.0))
infoText = "Merged Bullish FVG ||| " + str.tostring(mCnt) + " timeframes ||| Strength: " + str.tostring(math.round(mergedStrength * 10) / 10) + "/10 ||| " + fvgSession + " ||| Age: 0 bars ||| " + str.tostring(pips) + " pips ||| " + str.tostring(distancePips) + " pips away"
infoL := label.new(time, (normTop + normBot) / 2.0, infoText, xloc=xloc.bar_time, color=color.new(#089981, 100), textcolor=color.new(#089981, 0), style=label.style_label_left, size=getTextSize(), textalign=text.align_left)
array.push(bullZones, FvgZone.new(b=b, l=l, infoLabel=infoL, top=normTop, bot=normBot, bullish=true, session=fvgSession, formTime=mTime, strength=mergedStrength, confluenceCount=mCnt))
lastBullTime := mTime
bullNewZone := true
if showBearishFvg
float mTop = na
float mBot = na
float mStr = 0.0
int mSes = 0
int mCnt = 0
int mTime = na
if not na(rt1)
mTop := rt1
mBot := rb1
mStr := rs1
mSes := re1
mCnt := 1
mTime := rx1
if not na(rt2)
if na(mTop)
mTop := rt2
mBot := rb2
mStr := rs2
mSes := re2
mCnt := 1
mTime := rx2
else if zonesConfluent(mTop, mBot, rt2, rb2, refAtr)
mTop := math.max(mTop, rt2)
mBot := math.min(mBot, rb2)
mStr := math.max(mStr, rs2)
mCnt += 1
mTime := math.max(mTime, rx2)
if not na(rt3)
if na(mTop)
mTop := rt3
mBot := rb3
mStr := rs3
mSes := re3
mCnt := 1
mTime := rx3
else if zonesConfluent(mTop, mBot, rt3, rb3, refAtr)
mTop := math.max(mTop, rt3)
mBot := math.min(mBot, rb3)
mStr := math.max(mStr, rs3)
mCnt += 1
mTime := math.max(mTime, rx3)
mergedStrength = math.min(mStr + math.max(mCnt - 1, 0) * confluenceBonus, 10.0)
if mCnt >= minConfluence and not na(mTop) and mergedStrength >= minStrengthFilter and (na(lastBearTime) or mTime > lastBearTime)
bool alreadyExists = false
if array.size(bearZones) > 0
for k = 0 to array.size(bearZones) - 1
existing = array.get(bearZones, k)
if math.abs((existing.top + existing.bot) / 2.0 - (mTop + mBot) / 2.0) < refAtr * 0.5
alreadyExists := true
if not alreadyExists
fvgSession = sessionStr(mSes)
[normTop, normBot] = normalizeZone(mTop, mBot, refAtr, refClose)
rightT = math.max(mTime, time)
b = box.new(mTime, normTop, rightT, normBot, xloc=xloc.bar_time, border_color=bearishBorderColor, bgcolor=bearishFvgColor, border_width=fvgBorderWidth, extend=extend.right)
label l = na
if showLabels
l := label.new(mTime, normTop, "Bear FVG", xloc=xloc.bar_time, color=color.new(#f23645, 20), textcolor=chart.fg_color, style=label.style_label_down, size=size.small)
label infoL = na
if showInfo
pips = calcPips(normTop - normBot)
distancePips = calcPips(math.abs(close - (normTop + normBot) / 2.0))
infoText = "Merged Bearish FVG ||| " + str.tostring(mCnt) + " timeframes ||| Strength: " + str.tostring(math.round(mergedStrength * 10) / 10) + "/10 ||| " + fvgSession + " ||| Age: 0 bars ||| " + str.tostring(pips) + " pips ||| " + str.tostring(distancePips) + " pips away"
infoL := label.new(time, (normTop + normBot) / 2.0, infoText, xloc=xloc.bar_time, color=color.new(#f23645, 100), textcolor=color.new(#f23645, 0), style=label.style_label_left, size=getTextSize(), textalign=text.align_left)
array.push(bearZones, FvgZone.new(b=b, l=l, infoLabel=infoL, top=normTop, bot=normBot, bullish=false, session=fvgSession, formTime=mTime, strength=mergedStrength, confluenceCount=mCnt))
lastBearTime := mTime
bearNewZone := true
bullMitigated := cleanZones(bullZones, refHigh, refLow)
bearMitigated := cleanZones(bearZones, refHigh, refLow)
limitSize(bullZones, maxZones)
limitSize(bearZones, maxZones)
updateBoxes(bullZones)
updateBoxes(bearZones)
minInSecs = math.min(timeframe.in_seconds(tf1), math.min(timeframe.in_seconds(tf2), timeframe.in_seconds(tf3)))
if barstate.islast
if timeframe.in_seconds() > minInSecs
if na(warnLbl)
warnLbl := label.new(bar_index, high, "Chart TF is higher than an input TF — lower the chart to <= TF1 for exact zones", color=color.new(color.orange, 10), textcolor=chart.fg_color, style=label.style_label_down, size=size.normal)
label.set_xy(warnLbl, bar_index, high)
else if not na(warnLbl)
label.delete(warnLbl)
warnLbl := na
//---------------------------------------------------------------------------------------------------------------------}
// Alerts
//---------------------------------------------------------------------------------------------------------------------{
alertcondition(bullNewZone, title="Confluence Bullish FVG", message="New multi-timeframe confluence Bullish FVG detected")
alertcondition(bearNewZone, title="Confluence Bearish FVG", message="New multi-timeframe confluence Bearish FVG detected")
alertcondition(bullMitigated, title="Bullish FVG Mitigated", message="A Bullish FVG was mitigated")
alertcondition(bearMitigated, title="Bearish FVG Mitigated", message="A Bearish FVG was mitigated")
//---------------------------------------------------------------------------------------------------------------------}
