Code: //@version=5
indicator("Supply and Demand Zone Detector with Follow-through", overlay=true, max_lines_count=500, max_boxes_count=500)

// ‣ Input Parameters
avgBodyPeriod = input.int(title="Average Body Period", defval=10, minval=1)
bigCandleFactor = input.float(title="Big Candle Factor (Multiplier of Avg Body Size)", defval=1.5, minval=0.1)
maxBaseCandles = input.int(title="Maximum Base Candles", defval=6, minval=1)
maxBaseBodyPercent = input.float(title="Max Base Candle Body Size (% of Big Candle)", defval=50.0, minval=0.1, maxval=100.0)
drawHistoricalZones = input.bool(title="Draw Historical Zones", defval=true)
maxHistoricalZonesToDisplay = input.int(title="Number of Historical Zones to Display", defval=5, minval=1)
maxActiveSupplyZonesToDisplay = input.int(title="Number of Active Supply Zones to Display", defval=5, minval=1)
maxActiveDemandZonesToDisplay = input.int(title="Number of Active Demand Zones to Display", defval=5, minval=1)
// New input parameters for follow-through
minFollowThroughCandles = input.int(title="Minimum Follow-through Candles", defval=2, minval=1)
minFollowThroughPerc = input.float(title="Minimum Follow-through Movement (%)", defval=0.5, minval=0.1)
// ** New input parameter to show/hide markers **
showMarkers = input.bool(title="Show Markers", defval=true)

// ‣ Calculations
avgBodySize = ta.sma(math.abs(close - open), avgBodyPeriod)
bigCandleBodyThreshold = bigCandleFactor * avgBodySize
maxBaseCandleBodySize = (maxBaseBodyPercent / 100.0) * bigCandleBodyThreshold

bodySize = math.abs(close - open)
isBigGreenCandle = (close > open) and (bodySize >= bigCandleBodyThreshold)
isBigRedCandle = (close < open) and (bodySize >= bigCandleBodyThreshold)
isSmallBodyCandle = bodySize <= maxBaseCandleBodySize
isGreenCandle = close > open
isRedCandle = close < open

// ‣ Pattern Detection States
// Demand Zone Patterns
var int rbr_state = 0
var int rbr_baseCount = 0
var float rbr_highestBody = na
var float rbr_lowestLow = na
var int rbr_startIndex = na
var int rbr_legOutIndex = na  // Index of the potential Leg-out candle

var int dbr_state = 0
var int dbr_baseCount = 0
var float dbr_highestBody = na
var float dbr_lowestLow = na
var int dbr_startIndex = na
var int dbr_legOutIndex = na

// Supply Zone Patterns
var int dbd_state = 0
var int dbd_baseCount = 0
var float dbd_lowestBody = na
var float dbd_highestHigh = na
var int dbd_startIndex = na
var int dbd_legOutIndex = na

var int rbd_state = 0
var int rbd_baseCount = 0
var float rbd_lowestBody = na
var float rbd_highestHigh = na
var int rbd_startIndex = na
var int rbd_legOutIndex = na

// ‣ Arrays to Store Zones
// Each zone has:
//  - box object
//  - distal line (for violation checking)
//  - zone type (1 for RBR, 2 for DBR, 3 for DBD, 4 for RBD)
//  - isViolated flag (true if violated)
var box[] zones = array.new<box>()
var float[] distalLines = array.new<float>()
var int[] zoneTypes = array.new<int>()
var bool[] isViolated = array.new<bool>()

// Function to check for good follow-through
f_hasGoodFollowThrough(startIdx, direction) =>
    cumulativeMove = 0.0
    followThrough = false
    for i = 1 to minFollowThroughCandles
        idx = startIdx + i
        if idx <= bar_index
            priceMovePerc = ((close[idx] - close[startIdx]) / close[startIdx]) * 100.0
            if direction == "up" and priceMovePerc >= minFollowThroughPerc
                followThrough := true
                break
            else if direction == "down" and priceMovePerc <= -minFollowThroughPerc
                followThrough := true
                break
    followThrough

