Feb 24, 2022: War at the Open, Green by Close
February 24, 2022: Russia invaded Ukraine and US stocks gapped down, then closed green. The full reversal, plus oil, gold, defense, bonds and the aftermath.
In the early hours of February 24, 2022, Russia launched its full-scale invasion of Ukraine, and the US stock market produced one of the most-studied sessions of the decade. Index futures fell hard overnight, the Nasdaq-100 ETF QQQ opened -3.2% down, and closed +3.4%. This page receipts the whole event: the reversal minute by minute, what the S&P 500 and Dow did, where oil, gold, defense stocks, Treasuries and volatility went, and what the weeks after looked like. Every number is a stored query; expand any panel for the SQL.
Before the open: how the night unfolded
The invasion did not come out of nowhere. Through late 2021 and early 2022 Russia massed troops near Ukraine's border, Western estimates reported at the time ran well above 100,000, and markets had been trading the risk for weeks. On Monday, February 21, Moscow recognized the two self-declared separatist republics in eastern Ukraine; US markets were closed for the Washington's Birthday holiday, and the first session able to price the news was Tuesday the 22nd, a red one. The first tranche of Western sanctions landed that same Tuesday, alongside Germany's halt of the Nord Stream 2 pipeline certification.
Then, shortly before 6 a.m. Moscow time on Thursday, February 24, around 10 p.m. Wednesday evening in New York, Russia's president announced the operation, and explosions were reported across Ukrainian cities within the hour. US index futures, which trade through the night, sold off immediately. By sunrise the only open question was how far below Wednesday's close the 9:30 a.m. opening auction would print.
The day, on one row
One row holds the whole answer, prior close, gap, low, high, close, and volume:
| prior_close | rth_open | gap_pct | rth_low | low_et | rth_high | rth_close | day_change_pct | low_vs_prior_pct | trough_to_close_pct | close_to_high_pct | open_above_low_pct | day_shares_m | rth_minute_bars |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 329.41 | 318.84 | -3.2 | 318.26 | 09:30 | 341.04 | 340.65 | 3.4 | -3.4 | 7 | 0.11 | 0.18 | 128 | 390 |
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 = 'QQQ'
AND window_start >= toDateTime('2022-02-23 00:00:00') AND window_start < toDateTime('2022-02-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(argMaxIf(close, window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)) / minIf(toFloat64(low), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) - 1) * 100, 1) AS trough_to_close_pct,
round((maxIf(toFloat64(high), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) / toFloat64(argMaxIf(close, window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)) - 1) * 100, 2) AS close_to_high_pct,
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)) / minIf(toFloat64(low), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) - 1) * 100, 2) AS open_above_low_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 = 'QQQ'
AND window_start >= toDateTime('2022-02-24 04:00:00') AND window_start < toDateTime('2022-02-24 23:59:00')The receipt everyone should sit with: the session low of $318.26 printed at 09:30 ET, the opening minute, -3.4% below the prior close. From that first print, QQQ climbed for six and a half hours and finished at $340.65: +3.4% on the day, a 7% swing trough-to-close on 128 million shares. And the close was no fade-into-the-bell: it landed just 0.11% under the session high of $341.04, meaning the day ended essentially at its top. Anyone who sold in that first minute of trading sold within 0.18% of the session's ultimate low, the actual bottom tick printed moments after the opening cross, not at it (the auction print was $318.84; the low, $318.26).
The reversal, bucket by bucket
Half-hour buckets show the shape of the climb:
| et_time | bucket_close | bucket_low | shares_m |
|---|---|---|---|
| 09:30 | 325.41 | 318.26 | 18.6 |
| 10:00 | 326.04 | 323.11 | 13.8 |
| 10:30 | 328.14 | 324.6 | 9.5 |
| 11:00 | 329.12 | 327.3 | 8 |
| 11:30 | 330.29 | 327.6 | 6.1 |
| 12:00 | 328.03 | 326.93 | 6 |
| 12:30 | 328.18 | 327.16 | 4.7 |
| 13:00 | 328.8 | 327.76 | 5.1 |
| 13:30 | 331.72 | 328.77 | 7.6 |
| 14:00 | 333.3 | 330.7 | 7.6 |
| 14:30 | 335.94 | 333.29 | 8.1 |
| 15:00 | 336.89 | 335.48 | 7.3 |
| 15:30 | 340.65 | 336.82 | 15 |
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 = 'QQQ'
AND window_start >= toDateTime('2022-02-24 04:00:00') AND window_start < toDateTime('2022-02-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_timeThe staircase was not perfectly clean, the 12:00 ET bucket closed at $328.03, under the 11:30 close of $330.29, but the direction never seriously flipped, and the final bucket closed at $340.65, the top of the table. Volume traced the usual U: 18.6 million shares in the opening half hour and 15 million in the last, against 4.7 million around lunch. It is the mirror image of 2011's staircase down. Sessions like this are why "the market fell on the invasion" retellings mislead: the futures fell overnight; the cash session rallied from its first minute. Which one you experienced depended entirely on when your orders arrived, the theme overnight gaps measures on calmer weeks.
Not just the Nasdaq: the whole market reversed
A one-index story would leave the obvious question open: was this a growth-stock quirk or a market-wide reversal? The panel below puts eleven tickers on one yardstick, the three major index ETFs, then the assets a geopolitical-shock playbook reaches for: energy, gold, long Treasuries, a volatility ETF, and four defense contractors.
| ticker | prior_close | gap_pct | low_vs_prior_pct | high_vs_prior_pct | day_change_pct |
|---|---|---|---|---|---|
| QQQ | 329.41 | -3.2 | -3.4 | 3.5 | 3.4 |
| SPY | 421.97 | -2.6 | -2.7 | 1.6 | 1.5 |
| DIA | 331.44 | -2.6 | -2.6 | 0.4 | 0.3 |
| XLE | 67.64 | 2.1 | -3.4 | 2.2 | -0.9 |
| GLD | 178.28 | 2.4 | -1.7 | 2.4 | -0.6 |
| TLT | 136.62 | 1.8 | -0.3 | 2 | 0.2 |
| VIXY | 20.17 | 11.1 | -3.8 | 11.2 | -3 |
| LMT | 389.14 | 2.3 | -0.4 | 2.5 | 1.7 |
| NOC | 386.1 | 2.5 | 1 | 4.4 | 2.4 |
| RTX | 92.27 | 0.8 | -3.2 | 2.6 | 2.1 |
| GD | 216.25 | 0.2 | -2.2 | 1.2 | 1.1 |
The exact SQL behind every number
SELECT
ticker,
round(prior_close, 2) AS prior_close,
round((rth_open / prior_close - 1) * 100, 1) AS gap_pct,
round((rth_low / prior_close - 1) * 100, 1) AS low_vs_prior_pct,
round((rth_high / prior_close - 1) * 100, 1) AS high_vs_prior_pct,
round((rth_close / prior_close - 1) * 100, 1) AS day_change_pct
FROM (
SELECT
ticker,
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('2022-02-23')) AS prior_close,
argMinIf(toFloat64(open), 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('2022-02-24')) AS rth_open,
minIf(toFloat64(low), (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('2022-02-24')) AS rth_low,
maxIf(toFloat64(high), (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('2022-02-24')) AS rth_high,
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('2022-02-24')) AS rth_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('QQQ', 'SPY', 'DIA', 'XLE', 'GLD', 'TLT', 'VIXY', 'LMT', 'NOC', 'RTX', 'GD')
AND window_start >= toDateTime('2022-02-23 00:00:00') AND window_start < toDateTime('2022-02-24 23:59:00')
GROUP BY ticker
)
ORDER BY indexOf(['QQQ', 'SPY', 'DIA', 'XLE', 'GLD', 'TLT', 'VIXY', 'LMT', 'NOC', 'RTX', 'GD'], ticker)The reversal was market-wide, but its size didn't simply track the depth of the morning's gap: QQQ gapped -3.2% at the open and closed +3.4%. SPY and DIA gapped by almost the same amount, -2.6% and -2.6% respectively, yet SPY closed +1.5% against DIA's +0.3%, barely green. The growth-heavy index reversed hardest; the industrial-heavy Dow, least, composition, not gap size, separates SPY from DIA.
Where the money went: oil, gold, defense, volatility
Now read the lower rows of the same panel, the "war trade". Energy opened at its highs and closed red: XLE gapped +2.1%, traded as deep as -3.4% below the prior close intraday, and finished -0.9%. Gold made the same round trip: GLD opened +2.4% and closed -0.6%. VIXY, an ETF holding short-term VIX futures, the closest tradable proxy for the volatility index, gapped +11.1% at the open and closed -3%, below the prior day's close. The classic fear assets peaked exactly when the headlines were worst and faded alongside the equity recovery: the equity reversal, inverted.
The defense contractors were the exception that held. All four closed green, LMT +1.7%, NOC +2.4%, RTX +2.1%, GD +1.1%, and Northrop Grumman never printed below Wednesday's close all session (its intraday low was +1%), the only one of the four that didn't touch red.
Treasuries: the safe-haven bid came later
A falling Treasury yield means bond prices are being bid up, the textbook safe-haven flow. On invasion day itself, that flow barely registered at the close:
| session | yield_2_year | yield_10_year |
|---|---|---|
| 2022-02-18 | 1.47 | 1.92 |
| 2022-02-22 | 1.56 | 1.94 |
| 2022-02-23 | 1.58 | 1.99 |
| 2022-02-24 | 1.54 | 1.96 |
| 2022-02-25 | 1.55 | 1.97 |
| 2022-02-28 | 1.44 | 1.83 |
| 2022-03-01 | 1.31 | 1.72 |
| 2022-03-02 | 1.5 | 1.86 |
| 2022-03-03 | 1.53 | 1.86 |
| 2022-03-04 | 1.5 | 1.74 |
| 2022-03-07 | 1.55 | 1.78 |
| 2022-03-08 | 1.63 | 1.86 |
The exact SQL behind every number
SELECT
toString(date) AS session,
round(yield_2_year, 2) AS yield_2_year,
round(yield_10_year, 2) AS yield_10_year
FROM global_markets.treasury_yields
WHERE date BETWEEN '2022-02-18' AND '2022-03-08'
ORDER BY dateThe 10-year yield closed at 1.96% on February 24, just under the prior day's 1.99%. The real flight arrived the following week: by 2022-03-01 the 10-year printed 1.72%, the lowest close in this window, and the 2-year fell from 1.58% to 1.31% over the same stretch. TLT, the long-Treasury ETF in the panel above, told the intraday version of the same story: a +1.8% gap at the open that faded to +0.2% by the close.
The days around it
Three weeks of daily closes put the reversal in context, and quietly settle whether it held:
| session | close_usd | change_pct | shares_m |
|---|---|---|---|
| 2022-02-22 | 338.09 | -1 | 85.1 |
| 2022-02-23 | 329.41 | -2.6 | 85.1 |
| 2022-02-24 | 340.65 | 3.4 | 128.5 |
| 2022-02-25 | 345.87 | 1.5 | 77.6 |
| 2022-02-28 | 347.15 | 0.4 | 76.4 |
| 2022-03-01 | 341.56 | -1.6 | 66.8 |
| 2022-03-02 | 347.18 | 1.6 | 69.8 |
| 2022-03-03 | 342.26 | -1.4 | 70.6 |
| 2022-03-04 | 337.35 | -1.4 | 75.8 |
| 2022-03-07 | 324.83 | -3.7 | 89.2 |
| 2022-03-08 | 323.28 | -0.5 | 115.3 |
| 2022-03-09 | 335.05 | 3.6 | 63.8 |
| 2022-03-10 | 331.42 | -1.1 | 68.7 |
| 2022-03-11 | 324.34 | -2.1 | 65.6 |
| 2022-03-14 | 318.21 | -1.9 | 78.9 |
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 = 'QQQ'
AND window_start >= toDateTime('2022-02-18 00:00:00') AND window_start < toDateTime('2022-03-15 23:59:00')
GROUP BY et_date
)
)
WHERE et_date >= toDate('2022-02-22') AND et_date <= toDate('2022-03-14')
ORDER BY et_dateThe sessions before the invasion were red (-1% and -2.6% on February 22–23) as the buildup dominated headlines, and the invasion day itself closed +3.4%, followed by another +1.5% the next day. Markets falling on the threat and rallying on the event is a pattern old enough to have a Wall Street proverb attached, and this tape is its cleanest modern receipt. But keep reading the table: it ends at $318.21 on 2022-03-14, below where the invasion morning's panic printed.
Did the rally hold?
No, and now the claim has receipts:
| invasion_close | invasion_low | first_close_below_invasion_low | calendar_days_to_new_low | march_low_close | march_low_date | march_low_vs_invasion_close_pct | year_low_close | year_low_date | year_low_vs_invasion_close_pct |
|---|---|---|---|---|---|---|---|---|---|
| 340.65 | 318.26 | 2022-03-14 | 18 | 318.21 | 2022-03-14 | -6.6 | 260.02 | 2022-12-28 | -23.7 |
The exact SQL behind every number
WITH
daily AS (
SELECT
toDate(toTimeZone(window_start, 'America/New_York')) AS d,
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 c
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'QQQ'
AND window_start >= toDateTime('2022-02-24 04:00:00') AND window_start < toDateTime('2023-01-01 00:00:00')
GROUP BY d
),
(SELECT c FROM daily WHERE d = toDate('2022-02-24')) AS invasion_close,
(
SELECT round(minIf(toFloat64(low), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2)
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'QQQ'
AND window_start >= toDateTime('2022-02-24 04:00:00') AND window_start < toDateTime('2022-02-24 23:59:00')
) AS invasion_low
SELECT
invasion_close,
invasion_low,
toString(minIf(d, c < invasion_low AND d > toDate('2022-02-24'))) AS first_close_below_invasion_low,
dateDiff('day', toDate('2022-02-24'), minIf(d, c < invasion_low AND d > toDate('2022-02-24'))) AS calendar_days_to_new_low,
round(minIf(c, d BETWEEN toDate('2022-02-25') AND toDate('2022-03-31')), 2) AS march_low_close,
toString(argMinIf(d, (c, d), d BETWEEN toDate('2022-02-25') AND toDate('2022-03-31'))) AS march_low_date,
round((minIf(c, d BETWEEN toDate('2022-02-25') AND toDate('2022-03-31')) / invasion_close - 1) * 100, 1) AS march_low_vs_invasion_close_pct,
round(minIf(c, d > toDate('2022-02-24')), 2) AS year_low_close,
toString(argMinIf(d, (c, d), d > toDate('2022-02-24'))) AS year_low_date,
round((minIf(c, d > toDate('2022-02-24')) / invasion_close - 1) * 100, 1) AS year_low_vs_invasion_close_pct
FROM daily18 calendar days after the invasion, on 2022-03-14, QQQ closed below the $318.26 panic low from the invasion morning's opening minute. That March low close of $318.21 sat -6.6% under the invasion-day close, and the year kept going: by 2022-12-28, QQQ closed at $260.02, -23.7% below where invasion day ended. The February 24 reversal was real, violent, and local. The rate-hike cycle, not the war, set 2022's direction, the same lesson the March 2020 crash taught in the opposite direction.
How rare is a reversal this size?
Scanning every QQQ session from 2003 through the end of 2025 finds exactly 2 sessions that traded at least 3% below the prior close intraday and still finished at least 1% above it:
| session | low_vs_prior_pct | close_vs_prior_pct | trough_to_close_pct |
|---|---|---|---|
| 2022-02-24 | -3.4 | 3.4 | 7 |
| 2022-10-13 | -3.2 | 2.3 | 5.7 |
The exact SQL behind every number
SELECT
toString(d) AS session,
low_vs_prior_pct,
close_vs_prior_pct,
trough_to_close_pct
FROM (
SELECT
d,
round((lo / prev_c - 1) * 100, 1) AS low_vs_prior_pct,
round((c / prev_c - 1) * 100, 1) AS close_vs_prior_pct,
round((c / lo - 1) * 100, 1) AS trough_to_close_pct
FROM (
SELECT d, c, lo,
lagInFrame(c) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_c
FROM (
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,
minIf(toFloat64(low), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) AS lo
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'QQQ'
AND window_start >= toDateTime('2003-01-01 00:00:00') AND window_start < toDateTime('2026-01-01 00:00:00')
GROUP BY d
)
)
WHERE prev_c > 0 AND isFinite(prev_c)
)
WHERE low_vs_prior_pct <= -3 AND close_vs_prior_pct >= 1
ORDER BY sessionFebruary 24, 2022 is one. The other is 2022-10-13, the session that opened an hour after a hotter-than-expected inflation report, another gap-down-close-green day. The invasion-day reversal was the larger of the two: a 7% trough-to-close swing against 5.7%. Twenty-three years of tape, and both prints landed in the same year.
What this day teaches
Three durable mechanics live in this tape. First: by the time a widely anticipated event happens, positioning has often already happened, the two red sessions before the invasion carried the anticipation. Second: the opening auction is where accumulated overnight fear clears at a single price, and that price can be the day's extreme in either direction. Third: a violent one-day reversal tells you about that day's flows, not the next quarter's direction, the weeks after this session went to new lows anyway. Event days reward knowing the machinery; they punish confusing a session's shape with a regime's.
The volume detail confirms who was doing what: 128.5 million shares made it the heaviest session in the three-week table above, and heavy volume on a trough-to-close climb means the buying was real size meeting real selling, not an empty-tape drift. Compare the same signature in March 24, 2020's rally, where conviction volume also marked the turn of a panic, and contrast it with thin bounce days that fade the following session.
Invasion day FAQ
What did the stock market do when Russia invaded Ukraine?
On the day itself, February 24, 2022, the Nasdaq-100 ETF QQQ opened -3.2% down with its low in the opening minute, then climbed all session to close +3.4%. The S&P 500 ETF SPY closed +1.5% and the Dow ETF DIA +0.3%.
Why did stocks go up when Russia invaded Ukraine?
This page doesn't assign motives; the tape records the sequence, heavy selling pressure was fully expressed at the 9:30 open, and buying dominated every subsequent hour. The falling-on-threat, rallying-on-event pattern has many historical parallels.
What did oil and gold do when Russia invaded Ukraine?
Both spiked at the open and faded: the energy ETF XLE gapped +2.1% and closed -0.9%, and the gold ETF GLD gapped +2.4% and closed -0.6%. Defense stocks held their gains: all four majors in the panel above closed green.
Did the rally after the invasion last?
No. QQQ closed below its invasion-day panic low 18 calendar days later, on 2022-03-14, and by its 2022 low on 2022-12-28 it stood -23.7% below the invasion-day close.
How rare is a reversal like February 24, 2022?
Very. From 2003 through the end of 2025, only 2 QQQ sessions traded 3% or more below the prior close intraday and still closed at least 1% higher, this one, and 2022-10-13.
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.