March 24, 2020: The Day the Rally Turned
March 24, 2020, the morning after the Fed announced unlimited QE: one of the largest single-day US rallies since 1933, replayed from the minute tape.
On March 23, 2020, the Federal Reserve announced open-ended asset purchases — QE without a stated limit — while Congress converged on the largest fiscal package in US history. March 24 was the market's answer: one of the biggest single-day rallies since the 1930s, and — visible only in hindsight — the second day of a new bull market. The bottom itself had closed the afternoon before the Fed's announcement landed. Every number is a stored query; expand any panel for the SQL.
The day, on one row
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 — the bear market's final close — SPY gapped +5.4% to open at $234.42, printed its session low of $233.8 in the opening minutes (09:31 ET), and never came back for it: the close at $243.59 was +9.5% on the day. A rally whose low prints in the first minute is the mirror image of a crash that closes on its low — one-directional conviction, sustained for six and a half hours.
The shape of the 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_timeEvery bucket steps higher into the close. Contrast this with the bear-market rallies of the preceding weeks — sharp morning bounces that bled away by afternoon. Sustained bucket-over-bucket climbs on a 232.4-million-share tape read differently from thin drifts, and this one kept going.
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_dateThe turn compounded immediately: +9.5% on the 24th, +1.3% on the 25th, +5.9% on the 26th — a three-session sequence that undid weeks of collapse and stands among the fastest bursts on record. The March 2020 pattern rhymes deliberately with March 2009: in both, the decisive policy response arrived near the low, the first rally leg was violent, and the day of maximum opportunity looked — that morning — indistinguishable from another bear-market bounce.
What this day teaches
Three receipts worth internalizing. First, the gap did the work: a large slice of the day's gain existed before the open, so anyone waiting for a comfortable regular-hours entry paid up all day — the overnight gap mechanics at maximum amplitude. Second, the bottom preceded the confirmation: March 23's close was the low, but it became visible as the low only after sessions like this one refused to revisit it. Third, velocity cuts both ways symmetrically: the same market structure that compressed a bear market into 23 sessions compressed the initial recovery into days. Speed is a property of the era's market plumbing, not of good news or bad.
One more receipt for the patient: the March 2020 round trip — all-time high to bear-market bottom to recovered high — completed in under six months end to end, where 2008's equivalent journey took over five years. Two crises, two speeds, one market that had changed its metabolism in between. Every crash page on this shelf is partly a study in that acceleration.
COVID rally FAQ
What happened 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% — one of the largest single-day US equity gains since the 1930s.
Was March 24 the start of the bull market?
The closing low printed the day before, March 23. The 24th was the first day of the advance — and the three-day burst it opened ran nearly eighteen percent, receipted above.
Could you have bought after the news and still done well?
The tape's answer: the open gapped 5.4% before regular trading began, and the day kept climbing after it — the entry was worse than the prior close at every moment, and still far below where the month ended. Gaps take; trends give back.
Every panel above is a stored, versioned query over the historical tape — expand the SQL to see each measurement. Want to feel this day instead of reading it? It is one of the playable scenarios in the Strasmore Labs trading simulator.