// ‣ RBR Pattern Detection (Demand Zone)
if (rbr_state == 0)
    if (isBigGreenCandle)
        // RBR Leg-in detected
        rbr_state := 1
        rbr_baseCount := 0
        rbr_startIndex := bar_index
        rbr_highestBody := na
        rbr_lowestLow := na
else if (rbr_state == 1)
    if (isSmallBodyCandle)
        // RBR Base candle detected
        rbr_baseCount += 1
        if (rbr_baseCount > maxBaseCandles)
            // Exceeded maximum base candles, reset pattern
            rbr_state := 0
            rbr_baseCount := 0
        else
            // Update highest body (proximal line)
            candleHighBody = math.max(open, close)
            rbr_highestBody := na(rbr_highestBody) ? candleHighBody : math.max(rbr_highestBody, candleHighBody)
            // Update lowest low (distal line)
            rbr_lowestLow := na(rbr_lowestLow) ? low : math.min(rbr_lowestLow, low)
    else if (isGreenCandle)
        // Potential Leg-out detected
        rbr_legOutIndex := bar_index
        // Check if Leg-out candle is big or small
        if (not isBigGreenCandle)
            // Leg-out candle is small, check for good follow-through
            hasFollowThrough = f_hasGoodFollowThrough(rbr_legOutIndex, "up")
            if hasFollowThrough
                // Complete RBR pattern
                if (rbr_baseCount >= 1)
                    // Include Leg-out candle's low in distal line
                    rbr_lowestLow := math.min(rbr_lowestLow, low)
                    // Create Demand Zone
                    newZone = box.new(left=rbr_startIndex, right=bar_index, top=rbr_highestBody, bottom=rbr_lowestLow,
                                     border_color=color.green, bgcolor=color.new(color.green, 85))
                    box.set_extend(newZone, extend.right)
                    // Initialize isViolated flag as false
                    violationFlag = false
                    // Store zone details
                    array.push(zones, newZone)
                    array.push(distalLines, rbr_lowestLow)
                    array.push(zoneTypes, 1)  // 1 for RBR
                    array.push(isViolated, violationFlag)  // initially not violated
                // Reset RBR pattern
                rbr_state := 0
                rbr_baseCount := 0
            else
                // Not enough follow-through, wait
                na
        else
            // Leg-out candle is big, proceed as usual
            if (rbr_baseCount >= 1)
                // Include Leg-out candle's low in distal line
                rbr_lowestLow := math.min(rbr_lowestLow, low)
                // Create Demand Zone
                newZone = box.new(left=rbr_startIndex, right=bar_index, top=rbr_highestBody, bottom=rbr_lowestLow,
                                 border_color=color.green, bgcolor=color.new(color.green, 85))
                box.set_extend(newZone, extend.right)
                // Initialize isViolated flag as false
                violationFlag = false
                // Store zone details
                array.push(zones, newZone)
                array.push(distalLines, rbr_lowestLow)
                array.push(zoneTypes, 1)  // 1 for RBR
                array.push(isViolated, violationFlag)  // initially not violated
            // Reset RBR pattern
            rbr_state := 0
            rbr_baseCount := 0
    else
        // Pattern invalidated, reset RBR state
        rbr_state := 0
        rbr_baseCount := 0

// ‣ DBR Pattern Detection (Demand Zone)
if (dbr_state == 0)
    if (isBigRedCandle)
        // DBR Leg-in detected
        dbr_state := 1
        dbr_baseCount := 0
        dbr_startIndex := bar_index
        dbr_highestBody := na
        dbr_lowestLow := na
