Confluence FVG Finder

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")

//---------------------------------------------------------------------------------------------------------------------}


Leave a Comment

Your email address will not be published. Required fields are marked *