What Is the TTM Squeeze? Formula and Limits
The TTM Squeeze fires when Bollinger bands contract inside the Keltner channel. Here is the exact formula, and the settings that make two charts disagree.
The TTM Squeeze is a volatility compression indicator: it fires when a stock's Bollinger bands contract entirely inside its Keltner channel. Both tools measure how much a stock has been moving lately, one from the standard deviation of closing prices and the other from average true range. A squeeze is the moment the first measure drops below the second, and on its own it says nothing about direction.
What is the TTM Squeeze indicator?
John Carter's Trade The Markets desk popularised the pairing in the 2000s, which is where the TTM initials come from. Most platforms draw it as a row of dots under the price chart: one colour while the Bollinger bands sit inside the Keltner channel (squeeze on) and another once they push back outside (squeeze off). The momentum histogram that usually sits beside the dots is a separate calculation with its own parameters, and it is not part of the squeeze test.
The dots are the part every reader recognises. The arithmetic behind them is the part the documentation tends to skip, which is how two platforms end up lighting different dots on the same stock on the same day. Everything below is that arithmetic in full, followed by the settings that split the answer.
The TTM Squeeze formula, both bands in full
Bollinger bands, from John Bollinger's 1980s work, measure dispersion of the close around a moving average:
- Basis: the 20-period simple moving average of the close.
- Sigma: the standard deviation of those same 20 closes.
- Upper band is basis plus 2 sigma. Lower band is basis minus 2 sigma.
Keltner channels measure the average size of a bar instead:
- True range of one bar: the largest of the bar's high minus its low, the absolute distance from the prior close to the high, and the absolute distance from the prior close to the low.
- ATR: the average true range over the lookback, 20 bars in the classic squeeze pairing.
- Upper channel is basis plus 1.5 ATR. Lower channel is basis minus 1.5 ATR.
The squeeze test is a containment test on those four lines. The Bollinger upper band must sit below the Keltner upper channel, and the Bollinger lower band must sit above the Keltner lower channel. When both use the same 20-period basis, the basis cancels out of both sides and the whole indicator collapses into a single comparison of widths: a squeeze is on while 4 sigma is less than 3 ATR, which is 2 sigma each side measured against 1.5 ATR each side.
That one inequality is the indicator. The panel below traces both widths for Coca-Cola (KO) across the first quarter of 2026, each stated as a percentage of the 20-day basis so the two are directly comparable, with their ratio in the last column.
| date | date_label | bb_width_pct | kc_width_pct | squeeze_ratio |
|---|---|---|---|---|
| 2026-01-02 | Jan 2 | 2.44 | 3.39 | 0.719 |
| 2026-01-05 | Jan 5 | 3.61 | 3.45 | 1.047 |
| 2026-01-06 | Jan 6 | 4.5 | 3.42 | 1.318 |
| 2026-01-07 | Jan 7 | 5.35 | 3.4 | 1.571 |
| 2026-01-08 | Jan 8 | 5.35 | 3.67 | 1.46 |
| 2026-01-09 | Jan 9 | 5.41 | 3.83 | 1.41 |
| 2026-01-12 | Jan 12 | 5.42 | 3.58 | 1.514 |
| 2026-01-13 | Jan 13 | 5.64 | 3.45 | 1.638 |
| 2026-01-14 | Jan 14 | 5.82 | 3.42 | 1.701 |
| 2026-01-15 | Jan 15 | 5.84 | 3.48 | 1.679 |
| 2026-01-16 | Jan 16 | 5.8 | 3.5 | 1.657 |
| 2026-01-20 | Jan 20 | 6.23 | 3.79 | 1.646 |
| 2026-01-21 | Jan 21 | 6.75 | 3.93 | 1.718 |
| 2026-01-22 | Jan 22 | 7.12 | 3.93 | 1.812 |
| 2026-01-23 | Jan 23 | 7.89 | 4.08 | 1.935 |
| 2026-01-26 | Jan 26 | 8.37 | 4.14 | 2.021 |
| 2026-01-27 | Jan 27 | 9.19 | 4.31 | 2.13 |
| 2026-01-28 | Jan 28 | 9.65 | 4.41 | 2.19 |
| 2026-01-29 | Jan 29 | 10.15 | 4.52 | 2.245 |
| 2026-01-30 | Jan 30 | 11.11 | 4.75 | 2.337 |
The exact SQL behind every number
WITH
px AS
(
SELECT
date,
toFloat64(any(close)) AS c,
toFloat64(any(high)) AS h,
toFloat64(any(low)) AS l
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'KO'
AND date >= '2025-11-03'
AND date < '2026-04-01'
GROUP BY date
),
tr AS
(
SELECT
date,
c,
h,
l,
lagInFrame(c) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
FROM px
),
stat AS
(
SELECT
date,
concat(formatDateTime(date, '%b'), ' ', toString(toDayOfMonth(date))) AS date_label,
avg(c) OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS basis,
stddevPop(c) OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS sd,
count() OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS bars,
avg(if(prev_c > 0, greatest(h - l, abs(h - prev_c), abs(l - prev_c)), h - l))
OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS atr
FROM tr
)
SELECT
toString(date) AS date,
date_label,
round(100 * 4 * sd / basis, 2) AS bb_width_pct,
round(100 * 3 * atr / basis, 2) AS kc_width_pct,
round(4 * sd / (3 * atr), 3) AS squeeze_ratio
FROM stat
WHERE bars = 20
AND date >= '2026-01-02'
ORDER BY dateOver the 61 sessions between Jan 2 and Mar 31, the Bollinger width opened at 2.44% of the basis against a Keltner width of 3.39%. The last column divides the first by the second. A squeeze ratio under 1.00 is a squeeze, since the bands are then inside the channel. It began this stretch at 0.719 and finished at 1.242. Read the two lines rather than a dot: the squeeze is any region where the Bollinger line dips beneath the Keltner line, and the ratio shows how close a near miss was.
Why two charts disagree about the same squeeze
Nothing in that formula is standardised, and each platform picks its own defaults. The ones in common use as of September 2026 vary along six axes:
- The standard deviation denominator. Dividing by n (population) or by n minus 1 (sample) changes sigma by about 2.6% at a 20-bar lookback, which is enough to flip a borderline session.
- The ATR lookback. 20 bars in the original pairing, 14 in Wilder's convention, 10 on several charting defaults.
- The ATR smoothing. A plain average of true ranges against Wilder's smoothing, which carries old bars forward and moves more slowly.
- The Keltner multiplier. 1.5 is the squeeze convention, while stock Keltner channel tools commonly ship 2.0, and some squeeze variants use 1.0.
- The Keltner basis. A simple moving average of the close on some platforms, an exponential average on others, and the typical price, meaning high plus low plus close over three, on a few.
- The bar set. Whether extended-hours bars are in the series at all, which changes highs, lows, and closes before any of the above runs.
The multiplier is the loudest of the six. The next panel holds the price history, the basis, and the Bollinger settings fixed, then counts how many of three years of KO sessions each parameter set calls a squeeze.
| label | squeeze_count | squeeze_share_pct |
|---|---|---|
| KC ATR20 x2.0 | 377 | 50.2 |
| BB 1.5 sigma, KC ATR20 x1.5 | 377 | 50.2 |
| KC ATR14 x1.5 | 135 | 18 |
| KC ATR10 x1.5 | 133 | 17.7 |
| KC ATR20 x1.5 (squeeze default) | 126 | 16.8 |
| KC ATR20 x1.0 | 13 | 1.7 |
The exact SQL behind every number
WITH
px AS
(
SELECT
date,
toFloat64(any(close)) AS c,
toFloat64(any(high)) AS h,
toFloat64(any(low)) AS l
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'KO'
AND date >= '2023-07-01'
AND date < '2026-09-01'
GROUP BY date
),
tr AS
(
SELECT
date,
c,
h,
l,
lagInFrame(c) OVER (ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
FROM px
),
stat AS
(
SELECT
date,
stddevPop(c) OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS sd,
count() OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS bars,
avg(if(prev_c > 0, greatest(h - l, abs(h - prev_c), abs(l - prev_c)), h - l))
OVER (ORDER BY date ROWS BETWEEN 9 PRECEDING AND CURRENT ROW) AS atr10,
avg(if(prev_c > 0, greatest(h - l, abs(h - prev_c), abs(l - prev_c)), h - l))
OVER (ORDER BY date ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS atr14,
avg(if(prev_c > 0, greatest(h - l, abs(h - prev_c), abs(l - prev_c)), h - l))
OVER (ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS atr20
FROM tr
),
graded AS
(
SELECT
label,
2 * sd_mult * sd AS bb_span,
2 * atr_mult * multiIf(atr_len = 10, atr10, atr_len = 14, atr14, atr20) AS kc_span
FROM stat
ARRAY JOIN
['KC ATR20 x1.5 (squeeze default)', 'KC ATR20 x1.0', 'KC ATR20 x2.0',
'KC ATR14 x1.5', 'KC ATR10 x1.5', 'BB 1.5 sigma, KC ATR20 x1.5'] AS label,
[2.0, 2.0, 2.0, 2.0, 2.0, 1.5] AS sd_mult,
[20, 20, 20, 14, 10, 20] AS atr_len,
[1.5, 1.0, 2.0, 1.5, 1.5, 1.5] AS atr_mult
WHERE bars = 20
AND date >= '2023-09-01'
)
SELECT
label,
countIf(bb_span < kc_span) AS squeeze_count,
round(100 * countIf(bb_span < kc_span) / count(), 1) AS squeeze_share_pct
FROM graded
GROUP BY label
ORDER BY squeeze_count DESCRanked from most squeeze sessions to fewest, the widest channel in the group tags 377 sessions, 50.2% of the sample, and the tightest tags 13. Same stock, same closes, same three years. A reader flipping between two platforms is comparing two different questions, and the settings panel is the only place that difference is visible. This is the same gap behind why RSI differs between platforms: a named indicator is a family of formulas, and the family name alone does not pin a number.
How often is a stock in a squeeze?
Compression is not rare, and it is not spread evenly across names. The next panel runs the default test, 4 sigma against 3 ATR, on six liquid tickers over the same three years.
| ticker | squeeze_count | squeeze_share_pct |
|---|---|---|
| JNJ | 130 | 17.3 |
| KO | 126 | 16.8 |
| MSFT | 113 | 15 |
| NVDA | 77 | 10.3 |
| SPY | 70 | 9.3 |
| AAPL | 47 | 6.3 |
The exact SQL behind every number
WITH
px AS
(
SELECT
ticker,
date,
toFloat64(any(close)) AS c,
toFloat64(any(high)) AS h,
toFloat64(any(low)) AS l
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('AAPL', 'MSFT', 'NVDA', 'SPY', 'KO', 'JNJ')
AND date >= '2023-07-01'
AND date < '2026-09-01'
GROUP BY ticker, date
),
tr AS
(
SELECT
ticker,
date,
c,
h,
l,
lagInFrame(c) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
FROM px
),
stat AS
(
SELECT
ticker,
date,
stddevPop(c) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS sd,
count() OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS bars,
avg(if(prev_c > 0, greatest(h - l, abs(h - prev_c), abs(l - prev_c)), h - l))
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS atr
FROM tr
)
SELECT
ticker,
countIf(4 * sd < 3 * atr) AS squeeze_count,
round(100 * countIf(4 * sd < 3 * atr) / count(), 1) AS squeeze_share_pct
FROM stat
WHERE bars = 20
AND date >= '2023-09-01'
GROUP BY ticker
ORDER BY squeeze_share_pct DESCJNJ spent the largest share of those sessions in a squeeze at 17.3%, and AAPL the smallest at 6.3%. The mechanics behind the spread are worth holding onto. A name that grinds sideways keeps its 20 closes bunched near their own average, so sigma collapses while daily ranges carry on, and the test registers often. A name that trends hard pulls those same closes apart, sigma stays wide, and the squeeze rarely appears no matter how ordinary the daily ranges are. The reading is a statement about the shape of the recent path, which is also why the names on the lowest volatility stocks screen do not automatically top this one.
Does a squeeze say anything about direction?
No. The test compares two widths, and a width has no sign. Nothing in 4 sigma against 3 ATR distinguishes an upward resolution from a downward one. The panel below splits every session in the same basket into squeeze and no squeeze, then measures what the following ten sessions did from each group.
| label | observation_count | up_share_pct | avg_abs_move_pct |
|---|---|---|---|
| In a squeeze | 558 | 55 | 3.52 |
| No squeeze | 3888 | 57.8 | 3.78 |
The exact SQL behind every number
WITH
px AS
(
SELECT
ticker,
date,
toFloat64(any(close)) AS c,
toFloat64(any(high)) AS h,
toFloat64(any(low)) AS l
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('AAPL', 'MSFT', 'NVDA', 'SPY', 'KO', 'JNJ')
AND date >= '2023-07-01'
AND date < '2026-09-01'
GROUP BY ticker, date
),
tr AS
(
SELECT
ticker,
date,
c,
h,
l,
lagInFrame(c) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING) AS prev_c
FROM px
),
stat AS
(
SELECT
ticker,
date,
c,
stddevPop(c) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS sd,
count() OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS bars,
avg(if(prev_c > 0, greatest(h - l, abs(h - prev_c), abs(l - prev_c)), h - l))
OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN 19 PRECEDING AND CURRENT ROW) AS atr
FROM tr
),
fwd AS
(
SELECT
date,
c,
bars,
if(4 * sd < 3 * atr, 'In a squeeze', 'No squeeze') AS label,
leadInFrame(c, 10) OVER (PARTITION BY ticker ORDER BY date ROWS BETWEEN CURRENT ROW AND 10 FOLLOWING) AS c_fwd
FROM stat
)
SELECT
label,
count() AS observation_count,
round(100 * countIf(c_fwd > c) / count(), 1) AS up_share_pct,
round(100 * avg(abs(c_fwd / c - 1)), 2) AS avg_abs_move_pct
FROM fwd
WHERE bars = 20
AND date >= '2023-09-01'
AND c_fwd > 0
GROUP BY label
ORDER BY labelAcross 558 squeeze sessions, 55% closed higher ten sessions later, against 57.8% of the 3888 sessions with no squeeze. The average absolute move over the following ten sessions measured 3.52% from a squeeze reading and 3.78% otherwise. Both direction figures sit near the middle. The compression test contains no directional term, and these counts do not add one. Treat the numbers as a co-occurrence count over one basket and one window, not as a tested strategy.
Compression is a setup, not a forecast
Everything the squeeze knows is behind it. Sigma and ATR are both measured on bars that have already printed, which makes the indicator a description of realised volatility relative to that stock's own recent range. A tight reading tells you the last 20 bars were quiet in one specific sense. It does not carry an expectation about the next 20.
The forward-looking version of the same question lives in the options market, where implied volatility is the volatility number embedded in option prices for a future window. Historical volatility versus implied volatility walks the two side by side, and what implied volatility is defines the forward measure from scratch. A reader asking whether the quiet is expected to continue is asking an implied volatility question, and band widths cannot answer it. Readers often check compression alongside participation measures such as relative volume, or against a cost basis line like anchored VWAP, for context the squeeze itself leaves out.
How these panels were computed
One convention was fixed everywhere above so the parameter comparison isolates one thing at a time. Sigma is the population standard deviation of the last 20 closing prices. ATR is a plain average of true ranges over its lookback, with the first bar of each series excluded from published rows through a warmup buffer, since a true range needs a prior close. Both bands share a 20-period simple moving average basis, which is what allows the containment test to reduce to the width comparison. Daily bars are regular-session aggregates, so an intraday chart with a different bar set will not reproduce these exact widths. Every panel is pinned to a closed historical window, so the figures quoted in the prose stay fixed.
FAQ
What does it mean when the TTM Squeeze fires?
It means the Bollinger bands have contracted entirely inside the Keltner channel, which happens when 4 sigma of the last 20 closes is less than 3 ATR. In plain terms, the dispersion of recent closing prices has fallen below the average size of a recent bar.
What are the default TTM Squeeze settings?
The common setup is Bollinger bands at 20 periods and 2 standard deviations, paired with a Keltner channel at 20 periods and a 1.5 ATR multiplier, both on the same 20-period basis. Platforms vary the ATR lookback, the ATR smoothing, the multiplier, and the basis type, and a squeeze indicator with a 1.0 or 2.0 multiplier is a different measurement wearing the same name.
Why does the TTM Squeeze look different on two platforms?
Each platform fills in the parameters the name leaves open. Changing only the Keltner multiplier across three years of one stock changed the number of squeeze sessions found by a wide margin in the panel above, with the price history held identical.
Is a TTM Squeeze bullish or bearish?
Neither. The calculation compares two unsigned widths, so it has no way to express direction, and the forward counts above land near even on both sides of the split. A squeeze describes the state of recent movement and nothing more.
Does the TTM Squeeze work on shorter timeframes?
The formula applies to any bar interval, since it only needs highs, lows, and closes. The reading is not comparable across intervals, though: a 5-minute squeeze and a daily squeeze measure compression over very different spans of time, and an intraday series that includes extended-hours bars will produce different widths again.
Every panel here carries the exact SQL that produced it, so you can expand one and see how a squeeze was counted. To run the same width comparison on a ticker you follow, ask for it in plain English on the Strasmore terminal.