else if (dbr_state == 1)
    if (isSmallBodyCandle)
        // DBR Base candle detected
        dbr_baseCount += 1
        if (dbr_baseCount > maxBaseCandles)
            // Exceeded maximum base candles, reset pattern
            dbr_state := 0
            dbr_baseCount := 0
        else
            // Update highest body (proximal line)
            candleHighBody = math.max(open, close)
            dbr_highestBody := na(dbr_highestBody) ? candleHighBody : math.max(dbr_highestBody, candleHighBody)
            // Update lowest low (distal line)
            dbr_lowestLow := na(dbr_lowestLow) ? low : math.min(dbr_lowestLow, low)
    else if (isGreenCandle)
        // Potential Leg-out detected
        dbr_legOutIndex := bar_index
        // Check if Leg-out candle is big or small
        if (not isBigGreenCandle)
            // Leg-out candle is small, check for good follow-through
            hasFollowThrough = f_hasGoodFollowThrough(dbr_legOutIndex, "up")
            if hasFollowThrough
                // Complete DBR pattern
                if (dbr_baseCount >= 1)
                    // Include Leg-out candle's low in distal line
                    dbr_lowestLow := math.min(dbr_lowestLow, low)
                    // Create Demand Zone
                    newZone = box.new(left=dbr_startIndex, right=bar_index, top=dbr_highestBody, bottom=dbr_lowestLow,
                                     border_color=color.blue, bgcolor=color.new(color.blue, 85))
                    box.set_extend(newZone, extend.right)
                    // Initialize isViolated flag as false
                    violationFlag = false
                    // Store zone details
                    array.push(zones, newZone)
                    array.push(distalLines, dbr_lowestLow)
                    array.push(zoneTypes, 2)  // 2 for DBR
                    array.push(isViolated, violationFlag)  // initially not violated
                // Reset DBR pattern
                dbr_state := 0
                dbr_baseCount := 0
            else
                // Not enough follow-through, wait
                na
        else
            // Leg-out candle is big, proceed as usual
            if (dbr_baseCount >= 1)
                // Include Leg-out candle's low in distal line
                dbr_lowestLow := math.min(dbr_lowestLow, low)
                // Create Demand Zone
                newZone = box.new(left=dbr_startIndex, right=bar_index, top=dbr_highestBody, bottom=dbr_lowestLow,
                                 border_color=color.blue, bgcolor=color.new(color.blue, 85))
                box.set_extend(newZone, extend.right)
                // Initialize isViolated flag as false
                violationFlag = false
                // Store zone details
                array.push(zones, newZone)
                array.push(distalLines, dbr_lowestLow)
                array.push(zoneTypes, 2)  // 2 for DBR
                array.push(isViolated, violationFlag)  // initially not violated
            // Reset DBR pattern
            dbr_state := 0
            dbr_baseCount := 0
    else
        // Pattern invalidated, reset DBR state
        dbr_state := 0
        dbr_baseCount := 0

// ‣ DBD Pattern Detection (Supply Zone)
if (dbd_state == 0)
    if (isBigRedCandle)
        // DBD Leg-in detected
        dbd_state := 1
        dbd_baseCount := 0
        dbd_startIndex := bar_index
        dbd_lowestBody := na
        dbd_highestHigh := na
