Strasmore Research
Deep dive investigation Matt ConnorBy Matt Connor · Updated 2026-07-25

March 24, 2020: Di Day Wey Di Market Turn Around

March 24, 2020, replay from di tape: di crash and di dash for cash, di Fed package wey different, sector breadth, and di retest wey never come.

For March 23, 2020, di Federal Reserve come announce say dem go dey buy assets open-ended — QE wey no get any stated limit — plus one tin wey dem never do before: facilities to buy corporate bonds.

March 24, 2020 na how di market take answer: di COVID stimulus rally, one of di biggest single-day US equity gains wey dem don record, and — you go only see am when you look back — di second day of new bull market.

Di bottom itself don close that afternoon before di announcement even land.

Di crash before di turn

Di crash itself get im own page; dis one na di receipt wey dis one stand on — every session from di all-time high go di bottom, plus di 10-year Treasury yield alongside for basis points:

QuerySPY daily close, drawdown, and the 10-year yield — Feb 19 to Mar 23, 2020
The exact SQL behind every number
WITH daily AS (
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
        round(argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2) AS close_usd
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= toDateTime('2020-02-19 00:00:00') AND window_start < toDateTime('2020-03-24 04:00:00')
    GROUP BY et_date
    HAVING et_date >= toDate('2020-02-19') AND et_date <= toDate('2020-03-23') AND close_usd > 0
)
SELECT
    toString(d.et_date) AS session,
    d.close_usd,
    round((d.close_usd / (SELECT max(close_usd) FROM daily) - 1) * 100, 1) AS drawdown_pct,
    round(t.yield_10_year * 100) AS y10_bps
FROM daily AS d
LEFT JOIN global_markets.treasury_yields AS t ON t.date = d.et_date
ORDER BY d.et_date

From di all-time-high close of $338.31 for 2020-02-19, SPY fall reach $222.51 by 2020-03-23 — one -34.2% drawdown, wit every session of di slide inside di panel 24 rows.

Di yield column na di tell. Through 2020-03-09 di 10-year yield collapse from 156 basis points to one then-record 54 — di classic flight to safety, money dey comot stock commot bond. Den di pattern break: by 2020-03-18 di yield don more dan double reach 118 basis points while SPY drawdown deepen from -18.9% to -28.8%. Stocks and Treasuries dey fall together na di sign of forced selling — holders dey raise cash by selling wetin still get bid. By mid-March di crisis don concern di plumbing of credit itself, no be di price of stocks.

Wetin Fed really announce — and wetin no work before

Rate cut don already dey try before. The dated sequence, each entry you fit read for the panel above:

  • 2020-03-03 — one emergency half-point rate cut, the first between-meeting cut since 2008. SPY close at $300.34, -11.2% off the high, and the slide continue.
  • March 15, Sunday — rates cut to zero plus $700 billion asset-purchase program. The next session, 2020-03-16, close -29.2% below the February high — among the worst single sessions of the entire crash.
  • March 23, 8:00 am ET — Fed remove the ceiling: purchases of Treasuries and mortgage-backed securities goin continue "in the amounts needed," open-ended. Attached na the PMCCF and SMCCF — Fed first-ever corporate-bond-buying facilities — plus revived TALF backstopping consumer credit. SPY still close at the bear's low that afternoon.
  • March 24 — the session below. Overnight, Congress converge on the roughly $2 trillion CARES Act; dem sign am on March 27, inside the three-day burst wey you go see further down.

Two design differences separate the March 23 package from the previous attempts. Fixed-size program get end date wey stressed market fit see past; "in the amounts needed" no get any. And the corporate facilities put one public buyer directly inside the credit markets wia the plumbing don seize. Whether those differences na wetin really matter na judgment call; the tape just record the sequence — two escalations, decline continue, third design, then turn.

The day, on one row

QuerySPY on March 24, 2020 — the turn, receipted
The exact SQL behind every number
WITH
    (
        SELECT argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND window_start >= toDateTime('2020-03-23 00:00:00') AND window_start < toDateTime('2020-03-24 04:00:00')
    ) AS prior_rth_close
