What this indicator do: //@version=5
indicator("Demand Zone Detector (RBR and DBR)", 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 Demand Zones", defval=true)
maxHistoricalZonesToDisplay = input.int(title="Number of Historical Demand Zones to Display", defval=5, minval=1)
maxActiveZonesToDisplay = input.int(title="Number of Active Demand Zones to Display", defval=5, minval=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

// ‣ Pattern Detection States
var int rbr_state = 0  // RBR: 0 = idle, 1 = collecting base
var int rbr_baseCount = 0
var float rbr_highestBody = na
var float rbr_lowestLow = na
var int rbr_startIndex = na

var int dbr_state = 0  // DBR: 0 = idle, 1 = collecting base
var int dbr_baseCount = 0
var float dbr_highestBody = na
var float dbr_lowestLow = na
var int dbr_startIndex = na

// ‣ Arrays to Store Demand Zones
// Each zone has:
//  - box object
//  - distal line (for violation checking)
//  - zone type (1 for RBR, 2 for DBR)
//  - isViolated flag (true if violated)
var box[] demandZones = array.new_box()
var float[] distalLines = array.new_float()
var int[] zoneTypes = array.new_int()
var bool[] isViolated = array.new_bool()

// ‣ RBR Pattern Detection
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)
            float 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)
        // RBR Leg-out detected, 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
            box 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
            bool violationFlag = false
            // Store zone details
            array.push(demandZones, newZone)
            array.push(distalLines, rbr_lowestLow)
            array.push(zoneTypes, 1)  // 1 for RBR
            array.push(isViolated, violationFlag)  // initially not violated
            
            // Manage Active Demand Zones
            // Count current active zones
            int activeZonesCount = 0
            for idx = 0 to array.size(isViolated) -1
                if (not array.get(isViolated, idx))
                    activeZonesCount +=1
            // If active zones exceed the limit, remove the oldest active zone
            while (activeZonesCount > maxActiveZonesToDisplay)
                // Find the oldest active zone (first in the array)
                int oldestActiveIndex = na
                for idx = 0 to array.size(isViolated) -1
                    if (not array.get(isViolated, idx))
                        oldestActiveIndex := idx
                        break
                if (not na(oldestActiveIndex))
                    // Remove the oldest active zone
                    box dzBoxToRemove = array.get(demandZones, oldestActiveIndex)
                    box.delete(dzBoxToRemove)
                    // Remove from arrays
                    array.remove(demandZones, oldestActiveIndex)
                    array.remove(distalLines, oldestActiveIndex)
                    array.remove(zoneTypes, oldestActiveIndex)
                    array.remove(isViolated, oldestActiveIndex)
                    activeZonesCount -=1
                else
                    break  // No active zones found
        // Reset RBR pattern
        rbr_state := 0
        rbr_baseCount := 0
    else
        // Pattern invalidated, reset RBR state
        rbr_state := 0
        rbr_baseCount := 0

// ‣ DBR Pattern Detection
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)
            float candleHighBody = math.max(open, close)
            dbr_highestBody := na(dbr_highestBody) ? candleHighBody : math.max(dbr_highestBody, candleHighBody)
            // Update lowest low (distal line)
            dbr_lowestLow := math.min(dbr_lowestLow, low)
    else if (isGreenCandle)
        // DBR Leg-out detected, 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
            box 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
            bool violationFlag = false
            // Store zone details
            array.push(demandZones, newZone)
            array.push(distalLines, dbr_lowestLow)
            array.push(zoneTypes, 2)  // 2 for DBR
            array.push(isViolated, violationFlag)  // initially not violated
            
            // Manage Active Demand Zones
            // Count current active zones
            int activeZonesCount_DB = 0
            for idx = 0 to array.size(isViolated) -1
                if (not array.get(isViolated, idx))
                    activeZonesCount_DB +=1
            // If active zones exceed the limit, remove the oldest active zone
            while (activeZonesCount_DB > maxActiveZonesToDisplay)
                // Find the oldest active zone (first in the array)
                int oldestActiveIndex_DB = na
                for idx = 0 to array.size(isViolated) -1
                    if (not array.get(isViolated, idx))
                        oldestActiveIndex_DB := idx
                        break
                if (not na(oldestActiveIndex_DB))
                    // Remove the oldest active zone
                    box dzBoxToRemove = array.get(demandZones, oldestActiveIndex_DB)
                    box.delete(dzBoxToRemove)
                    // Remove from arrays
                    array.remove(demandZones, oldestActiveIndex_DB)
                    array.remove(distalLines, oldestActiveIndex_DB)
                    array.remove(zoneTypes, oldestActiveIndex_DB)
                    array.remove(isViolated, oldestActiveIndex_DB)
                    activeZonesCount_DB -=1
                else
                    break  // No active zones found
        // Reset DBR pattern
        dbr_state := 0
        dbr_baseCount := 0
    else
        // Pattern invalidated, reset DBR state
        dbr_state := 0
        dbr_baseCount := 0

// ‣ Zone Violation Checking and Management
if (array.size(demandZones) > 0)
    // Iterate through all demand zones
    int i = array.size(demandZones) -1
    while (i >= 0)
        box currentZone = array.get(demandZones, i)
        float currentDistal = array.get(distalLines, i)
        int currentType = array.get(zoneTypes, i)
        bool violated = array.get(isViolated, i)
        
        // Determine violation based on zone type and close price
        // For both RBR and DBR, violation is close < distal line
        bool isCurrentlyViolated = false
        if (currentType == 1 or currentType ==2)
            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(demandZones, 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)
        int 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)
            bool found = false
            for idx = 0 to array.size(isViolated)-1
                if (array.get(isViolated, idx))
                    // Remove this zone
                    box dzBoxToRemove = array.shift(demandZones)  // Removes and returns the first element
                    box.delete(dzBoxToRemove)
                    array.shift(distalLines)
                    array.shift(zoneTypes)
                    array.shift(isViolated)
                    historicalZonesCount -=1
                    found := true
                    break
            if (not found)
                break  // No more historical zones to remove

// ‣ Plot Markers (Optional)

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