Strasmore Research
Learn Matt ConnorBy Matt Connor

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.

QueryBollinger width against Keltner width, KO, first quarter 2026
61 rows (showing 20)
datedate_labelbb_width_pctkc_width_pctsqueeze_ratio
2026-01-02Jan 22.443.390.719
2026-01-05Jan 53.613.451.047
2026-01-06Jan 64.53.421.318
2026-01-07Jan 75.353.41.571
2026-01-08Jan 85.353.671.46
2026-01-09Jan 95.413.831.41
2026-01-12Jan 125.423.581.514
2026-01-13Jan 135.643.451.638
2026-01-14Jan 145.823.421.701
2026-01-15Jan 155.843.481.679
2026-01-16Jan 165.83.51.657
2026-01-20Jan 206.233.791.646
2026-01-21Jan 216.753.931.718
2026-01-22Jan 227.123.931.812
2026-01-23Jan 237.894.081.935
2026-01-26Jan 268.374.142.021
2026-01-27Jan 279.194.312.13
2026-01-28Jan 289.654.412.19
2026-01-29Jan 2910.154.522.245
2026-01-30Jan 3011.114.752.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 date
Run this yourself

Over 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.

QuerySqueeze sessions found by each parameter set, KO, three years
labelsqueeze_countsqueeze_share_pct
KC ATR20 x2.037750.2
BB 1.5 sigma, KC ATR20 x1.537750.2
KC ATR14 x1.513518
KC ATR10 x1.513317.7
KC ATR20 x1.5 (squeeze default)12616.8
KC ATR20 x1.0131.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 DESC
Run this yourself

Ranked 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.

QueryShare of sessions in a squeeze, six liquid names, three years
tickersqueeze_countsqueeze_share_pct
JNJ13017.3
KO12616.8
MSFT11315
NVDA7710.3
SPY709.3
AAPL476.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 DESC
Run this yourself

JNJ 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.

QueryWhat the next ten sessions did, squeeze against no squeeze
labelobservation_countup_share_pctavg_abs_move_pct
In a squeeze558553.52
No squeeze388857.83.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 label
Run this yourself

Across 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.

#indicators#bollinger bands#keltner channel#volatility#technical analysis