SELECT
    round(prior_rth_close, 2) AS prior_close,
    round(toFloat64(argMinIf(open, window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)), 2) AS rth_open,
    round((toFloat64(argMinIf(open, window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)) / prior_rth_close - 1) * 100, 1) AS gap_pct,
    round(minIf(toFloat64(low), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2) AS rth_low,
    formatDateTime(toTimeZone(argMinIf(window_start, (toFloat64(low), toInt64(toUnixTimestamp(window_start))), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 'America/New_York'), '%H:%i') AS low_et,
    round(maxIf(toFloat64(high), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2) AS rth_high,
    round(toFloat64(argMaxIf(close, window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)), 2) AS rth_close,
    round((toFloat64(argMaxIf(close, window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)) / prior_rth_close - 1) * 100, 1) AS day_change_pct,
    round((minIf(toFloat64(low), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) / prior_rth_close - 1) * 100, 1) AS low_vs_prior_pct,
    round(toFloat64(sum(volume)) / 1e6, 1) AS day_shares_m,
    countIf((toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) AS rth_minute_bars
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
  AND window_start >= toDateTime('2020-03-24 04:00:00') AND window_start < toDateTime('2020-03-24 23:59:00')

From the March 23 close of $222.51 — di bear market final close — SPY gapp +5.4% to open at $234.42, print im session low of $233.8 for di opening minutes (09:31 ET), and e never come back for am: di close at $243.59 be +9.5% for di day. One rally wey im low dey print for di first minute na di mirror image of one crash wey dey close on im low. One row, though, dey flatten di path — di half-hour tape below dey show di fade wey di session suppose absorb.

How the session take shape

QuerySPY by half-hour — March 24, 2020 regular session
The exact SQL behind every number
SELECT
    formatDateTime(toStartOfInterval(toTimeZone(window_start, 'America/New_York'), INTERVAL 30 MINUTE), '%H:%i') AS et_time,
    round(toFloat64(argMax(close, window_start)), 2) AS bucket_close,
    round(min(toFloat64(low)), 2) AS bucket_low,
    round(toFloat64(sum(volume)) / 1e6, 1) AS shares_m
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
  AND window_start >= toDateTime('2020-03-24 04:00:00') AND window_start < toDateTime('2020-03-24 23:59:00')
  AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY et_time
ORDER BY et_time

The panel dey honest: the path bend. The low of $233.8 print for the opening minute and dem never come back to am — that part of the one-direction story na true. But after e reach $242.24 by the noon bucket, the tape give ground for two hours — the 13:30 bucket close at $237.25, the 14:00 bucket print the afternoon deepest low, $235.05 — the stretch where the day fit don turn to one more bounce wey fail. Instead the give-back hold far above the opening low, and the final half hour deliver the session highest bucket close, $243.59, on 34.7 million shares — the heaviest bucket of the 232.4-million-share day, even pass the open 27.3 million. Midday dip wey find buyers, then close on the highs at peak volume.

Wetin spread well, or just small corner?

One index fit dey push up because of small number of big-big companies. But na not that one happen. Twelve funds — the three main index trackers plus nine S&P sector funds — for the same two closes:

QueryMarch 24, 2020 across index and sector ETFs — everything up, cyclicals first
The exact SQL behind every number
SELECT
    ticker,
    round(argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959 AND toDate(toTimeZone(window_start, 'America/New_York')) = toDate('2020-03-23')), 2) AS mar23_close,
    round(argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959 AND toDate(toTimeZone(window_start, 'America/New_York')) = toDate('2020-03-24')), 2) AS mar24_close,
    round((argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959 AND toDate(toTimeZone(window_start, 'America/New_York')) = toDate('2020-03-24')) / argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959 AND toDate(toTimeZone(window_start, 'America/New_York')) = toDate('2020-03-23')) - 1) * 100, 1) AS change_pct
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'DIA', 'QQQ', 'IWM', 'XLE', 'XLI', 'XLF', 'XLK', 'XLY', 'XLV', 'XLU', 'XLP')
  AND window_start >= toDateTime('2020-03-23 00:00:00') AND window_start < toDateTime('2020-03-25 00:00:00')
