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.
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.
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 usingta.sma()andta.stdev(), which both accept series int. - Overlay safety: In
overlay = truescripts, useplot(..., force_overlay = false)for sub-pane reference lines instead ofhline(), which always renders in the indicator's primary pane and would distort the price scale.
Ideas for Further Development
- 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.
- Multi-timeframe Band Width comparison: Use
request.security()withclose[1]andbarmerge.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.
Comments
Post a Comment