else if (dbd_state == 1)
    if (isSmallBodyCandle)
        // DBD Base candle detected
        dbd_baseCount += 1
        if (dbd_baseCount > maxBaseCandles)
            // Exceeded maximum base candles, reset pattern
            dbd_state := 0
            dbd_baseCount := 0
        else
            // Update lowest body (proximal line)
            candleLowBody = math.min(open, close)
            dbd_lowestBody := na(dbd_lowestBody) ? candleLowBody : math.min(dbd_lowestBody, candleLowBody)
            // Update highest high (distal line)
            dbd_highestHigh := na(dbd_highestHigh) ? high : math.max(dbd_highestHigh, high)
    else if (isRedCandle)
        // Potential Leg-out detected
        dbd_legOutIndex := bar_index
        // Check if Leg-out candle is big or small
        if (not isBigRedCandle)
            // Leg-out candle is small, check for good follow-through
            hasFollowThrough = f_hasGoodFollowThrough(dbd_legOutIndex, "down")
            if hasFollowThrough
                // Complete DBD pattern
                if (dbd_baseCount >= 1)
                    // Include Leg-out candle's high in distal line
                    dbd_highestHigh := math.max(dbd_highestHigh, high)
                    // Create Supply Zone
                    newZone = box.new(left=dbd_startIndex, right=bar_index, top=dbd_highestHigh, bottom=dbd_lowestBody,
                                     border_color=color.red, bgcolor=color.new(color.red, 85))
                    box.set_extend(newZone, extend.right)
                    // Initialize isViolated flag as false
                    violationFlag = false
                    // Store zone details
                    array.push(zones, newZone)
                    array.push(distalLines, dbd_highestHigh)
                    array.push(zoneTypes, 3)  // 3 for DBD
                    array.push(isViolated, violationFlag)  // initially not violated
                // Reset DBD pattern
                dbd_state := 0
                dbd_baseCount := 0
            else
                // Not enough follow-through, wait
                na
        else
            // Leg-out candle is big, proceed as usual
            if (dbd_baseCount >= 1)
                // Include Leg-out candle's high in distal line
                dbd_highestHigh := math.max(dbd_highestHigh, high)
                // Create Supply Zone
                newZone = box.new(left=dbd_startIndex, right=bar_index, top=dbd_highestHigh, bottom=dbd_lowestBody,
                                 border_color=color.red, bgcolor=color.new(color.red, 85))
                box.set_extend(newZone, extend.right)
                // Initialize isViolated flag as false
                violationFlag = false
                // Store zone details
                array.push(zones, newZone)
                array.push(distalLines, dbd_highestHigh)
                array.push(zoneTypes, 3)  // 3 for DBD
                array.push(isViolated, violationFlag)  // initially not violated
            // Reset DBD pattern
            dbd_state := 0
            dbd_baseCount := 0
    else
        // Pattern invalidated, reset DBD state
        dbd_state := 0
        dbd_baseCount := 0

// ‣ RBD Pattern Detection (Supply Zone)
if (rbd_state == 0)
    if (isBigGreenCandle)
        // RBD Leg-in detected
        rbd_state := 1
        rbd_baseCount := 0
        rbd_startIndex := bar_index
        rbd_lowestBody := na
        rbd_highestHigh := na
else if (rbd_state == 1)
    if (isSmallBodyCandle)
        // RBD Base candle detected
        rbd_baseCount += 1
        if (rbd_baseCount > maxBaseCandles)
            // Exceeded maximum base candles, reset pattern
            rbd_state := 0
            rbd_baseCount := 0
        else
            // Update lowest body (proximal line)
            candleLowBody = math.min(open, close)
            rbd_lowestBody := na(rbd_lowestBody) ? candleLowBody : math.min(rbd_lowestBody, candleLowBody)
            // Update highest high (distal line)
            rbd_highestHigh := na(rbd_highestHigh) ? high : math.max(rbd_highestHigh, high)
    else if (isRedCandle)
        // Potential Leg-out detected
        rbd_legOutIndex := bar_index
        // Check if Leg-out candle is big or small
        if (not isBigRedCandle)
            // Leg-out candle is small, check for good follow-through
            hasFollowThrough = f_hasGoodFollowThrough(rbd_legOutIndex, "down")
            if hasFollowThrough
                // Complete RBD pattern
                if (rbd_baseCount >= 1)
                    // Include Leg-out candle's high in distal line
                    rbd_highestHigh := math.max(rbd_highestHigh, high)
                    // Create Supply Zone
                    newZone = box.new(left=rbd_startIndex, right=bar_index, top=rbd_highestHigh, bottom=rbd_lowestBody,
                                     border_color=color.orange, bgcolor=color.new(color.orange, 85))
                    box.set_extend(newZone, extend.right)
                    // Initialize isViolated flag as false
                    violationFlag = false
                    // Store zone details
                    array.push(zones, newZone)
                    array.push(distalLines, rbd_highestHigh)
                    array.push(zoneTypes, 4)  // 4 for RBD
                    array.push(isViolated, violationFlag)  // initially not violated
                // Reset RBD pattern
                rbd_state := 0
                rbd_baseCount := 0
            else
                // Not enough follow-through, wait
                na
        else
            // Leg-out candle is big, proceed as usual
            if (rbd_baseCount >= 1)
                // Include Leg-out candle's high in distal line
                rbd_highestHigh := math.max(rbd_highestHigh, high)
                // Create Supply Zone
                newZone = box.new(left=rbd_startIndex, right=bar_index, top=rbd_highestHigh, bottom=rbd_lowestBody,
                                 border_color=color.orange, bgcolor=color.new(color.orange, 85))
                box.set_extend(newZone, extend.right)
                // Initialize isViolated flag as false
                violationFlag = false
                // Store zone details
                array.push(zones, newZone)
                array.push(distalLines, rbd_highestHigh)
                array.push(zoneTypes, 4)  // 4 for RBD
                array.push(isViolated, violationFlag)  // initially not violated
            // Reset RBD pattern
            rbd_state := 0
            rbd_baseCount := 0
    else
        // Pattern invalidated, reset RBD state
        rbd_state := 0
        rbd_baseCount := 0

