What it does
Markets tend to alternate between quiet, narrow ranges and fast, wide moves. This indicator plots standard Bollinger Bands, detects when the bands become unusually narrow (a "squeeze"), and marks the first bar that closes outside the bands shortly after that squeeze. The idea is to separate breakouts that start from compressed volatility from ordinary band touches during an already volatile market.
Squeeze bars are marked with small dots and the bands change color while the squeeze is active. Bullish and bearish breakouts get triangles and matching alerts.
How it works
- Basis = simple moving average of the source over Length bars. Upper and lower bands = basis ± multiplier × standard deviation of the source over the same bars.
- Bandwidth = (upper − lower) ÷ basis × 100, so it is comparable across instruments with different prices.
- The script ranks the current bandwidth against the previous Bandwidth lookback bars using a percent rank. When the rank is at or below the Squeeze percentile, the bar is in a squeeze.
- A squeeze stays "armed" for the chosen number of bars after the last squeeze bar.
- A bullish breakout prints when the bar closes above the upper band, the previous close was at or below the previous upper band, and a squeeze is armed. A bearish breakout is the mirror image below the lower band.
- Signals are only evaluated on confirmed (closed) bars, so a marker never appears and then disappears on the live bar.
How to use it
Treat a squeeze as a heads-up, not a trade. It tells you that the range is compressed relative to its own recent history; it does not tell you which way price will go. The breakout marker is the first piece of directional evidence.
Many traders look for confirmation before acting: a close beyond a nearby support or resistance level, a retest of the band or basis, or agreement with the trend on a higher timeframe. A logical stop-loss for a breakout trade often sits back inside the bands or beyond the squeeze range. Size positions so that a failed breakout is an acceptable loss; the Position Size Calculator helps with that.
H1 to D1 charts usually give cleaner squeezes than very short timeframes, where spreads and random noise matter more.
Settings
| Input | Default | What it changes |
|---|---|---|
| Source | close | Price used for the bands. |
| Length | 20 | Bars for the basis and standard deviation. |
| Standard deviation multiplier | 2.0 | Band width. Higher values mean fewer, larger breakouts. |
| Bandwidth lookback (bars) | 120 | History used to rank bandwidth. |
| Squeeze percentile | 10 | Lower values require a tighter squeeze. |
| Bars a squeeze stays armed | 5 | Window after a squeeze in which a breakout counts. |
| Fill bands / Mark squeeze bars | on | Visual options only. |
Limitations
- A squeeze can end with a false breakout in one direction followed by a real move in the other. The indicator will mark the first one it sees.
- Bollinger Bands lag because they are built on a moving average and a rolling standard deviation.
- The percent rank needs a full lookback of history before squeezes can be detected, so the first bars on the chart show none.
- Breakouts that happen while bandwidth is already high are ignored by design. Raising the squeeze percentile and the armed window loosens the filter, but if you want every band break, a plain Bollinger Bands indicator is a better fit.
- News releases can widen bands in a single bar and produce breakouts that reverse quickly.
Pine Script source
In TradingView open Pine Editor, paste the code, click Save and then Add to chart.
FXP_BollingerSqueezeBreakout.pine
58 lines
// © FXPIPS ONLINE — https://fxpips.online/tradingview-indicators/bollinger-squeeze-breakout/
// License: MIT. Educational tool — not financial advice. Trading involves substantial risk.
//@version=6
indicator("FXP Bollinger Squeeze Breakout", shorttitle = "FXP BB Squeeze", overlay = true)
// ── Inputs ─────────────────────────────────────────────────────────────
grpBB = "Bollinger Bands"
src = input.source(close, "Source", group = grpBB)
bbLen = input.int(20, "Length", minval = 2, group = grpBB)
bbMult = input.float(2.0, "Standard deviation multiplier", minval = 0.1, step = 0.1, group = grpBB)
grpSq = "Squeeze"
sqLook = input.int(120, "Bandwidth lookback (bars)", minval = 20, group = grpSq, tooltip = "Bars used to rank the current bandwidth against its own history.")
sqPct = input.float(10.0, "Squeeze percentile", minval = 1.0, maxval = 50.0, step = 1.0, group = grpSq, tooltip = "A squeeze is active when bandwidth is in the lowest X% of the lookback.")
armBars = input.int(5, "Bars a squeeze stays armed", minval = 0, group = grpSq, tooltip = "A breakout only counts if a squeeze was active within this many bars.")
grpVis = "Display"
showFill = input.bool(true, "Fill bands", group = grpVis)
showDots = input.bool(true, "Mark squeeze bars", group = grpVis)
bullCol = input.color(color.teal, "Bullish color", group = grpVis)
bearCol = input.color(color.red, "Bearish color", group = grpVis)
sqCol = input.color(color.orange, "Squeeze color", group = grpVis)
// ── Calculation ────────────────────────────────────────────────────────
basis = ta.sma(src, bbLen)
dev = bbMult * ta.stdev(src, bbLen)
upper = basis + dev
lower = basis - dev
bandwidth = basis != 0 ? (upper - lower) / basis * 100.0 : na
bwRank = ta.percentrank(bandwidth, sqLook)
bool inSqueeze = not na(bwRank) and bwRank <= sqPct
bool sqStart = inSqueeze and not inSqueeze[1]
barsSinceSq = ta.barssince(inSqueeze)
bool armed = not na(barsSinceSq) and barsSinceSq <= armBars
bool crossUp = close > upper and close[1] <= upper[1]
bool crossDn = close < lower and close[1] >= lower[1]
bool bullBreak = barstate.isconfirmed and armed and crossUp
bool bearBreak = barstate.isconfirmed and armed and crossDn
// ── Plots ──────────────────────────────────────────────────────────────
plot(basis, "Basis", color = color.new(color.gray, 20))
pUpper = plot(upper, "Upper band", color = inSqueeze ? sqCol : color.new(bullCol, 30))
pLower = plot(lower, "Lower band", color = inSqueeze ? sqCol : color.new(bearCol, 30))
fill(pUpper, pLower, color = showFill ? color.new(inSqueeze ? sqCol : color.gray, 90) : na, title = "Band fill")
plot(bandwidth, "Bandwidth %", display = display.data_window)
plot(bwRank, "Bandwidth percentile", display = display.data_window)
plotshape(showDots and inSqueeze, "Squeeze", shape.circle, location.belowbar, sqCol, size = size.tiny)
plotshape(bullBreak, "Bullish breakout", shape.triangleup, location.belowbar, bullCol, size = size.small)
plotshape(bearBreak, "Bearish breakout", shape.triangledown, location.abovebar, bearCol, size = size.small)
// ── Alerts ─────────────────────────────────────────────────────────────
alertcondition(sqStart and barstate.isconfirmed, "Squeeze started", "FXP BB Squeeze: bandwidth entered squeeze on {{ticker}} {{interval}}")
alertcondition(bullBreak, "Bullish squeeze breakout", "FXP BB Squeeze: bullish breakout on {{ticker}} {{interval}} at {{close}}")
alertcondition(bearBreak, "Bearish squeeze breakout", "FXP BB Squeeze: bearish breakout on {{ticker}} {{interval}} at {{close}}")

