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

COVID Crash 2020: Four Halts from Peak to Trough

COVID crash of 2020, na receipt: March 9 halt minutes missing for tape, four circuit breakers inside eight days, plus SPY peak-to-trough map.

COVID crash for 2020 carry S&P 500 from record close for February 19 reach -34.2% peak-to-trough loss inside 23 trading days. Four market-wide circuit-breaker halts happen along the way, and na the first time since 1997. This page dey replay the full move from the tape: map of the crash, the March 9 halt session minute by minute, including the minutes wey data no get, wey be the strongest receipt for this page, the names wey fall pass, plus wetin Treasuries and the options tape do alongside. Every number come from stored query. Expand any panel to see the SQL.

The whole crash, for one row

Before the famous one-day fall, make we first see the full picture. One query on SPY daily regular-session closes show the peak, the low, how fast e happen, and the recovery. E also run the same measurement for the 2008 bear market so we fit compare.

QueryCOVID crash, peak go trough reach recovery: SPY daily closes, one row
peak datepeak closetrough datetrough closedecline pctsessions from peak reach troughcalendar dayssessions reach minus 30GFC 2008 sessions reach minus 30recovery datesessions from trough reach recovery
2020-02-19338.312020-03-23222.51-34.22333222502020-08-18103
The exact SQL behind every number
WITH dailies AS (
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        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 c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= toDateTime('2020-02-01 00:00:00') AND window_start < toDateTime('2020-09-01 00:00:00')
    GROUP BY d
),
peak AS (SELECT argMax(d, (c, -toInt32(d))) AS pd, max(c) AS pc FROM dailies WHERE d < toDate('2020-03-01')),
trough AS (SELECT argMin(d, (c, toInt32(d))) AS td, min(c) AS tc FROM dailies WHERE d < toDate('2020-04-01')),
gfc AS (
    SELECT
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        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 c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= toDateTime('2007-06-01 00:00:00') AND window_start < toDateTime('2009-01-01 00:00:00')
    GROUP BY d
),
gpeak AS (SELECT argMax(d, (c, -toInt32(d))) AS pd, max(c) AS pc FROM gfc WHERE d < toDate('2008-01-01'))
SELECT
    toString((SELECT pd FROM peak)) AS peak_date,
    round((SELECT pc FROM peak), 2) AS peak_close,
    toString((SELECT td FROM trough)) AS trough_date,
    round((SELECT tc FROM trough), 2) AS trough_close,
    round(((SELECT tc FROM trough) / (SELECT pc FROM peak) - 1) * 100, 1) AS decline_pct,
    countIf(d > (SELECT pd FROM peak) AND d <= (SELECT td FROM trough)) AS sessions_peak_to_trough,
    dateDiff('day', (SELECT pd FROM peak), (SELECT td FROM trough)) AS calendar_days,
    countIf(d > (SELECT pd FROM peak) AND d <= (SELECT min(d) FROM dailies WHERE c <= 0.7 * (SELECT pc FROM peak))) AS sessions_to_minus_30,
    (SELECT countIf(d > (SELECT pd FROM gpeak) AND d <= (SELECT min(d) FROM gfc WHERE c <= 0.7 * (SELECT pc FROM gpeak))) FROM gfc) AS gfc_2008_sessions_to_minus_30,
    toString((SELECT min(d) FROM dailies WHERE d > (SELECT td FROM trough) AND c >= (SELECT pc FROM peak))) AS recovery_date,
    countIf(d > (SELECT td FROM trough) AND d <= (SELECT min(d) FROM dailies WHERE d > (SELECT td FROM trough) AND c >= (SELECT pc FROM peak))) AS sessions_trough_to_recovery
FROM dailies
Run am yourself

SPY peak close na $338.31 on 2020-02-19. The low close, $222.51 on 2020-03-23, dey -34.2% below am. Na bear market wey squeeze enter 23 trading sessions and 33 calendar days. People often call am the “fastest thirty-percent drawdown on record.” The same row confirm am: SPY close first break below 30% from the peak after only 22 sessions. The 2008 bear market, measured the same way from the October 2007 peak close, need 250 sessions to lose the same 30%. Wetin take roughly one year in 2008 happen in about one month for 2020.

The crash timeline, with every number sourced from the panels for this page:

  • February 19, 2020, SPY record close, $338.31.
  • March 9, the first market-wide circuit breaker since 1997. SPY close -7.7% lower. The minute-by-minute evidence dey below.
  • March 12, 16, and 18, three more Level-1 halts. March 16 close -11.6% lower, the worst single session of the crash.
  • March 23, the closing low: $222.51, -34.2% below the peak.
  • March 24, the turning point. The March 24 stimulus rally carry that market move.
  • 2020-08-18, SPY close above the February peak again, 103 sessions after the low.

