Add Option to Hide Colorful Marker: //@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)
maxActiveZonesToDisplay = input.int(title="Number of Active 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)

// ‣ 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 > distal line
            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
        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
            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

// ‣ Plot Markers (Optional)

// Demand Zone Markers

// RBR Markers
plotshape(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(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(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(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(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(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(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(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)