//@version=6
indicator("PRC Range Detector with Breakout Signals", overlay=true)
// Inputs
length = [Link](20, "Minimum Range Length", minval=1)
mult = [Link](1.0, "Range Width Multiplier")
atrLen = [Link](500, "ATR Length")
showSignals = [Link](true, "Show Buy/Sell Signals")
// ATR and Moving Average
atrValue = [Link](atrLen) * mult
ma = [Link](close, length)
// Variables to store range box data
var int x = 0
var int[] boxLeftTime = array.new_int()
var float[] boxTop = array.new_float()
var float[] boxBot = array.new_float()
var int[] boxRightTime = array.new_int()
var int[] levelX1Time = array.new_int()
var float[] levelY1 = array.new_float()
var int[] levelX2Time = array.new_int()
var float[] levelY2 = array.new_float()
var int[] rBox = array.new_int()
var int[] gBox = array.new_int()
var int[] bBox = array.new_int()
var int[] startTime = array.new_int()
var bool[] breakoutOccurred = array.new_bool()
// Variables for signal control
var bool wasInRange = false
var bool signalFired = false
var float lastBoxTop = na
var float lastBoxBot = na
// Count bars outside ATR bands around MA
count = 0
for j = 0 to length - 1
if [Link](close[j] - ma) > atrValue
count += 1
// Times for current bar and bar length bars before
curTime = time
pastTime = time[length]
// Box creation/extension logic
if count == 0 and (bar_index == length or (nz(count[1]) != count))
if x > 0 and [Link](boxRightTime) > 0 and [Link](boxRightTime, x - 1) >=
pastTime
// Update existing box top and bottom if overlapping
maxi = [Link](ma + atrValue, [Link](boxTop, x - 1))
mini = [Link](ma - atrValue, [Link](boxBot, x - 1))
[Link](boxTop, x - 1, maxi)
[Link](boxBot, x - 1, mini)
[Link](boxRightTime, x - 1, curTime)
avg = (maxi + mini) / 2.0
[Link](levelY1, x - 1, avg)
[Link](levelX2Time, x - 1, curTime)
[Link](levelY2, x - 1, avg)
else
if not na(pastTime)
// New box - reset signal tracking
[Link](boxTop, ma + atrValue)
[Link](boxBot, ma - atrValue)
[Link](boxRightTime, curTime)
[Link](boxLeftTime, pastTime)
[Link](levelY1, ma)
[Link](levelX2Time, curTime)
[Link](levelY2, ma)
[Link](levelX1Time, pastTime)
[Link](rBox, 33)
[Link](gBox, 87)
[Link](bBox, 243)
[Link](startTime, curTime)
[Link](breakoutOccurred, false)
x += 1
signalFired := false
wasInRange := false
else if count == 0 and x > 0
[Link](boxRightTime, x - 1, curTime)
[Link](levelX2Time, x - 1, curTime)
// Breakout detection variables
bool buySignal = false
bool sellSignal = false
// Only check for signals on the most recent active box
if x > 0 and [Link](boxTop) > 0 and [Link](boxBot) > 0
lastTop = [Link](boxTop, x - 1)
lastBot = [Link](boxBot, x - 1)
hasBreakout = [Link](breakoutOccurred, x - 1)
// Track box level changes
bool boxChanged = lastTop != lastBoxTop or lastBot != lastBoxBot
if boxChanged
lastBoxTop := lastTop
lastBoxBot := lastBot
wasInRange := false
signalFired := false
// Check if price is currently in range
bool currentlyInRange = close >= lastBot and close <= lastTop
// Only generate signal once per box breakout
if not hasBreakout and not signalFired
// Price was in range on previous bar
if close[1] >= lastBot and close[1] <= lastTop
wasInRange := true
// Bullish breakout - price breaks above after being in range
if wasInRange and close > lastTop
[Link](rBox, x - 1, 8)
[Link](gBox, x - 1, 153)
[Link](bBox, x - 1, 119)
[Link](breakoutOccurred, x - 1, true)
buySignal := true
signalFired := true
wasInRange := false
// Bearish breakout - price breaks below after being in range
else if wasInRange and close < lastBot
[Link](rBox, x - 1, 242)
[Link](gBox, x - 1, 54)
[Link](bBox, x - 1, 69)
[Link](breakoutOccurred, x - 1, true)
sellSignal := true
signalFired := true
wasInRange := false
// Update wasInRange status
if currentlyInRange and not signalFired
wasInRange := true
// Draw only the most recent boxes (last 50 to avoid clutter)
if x > 0
startIdx = [Link](0, [Link](boxTop) - 50)
for i = startIdx to [Link](boxTop) - 1
rectLeftTime = [Link](boxLeftTime, i)
rectRightTime = [Link](boxRightTime, i)
rectTop = [Link](boxTop, i)
rectBottom = [Link](boxBot, i)
colorFill = [Link]([Link]([Link](rBox, i), [Link](gBox, i),
[Link](bBox, i)), 50)
colorBorder = [Link]([Link](rBox, i), [Link](gBox, i),
[Link](bBox, i))
levelLeftTime = [Link](levelX1Time, i)
levelRightTime = [Link](levelX2Time, i)
levelY = [Link](levelY1, i)
levelY2v = [Link](levelY2, i)
colorLevel = [Link](colorBorder, 80)
[Link](left=rectLeftTime, top=rectTop, right=rectRightTime,
bottom=rectBottom, xloc=xloc.bar_time, border_color=colorLevel, bgcolor=colorFill)
[Link](x1=levelLeftTime, y1=levelY, x2=levelRightTime, y2=levelY2v,
xloc=xloc.bar_time, color=colorLevel, style=line.style_dotted, width=2)
[Link](x1=levelLeftTime, y1=rectBottom, x2=curTime, y2=rectBottom,
xloc=xloc.bar_time, color=colorLevel, width=1)
[Link](x1=levelLeftTime, y1=rectTop, x2=curTime, y2=rectTop,
xloc=xloc.bar_time, color=colorLevel, width=1)
// Plot buy/sell signals
plotshape(showSignals and buySignal, title="Buy Signal", style=[Link],
location=[Link], color=[Link]([Link], 0), text="BUY",
textcolor=[Link], size=[Link])
plotshape(showSignals and sellSignal, title="Sell Signal", style=[Link],
location=[Link], color=[Link]([Link], 0), text="SELL",
textcolor=[Link], size=[Link])
// Alerts
alertcondition(buySignal, title="Bullish Breakout", message="Price broke above
range - BUY signal")
alertcondition(sellSignal, title="Bearish Breakout", message="Price broke below
range - SELL signal")