Strasmore Research
Deep Dives Matt ConnorBy Matt Connor · data as of September 19, 2026 · refreshed weekly

What Happens When the Yield Curve Un-Inverts

What happens when the yield curve un-inverts: each 2s10s un-inversion on record, the spread and SPY 3, 6 and 12 months later, bull vs bear steepener labeled.

What happens when the yield curve un-inverts? The 2s10s spread, the 10-year Treasury yield minus the 2-year, closes back above zero and the curve returns to its normal upward slope. The record below finds every such crossing in the daily Treasury yield history, tracks the spread and SPY over the following 3, 6 and 12 months, and labels each episode by whether the steepening came from the 2-year falling or the 10-year rising.

What does it mean when the yield curve un-inverts?

The yield curve is the line you get by plotting Treasury yields from the shortest maturity to the longest. Most of the time it slopes upward: lenders ask more yield to lock money up for ten years than for two. An inversion is the opposite shape, with the 2-year yield above the 10-year, and it shows up as a negative 2s10s spread. Un-inversion is the day that spread closes back above zero.

One close above zero is noise. The test used on every panel below requires a sustained change of shape: the spread must have been negative on at least 35 of the prior 60 sessions and must stay positive on at least 19 of the next 21. The first session that passes is the crossing date. In several past cycles the un-inversion, rather than the inversion, was the event that sat closest to the start of a recession, and that timing is what earned the crossing its own name. The sister measure, the 3-month/10-year spread, tends to cross later, since the 3-month bill tracks the policy rate and falls only once the Federal Reserve actually cuts.

How many times has the 2s10s un-inverted?

The monthly picture first. Each point is the average daily spread for that month, so the inversions read as dips below the zero line and the un-inversions as the climbs back through it.

QueryThe 2s10s spread by month, full history
604 rows (showing 20)
monthmonth_labelspread_2s10sy2_yieldy10_yield
1976-06-01Jun 19760.87.067.86
1976-07-01Jul 19760.986.857.83
1976-08-01Aug 19761.146.637.77
1976-09-01Sep 19761.186.427.59
1976-10-01Oct 19761.435.987.41
1976-11-01Nov 19761.485.817.29
1976-12-01Dec 19761.495.386.87
1977-01-01Jan 19771.315.97.21
1977-02-01Feb 19771.36.097.39
1977-03-01Mar 19771.376.097.46
1977-04-01Apr 19771.415.967.37
1977-05-01May 19771.26.257.46
1977-06-01Jun 19771.156.137.28
1977-07-01Jul 19771.066.277.33
1977-08-01Aug 19770.796.617.4
1977-09-01Sep 19770.636.717.34
1977-10-01Oct 19770.427.117.52
1977-11-01Nov 19770.437.147.58
1977-12-01Dec 19770.517.187.69
1978-01-01Jan 19780.477.497.96
The exact SQL behind every number
SELECT
    toStartOfMonth(date)                                                  AS month,
    formatDateTime(month, '%b %Y')                                        AS month_label,
    round(avg(toFloat64(yield_10_year) - toFloat64(yield_2_year)), 2)     AS spread_2s10s,
    round(avg(toFloat64(yield_2_year)), 2)                                AS y2_yield,
    round(avg(toFloat64(yield_10_year)), 2)                               AS y10_yield
FROM global_markets.treasury_yields
WHERE yield_2_year > 0 AND yield_10_year > 0
GROUP BY month
ORDER BY month
Run this yourself

The history runs from Jun 1976 across 604 months. Applying the session-level test to the daily data inside that span, the count of un-inversions that pass it and have a full year of follow-on data is 7, running from May 1980 to Sep 2024. That is the entire sample. It is a short list of dated events rather than a distribution: nothing on this page is an average or a hit rate, and a handful of episodes cannot support one.

