Strasmore Research
Market Recap

Q2 2026 market recap: ETF returns double digits

Matt ConnorBy Matt Connor Updated 2026-07-25

Di second quarter of 2026 na di recovery quarter: after di first quarter wey negative, every major index ETF rise double digits, with QQQ dey lead at 26.5%. Di panels wey dey below carry di shape of di quarter — di monthly staircase, di rates backdrop, and di calendar — plus im two data caveats, wey dem disclose inline. Every number na stored query result; expand any panel for di exact SQL.

Di quarter wey dey for board

How big di swing from Q1 be? Dis panel dey calculate both quarters returns inside one query — di Q1 reference dey recompute live on di same definitions, e no dey read from anywhere.

QueryQ2 2026 returns for di four index ETFs, with Q1 recomputed live for contrast
The exact SQL behind every number
SELECT ticker,
    round((argMaxIf(toFloat64(close), window_start, toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-06-30') AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)
         / argMinIf(toFloat64(open), window_start, toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2026-04-01') AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) - 1) * 100, 1) AS q2_return_pct,
    round((argMaxIf(toFloat64(close), window_start, toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-03-31') AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959)
         / argMinIf(toFloat64(open), window_start, toDate(toTimeZone(window_start, 'America/New_York')) >= toDate('2026-01-01') AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) - 1) * 100, 1) AS q1_return_pct,
    round(argMaxIf(toFloat64(close), window_start, toDate(toTimeZone(window_start, 'America/New_York')) <= toDate('2026-06-30') AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2) AS q2_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'DIA', 'IWM')
  AND window_start >= toDateTime('2026-01-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')
GROUP BY ticker
ORDER BY ticker

QQQ rise 26.5% for di quarter after a -6.9% Q1; SPY 14.1% after -5.2%; IWM 20.2%. Every index wey fall for first quarter more dan recover for second quarter. Di four dey tell three different stories: di growth index collect di deepest Q1 drawdown and post di biggest recovery; DIA — di price-weighted industrials index — swing from -3.9% to 12.1%; and IWM no really fall at all (0.1% for Q1), so im 20.2% quarter compound a flat start instead of repairing a drawdown. Recovery and momentum look identical inside a quarterly return column; na di Q1 column dey tell dem apart.

Di quarter against every second quarter wey dey for tape

Recovery quarters dey feel like history from inside dem; di tape fit tell us if dis one measure like dat. Di panels dey recompute di SAME April-to-June return for every year of minute history inside one query, identical arithmetic each year, den rank dis quarter against di rest.

QueryEvery second quarter wey dey di tape: SPY and QQQ, recomputed identically by year (session counts shown)
The exact SQL behind every number
SELECT toYear(toTimeZone(window_start, 'America/New_York')) AS y,
       ticker,
       uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions,
       round((argMax(toFloat64(close), window_start) / argMin(toFloat64(open), window_start) - 1) * 100, 1) AS q2_return_pct
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ')
  AND window_start >= toDateTime('2003-01-01 00:00:00')
  AND window_start < toDateTime('2026-07-01 00:00:00')
  AND toMonth(toTimeZone(window_start, 'America/New_York')) BETWEEN 4 AND 6
  AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
GROUP BY y, ticker
HAVING sessions >= 50
ORDER BY y ASC, ticker ASC
QueryDi rank receipts: dis quarter against every prior second quarter (rank 1 na best; self-excluded)
The exact SQL behind every number
SELECT ticker,
       round(anyIf(ret, y = 2026), 1) AS q2_2026_pct,
       arrayCount(x -> x > anyIf(ret, y = 2026), groupArrayIf(ret, y != 2026)) + 1 AS rank_best,
       count() AS q2s_compared,
       min(y) AS first_year,
       anyIf(sessions, y = 2026) AS sessions_2026
FROM (
    SELECT toYear(toTimeZone(window_start, 'America/New_York')) AS y,
           ticker,
           uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions,
           (argMax(toFloat64(close), window_start) / argMin(toFloat64(open), window_start) - 1) * 100 AS ret
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ')
      AND window_start >= toDateTime('2003-01-01 00:00:00')
      AND window_start < toDateTime('2026-07-01 00:00:00')
      AND toMonth(toTimeZone(window_start, 'America/New_York')) BETWEEN 4 AND 6
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY y, ticker
    HAVING sessions >= 50
)
GROUP BY ticker
ORDER BY ticker ASC

E dey measure close to historic: QQQ im 26.5% rank na 2 out of 17 comparable second quarters (rank 1 = best) — exactly one prior second quarter finish higher, and di by-year table above show which one. SPY im 14.1% rank na 3 out of 23 since 2004. Two disclosures dey follow am: QQQ im series short (di fund trade under different root symbol for some years — a session-count guard exclude dose years visibly, with per-year counts for di table), and every history query pin im upper bound for dis quarter end, so di comparison set no fit silently grow when later years land. Basis for every rank for dis page: regular-hours open-to-close inside di calendar quarter.

Di monthly staircase

QueryQ2 month by month: April, May, June (SPY and QQQ, recomputed identically)
The exact SQL behind every number
SELECT toString(toStartOfMonth(toDate(toTimeZone(window_start, 'America/New_York')))) AS period_start, ticker,
    round((argMaxIf(toFloat64(close), window_start, (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199) / argMinIf(toFloat64(open), window_start, (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199) - 1) * 100, 1) AS month_return_pct,
    round(argMaxIf(toFloat64(close), window_start, (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199), 2) AS month_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ')
  AND window_start >= toDateTime('2026-04-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')
GROUP BY period_start, ticker
ORDER BY period_start, ticker

Di quarter bin front-loaded: April do di heavy lifting (SPY 9.9%, QQQ 14.8%), May add more (4.9% and 10.3%), and June give small back — SPY -1.2%. Di June recap carry di month for detail, including di breadth split under am.

Di leaders for di tape, quarter scale

QueryQ2 regular-hours dollar volume, whole tape (one reused-symbol listing dey excluded pending entity verification)
The exact SQL behind every number
SELECT ticker,
    round(sum(toFloat64(close) * toFloat64(volume)) / 1e9, 1) AS dollar_bn,
    round(100 * sum(toFloat64(close) * toFloat64(volume)) / max(sum(toFloat64(close) * toFloat64(volume))) OVER (), 1) AS pct_of_leader
FROM global_markets.delayed_stocks_minute_aggs
WHERE window_start >= toDateTime('2026-04-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')
  AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
  AND ticker NOT IN ('SPCX')
GROUP BY ticker
ORDER BY dollar_bn DESC
LIMIT 8

For di full quarter, MU ($2047.5 billion) edge out SPY ($2032 billion) for di top of di tape — na one single stock wey out-trade di flagship index fund across three months, by 99.2% of di leader total. Behind dem: QQQ ($1658.5 billion), den NVDA at $1591.8 billion — wey dem break down June alone tick by tick here — and TSLA at $1198.8 billion. Make una note wetin di list be: na regular-hours turnover, no be performance — one name fit dominate di tape for one month wey dey fall. Dem break down di leader own June tick by tick for im deep-dive. Basis: April 1–June 30 regular hours; dem exclude one reused-symbol June listing pending entity verification (im receipts).

Breadth: di grain for today

QuerySPY sessions up vs down across di quarter, one cheap receipt
The exact SQL behind every number
SELECT countIf(day_ret > 0) AS up_sessions,
       countIf(day_ret < 0) AS down_sessions,
       countIf(day_ret = 0) AS flat_sessions,
       count() AS sessions_total
FROM (
    SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
           round((argMax(toFloat64(close), window_start) / argMin(toFloat64(open), window_start) - 1) * 100, 2) AS day_ret
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= toDateTime('2026-04-01 00:00:00')
      AND window_start < toDateTime('2026-07-01 00:00:00')
      AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
    GROUP BY d
)

36 sessions out of di quarter 62 sessions wey close, SPY bin high pass 26 wey low — dat up-day dominance match one double-digit quarter. Di ticker-grain count (every advancer plus decliner) get border for quarter level: di single whole-market scan pass di query budget wey dis page fit generate under (as dem measure am during authoring), so di equal-count breadth dey stay for month level — June own advancer/decliner split, where more fall dan rise, dey inside di June recap — and dem disclose di border here instead of make dem narrow am for silence.

Di options tape, month by month

Di quarter microstructure story, dem tell am for di only scale wey fit tell am — month over month across di whole listed options tape. Volume rise as quarter dey end: 1386.9 million contracts for April to 1477.9 million for June, wey be di busiest month for di quarter. Di same-day-expiry (0DTE) share rise join body, from 30.3% to 34.3% — June share na di highest for di half, and di January-onward evolution dey inside di H1 recap.

QueryApril: whole-tape options contract volume and same-day-expiry share (one scan)
The exact SQL behind every number
SELECT round(sum(toFloat64(volume)) / 1e6, 1) AS contracts_mm,
       round(100.0 * sumIf(toFloat64(volume), toDateOrNull(concat('20', substring(ticker, length(ticker) - 14, 6))) = toDate(toTimeZone(window_start, 'America/New_York'))) / sum(toFloat64(volume)), 1) AS zero_dte_pct,
       uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime('2026-04-01 00:00:00')
  AND window_start < toDateTime('2026-05-01 00:00:00')
QueryMay: whole-tape options contract volume and same-day-expiry share (one scan)
The exact SQL behind every number
SELECT round(sum(toFloat64(volume)) / 1e6, 1) AS contracts_mm,
       round(100.0 * sumIf(toFloat64(volume), toDateOrNull(concat('20', substring(ticker, length(ticker) - 14, 6))) = toDate(toTimeZone(window_start, 'America/New_York'))) / sum(toFloat64(volume)), 1) AS zero_dte_pct,
       uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime('2026-05-01 00:00:00')
  AND window_start < toDateTime('2026-06-01 00:00:00')
QueryJune: whole-tape options contract volume and same-day-expiry share (one scan)
The exact SQL behind every number
SELECT round(sum(toFloat64(volume)) / 1e6, 1) AS contracts_mm,
       round(100.0 * sumIf(toFloat64(volume), toDateOrNull(concat('20', substring(ticker, length(ticker) - 14, 6))) = toDate(toTimeZone(window_start, 'America/New_York'))) / sum(toFloat64(volume)), 1) AS zero_dte_pct,
       uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions
FROM global_markets.options_minute_aggs
WHERE window_start >= toDateTime('2026-06-01 00:00:00')
  AND window_start < toDateTime('2026-07-01 00:00:00')

How much e dey cost to trade, wey dem dey check every month

QuerySPY median quoted spread on one labeled sample session per month of di quarter (second Wednesdays)
The exact SQL behind every number
SELECT toDate(toTimeZone(sip_timestamp, 'America/New_York')) AS session,
       round(quantileDeterministicIf(0.5)((toFloat64(ask_price) - toFloat64(bid_price)) / ((toFloat64(ask_price) + toFloat64(bid_price)) / 2) * 10000, toUInt64(toUnixTimestamp64Micro(sip_timestamp)), bid_price > 0 AND ask_price >= bid_price), 3) AS med_spread_bps,
       round(count() / 1e6, 2) AS quote_updates_m,
       countIf(NOT (bid_price > 0 AND ask_price > 0 AND ask_price >= bid_price)) AS invalid_dropped
FROM global_markets.cache_stocks_quotes
WHERE ticker = 'SPY'
  AND ((sip_timestamp >= toDateTime64('2026-04-15 13:30:00', 9) AND sip_timestamp < toDateTime64('2026-04-15 20:00:00', 9))
    OR (sip_timestamp >= toDateTime64('2026-05-13 13:30:00', 9) AND sip_timestamp < toDateTime64('2026-05-13 20:00:00', 9))
    OR (sip_timestamp >= toDateTime64('2026-06-10 13:30:00', 9) AND sip_timestamp < toDateTime64('2026-06-10 20:00:00', 9)))
GROUP BY session
ORDER BY session ASC

SPY own median quoted spread for di quarter monthly sample sessions: na 0.286 bps for April sample, 0.27 for May own, 0.409 for June own — di June sample na im wide pass. Wetin we use: na single sample sessions wey dem mark (di second Wednesday of every month), no be medians wey cover di whole month; dem dey count invalid quotes inside di panel, dem no dey comot am.

Rates: di quarter wey di curve flat

QueryDi 2s10s spread and di 10-year through Q2, daily
The exact SQL behind every number
SELECT toString(date) AS d,
    round(yield_10_year, 2) AS y10,
    round((yield_10_year - yield_2_year) * 100, 0) AS spread_2s10s_bp
FROM global_markets.treasury_yields
WHERE date >= toDate('2026-04-01') AND date <= toDate('2026-06-30')
  AND isNotNull(yield_10_year) AND isNotNull(yield_2_year)
ORDER BY date

Di 2s10s spread enter di quarter for 52 basis points and end for 30 — na flattening quarter, with di 10-year finish for 4.44%. Di six-month version of dis story dey inside di H1 recap. Mechanically, flattening mean say di short end close gap with di long end; whether e go continue, dis table no fit answer, and dis page no try.

Di calendar, quarter scale

QueryQ2 corporate calendar (di June 30 filing-index gap disclosed)
The exact SQL behind every number
SELECT
    (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= toDate('2026-04-01') AND ex_dividend_date <= toDate('2026-06-30')) AS ex_div_events,
    (SELECT count() FROM global_markets.stocks_splits WHERE execution_date >= toDate('2026-04-01') AND execution_date <= toDate('2026-06-30')) AS splits,
    (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date >= toDate('2026-04-01') AND listing_date <= toDate('2026-06-30')) AS ipos,
    (SELECT uniqExact(accession_number) FROM global_markets.stocks_sec_edgar_index WHERE filing_date >= toDate('2026-04-01') AND filing_date <= toDate('2026-06-30')) AS q2_filings,
    (SELECT uniqExact(accession_number) FROM global_markets.stocks_sec_edgar_index WHERE filing_date = toDate('2026-04-30')) AS filings_apr30,
    (SELECT uniqExact(accession_number) FROM global_markets.stocks_sec_edgar_index WHERE filing_date = toDate('2026-06-30')) AS filings_jun30

105 companies wey list for di quarter, 435 splits wey dem execute, and 15402 ex-dividend events wey dem pay out. Di filing count (235075) carry di disclosed gap: di index hold only 31 filings for June 30 — di quarter last day — and 34 for April 30, against thousands for neighboring days, so di quarter total cut TWO month-end days and e understated until di feed backfill. Di month-end gap get im own diagnostic note.

Di calendar ramp wey lead to quarter-end

QueryListings, splits, and ex-dividend events by month through di quarter
The exact SQL behind every number
SELECT m, ipos, splits, ex_divs FROM (
    SELECT '2026-04' AS m,
        (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date >= toDate('2026-04-01') AND listing_date <= toDate('2026-04-30')) AS ipos,
        (SELECT count() FROM global_markets.stocks_splits WHERE execution_date >= toDate('2026-04-01') AND execution_date <= toDate('2026-04-30')) AS splits,
        (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= toDate('2026-04-01') AND ex_dividend_date <= toDate('2026-04-30')) AS ex_divs, 1 AS o
    UNION ALL SELECT '2026-05',
        (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date >= toDate('2026-05-01') AND listing_date <= toDate('2026-05-31')),
        (SELECT count() FROM global_markets.stocks_splits WHERE execution_date >= toDate('2026-05-01') AND execution_date <= toDate('2026-05-31')),
        (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= toDate('2026-05-01') AND ex_dividend_date <= toDate('2026-05-31')), 2
    UNION ALL SELECT '2026-06',
        (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date >= toDate('2026-06-01') AND listing_date <= toDate('2026-06-30')),
        (SELECT count() FROM global_markets.stocks_splits WHERE execution_date >= toDate('2026-06-01') AND execution_date <= toDate('2026-06-30')),
        (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= toDate('2026-06-01') AND ex_dividend_date <= toDate('2026-06-30')), 3
) ORDER BY o

Di corporate calendar tight as di quarter dey old: ex-dividend events climb every month, from 3947 for April go reach 6651 for June, and splits rise join body (129 to 164). Listings hold steady across di three months — di wave metaphor na for dividends, no be IPOs, dis quarter.

Di session receipt

Query62 sessions for di quarter, verified from di tape, month by month
The exact SQL behind every number
SELECT
    (SELECT uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) FROM global_markets.delayed_stocks_minute_aggs
     WHERE ticker = 'SPY' AND window_start >= toDateTime('2026-04-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')) AS q2_sessions,
    (SELECT uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) FROM global_markets.delayed_stocks_minute_aggs
     WHERE ticker = 'SPY' AND window_start >= toDateTime('2026-04-01 00:00:00') AND window_start < toDateTime('2026-05-01 00:00:00')) AS april_sessions,
    (SELECT uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) FROM global_markets.delayed_stocks_minute_aggs
     WHERE ticker = 'SPY' AND window_start >= toDateTime('2026-05-01 00:00:00') AND window_start < toDateTime('2026-06-01 00:00:00')) AS may_sessions,
    (SELECT uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) FROM global_markets.delayed_stocks_minute_aggs
     WHERE ticker = 'SPY' AND window_start >= toDateTime('2026-06-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')) AS june_sessions

62 sessions, we verify dem from bars wey we observe instead of just assuming from calendar: 21 for April, 20 for May (Memorial Day), 21 for June (Juneteenth). Di month-level receipts and closure evidence dey inside di June recap and di holiday explainer.

Data notes

Full data notes
  • Short interest end for June 15 settlement as at di time dem generate am — di end-of-June print never ready; di June recap get di full disclosure. Dis post go regenerate when e land.
  • Di month-end filing-index gap dem show am inside di text: June 30 AND April 30 fall inside dis quarter, and March 31 show di same signature — every 2026 month-end wey im last calendar day be weekday near-empty. Di diagnostic note get di receipts and di bounded diagnosis.
  • Microstructure (tick-level plumbing) dem deliberately comot am here — di June 29 deep-dive get full session tape anatomy.
  • Volume-leader exclusion: one reused-symbol June listing dem exclude am pending entity verification.

How We Do Am

  • Di period na April 1 go reach June 30, 2026 — 62 sessions, wey we verify from di bars wey show. Quarterly returns na from first regular-hours open to last regular-hours close inside di period; di Q1 contrast wey we recompute live dey use di same definitions.
  • Timestamps dey stored for UTC, wey we filter with raw UTC bounds; regular hours na 13:30–20:00 UTC across di (EDT) quarter. Dollar volume na minute close times minute volume over regular hours. Multi-year history blocks dey filter regular hours on di Eastern wall clock (wey dem convert per row — di DST-safe convention across decades) and pin dia upper bound at dis quarter end; di depth verification (minute history go reach September 2003) dey inside di H1 recap methodology.
  • Generation dey run through di gated read-only path; di public page no dey ever query live. Warehouse state as of July 5, 2026.

Dis na di first edition of di standing quarterly recap; Q3 edition go link back here. Month detail: June. Di half wey dis quarter complete: H1 2026.