Consolidation Zones & Breakouts

Automatically detect price consolidation zones using a pivot-based algorithm, then trade breakouts from those zones with defined entries, stops, and targets.

A consolidation zone is a price range where neither buyers nor sellers are winning. Volume contracts, bars get smaller, and price bounces between an informal ceiling and floor as the market builds energy for its next directional move. When the zone finally breaks, the move that follows is often sharp and sustained — exactly because it was preceded by compression.

Consolidation zones form at all timeframes and in all instruments. On a daily chart they might last weeks; on a 5-minute chart they might form and resolve in an hour. The underlying mechanics are the same: trapped participants, coiling energy, and a release.

The indicator below uses a ZigZag pivot algorithm to detect when price has been making pivots within a confined range for a minimum number of bars. When the zone is confirmed, it draws the upper and lower boundaries. When a new pivot forms outside those boundaries, a breakout signal fires. The code has been ported to Pine Script v6 from an original v4 implementation, with all deprecated functions updated and the logic preserved exactly.

How the detection algorithm works

The indicator uses a ZigZag pivot approach — a technique that identifies the sequence of swing highs and lows that price is currently making, then checks whether those swings are staying within a confined range.

The variable `dir` tracks the current swing direction: `1` when the most recent extreme was a high, `-1` when it was a low. Direction only changes when a clear new extreme forms in the opposite direction.

### Step 2 — Identifying the dominant pivot A `for` loop walks back through the last 1,000 bars, stopping when it either runs out of data or encounters a bar where the direction was different. Within the current swing, it finds the most extreme pivot value — the highest high in an upswing, the lowest low in a downswing. This is stored as `pp` (pivot point).

When the counter reaches the minimum consolidation length, the zone boundaries are locked in from the rolling high/low of that window. From that point forward, the zone expands if price temporarily pokes beyond it, but a confirmed new pivot outside it triggers the breakout.

Trading the breakout

A breakout signal from a confirmed consolidation zone is the beginning of the trade setup, not the complete trade. The zone provides the structure; your entry, stop, and target rules complete it.

### Stop placement The zone itself provides the natural stop location. For a bullish breakout, a stop below the lower boundary of the zone (condlow) means your trade is invalidated if price re-enters the full zone — not just dips back to the breakout level. A tighter alternative is a stop just below the upper boundary of the zone (the level that was just broken), accepting that a close back inside the zone exits the trade.

Use ATR to verify the stop makes mechanical sense: `condlow` should ideally be at least 1× ATR away from your entry, otherwise the zone was too narrow to provide a meaningful stop.

This is a measured move approach — the logic is that the coiled energy within the zone projects an approximately equal distance once released. The target is a guide, not a guarantee; trailing the stop with ATR or a Supertrend line often captures more when the move extends.

Filters that improve breakout quality

Not every consolidation breakout continues. False breakouts — where price clears the zone boundary then immediately reverses — are a real risk, especially in low-volume or choppy conditions. These filters significantly improve the selectivity of the setup:

### ATR expansion filter A genuine breakout is accompanied by an expansion of volatility. If ATR on the breakout bar is above its 20-bar average, the move has volatility behind it. A breakout on declining ATR is more likely to fail.

### Volume confirmation Where volume data is available (equities, futures), a breakout bar should show above-average volume. An OBV break simultaneously with the price break — where OBV also exceeds its own recent high — is a strong confirmation.

### Higher-timeframe alignment A bullish consolidation breakout on the 15-minute chart that fires while the hourly chart is in a downtrend is fighting the dominant structure. Use Higher Timeframe Confirmation to ensure the breakout direction aligns with the higher-timeframe trend.

### Minimum consolidation length The `conslen` input controls how many bars the zone must persist before it qualifies. Longer consolidations (20+ bars on the timeframe you are trading) tend to produce more sustained breakouts because more trapped participants and accumulated stop orders have built up. Very short consolidations (5–7 bars) can break cleanly but are also more prone to immediate reversals.

### Psychological level confluence If the upper or lower boundary of the consolidation zone sits at or very near a psychological level, the breakout carries additional weight. Institutions watch both the technical zone and the round number; a breakout that clears both simultaneously attracts much larger order flow.

Pine Script v4 to v6: what changed

The original indicator was written in Pine Script v4. Porting it to v6 required updating several deprecated functions and syntax patterns. Understanding these changes is useful if you encounter other v4 scripts you want to modernise.

| v4 function / syntax | v6 equivalent | |---|---| | `study()` | `indicator()` | | `input(defval=...)` | `input.int()`, `input.bool()`, `input.color()` | | `highestbars(prd)` | `ta.highestbars(high, prd)` | | `lowestbars(prd)` | `ta.lowestbars(low, prd)` | | `iff(cond, a, b)` | `cond ? a : b` (ternary operator) | | `highest(conslen)` | `ta.highest(high, conslen)` | | `lowest(conslen)` | `ta.lowest(low, conslen)` | | `change(pp)` | Custom `ppChanged` logic (see code) | | `max(a, b)` | `math.max(a, b)` | | `min(a, b)` | `math.min(a, b)` | | `conscnt := conscnt + 1` | `conscnt += 1` |