QueryEvery 2s10s un-inversion: the crossing and the spread 3, 6 and 12 months later
episodecrossed_oncrossed_on_labelnegative_sessions_priortrough_spreadspread_3m_laterspread_6m_laterspread_12m_later
May 19801980-05-02May 2, 1980423-2.410.98-0.62-0.9
Oct 19811981-10-28Oct 28, 1981453-2.41-0.06-0.220.88
Jul 19821982-07-19Jul 19, 1982400-1.70.781.110.67
Jun 19891989-06-30Jun 30, 1989132-0.45-0.150.060.18
Dec 20002000-12-27Dec 27, 2000226-0.520.671.21.94
Mar 20072007-03-21Mar 21, 2007220-0.190.170.531.75
Sep 20242024-09-04Sep 4, 2024538-1.080.060.290.58
The exact SQL behind every number
WITH
daily AS
(
    SELECT
        date,
        dateDiff('day', toDate('1970-01-01'), date)   AS dnum,
        ifNull(toFloat64(max(yield_2_year)), 0.)      AS y2,
        ifNull(toFloat64(max(yield_10_year)), 0.)     AS y10,
        y10 - y2                                      AS spread
    FROM global_markets.treasury_yields
    WHERE yield_2_year > 0 AND yield_10_year > 0
    GROUP BY date
),
scored AS
(
    SELECT
        date, dnum, y2, y10, spread,
        count()         OVER (ORDER BY date ROWS BETWEEN 60 PRECEDING AND 1 PRECEDING) AS rows_prior,
        sum(spread < 0) OVER (ORDER BY date ROWS BETWEEN 60 PRECEDING AND 1 PRECEDING) AS neg_prior,
        sum(spread > 0) OVER (ORDER BY date ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING) AS pos_next
    FROM daily
),
candidates AS
(
    SELECT
        date, dnum, y2, y10, spread,
        lagInFrame(dnum, 1, toInt64(-100000)) OVER (ORDER BY dnum ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS prev_dnum
    FROM scored
    WHERE spread > 0 AND rows_prior = 60 AND neg_prior >= 35 AND pos_next >= 19
),
episodes AS
(
    SELECT date AS cross_date, y2, y10, spread
    FROM candidates
    WHERE dnum - prev_dnum > 180
      AND date <= today() - 380
)
SELECT
    formatDateTime(e.cross_date, '%b %Y')                                       AS episode,
    toString(toDate(e.cross_date))                                              AS crossed_on,
    concat(formatDateTime(e.cross_date, '%b '), toString(toDayOfMonth(e.cross_date)), ', ', toString(toYear(e.cross_date))) AS crossed_on_label,
    countIf(d.spread < 0 AND d.date < e.cross_date)                             AS negative_sessions_prior,
    round(minIf(d.spread, d.date < e.cross_date), 2)                            AS trough_spread,
    round(argMinIf(d.spread, d.date, d.date >= addDays(e.cross_date, 91)), 2)   AS spread_3m_later,
    round(argMinIf(d.spread, d.date, d.date >= addDays(e.cross_date, 182)), 2)  AS spread_6m_later,
    round(argMinIf(d.spread, d.date, d.date >= addDays(e.cross_date, 365)), 2)  AS spread_12m_later
FROM episodes AS e
CROSS JOIN daily AS d
WHERE d.date BETWEEN addDays(e.cross_date, -800) AND addDays(e.cross_date, 380)
GROUP BY e.cross_date
HAVING countIf(d.date >= addDays(e.cross_date, 365)) > 0
ORDER BY e.cross_date
Run this yourself

What did the spread do in the 3, 6 and 12 months after?

Read each row left to right. The trough column is the most negative close of the inversion that preceded the crossing, the sessions column counts how many closes below zero came in the 800 days before it, and the three forward columns are the spread's level on the first session at or after 91, 182 and 365 days from the crossing date.

Take the most recent episode. That inversion logged 538 negative closes and bottomed at -1.08 percentage points. The spread crossed zero on Sep 4, 2024, then read 0.06 three months later, 0.29 at six months and 0.58 a year on. The earlier rows carry their own paths, and the differences between them are the point: an un-inversion fixes the sign of the spread on one day and says nothing about its slope afterwards.

Bull steepener or bear steepener: which kind was each?

A steepener is any move that widens the spread. Two very different markets produce one.

A bull steepener is a steepening where yields fall and the 2-year falls faster than the 10-year. Bond prices rise as yields fall, hence "bull". It is the shape of a market pricing in rate cuts, and the 2-year, which sits closest to the policy rate, moves first; the mechanics are laid out in how markets price Fed rate odds.

A bear steepener is a steepening where yields rise and the 10-year rises faster than the 2-year. Bond prices fall, hence "bear". It is the shape of a market asking for more yield to hold long maturities, whether for inflation or for the supply of new Treasury debt.

The panel measures each leg over the 91 days before the crossing and labels the episode by the leg that moved most.

QueryWhich leg did the work: 2-year and 10-year moves over the 91 days before each crossing
episodecrossed_ony2_91d_beforey2_at_crossy10_91d_beforey10_at_crossy2_change_bpy10_change_bpsteepener_type
May 19801980-05-0212.0410.0411.2910.24-200-105bull steepener (2-year fell most)
Oct 19811981-10-2815.7615.4214.6115.44-3483bear steepener (10-year rose most)
Jul 19821982-07-1914.1813.5213.6613.68-662bull steepener (2-year fell most)
Jun 19891989-06-309.738.089.38.1-165-120bull steepener (2-year fell most)
Dec 20002000-12-2765.15.835.11-90-72bull steepener (2-year fell most)
Mar 20072007-03-214.714.514.64.53-20-7bull steepener (2-year fell most)
Sep 20242024-09-044.723.764.293.77-96-52bull steepener (2-year fell most)
The exact SQL behind every number
WITH
daily AS
(
    SELECT
        date,
        dateDiff('day', toDate('1970-01-01'), date)   AS dnum,
        ifNull(toFloat64(max(yield_2_year)), 0.)      AS y2,
        ifNull(toFloat64(max(yield_10_year)), 0.)     AS y10,
        y10 - y2                                      AS spread
    FROM global_markets.treasury_yields
    WHERE yield_2_year > 0 AND yield_10_year > 0
    GROUP BY date
),
scored AS
(
    SELECT
        date, dnum, y2, y10, spread,
        count()         OVER (ORDER BY date ROWS BETWEEN 60 PRECEDING AND 1 PRECEDING) AS rows_prior,
        sum(spread < 0) OVER (ORDER BY date ROWS BETWEEN 60 PRECEDING AND 1 PRECEDING) AS neg_prior,
        sum(spread > 0) OVER (ORDER BY date ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING) AS pos_next
    FROM daily
),
candidates AS
(
    SELECT
        date, dnum, y2, y10, spread,
        lagInFrame(dnum, 1, toInt64(-100000)) OVER (ORDER BY dnum ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS prev_dnum
    FROM scored
    WHERE spread > 0 AND rows_prior = 60 AND neg_prior >= 35 AND pos_next >= 19
),
episodes AS
(
    SELECT date AS cross_date, y2, y10, spread
    FROM candidates
    WHERE dnum - prev_dnum > 180
      AND date <= today() - 380
)
SELECT
    episode,
    crossed_on,
    y2_91d_before,
    y2_at_cross,
    y10_91d_before,
    y10_at_cross,
    y2_change_bp,
    y10_change_bp,
    multiIf(
        y2_change_bp < 0 AND abs(y2_change_bp) >= abs(y10_change_bp), 'bull steepener (2-year fell most)',
        y10_change_bp > 0 AND abs(y10_change_bp) > abs(y2_change_bp), 'bear steepener (10-year rose most)',
        'mixed'
    ) AS steepener_type
FROM
(
    SELECT
        formatDateTime(e.cross_date, '%b %Y')                                       AS episode,
        toString(toDate(e.cross_date))                                              AS crossed_on,
        round(argMinIf(d.y2, d.date, d.date >= addDays(e.cross_date, -91)), 2)      AS y2_91d_before,
        round(any(e.y2), 2)                                                         AS y2_at_cross,
        round(argMinIf(d.y10, d.date, d.date >= addDays(e.cross_date, -91)), 2)     AS y10_91d_before,
        round(any(e.y10), 2)                                                        AS y10_at_cross,
        round((any(e.y2)  - argMinIf(d.y2,  d.date, d.date >= addDays(e.cross_date, -91))) * 100) AS y2_change_bp,
        round((any(e.y10) - argMinIf(d.y10, d.date, d.date >= addDays(e.cross_date, -91))) * 100) AS y10_change_bp
    FROM episodes AS e
    CROSS JOIN daily AS d
    WHERE d.date BETWEEN addDays(e.cross_date, -100) AND e.cross_date
    GROUP BY e.cross_date
)
ORDER BY crossed_on
Run this yourself

In the Sep 2024 episode the 2-year moved -96 basis points and the 10-year moved -52 basis points over that window (a basis point is one hundredth of a percentage point), and the rule files it as a bull steepener (2-year fell most). Compare the rows and the labels do not all match. That is the first lesson of the sample: "the curve un-inverted" describes an outcome, and the two routes to it are different markets.

What did the S&P 500 do after the curve un-inverted?

The proxy here is SPY, the S&P 500 ETF, and every window is measured from its close on the crossing date. SPY's price history covers 2 of the episodes, the earliest being Mar 2007.

QuerySPY's change from the crossing-day close, 3, 6 and 12 months after each un-inversion
episodespy_at_crossspy_3m_pctspy_6m_pctspy_12m_pct
Mar 2007143.295.57-7.8
Sep 2024550.9510.35.817.8
The exact SQL behind every number
WITH
daily AS
(
    SELECT
        date,
        dateDiff('day', toDate('1970-01-01'), date)   AS dnum,
        ifNull(toFloat64(max(yield_2_year)), 0.)      AS y2,
        ifNull(toFloat64(max(yield_10_year)), 0.)     AS y10,
        y10 - y2                                      AS spread
    FROM global_markets.treasury_yields
    WHERE yield_2_year > 0 AND yield_10_year > 0
    GROUP BY date
),
scored AS
(
    SELECT
        date, dnum, y2, y10, spread,
        count()         OVER (ORDER BY date ROWS BETWEEN 60 PRECEDING AND 1 PRECEDING) AS rows_prior,
        sum(spread < 0) OVER (ORDER BY date ROWS BETWEEN 60 PRECEDING AND 1 PRECEDING) AS neg_prior,
        sum(spread > 0) OVER (ORDER BY date ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING) AS pos_next
    FROM daily
),
candidates AS
(
    SELECT
        date, dnum, y2, y10, spread,
        lagInFrame(dnum, 1, toInt64(-100000)) OVER (ORDER BY dnum ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS prev_dnum
    FROM scored
    WHERE spread > 0 AND rows_prior = 60 AND neg_prior >= 35 AND pos_next >= 19
),
episodes AS
(
    SELECT date AS cross_date, y2, y10, spread
    FROM candidates
    WHERE dnum - prev_dnum > 180
      AND date <= today() - 380
),
spy AS
(
    SELECT date, max(toFloat64(close)) AS px
    FROM global_markets.stocks_daily_aggs
    WHERE ticker = 'SPY'
    GROUP BY date
)
SELECT
    formatDateTime(e.cross_date, '%b %Y')                                                                                        AS episode,
    round(argMinIf(s.px, s.date, s.date >= e.cross_date), 2)                                                                     AS spy_at_cross,
    round((argMinIf(s.px, s.date, s.date >= addDays(e.cross_date, 91))  / argMinIf(s.px, s.date, s.date >= e.cross_date) - 1) * 100, 1) AS spy_3m_pct,
    round((argMinIf(s.px, s.date, s.date >= addDays(e.cross_date, 182)) / argMinIf(s.px, s.date, s.date >= e.cross_date) - 1) * 100, 1) AS spy_6m_pct,
    round((argMinIf(s.px, s.date, s.date >= addDays(e.cross_date, 365)) / argMinIf(s.px, s.date, s.date >= e.cross_date) - 1) * 100, 1) AS spy_12m_pct
FROM episodes AS e
CROSS JOIN spy AS s
WHERE s.date BETWEEN e.cross_date AND addDays(e.cross_date, 380)
GROUP BY e.cross_date
HAVING countIf(s.date <= addDays(e.cross_date, 7)) > 0
   AND countIf(s.date >= addDays(e.cross_date, 365)) > 0
ORDER BY e.cross_date
Run this yourself

After the Sep 2024 crossing, SPY's change from its crossing-day close measured 10.3% at three months, 5.8% at six and 17.8% at twelve. The other rows read the same way, and they do not agree with each other. With this few episodes the honest reading is one row at a time: a crossing that preceded a drawdown is one data point, a crossing that preceded new highs is another, and neither cancels the other. A twelve-month window also spans whatever else happened in that year, none of which the spread can see.

Where does the 2s10s stand today?

The last panel applies the same leg-by-leg test to the trailing six months, week by week, using each week's final close. The move columns measure each yield against the first week in the window, and the label answers which kind of steepening, if any, the current print is.

QueryThe 2s10s over the trailing six months, with the 2-year and 10-year moves that produced it
26 rows (showing 20)
weekweek_labely2_yieldy10_yieldspread_2s10sy2_move_bpy10_move_bpsteepening_type
2026-03-23Mar 233.884.440.5600no steepening over the window
2026-03-30Mar 303.844.350.51-4-9no steepening over the window
2026-04-06Apr 63.814.310.5-7-13no steepening over the window
2026-04-13Apr 133.714.260.55-17-18no steepening over the window
2026-04-20Apr 203.784.310.53-10-13no steepening over the window
2026-04-27Apr 273.884.390.510-5no steepening over the window
2026-05-04May 43.94.380.482-6no steepening over the window
2026-05-11May 114.094.590.52115no steepening over the window
2026-05-18May 184.134.560.432512no steepening over the window
2026-05-25May 253.984.450.47101no steepening over the window
2026-06-01Jun 14.174.550.382911no steepening over the window
2026-06-08Jun 84.094.480.39214no steepening over the window
2026-06-15Jun 154.194.460.27312no steepening over the window
2026-06-22Jun 224.074.380.3119-6no steepening over the window
2026-06-29Jun 294.144.490.35265no steepening over the window
2026-07-06Jul 64.214.560.353312no steepening over the window
2026-07-13Jul 134.184.550.373011no steepening over the window
2026-07-20Jul 204.334.690.364525no steepening over the window
2026-07-27Jul 274.284.750.474031no steepening over the window
2026-08-03Aug 34.194.650.463121no steepening over the window
The exact SQL behind every number
SELECT
    week,
    week_label,
    y2_yield,
    y10_yield,
    spread_2s10s,
    y2_move_bp,
    y10_move_bp,
    multiIf(
        (y10_move_bp - y2_move_bp) <= 0,                        'no steepening over the window',
        y2_move_bp < 0 AND abs(y2_move_bp) >= abs(y10_move_bp), 'bull steepener (2-year fell most)',
        'bear steepener (10-year rose most)'
    ) AS steepening_type
FROM
(
    SELECT
        week,
        week_label,
        y2_yield,
        y10_yield,
        spread_2s10s,
        round((y2_yield  - first_value(y2_yield)  OVER (ORDER BY week ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) * 100) AS y2_move_bp,
        round((y10_yield - first_value(y10_yield) OVER (ORDER BY week ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)) * 100) AS y10_move_bp
    FROM
    (
        SELECT
            toStartOfWeek(date, 1)                                                   AS week,
            formatDateTime(week, '%b %e')                                            AS week_label,
            round(argMax(ifNull(toFloat64(yield_2_year), 0.), date), 2)              AS y2_yield,
            round(argMax(ifNull(toFloat64(yield_10_year), 0.), date), 2)             AS y10_yield,
            round(argMax(ifNull(toFloat64(yield_10_year), 0.) - ifNull(toFloat64(yield_2_year), 0.), date), 2) AS spread_2s10s
        FROM global_markets.treasury_yields
        WHERE date >= today() - 182
          AND yield_2_year > 0 AND yield_10_year > 0
        GROUP BY week
    )
)
ORDER BY week
Run this yourself

As of the week of Sep 14, the 2-year yields 4.67%, the 10-year yields 4.94%, and the 2s10s stands at 0.27 percentage points. Since the week of Mar 23 the 2-year has moved 79 basis points and the 10-year 50, which by the rule above reads as: no steepening over the window. Set that beside the labeled episodes and the type, more than the sign, is the useful comparison. For the first half of the year in detail, the first-half 2026 Treasury curve recap walks the curve month by month.

FAQ

What does it mean when the yield curve un-inverts?

It means the 2-year Treasury yield has dropped back below the 10-year, so the 2s10s spread is positive again and the curve slopes upward as it usually does. In several past cycles the un-inversion sat closer to the start of a recession than the inversion did, and that timing is the reason the crossing is watched so closely.

Is a yield curve un-inversion bullish or bearish for stocks?

The record does not settle it. The daily Treasury history holds only a handful of sustained un-inversions, and SPY's 3, 6 and 12-month paths after them differ from episode to episode, with some crossings preceding drawdowns and others preceding new highs. A sample that small describes each episode; it does not produce a rule.

What is the difference between a bull steepener and a bear steepener?

Both widen the gap between long and short yields. In a bull steepener yields fall and the short end falls fastest, the pattern that appears when rate cuts are being priced. In a bear steepener yields rise and the long end rises fastest, the pattern that appears when investors ask for more yield to hold long maturities.

How long does the yield curve stay inverted?

There is no usual length. The episodes panel counts the sessions that closed below zero ahead of each crossing, and the counts differ widely from row to row. The most recent inversion logged 538 negative closes before it crossed.

Does the 3-month/10-year spread un-invert at the same time as the 2s10s?

Usually not. The 3-month bill tracks the Federal Reserve's policy rate, so the 3m10y spread stays inverted until the Fed has cut enough to bring bill yields under the 10-year, while the 2-year can fall ahead of the cuts on expectations alone. The 2s10s tends to cross first.


Every panel above carries its SQL beneath it; the crossing test and the leg-by-leg labels are there to read. To rerun them with a different threshold, or to test a different pair of maturities, ask the question in plain English on the Strasmore terminal.

#yield curve#2s10s#un-inversion#bull steepener#recession#treasuries