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.
| month | month_label | spread_2s10s | y2_yield | y10_yield |
|---|---|---|---|---|
| 1976-06-01 | Jun 1976 | 0.8 | 7.06 | 7.86 |
| 1976-07-01 | Jul 1976 | 0.98 | 6.85 | 7.83 |
| 1976-08-01 | Aug 1976 | 1.14 | 6.63 | 7.77 |
| 1976-09-01 | Sep 1976 | 1.18 | 6.42 | 7.59 |
| 1976-10-01 | Oct 1976 | 1.43 | 5.98 | 7.41 |
| 1976-11-01 | Nov 1976 | 1.48 | 5.81 | 7.29 |
| 1976-12-01 | Dec 1976 | 1.49 | 5.38 | 6.87 |
| 1977-01-01 | Jan 1977 | 1.31 | 5.9 | 7.21 |
| 1977-02-01 | Feb 1977 | 1.3 | 6.09 | 7.39 |
| 1977-03-01 | Mar 1977 | 1.37 | 6.09 | 7.46 |
| 1977-04-01 | Apr 1977 | 1.41 | 5.96 | 7.37 |
| 1977-05-01 | May 1977 | 1.2 | 6.25 | 7.46 |
| 1977-06-01 | Jun 1977 | 1.15 | 6.13 | 7.28 |
| 1977-07-01 | Jul 1977 | 1.06 | 6.27 | 7.33 |
| 1977-08-01 | Aug 1977 | 0.79 | 6.61 | 7.4 |
| 1977-09-01 | Sep 1977 | 0.63 | 6.71 | 7.34 |
| 1977-10-01 | Oct 1977 | 0.42 | 7.11 | 7.52 |
| 1977-11-01 | Nov 1977 | 0.43 | 7.14 | 7.58 |
| 1977-12-01 | Dec 1977 | 0.51 | 7.18 | 7.69 |
| 1978-01-01 | Jan 1978 | 0.47 | 7.49 | 7.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 monthThe 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.
| episode | crossed_on | crossed_on_label | negative_sessions_prior | trough_spread | spread_3m_later | spread_6m_later | spread_12m_later |
|---|---|---|---|---|---|---|---|
| May 1980 | 1980-05-02 | May 2, 1980 | 423 | -2.41 | 0.98 | -0.62 | -0.9 |
| Oct 1981 | 1981-10-28 | Oct 28, 1981 | 453 | -2.41 | -0.06 | -0.22 | 0.88 |
| Jul 1982 | 1982-07-19 | Jul 19, 1982 | 400 | -1.7 | 0.78 | 1.11 | 0.67 |
| Jun 1989 | 1989-06-30 | Jun 30, 1989 | 132 | -0.45 | -0.15 | 0.06 | 0.18 |
| Dec 2000 | 2000-12-27 | Dec 27, 2000 | 226 | -0.52 | 0.67 | 1.2 | 1.94 |
| Mar 2007 | 2007-03-21 | Mar 21, 2007 | 220 | -0.19 | 0.17 | 0.53 | 1.75 |
| Sep 2024 | 2024-09-04 | Sep 4, 2024 | 538 | -1.08 | 0.06 | 0.29 | 0.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_dateWhat 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.
| episode | crossed_on | y2_91d_before | y2_at_cross | y10_91d_before | y10_at_cross | y2_change_bp | y10_change_bp | steepener_type |
|---|---|---|---|---|---|---|---|---|
| May 1980 | 1980-05-02 | 12.04 | 10.04 | 11.29 | 10.24 | -200 | -105 | bull steepener (2-year fell most) |
| Oct 1981 | 1981-10-28 | 15.76 | 15.42 | 14.61 | 15.44 | -34 | 83 | bear steepener (10-year rose most) |
| Jul 1982 | 1982-07-19 | 14.18 | 13.52 | 13.66 | 13.68 | -66 | 2 | bull steepener (2-year fell most) |
| Jun 1989 | 1989-06-30 | 9.73 | 8.08 | 9.3 | 8.1 | -165 | -120 | bull steepener (2-year fell most) |
| Dec 2000 | 2000-12-27 | 6 | 5.1 | 5.83 | 5.11 | -90 | -72 | bull steepener (2-year fell most) |
| Mar 2007 | 2007-03-21 | 4.71 | 4.51 | 4.6 | 4.53 | -20 | -7 | bull steepener (2-year fell most) |
| Sep 2024 | 2024-09-04 | 4.72 | 3.76 | 4.29 | 3.77 | -96 | -52 | bull 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_onIn 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.
| episode | spy_at_cross | spy_3m_pct | spy_6m_pct | spy_12m_pct |
|---|---|---|---|---|
| Mar 2007 | 143.29 | 5.5 | 7 | -7.8 |
| Sep 2024 | 550.95 | 10.3 | 5.8 | 17.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_dateAfter 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.
| week | week_label | y2_yield | y10_yield | spread_2s10s | y2_move_bp | y10_move_bp | steepening_type |
|---|---|---|---|---|---|---|---|
| 2026-03-23 | Mar 23 | 3.88 | 4.44 | 0.56 | 0 | 0 | no steepening over the window |
| 2026-03-30 | Mar 30 | 3.84 | 4.35 | 0.51 | -4 | -9 | no steepening over the window |
| 2026-04-06 | Apr 6 | 3.81 | 4.31 | 0.5 | -7 | -13 | no steepening over the window |
| 2026-04-13 | Apr 13 | 3.71 | 4.26 | 0.55 | -17 | -18 | no steepening over the window |
| 2026-04-20 | Apr 20 | 3.78 | 4.31 | 0.53 | -10 | -13 | no steepening over the window |
| 2026-04-27 | Apr 27 | 3.88 | 4.39 | 0.51 | 0 | -5 | no steepening over the window |
| 2026-05-04 | May 4 | 3.9 | 4.38 | 0.48 | 2 | -6 | no steepening over the window |
| 2026-05-11 | May 11 | 4.09 | 4.59 | 0.5 | 21 | 15 | no steepening over the window |
| 2026-05-18 | May 18 | 4.13 | 4.56 | 0.43 | 25 | 12 | no steepening over the window |
| 2026-05-25 | May 25 | 3.98 | 4.45 | 0.47 | 10 | 1 | no steepening over the window |
| 2026-06-01 | Jun 1 | 4.17 | 4.55 | 0.38 | 29 | 11 | no steepening over the window |
| 2026-06-08 | Jun 8 | 4.09 | 4.48 | 0.39 | 21 | 4 | no steepening over the window |
| 2026-06-15 | Jun 15 | 4.19 | 4.46 | 0.27 | 31 | 2 | no steepening over the window |
| 2026-06-22 | Jun 22 | 4.07 | 4.38 | 0.31 | 19 | -6 | no steepening over the window |
| 2026-06-29 | Jun 29 | 4.14 | 4.49 | 0.35 | 26 | 5 | no steepening over the window |
| 2026-07-06 | Jul 6 | 4.21 | 4.56 | 0.35 | 33 | 12 | no steepening over the window |
| 2026-07-13 | Jul 13 | 4.18 | 4.55 | 0.37 | 30 | 11 | no steepening over the window |
| 2026-07-20 | Jul 20 | 4.33 | 4.69 | 0.36 | 45 | 25 | no steepening over the window |
| 2026-07-27 | Jul 27 | 4.28 | 4.75 | 0.47 | 40 | 31 | no steepening over the window |
| 2026-08-03 | Aug 3 | 4.19 | 4.65 | 0.46 | 31 | 21 | no 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 weekAs 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.