March 9, 2020: session wey trip circuit breaker

Market-wide circuit breakers dey use S&P 500 as reference, so this panel dey read the halt through SPY, the S&P 500 ETF, instead of any other proxy. During the weekend before am, pandemic repricing collide with oil-supply shock (we go talk more about that below), and market no really open in an orderly way.

QuerySPY for March 9, 2020: circuit-breaker session, receipt don show
previous closeRTH opengap pctfirst missing ET for halthalt missing minutesRTH lowlow ETRTH closeday change pctday shares mRTH minute bars
297.42275.3-7.409:3514273.4514:44274.4-7.7304.6376
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-06 00:00:00') AND window_start < toDateTime('2020-03-09 04:00:00')
    ) AS prior_rth_close,
    (
        SELECT (formatDateTime(toTimeZone(addMinutes(prev_bar, 1), 'America/New_York'), '%H:%i'), gap_minutes - 1)
        FROM (
            SELECT
                lagInFrame(window_start) OVER (ORDER BY window_start ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_bar,
                dateDiff('minute', prev_bar, window_start) AS gap_minutes
            FROM global_markets.delayed_stocks_minute_aggs
            WHERE ticker = 'SPY'
              AND window_start >= toDateTime('2020-03-09 04:00:00') AND window_start < toDateTime('2020-03-09 23:59:00')
              AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
        )
        WHERE prev_bar > toDateTime('2020-03-09 00:00:00')
        ORDER BY gap_minutes DESC
        LIMIT 1
    ) AS halt_gap
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,
    halt_gap.1 AS halt_first_missing_et,
    halt_gap.2 AS halt_missing_minutes,
    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(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(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-09 04:00:00') AND window_start < toDateTime('2020-03-09 23:59:00')
Run am yourself

Make we start with two figures. First, the gap: SPY open at $275.3, -7.4% below Friday closing price of $297.42, and within the first few minutes, S&P 500 decline reach the 7% Level-1 threshold. Second, and better: 376 regular-session minute bars instead of the normal 390. The tape stop after 9:34 am: from 09:35 ET, 14 consecutive minutes no print any bar at all. Nothing trade anywhere. The gap inside the data na the circuit breaker. Session close at $274.4, -7.7% down, on 304.6 million shares.

Halted session shape

QuerySPY by half-hour: March 9, 2020 regular session
ET timebucket closebucket lowshares m
09:30279.93273.517.4
10:00280.72275.623.4
10:30278.61275.519.1
11:00281.03275.517.2
11:30282.69280.0619.1
12:00281.01280.9517.2
12:30279.71275.9513.4
13:00278.59278.2411.5
13:30275.84275.3419.4
14:00274.95274.0621.2
14:30276.59273.4525.9
15:00277.9275.5524.5
15:30274.4274.3746.7
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-09 04:00:00') AND window_start < toDateTime('2020-03-09 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
Run am yourself

First opening bucket carry the crash, the halt, and the resumption together: e low of $273.5 dey just few cents near the full-day low of $273.45, while afternoon print for 14:44 ET only retest the morning level. After market reopen, the tape almost no move: every half-hour close stay within few dollars of the reopening price, and the day’s damage happen during the first few minutes of trading. Volume tell the same story from the other side. The final half hour, at 46.7 million shares, na the heaviest bucket of the day as closing flows cross. Circuit breakers dey designed to create exactly this shape: stop the waterfall, force reopening auction, then allow continuous trading to resume for the repriced level. The market clock don get this machinery built inside am permanently now.

Four halts for eight sessions

March 9 na just the beginning.

QueryThe four circuit-breaker sessions for March 2020: SPY close, change, volume
sessionclose USDchange pctshares m
2020-03-09274.4-7.7304.6
2020-03-12248.1-9.6389.3
2020-03-16239.41-11.6291.5
2020-03-18241.01-5.2324.7
The exact SQL behind every number
SELECT
    toString(et_date) AS session,
    close_usd,
    change_pct,
    shares_m
FROM (
    SELECT et_date, close_usd, shares_m,
           round((close_usd / lagInFrame(close_usd) OVER (ORDER BY et_date ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) - 1) * 100, 1) AS change_pct
    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-06 00:00:00') AND window_start < toDateTime('2020-03-19 00:00:00')
        GROUP BY et_date
    )
)
WHERE toString(et_date) IN ('2020-03-09', '2020-03-12', '2020-03-16', '2020-03-18')
ORDER BY et_date
Run am yourself

Level-1 breakers trip again on March 12 (market close -9.6%), March 16 (market close -11.6% at $239.41, the S&P biggest one-day percentage drop since 1987), and March 18 (halt intraday, market close -5.2%). Na four market-wide halts within eight trading days, after twenty-three years without any. Make una note wetin no happen: every halt na Level 1, the 7% tier. Dem no ever reach the 13% and 20% tiers, even on March 16. After the halt, the selloff slow down and the session finish inside the Level-2 line.

Wetin fall pass

One index figure fit hide how different the losses really be. If you use the same daily-close calculation across some representative large-cap stocks, you go see where the crash hit hardest, both on March 9 and through the full February 19 to March 23 decline.

QueryMarch 9 and full-crash moves: cruise, airline, energy, bank, tech, staples vs SPY
tickerMar 9 (%)Crash (%)
CCL-19.9-72.3
UAL-10.4-66.9
XOM-12.2-47.9
JPM-13.7-42.6
SPY-7.7-34.2
AAPL-7.9-31
WMT-0.1-2.9
The exact SQL behind every number
WITH dailies AS (
    SELECT
        ticker,
        toDate(toTimeZone(window_start, 'America/New_York')) AS d,
        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 c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('UAL', 'CCL', 'XOM', 'JPM', 'AAPL', 'WMT', 'SPY')
      AND window_start >= toDateTime('2020-02-18 00:00:00') AND window_start < toDateTime('2020-03-25 00:00:00')
    GROUP BY ticker, d
)
SELECT
    ticker,
    round((anyIf(c, d = toDate('2020-03-09')) / anyIf(c, d = toDate('2020-03-06')) - 1) * 100, 1) AS mar9_pct,
    round((anyIf(c, d = toDate('2020-03-23')) / anyIf(c, d = toDate('2020-02-19')) - 1) * 100, 1) AS crash_pct
FROM dailies
GROUP BY ticker
ORDER BY crash_pct ASC
Run am yourself

The name wey suffer pass for the group, CCL (Carnival, the cruise operator), lost -19.9% on March 9 alone and -72.3% from peak to trough. United Airlines lost -66.9% during the crash; ExxonMobil -47.9%; JPMorgan -42.6%. Compared with dem, SPY’s -34.2% look almost mild. Apple fell roughly in line with the market at -31%, while Walmart, groceries and essential goods wey remain open during lockdowns, ended the same period at only -2.9%. Travel and energy prices adjust sharply because the world stop moving; staple stocks barely move.

Oil-price war, quick explanation

The energy matter wey dey above deserve one short explanation, because “oil-price war” na the other half of the March 9 story. OPEC+ production talks between Saudi Arabia and Russia end without agreement on Friday, March 6, 2020. Over the weekend, Saudi Arabia cut its official selling prices and announce plans to increase output, even as travel demand for the whole world don already start shut down. Na supply shock plus demand shock. Monday equity market open carry both shocks at the same time. The result dey for panel above: ExxonMobil fall -12.2% that Monday, compared with SPY’s -7.7%.

The flight to safety, and the week e break

Equities na only half of the story. The other half na Treasury market, where the classic flight to safety reach record level, then, for one remarkable week, turn back.

Query10-year and 3-month Treasury yields through the crash, daily closes
31 rows (showing 20)
Date10-year Treasury (%)3-month (%)
2020-02-181.551.58
2020-02-191.561.58
2020-02-201.521.58
2020-02-211.461.56
2020-02-241.381.53
2020-02-251.331.53
2020-02-261.331.53
2020-02-271.31.45
2020-02-281.131.27
2020-03-021.11.13
2020-03-031.020.95
2020-03-041.020.72
2020-03-050.920.62
2020-03-060.740.45
2020-03-090.540.33
2020-03-100.760.44
2020-03-110.820.42
2020-03-120.880.33
2020-03-130.940.28
2020-03-160.730.24
The exact SQL behind every number
SELECT
    toString(date) AS date,
    round(yield_10_year, 2) AS ten_year_pct,
    round(yield_3_month, 2) AS three_month_pct
FROM global_markets.treasury_yields
WHERE date >= '2020-02-18' AND date <= '2020-03-31'
  AND isNotNull(yield_10_year)
ORDER BY date
Run am yourself

The 10-year yield dey at 1.56% on the February 19 equity peak. By March 9, the halt session, e close at 0.54%, the lowest level ever recorded. Buyers accept half a percent per year for ten years in exchange for safety. Then the pattern turn upside down: into March 18, the 10-year yield rise to 1.18% while equities still dey fall. Na the "dash for cash," one week when funds, companies and foreign holders sell anything wey get liquidity, including Treasuries, to raise dollars. Meanwhile, the 3-month bill close that day at 0.02%, with cash equivalents pinned to zero. When the safest asset and the riskiest asset fall together, na liquidity itself people dey buy. The yield curve carry this episode permanently for its history.

Wetin options tape dey show

SPY listed options, wey be the market’s main hedging venue, also show say panic enter the market. The timing na the interesting part.

QuerySPY options: puts vs calls wey dem trade, March 2–13, 2020 (millions of contracts)
sessionPut contracts (m)Call contracts (m)Put/Call ratio
2020-03-023.742.541.47
2020-03-033.142.241.4
2020-03-043.062.271.35
2020-03-053.141.821.73
2020-03-064.642.381.95
2020-03-094.352.381.83
2020-03-103.612.41.5
2020-03-113.752.441.54
2020-03-124.533.431.32
2020-03-134.633.271.41
The exact SQL behind every number
SELECT
    toString(toDate(toTimeZone(sip_timestamp, 'America/New_York'))) AS session,
    round(sumIf(toFloat64(size), substring(ticker, 12, 1) = 'P') / 1e6, 2) AS put_contracts_m,
    round(sumIf(toFloat64(size), substring(ticker, 12, 1) = 'C') / 1e6, 2) AS call_contracts_m,
    round(sumIf(toFloat64(size), substring(ticker, 12, 1) = 'P') / sumIf(toFloat64(size), substring(ticker, 12, 1) = 'C'), 2) AS put_call_ratio
FROM global_markets.options_trades
WHERE ticker >= 'O:SPY2' AND ticker < 'O:SPY3'
  AND sip_timestamp >= toDateTime('2020-03-02 00:00:00') AND sip_timestamp < toDateTime('2020-03-14 00:00:00')
GROUP BY session
ORDER BY session
Run am yourself

Put/call volume ratio peak at 1.95 on Friday, March 6, before the halt session. Nearly two puts trade for every call. On March 9, 4.35 million puts trade against 2.38 million calls, giving ratio of 1.83. By March 12, a -9.6% session, ratio don ease to 1.32 as call volume rise alongside puts. The strongest put skew happen for the beginning of the cascade, no be for the bottom. Investors buy protection early, while the later and bigger down days trade with options tape wey dey more balanced between puts and calls.

How market-wide halt dey really work

The mechanics dey work like this, because dem no dey happen often and people fit forget how dem work between uses: market-wide breakers dey track how much S&P 500 fall from the previous close. A 7% fall (Level 1) or 13% fall (Level 2) go trigger a 15-minute halt if e happen before 3:25 pm ET. A 20% fall (Level 3) go end the session completely. During the halt, nothing dey trade for any exchange. Orders dey queue for reopening auction, wey go gather all the pending interest into one price, just like the opening auction wey happen every morning. This design come after the 2010 flash crash, wey show say the older thresholds too loose to trigger even during a real market cascade.

March 2020 na the first live test of this design, and so far na still the only one. The verdict dey for the panels above: four Level-1 halts, and each one get orderly reopening. Level 2 or Level 3 no ever trigger. Market still function for all the 23 sessions of the fastest large drawdown in the index history. Dem leave the rules unchanged after that: the same 7/13/20 thresholds, based on S&P 500, still dey apply today. The system bend, but the market plumbing hold.

COVID crash FAQ

March 9, 2020 wetin trigger the circuit breaker?

S&P 500 fall 7% from the previous close, wey be Level-1 market-wide threshold, within the first minutes after market open. Trading stop for 15 minutes across the market, then auction reopen am. For this page minute tape, you fit see the halt as 14 consecutive missing bars wey start from 09:35 ET.

How much stock market fall during the COVID crash?

For SPY daily closes, -34.2%, from $338.31 on February 19, 2020 reach $222.51 on March 23, 2020, across 23 trading sessions.

How many circuit breakers dem hit for March 2020?

Four market-wide Level-1 halts: March 9, 12, 16, and 18. Na the first ones since October 1997.

Dem ever hit Level 2 or Level 3 circuit breakers for 2020?

No. All four 2020 halts na Level 1 (7%). No Level-2 (13%) or Level-3 (20%) market-wide halt don ever fire under the current design, not even on March 16, wey be the worst session for the crash at -11.6%.

Market recover fully from the COVID crash?

Yes. SPY close above its February 19, 2020 peak on 2020-08-18, 103 trading sessions after the March 23 low. Na one of the fastest full recoveries from bear market of this depth.


Every panel above na stored, versioned query wey dey run on the historical tape. Expand the SQL to see how dem calculate each measurement. You wan experience this day instead of just reading about am? Na one of the playable scenarios for the Strasmore Labs trading simulator.

#market history#covid crash#circuit breakers#spy