// ‣ Zone Violation Checking and Management
if array.size(zones) > 0
    // Iterate through all zones
    i = array.size(zones) -1
    while i >= 0
        currentZone = array.get(zones, i)
        currentDistal = array.get(distalLines, i)
        currentType = array.get(zoneTypes, i)
        violated = array.get(isViolated, i)
        
        // Determine violation based on zone type and close price
        isCurrentlyViolated = false
        if currentType == 1 or currentType == 2
            // Demand Zones: Violation when close < distal line
            isCurrentlyViolated := (close < currentDistal) and (not violated)
        else if currentType == 3 or currentType == 4
            // Supply Zones: Violation when close > currentDistal
            isCurrentlyViolated := (close > currentDistal) and (not violated)
        
        if isCurrentlyViolated
            if drawHistoricalZones
                // Mark zone as violated: change appearance to gray
                box.set_border_color(currentZone, color.gray)
                box.set_bgcolor(currentZone, color.new(color.gray, 85))
                // Update violation status
                array.set(isViolated, i, true)
            else
                // Delete the violated zone
                box.delete(currentZone)
                // Remove from arrays
                array.remove(zones, i)
                array.remove(distalLines, i)
                array.remove(zoneTypes, i)
                array.remove(isViolated, i)
        i -=1

    // After handling violations, manage the number of historical zones
    if drawHistoricalZones
        // Count how many zones have been violated (isViolated=true)
        historicalZonesCount = 0
        if array.size(isViolated) > 0
            for idx = 0 to array.size(isViolated)-1
                if array.get(isViolated, idx)
                    historicalZonesCount +=1

        // Remove oldest historical zones if count exceeds the limit
        while historicalZonesCount > maxHistoricalZonesToDisplay
            // Find the oldest historical zone (first in the array that is violated)
            found = false
            if array.size(isViolated) > 0
                for idx = 0 to array.size(isViolated)-1
                    if array.get(isViolated, idx)
                        // Remove this zone
                        zoneToRemove = array.get(zones, idx)
                        box.delete(zoneToRemove)
                        array.remove(zones, idx)
                        array.remove(distalLines, idx)
                        array.remove(zoneTypes, idx)
                        array.remove(isViolated, idx)
                        historicalZonesCount -=1
                        found := true
                        break
            if not found
                break  // No more historical zones to remove
    else
        // Remove all violated zones when drawHistoricalZones is false
        i = array.size(zones) -1
        while i >= 0
            if array.get(isViolated, i)
                zoneToRemove = array.get(zones, i)
                box.delete(zoneToRemove)
                array.remove(zones, i)
                array.remove(distalLines, i)
                array.remove(zoneTypes, i)
                array.remove(isViolated, i)
            i -=1

    // ‣ Manage the number of active zones separately for supply and demand
    activeSupplyZonesCount = 0
    activeDemandZonesCount = 0

    // First, count the number of active (non-violated) supply and demand zones
    if array.size(isViolated) > 0
        for idx = 0 to array.size(isViolated)-1
            if not array.get(isViolated, idx)
                currentType = array.get(zoneTypes, idx)
                if currentType == 1 or currentType == 2
                    activeDemandZonesCount += 1
                else if currentType == 3 or currentType == 4
                    activeSupplyZonesCount += 1

    // Now, remove oldest active supply zones if count exceeds limit
    while activeSupplyZonesCount > maxActiveSupplyZonesToDisplay
        // Find the oldest active supply zone
        found = false
        if array.size(isViolated) > 0
            for idx = 0 to array.size(isViolated)-1
                if not array.get(isViolated, idx)
                    currentType = array.get(zoneTypes, idx)
                    if currentType == 3 or currentType == 4  // Supply zone
                        // Remove this zone
                        zoneToRemove = array.get(zones, idx)
                        box.delete(zoneToRemove)
                        array.remove(zones, idx)
                        array.remove(distalLines, idx)
                        array.remove(zoneTypes, idx)
                        array.remove(isViolated, idx)
                        activeSupplyZonesCount -=1
                        found := true
                        break
        if not found
            break // No more active supply zones to remove

    // Similarly, remove oldest active demand zones if count exceeds limit
    while activeDemandZonesCount > maxActiveDemandZonesToDisplay
        // Find the oldest active demand zone
        found = false
        if array.size(isViolated) > 0
            for idx = 0 to array.size(isViolated)-1
                if not array.get(isViolated, idx)
                    currentType = array.get(zoneTypes, idx)
                    if currentType == 1 or currentType == 2  // Demand zone
                        // Remove this zone
                        zoneToRemove = array.get(zones, idx)
                        box.delete(zoneToRemove)
                        array.remove(zones, idx)
                        array.remove(distalLines, idx)
                        array.remove(zoneTypes, idx)
                        array.remove(isViolated, idx)
                        activeDemandZonesCount -=1
                        found := true
                        break
        if not found
            break // No more active demand zones to remove

