Bollinger BandWidth
Bollinger BandWidth quantifies the width of Bollinger Bands as a percentage, helping traders identify low-volatility squeeze conditions that often precede significant price moves.
Problem
You have Bollinger Bands but want to objectively measure when volatility is unusually low. Visually spotting a "squeeze" is subjective. BandWidth provides a numeric value you can compare against historical levels to identify when bands are at their historical lows.
What is BandWidth?
BandWidth measures the percentage difference between the upper and lower Bollinger Bands:
BandWidth = ((Upper Band - Lower Band) / Middle Band) × 100
When BandWidth drops to historically low levels, the bands are in a "squeeze". Periods of low volatility are often followed by high volatility, so a squeeze suggests a significant price move may be coming. The squeeze does not indicate direction, only that a breakout is likely.
Solution
DECLARE
@symbol := 'BTC-USDT',
@history_start := dateadd('M', -6, now()),
@display_start := dateadd('M', -1, now())
WITH daily_ohlc AS (
SELECT
timestamp,
symbol,
first(open) AS open,
max(high) AS high,
min(low) AS low,
last(close) AS close
FROM trades_ohlc_15m
WHERE symbol = @symbol
AND timestamp > @history_start
SAMPLE BY 1d
),
bands AS (
SELECT
timestamp,
symbol,
close,
AVG(close) OVER (
PARTITION BY symbol
ORDER BY timestamp
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS sma20,
AVG(close * close) OVER (
PARTITION BY symbol
ORDER BY timestamp
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS avg_close_sq
FROM daily_ohlc
),
bollinger AS (
SELECT
timestamp,
symbol,
close,
sma20,
sma20 + 2 * sqrt(avg_close_sq - (sma20 * sma20)) AS upper_band,
sma20 - 2 * sqrt(avg_close_sq - (sma20 * sma20)) AS lower_band
FROM bands
),
with_bandwidth AS (
SELECT
timestamp,
symbol,
close,
sma20,
upper_band,
lower_band,
(upper_band - lower_band) / sma20 * 100 AS bandwidth
FROM bollinger
),
with_range AS (
SELECT
timestamp,
symbol,
close,
sma20,
upper_band,
lower_band,
bandwidth,
min(bandwidth) OVER (PARTITION BY symbol) AS min_bw,
max(bandwidth) OVER (PARTITION BY symbol) AS max_bw
FROM with_bandwidth
)
SELECT
timestamp,
symbol,
round(close, 2) AS close,
round(sma20, 2) AS sma20,
round(upper_band, 2) AS upper_band,
round(lower_band, 2) AS lower_band,
round(bandwidth, 4) AS bandwidth,
round((bandwidth - min_bw) / (max_bw - min_bw) * 100, 1) AS range_position
FROM with_range
WHERE timestamp > @display_start
ORDER BY timestamp;
The query first aggregates 15-minute candles into daily OHLC, then calculates standard 20-day Bollinger Bands. This matches the traditional approach where SMA20 represents roughly one month of trading. The 6-month lookback (@history_start) establishes the historical range, while @display_start limits output to the last month. Standard deviation uses the variance formula sqrt(avg(x²) - avg(x)²).
The range_position shows where current BandWidth falls within the 6-month range: 0% means at the historical minimum, 100% at the maximum. This works well for identifying squeeze conditions since you're comparing against historical extremes.
Interpreting results
- Low range position (< 20%): BandWidth is near 6-month lows, indicating a squeeze
- High range position (> 80%): BandWidth is near 6-month highs, volatility is elevated
- Rising BandWidth: Volatility increasing, bands expanding
- Falling BandWidth: Volatility decreasing, bands contracting
A squeeze signals that volatility expansion is likely, but not the direction. Use price action or other indicators to determine breakout direction.