Pine Script v6
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © LuxAlgo
//@version=6
indicator("Rubber Band Liquidity Signals [LuxAlgo]", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500)
// --- Inputs ---
var grp_trend = "Trend & Range Settings"
trendLookback = input.int(20, "Trend Lookback", group = grp_trend)
trendAtrMult = input.float(2.0, "Trend Move ATR Multiplier", group = grp_trend, tooltip = "Minimum move size to be considered a strong trend")
rangeLength = input.int(10, "Consolidation Length", group = grp_trend)
rangeMaxAtr = input.float(3.5, "Range Max ATR Multiplier", group = grp_trend, tooltip = "Maximum width of the consolidation range")
maxRangeBars = input.int(50, "Max Range Duration", group = grp_trend)
var grp_style = "Style"
bullColor = input.color(#089981, "Bullish Color", group = grp_style)
bearColor = input.color(#f23645, "Bearish Color", group = grp_style)
var grp_alerts = "Alerts"
alertSweep = input.bool(true, "Alert on Sweep", group = grp_alerts)
alertBreakout = input.bool(true, "Alert on Breakout (B)", group = grp_alerts)
// --- Calculations ---
atr = ta.atr(14)
// 1. Identify Strong Trend
// Check if the price moved significantly over the trendLookback period
moveSize = close - close[trendLookback]
isBullTrend = moveSize > (trendAtrMult * atr[trendLookback])
isBearTrend = moveSize < -(trendAtrMult * atr[trendLookback])
// 2. Identify Range
// Calculate the highest and lowest points over the consolidation period
rangeHigh = ta.highest(high, rangeLength)
rangeLow = ta.lowest(low, rangeLength)
rangeWidth = rangeHigh - rangeLow
isConsolidating = rangeWidth < (rangeMaxAtr * atr) // State tracking var int state = 0 // 0 = searching, 1 = range formed (bull), -1 = range formed (bear), 2 = sweep bull, -2 = sweep bear var float activeRangeHigh = na var float activeRangeLow = na var int rangeStartBar = na var int sweepBarIndex = na var box currentBox = na // Only start looking for a range if we are in state 0 // We look back to see if there was a trend leading up to this consolidation bullRangeCondition = isBullTrend[rangeLength] and isConsolidating bearRangeCondition = isBearTrend[rangeLength] and isConsolidating if state == 0 if bullRangeCondition state := 1 activeRangeHigh := rangeHigh activeRangeLow := rangeLow rangeStartBar := bar_index - rangeLength currentBox := box.new(rangeStartBar, activeRangeHigh, bar_index, activeRangeLow, border_color = color.new(bullColor, 50), bgcolor = color.new(bullColor, 90)) else if bearRangeCondition state := -1 activeRangeHigh := rangeHigh activeRangeLow := rangeLow rangeStartBar := bar_index - rangeLength currentBox := box.new(rangeStartBar, activeRangeHigh, bar_index, activeRangeLow, border_color = color.new(bearColor, 50), bgcolor = color.new(bearColor, 90)) // Handle states once a range is formed if state == 1 // Bullish Trend -> Range -> Waiting for Sweep (Liquidity Grab below range low)
if not na(currentBox)
box.set_right(currentBox, bar_index)
// Check for liquidity grab (sweep below range low)
if low < activeRangeLow and close > activeRangeLow
state := 2
sweepBarIndex := bar_index
// Draw sweep indicator
line.new(bar_index, low, bar_index, activeRangeLow, color = bullColor, style = line.style_solid, width = 2)
label.new(bar_index, low, "Sweep", style = label.style_label_up, color = color.new(bullColor, 100), textcolor = bullColor, size = size.small)
if alertSweep
alert("Bullish Sweep Detected", alert.freq_once_per_bar_close)
// Invalidation (Price breaks out without a sweep)
else if close < activeRangeLow or close > activeRangeHigh
state := 0
if not na(currentBox)
currentBox.delete()
else if state == -1 // Bearish Trend -> Range -> Waiting for Sweep (Liquidity Grab above range high)
if not na(currentBox)
box.set_right(currentBox, bar_index)
// Check for liquidity grab (sweep above range high)
if high > activeRangeHigh and close < activeRangeHigh state := -2 sweepBarIndex := bar_index // Draw sweep indicator line.new(bar_index, activeRangeHigh, bar_index, high, color = bearColor, style = line.style_solid, width = 2) label.new(bar_index, high, "Sweep", style = label.style_label_down, color = color.new(bearColor, 100), textcolor = bearColor, size = size.small) if alertSweep alert("Bearish Sweep Detected", alert.freq_once_per_bar_close) // Invalidation (Price breaks out without a sweep) else if close > activeRangeHigh or close < activeRangeLow state := 0 if not na(currentBox) currentBox.delete() else if state == 2 // Swept low, waiting for continuation (breakout above range high) if not na(currentBox) box.set_right(currentBox, bar_index) if close > activeRangeHigh
// Continuation signaled
state := 0
label.new(bar_index, high, "B", style = label.style_label_down, color = bullColor, textcolor = color.white, size = size.small)
// Draw the V shape
line.new(rangeStartBar, activeRangeHigh, sweepBarIndex, activeRangeLow, color = color.yellow, width = 2)
line.new(sweepBarIndex, activeRangeLow, bar_index, activeRangeHigh, color = color.yellow, width = 2)
if alertBreakout
alert("Bullish Breakout (B)", alert.freq_once_per_bar_close)
else if close < activeRangeLow
// Invalidation (Fails after sweep)
state := 0
else if state == -2 // Swept high, waiting for continuation (breakout below range low)
if not na(currentBox)
box.set_right(currentBox, bar_index)
if close < activeRangeLow // Continuation signaled state := 0 label.new(bar_index, low, "B", style = label.style_label_up, color = bearColor, textcolor = color.white, size = size.small) // Draw the inverted V shape line.new(rangeStartBar, activeRangeLow, sweepBarIndex, activeRangeHigh, color = color.yellow, width = 2) line.new(sweepBarIndex, activeRangeHigh, bar_index, activeRangeLow, color = color.yellow, width = 2) if alertBreakout alert("Bearish Breakout (B)", alert.freq_once_per_bar_close) else if close > activeRangeHigh
// Invalidation (Fails after sweep)
state := 0
// Force states to reset if range gets too long
if state != 0 and (bar_index - rangeStartBar > maxRangeBars)
// If it never reached a sweep or continuation within the time limit, remove the box if it hasn't swept yet
if state == 1 or state == -1
if not na(currentBox)
currentBox.delete()
state := 0