// ‣ Plot Markers (Optional)

// Demand Zone Markers

// RBR Markers
plotshape(showMarkers and rbr_state == 1 and isBigGreenCandle and rbr_baseCount == 0,
          title="RBR Leg-in",
          style=shape.triangleup,
          location=location.belowbar,
          color=color.lime,
          size=size.small)
plotshape(showMarkers and rbr_state == 1 and isGreenCandle and rbr_baseCount >=1 and not isBigGreenCandle,
          title="RBR Leg-out",
          style=shape.triangleup,
          location=location.belowbar,
          color=color.green,
          size=size.small)

// DBR Markers
plotshape(showMarkers and dbr_state == 1 and isBigRedCandle and dbr_baseCount ==0,
          title="DBR Leg-in",
          style=shape.triangledown,
          location=location.abovebar,
          color=color.red,
          size=size.small)
plotshape(showMarkers and dbr_state ==1 and isGreenCandle and dbr_baseCount >=1 and not isBigRedCandle,
          title="DBR Leg-out",
          style=shape.triangleup,
          location=location.belowbar,
          color=color.blue,
          size=size.small) 

// Supply Zone Markers

// DBD Markers
plotshape(showMarkers and dbd_state == 1 and isBigRedCandle and dbd_baseCount == 0,
          title="DBD Leg-in",
          style=shape.triangledown,
          location=location.abovebar,
          color=color.red,
          size=size.small)
plotshape(showMarkers and dbd_state == 1 and isRedCandle and dbd_baseCount >=1 and not isBigRedCandle,
          title="DBD Leg-out",
          style=shape.triangledown,
          location=location.abovebar,
          color=color.maroon,
          size=size.small)

// RBD Markers
plotshape(showMarkers and rbd_state == 1 and isBigGreenCandle and rbd_baseCount ==0,
          title="RBD Leg-in",
          style=shape.triangleup,
          location=location.belowbar,
          color=color.green,
          size=size.small)
plotshape(showMarkers and rbd_state ==1 and isRedCandle and rbd_baseCount >=1 and not isBigGreenCandle,
          title="RBD Leg-out",
          style=shape.triangledown,
          location=location.abovebar,
          color=color.orange,
          size=size.small)