GROUP BY ticker
ORDER BY change_pct DESC

All 12 close higher. Energy (XLE) lead with +16.4%, then industrials (XLI, +12.8%) and financials (XLF, +12.7%) follow sharp-sharp — the parts of the market wey dey sensitive to economy pass, the ones wey shutdown go hit first. The Dow tracker DIA gain 11.3% — the move wey make that week get "best day since 1933" headline, that big word belong to Dow long price history. The smallest gains dey for the classic defensives, consumer staples (XLP, +5.1%) and healthcare (XLV, +7.4%), and the Nasdaq-100 QQQ (+7.7%) follow behind SPY +9.5%. Tape wey cyclicals lead and everything green na the opposite shape of narrow mega-cap squeeze — breadth na wetin sign that day.

How historic was it, really?

"Biggest day since 1933" na Dow stat from dat week newspaper. Na wetin our own tape fit verify, scoped to wetin e actually cover:

QueryMarch 24, 2020 ranked against every SPY session on the minute tape
The exact SQL behind every number
SELECT
    countIf(isFinite(day_pct)) AS sessions_measured,
    toString(min(et_date)) AS first_session,
    round(mar24_d, 1) AS mar24_gain_pct,
    countIf(isFinite(day_pct) AND day_pct > mar24_d) AS bigger_days,
    countIf(isFinite(day_pct) AND day_pct > mar24_d) + 1 AS mar24_rank,
    arrayStringConcat(arraySort(groupArrayIf(toString(et_date), isFinite(day_pct) AND day_pct > mar24_d)), ', ') AS bigger_day_dates,
    round(mar26_b, 1) AS burst3_gain_pct,
    countIf(isFinite(burst3_pct) AND burst3_pct > mar26_b) AS bigger_bursts,
    round(mar24_r30, 2) AS vol_vs_30d,
    round(mar24_r250, 2) AS vol_vs_250d,
    round(mar24_shares / 1e6, 0) AS mar24_shares_m
FROM (
    SELECT
        et_date, day_pct, burst3_pct,
        max(if(et_date = toDate('2020-03-24'), day_pct, -999)) OVER () AS mar24_d,
        max(if(et_date = toDate('2020-03-26'), burst3_pct, -999)) OVER () AS mar26_b,
        max(if(et_date = toDate('2020-03-24'), shares / vol_avg_30, -999)) OVER () AS mar24_r30,
        max(if(et_date = toDate('2020-03-24'), shares / vol_avg_250, -999)) OVER () AS mar24_r250,
        max(if(et_date = toDate('2020-03-24'), shares, -999)) OVER () AS mar24_shares
    FROM (
        SELECT
            et_date,
            (close_usd / lagInFrame(close_usd, 1) OVER (ORDER BY et_date ASC ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) - 1) * 100 AS day_pct,
            (close_usd / lagInFrame(close_usd, 3) OVER (ORDER BY et_date ASC ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) - 1) * 100 AS burst3_pct,
            shares,
            avg(shares) OVER (ORDER BY et_date ASC ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING) AS vol_avg_30,
            avg(shares) OVER (ORDER BY et_date ASC ROWS BETWEEN 250 PRECEDING AND 1 PRECEDING) AS vol_avg_250
        FROM (
            SELECT
                toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
                argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) AS close_usd,
                toFloat64(sum(volume)) AS shares
            FROM global_markets.delayed_stocks_minute_aggs
            WHERE ticker = 'SPY' AND window_start < toDateTime('2026-01-01 00:00:00')
            GROUP BY et_date
            HAVING close_usd > 0
        )
    )
)
GROUP BY mar24_d, mar26_b, mar24_r30, mar24_r250, mar24_shares

Since 2003-09-10 — di first session of our minute data — SPY don print 5613 close-to-close daily returns. March 24, 2020 +9.5% dey rank #3: exactly 2 sessions beat am (2008-10-13, 2008-10-28), both dey inside di October 2008 panic. Di three-day sequence still dey rarer: di +17.5% run from March 23 close go March 26 close get 0 larger three-session gains anywia for di sample. Inside everytin dis warehouse fit check, notin match di burst.