The `change(pp)` translation requires special care. In v4, `change(pp)` returned `pp - pp[1]`. In v6, `ta.change(pp)` does the same, but when `pp` is `na` on early bars, the comparison can behave unexpectedly. The explicit `ppChanged = not na(pp) and (na(pp[1]) or pp != pp[1])` guard handles both the case where `pp` was previously `na` and where it is changing to a new value.

//@version=6
indicator("Consolidation Zones & Breakouts", overlay=true, max_bars_back=1100)

// ── Inputs ─────────────────────────────────────────────────────────────
prd       = input.int(10,  "Pivot Lookback Period",            minval=2, maxval=50)
conslen   = input.int(5,   "Min Consolidation Length (bars)",  minval=2, maxval=20)
paintcons = input.bool(true, "Paint Consolidation Zone")
zonecol   = input.color(color.new(color.blue, 70), "Zone Colour")

// ── ZigZag pivot detection ─────────────────────────────────────────────
// hb_: current bar is the highest in the last prd bars
// lb_: current bar is the lowest in the last prd bars
float hb_ = ta.highestbars(high, prd) == 0 ? high : na
float lb_ = ta.lowestbars(low,  prd) == 0 ? low  : na

var int dir = 0   // 1 = swing up in progress, -1 = swing down in progress
float   zz  = na  // current zigzag extreme
float   pp  = na  // most extreme pivot in current direction (recalculated each bar)

// Update direction: changes only when a clear new extreme is identified
dir := not na(hb_) and na(lb_) ? 1 :
       not na(lb_) and na(hb_) ? -1 : dir

// When both hb_ and lb_ are on the same bar, use current direction to resolve
if not na(hb_) and not na(lb_)
    zz := dir == 1 ? hb_ : lb_
else
    zz := not na(hb_) ? hb_ : not na(lb_) ? lb_ : na

// Walk back through history to find the most extreme pivot in the current swing
for x = 0 to 1000
    if na(close[x]) or dir != dir[x]
        break
    if not na(zz[x])
        if na(pp)
            pp := zz[x]
        else
            if dir[x] == 1 and zz[x] > pp
                pp := zz[x]
            if dir[x] == -1 and zz[x] < pp
                pp := zz[x]

// ── Consolidation zone tracking ────────────────────────────────────────
var int   conscnt  = 0     // how many bars the consolidation has persisted
var float condhigh = na    // upper boundary of current consolidation zone
var float condlow  = na    // lower boundary of current consolidation zone

float H_ = ta.highest(high, conslen)   // rolling high of the consolidation window
float L_ = ta.lowest(low,   conslen)   // rolling low of the consolidation window

var line upline = na
var line dnline = na

bool breakoutup   = false
bool breakoutdown = false

// pp changes when the dominant swing pivot shifts to a new extreme
bool ppChanged = not na(pp) and (na(pp[1]) or pp != pp[1])

if ppChanged
    // A new pivot formed — check if it is inside or outside the current zone
    if conscnt > conslen
        if pp > condhigh
            breakoutup   := true     // pivot broke above the zone
        if pp < condlow
            breakoutdown := true     // pivot broke below the zone
    // Continue consolidation if new pivot stays within the existing zone
    if conscnt > 0 and pp <= condhigh and pp >= condlow
        conscnt += 1
    else
        conscnt := 0                 // reset — zone is broken or not yet established
else
    conscnt += 1                     // pp unchanged: another bar inside the zone

// When consolidation is confirmed (conslen bars reached), lock in the zone boundaries
if conscnt >= conslen
    if conscnt == conslen
        // First bar of confirmed consolidation: lock boundaries
        condhigh := H_
        condlow  := L_
    else
        // Continuing consolidation: expand boundaries if price exceeds them
        line.delete(upline)
        line.delete(dnline)
        condhigh := math.max(condhigh, high)
        condlow  := math.min(condlow,  low)

    // Redraw the zone boundary lines
    upline := line.new(bar_index, condhigh, bar_index - conscnt, condhigh,
                       color=color.red,  style=line.style_dashed, width=1)
    dnline := line.new(bar_index, condlow,  bar_index - conscnt, condlow,
                       color=color.lime, style=line.style_dashed, width=1)

// ── Visual output ──────────────────────────────────────────────────────
upperPlot = plot(condhigh, color=na, style=plot.style_stepline)
lowerPlot = plot(condlow,  color=na, style=plot.style_stepline)

// Fill the zone only once it is confirmed (conscnt > conslen)
fill(upperPlot, lowerPlot,
     color=paintcons and conscnt > conslen ? zonecol : color.new(color.white, 100))

// ATR for context (displayed in data window, used in stop guidance)
atrValue = ta.atr(14)
plot(atrValue, "ATR(14)", display=display.data_window)

// ── Breakout signals ───────────────────────────────────────────────────
plotshape(breakoutup,   style=shape.triangleup,   location=location.belowbar,
          color=color.green, size=size.normal, title="Breakout Up")
plotshape(breakoutdown, style=shape.triangledown, location=location.abovebar,
          color=color.red,   size=size.normal, title="Breakout Down")

// ── Alerts ─────────────────────────────────────────────────────────────
alertcondition(breakoutup,
               title="Consolidation Breakout Up",
               message="Breakout above consolidation zone on {{ticker}} — close: {{close}}")
alertcondition(breakoutdown,
               title="Consolidation Breakout Down",
               message="Breakout below consolidation zone on {{ticker}} — close: {{close}}")