How to Use Bollinger Bands in Pine Script v6: Volatility Measurement and Breakout Detection

Bollinger Bands are a statistical volatility envelope built from a simple moving average and a rolling standard deviation, giving traders a dynamic price channel that expands during high-volatility regimes and contracts during low-volatility regimes. Understanding the mathematics behind the bands — and the Pine Script v6 constraints that govern their implementation — is essential before writing production-quality indicators. This article walks through the exact formulas, type-system rules, and verified code patterns required to build a correct Bollinger Band indicator in Pine Script v6.

1. Mathematical Foundation

Given a source series $x_t$ and a lookback window of $n$ bars, the three Bollinger Band lines are defined as:

$$\text{Basis}_t = \frac{1}{n}\sum_{i=0}^{n-1} x_{t-i}$$

$$\text{Upper}_t = \text{Basis}_t + k \cdot \sigma_t$$

$$\text{Lower}_t = \text{Basis}_t - k \cdot \sigma_t$$

where $k$ is the standard-deviation multiplier (commonly $2.0$) and $\sigma_t$ is the population standard deviation of the same $n$-bar window:

$$\sigma_t = \sqrt{\frac{1}{n}\sum_{i=0}^{n-1}\left(x_{t-i} - \text{Basis}_t\right)^2}$$

Pine Script's ta.stdev() computes this value directly. Because both ta.sma() and ta.stdev() accept series int for their length parameter, dynamic (per-bar) lengths are legal with these two functions — unlike ta.ema() or ta.rsi(), which require simple int.

2. Key Pine Script v6 Type-System Rules for Bollinger Bands

Before writing any code, the following type constraints must be understood:

Function length qualifier Dynamic length legal?
ta.sma() series int ✅ Yes
ta.stdev() series int ✅ Yes
ta.bb() simple int ❌ No — use manual calculation instead
ta.ema() simple int ❌ No

Critical trap: The built-in ta.bb() wrapper requires a simple int length. If you want an adaptive or dynamically-sized band, you cannot use ta.bb(). You must compute the basis and standard deviation manually using ta.sma() and ta.stdev(), both of which accept series int.

Tuple return order for ta.bb(): The official v6 Reference Manual specifies the return order as [basis, upper, lower] — basis is always first. Never unpack as [upper, basis, lower].

3. Bollinger Band Width and %B

Two derived metrics are commonly used alongside the raw bands:

Band Width (BW) — measures the relative width of the channel, useful for detecting the Squeeze (low-volatility compression before a breakout):

$$BW_t = \frac{\text{Upper}_t - \text{Lower}_t}{\text{Basis}_t}$$

%B — normalizes the current price position within the band, ranging from 0 (at lower band) to 1 (at upper band):

$$\%B_t = \frac{x_t - \text{Lower}_t}{\text{Upper}_t - \text{Lower}_t}$$

A %B above 1.0 indicates price is above the upper band; below 0.0 indicates price is below the lower band. Both conditions are statistically significant given the $\pm 2\sigma$ envelope contains approximately 95% of observations under a normal distribution assumption.

graph TD A[Source Series: close] --> B[ta.sma length=bbLength] A --> C[ta.stdev length=bbLength] B --> D[Basis Middle Band] C --> E[dev Rolling Std Dev] D --> F[Upper = Basis + mult x dev] D --> G[Lower = Basis - mult x dev] F --> H[plot Upper Band red] D --> I[plot Basis blue] G --> J[plot Lower Band green] F --> K[Band Width = upper-lower divided by basis] G --> K D --> K K --> L[ta.lowest BW squeezeLookback] K --> M[percentB = src-lower divided by upper-lower] L --> N{BW <= lowestBW?} N -->|Yes| O[isSqueeze = true] N -->|No| P[isSqueeze = false] O --> Q[plotshape Squeeze Diamond orange] K --> R[plot BandWidth separate pane] M --> S[plot percentB separate pane]

4. Interpretation Logic: Breakout vs. Reversal

Bollinger Bands do not inherently signal direction — they signal volatility state and relative price position. The two primary interpretation frameworks are:

Condition Band Width State %B Reading Interpretation
Price closes above Upper Band Expanding > 1.0 Potential breakout continuation (momentum)
Price closes below Lower Band Expanding < 0.0 Potential breakdown continuation (momentum)
Price touches Upper Band then reverses Contracting Drops from >1.0 Potential mean-reversion short signal
Squeeze: BW at N-bar low At minimum Near 0.5 Volatility compression — breakout imminent (direction unknown)

5. Complete Pine Script v6 Implementation

The following script implements a full Bollinger Band indicator with Band Width, %B, and a Squeeze detector. It uses ta.sma() and ta.stdev() directly (both accept series int) rather than ta.bb(), making it compatible with dynamic length inputs. The Squeeze is detected when the current Band Width is at its lowest value over the past squeezeLookback bars.

🔽 [Click to expand] View Full Pine Script Code

6. Logic Flow Diagram

The diagram below maps the data flow from raw price input through to each visual output of the indicator.