Volume dey tell di quiet story: 232 million SPY shares na 2.86× di trailing-year average — and only 1.12× di average of di prior 30 sessions. Against any normal stretch of history di activity bin dey extraordinary; against di crash weeks wey come before am, e just be anoda day inside di storm. Historic turns no dey announce demsef with historic volume.

Di three-day burst

QuerySPY closes, March 23-26, 2020 — the three-day burst
The exact SQL behind every number
SELECT
    toString(et_date) AS session,
    close_usd,
    round(if(prev_close = 0, NULL, (close_usd / prev_close - 1) * 100), 1) AS change_pct,
    shares_m
FROM (
    SELECT et_date, close_usd, shares_m,
           lagInFrame(close_usd) OVER (ORDER BY et_date ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_close
    FROM (
        SELECT
            toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
            round(argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2) AS close_usd,
            round(toFloat64(sum(volume)) / 1e6, 1) AS shares_m
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND window_start >= toDateTime('2020-03-20 00:00:00') AND window_start < toDateTime('2020-03-26 23:59:00')
        GROUP BY et_date
    )
)
WHERE et_date >= toDate('2020-03-23') AND et_date <= toDate('2020-03-26')
ORDER BY et_date

Di turn compound sharp-sharp: +9.5% for di 24th, +1.3% for di 25th, +5.9% for di 26th — one three-session sequence wey cancel weeks of collapse, and no bigger one dey anywhere for di sample above. Di March 2020 pattern dey rhyme deliberate with March 2009: for both, di decisive policy response show near di low, di first rally leg violent, and di day of maximum opportunity look — dat morning — no different from another bear-market bounce.

Did the rally hold?

In real time, people dey call am "bear-market bounce," and the people wey no believe get their own proof: eleven days before, the tape don already produce one rally wey reach almost this size — from $248.1 on 2020-03-12 to $270.93 on 2020-03-13 — and the session after that one close for $239.41, the worst day of the whole crash. One green day no prove anything. Wetin make March 24 different na wetin follow am:

QueryDid it hold? The retest that never came, and two recovery clocks
The exact SQL behind every number
SELECT
    round(ath_close, 2) AS feb19_ath_close,
    round(bottom_close, 2) AS mar23_bottom_close,
    toString(rec_date) AS recovery_date,
    countIf(et_date > toDate('2020-03-23') AND et_date <= rec_date) AS sessions_to_new_high,
    dateDiff('day', toDate('2020-03-23'), rec_date) AS days_low_to_new_high,
    dateDiff('day', toDate('2020-02-19'), rec_date) AS round_trip_days,
    round(minIf(close_usd, et_date > toDate('2020-03-23') AND et_date <= rec_date), 2) AS lowest_close_after,
    toString(argMinIf(et_date, (close_usd, et_date), et_date > toDate('2020-03-23') AND et_date <= rec_date)) AS lowest_close_date,
    round((minIf(close_usd, et_date > toDate('2020-03-23') AND et_date <= rec_date) / bottom_close - 1) * 100, 1) AS closest_retest_pct,
    toString(peak07_date) AS peak_2007_date,
    toString(rec07_date) AS recovery_2013_date,
    dateDiff('day', peak07_date, rec07_date) AS round_trip_2008_days
FROM (
    SELECT
        et_date, close_usd, ath_close, bottom_close, peak07_date,
        min(if(et_date > toDate('2020-03-23') AND close_usd >= ath_close, et_date, toDate('2099-01-01'))) OVER () AS rec_date,
        min(if(et_date >= toDate('2008-01-01') AND close_usd >= peak07_close, et_date, toDate('2099-01-01'))) OVER () AS rec07_date
    FROM (
        SELECT
            et_date, close_usd,
            max(if(et_date = toDate('2020-02-19'), close_usd, -999)) OVER () AS ath_close,
            max(if(et_date = toDate('2020-03-23'), close_usd, -999)) OVER () AS bottom_close,
            max(if(et_date >= toDate('2007-01-01') AND et_date < toDate('2008-01-01'), (close_usd, et_date), (-999., toDate('1970-01-01')))) OVER () .1 AS peak07_close,
            max(if(et_date >= toDate('2007-01-01') AND et_date < toDate('2008-01-01'), (close_usd, et_date), (-999., toDate('1970-01-01')))) OVER () .2 AS peak07_date
        FROM (
            SELECT
                toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
                argMaxIf(toFloat64(close), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) AS close_usd
            FROM global_markets.delayed_stocks_minute_aggs
            WHERE ticker = 'SPY' AND window_start < toDateTime('2021-01-01 00:00:00')
            GROUP BY et_date
            HAVING close_usd > 0
        )
    )
)
GROUP BY ath_close, bottom_close, rec_date, peak07_date, rec07_date

The retest never come. After March 23, the lowest close wey SPY ever print na $243.59 — on 2020-03-24, the stimulus-rally day itself. The market never again close inside 9.5% of the bottom. The pre-COVID high of $338.31 dem recover am on 2020-08-18103 trading sessions after the low, one 181-day round trip from all-time high back to all-time high. The 2008 comparison, dem compute am the same way: SPY 2007-10-09 peak no see new closing high until 2013-03-14, 1983 days later. Two crises, two speeds, one market wey don change how e dey work in between.

Wetin this day dey teach

Three things wey you suppose hold for mind. First one, the gap do the work: most of the day gain don already before market open — anybody wey dey wait for normal-hours entry pay throughout the day — the overnight gap mechanics reach maximum amplitude. Second one, the bottom come before the confirmation: March 23 close na the lowest point, you only see am as lowest after sessions like this one refuse to go back there. Third one, speed fit work for both ways: the same market structure wey squeeze -34.2% drawdown into the 24 rows above squeeze the first recovery into three sessions. Speed na property of that era market plumbing, not of good news or bad.

The skeptics of that week no be fools: the breadth, the burst, the retest wey never come — every receipt wey separate March 24 from the failed rallies you fit only measure am for hindsight. The difference between bear-market rally and new bull market no dey visible for day one but within months e clear well well.

COVID rally FAQ

Wetin happen on March 24, 2020?

The first full session after the Fed's unlimited-QE announcement: SPY gapped +5.4% at the open and closed +9.5% — the #3 single-day gain in the 5613 sessions our minute tape covers, behind only two days in October 2008.

Wetin the Fed announce on March 23, 2020?

Open-ended purchases of Treasury and mortgage-backed securities — "in the amounts needed" — plus the PMCCF and SMCCF, its first-ever facilities for buying corporate bonds, and a revived TALF for consumer credit. The statement comot at 8:00 am ET, before the open; SPY still close that day at im bear-market low.

Was March 23, 2020 the bottom of the COVID crash?

Yes, by closing price: SPY's low close of $222.51 print on March 23, hours after the Fed's announcement. E never come back — the lowest close afterward come on 2020-03-24, 9.5% above the bottom.

When the stock market recover im pre-COVID high?

SPY first close above im February 19, 2020 all-time-high close on 2020-08-18, 103 trading sessions after the bottom — a 181-day round trip, against 1983 days for the 2008 crash's equivalent journey.

You fit don buy after the news and still do well?

The open gap +5.4% before regular trading start, and the day keep climbing — every later entry cost pass the prior close, and still sit far below where the market stand at the August 2020 recovery.

Data notes
  • Closes na regular-session (9:30 am-4:00 pm ET) last prints from the minute tape; dem fit differ from official auction closes by a few cents.
  • Recovery dates na price-only; a total-return measure (dividends reinvested) go recover sooner by construction.
  • The 10-year yield join the Treasury's daily series on trading dates; basis points na hundredths of a percentage point.
  • The rank panel's sample start 2003-09-10 — "since 2003" on this page mean that window, not all recorded market history.

Every panel above na stored, versioned query over the historical tape — expand the SQL to see each measurement. You wan feel this day instead of reading am? E be one of the playable scenarios for the Strasmore Labs trading simulator.

#market history#covid rally#bull markets#federal reserve#spy