graph TD A[Source Series: close] --> B[ta.sma length=bbLength] A --> C[ta.stdev length=bbLength] B --> D[Basis Middle Band] C --> E[dev Rolling Std Dev] D --> F[Upper = Basis + mult x dev] D --> G[Lower = Basis - mult x dev] F --> H[plot Upper Band red] D --> I[plot Basis blue] G --> J[plot Lower Band green] F --> K[Band Width = upper-lower divided by basis] G --> K D --> K K --> L[ta.lowest BW squeezeLookback] K --> M[percentB = src-lower divided by upper-lower] L --> N{BW <= lowestBW?} N -->|Yes| O[isSqueeze = true] N -->|No| P[isSqueeze = false] O --> Q[plotshape Squeeze Diamond orange] K --> R[plot BandWidth separate pane] M --> S[plot percentB separate pane]

7. Adaptive Bollinger Bands: Dynamic Length Pattern

Because ta.sma() and ta.stdev() both accept series int, you can compute a dynamic length that changes per bar — for example, scaling the window with ATR-based volatility. The key constraint is that ta.atr() requires simple int for its own length, so the ATR period must come from a static source such as input.int().

🔽 [Click to expand] View Adaptive Length Pattern
//@version=6
indicator("Adaptive BB — Dynamic Length Demo", overlay = true)

// Static ATR period — simple int required by ta.atr()
int    atrPeriod  = input.int(14, "ATR Period", minval = 1)
float  atrMult    = input.float(2.0, "ATR → Length Multiplier", minval = 0.1)
int    minLen     = input.int(10,  "Min BB Length", minval = 2)
int    maxLen     = input.int(100, "Max BB Length", minval = 10)
float  bbMult     = input.float(2.0, "BB Multiplier", minval = 0.1)

// ta.atr() requires simple int — input.int() satisfies this (input ≤ simple)
float atrValue = ta.atr(atrPeriod)

// Safe dynamic length
float rawLen = nz(math.round(atrValue * atrMult), minLen)

// Compute a dynamic length as a series int — legal for ta.sma() and ta.stdev()
// int(series float) → series int (qualifier unchanged)
int dynLen = math.max(minLen, math.min(maxLen, int(rawLen)))

// ta.sma() and ta.stdev() accept series int — dynamic length is fully legal

float basis = ta.sma(close, dynLen)
float dev   = ta.stdev(close, dynLen)
float upper = basis + bbMult * dev
float lower = basis - bbMult * dev

plot(basis, "Adaptive Basis", color.blue,  1)
plot(upper, "Adaptive Upper", color.red,   1)
plot(lower, "Adaptive Lower", color.green, 1)

Why this works: int(math.round(...)) produces a series int because the operand chain includes atrValue, which is a series float. Passing a series int to ta.sma() or ta.stdev() is legal. Passing the same series int to ta.ema() or ta.rsi() would cause a compile error because those functions require simple int.

8. Common Pitfalls and Anti-Patterns

Pitfall Wrong Pattern Correct Pattern
Using ta.bb() with a dynamic length ta.bb(close, dynLen, 2.0) — compile error (series int → simple int) Use ta.sma() + ta.stdev() manually
Wrong tuple unpack order from ta.bb() [upper, basis, lower] = ta.bb(...) [basis, upper, lower] = ta.bb(...)
Division by zero in Band Width (upper - lower) / basis — na if basis is 0 basis != 0 ? (upper - lower) / basis : na
Using hline() for %B reference lines in overlay script hline(1.0) inside overlay=true — squashes price scale plot(1.0, force_overlay = false)
Declaring input.source() result as input float input float src = input.source(close) — CE10159 float src = input.source(close)

9. Conclusion

  • Mathematical core: Bollinger Bands are a $\pm k\sigma$ envelope around a rolling SMA. The Band Width ratio and %B metric are the primary derived signals for volatility state and relative price position respectively.
  • Type-system constraint: ta.bb() requires simple int for length and cannot accept dynamic (series int) values. For adaptive bands, always compute basis and standard deviation manually using ta.sma() and ta.stdev(), which both accept series int.
  • Overlay safety: In overlay = true scripts, use plot(..., force_overlay = false) for sub-pane reference lines instead of hline(), which always renders in the indicator's primary pane and would distort the price scale.

Ideas for Further Development

  1. Bollinger Band Squeeze with momentum confirmation: Combine the Band Width Squeeze detector with a momentum oscillator (e.g., a manually computed linear regression slope of the source) to add directional bias to the squeeze signal, reducing false breakout alerts.
  2. Multi-timeframe Band Width comparison: Use request.security() with close[1] and barmerge.lookahead_on (the official non-repainting pattern) to fetch the confirmed Band Width from a higher timeframe and compare it against the current timeframe's Band Width, creating a cross-timeframe volatility compression signal.

Related Posts

Comments

Popular posts from this blog

How to Build a Volume Indicator in Pine Script v6: Buying & Selling Pressure Analysis

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

SuperTrend Indicator in Pine Script v6: Canonical Formula, Flip Logic, and Non-Repainting Signals