Strasmore Research
Market Recap

Market Recap: H1 2026 — How Di First Half Shake

Matt ConnorBy Matt Connor Updated 2026-07-25

Di first half of 2026 na two different markets wey dem join together: one first quarter wia every major index ETF fall or just stand still, and one second quarter wey repair everything well well. SPY finish di half up 8.8%; QQQ up 18.7%; and IWM's 21.3% na di best first half wey di small-cap ETF don print for di tape wey we dey hold — di receipt dey below, wey dem recompute against every year before am inside one query. Di rates market tell im own story: di 2s10s spread flatten from 72 basis points to 30 across di half without ever invert. This page na di full ledger of di half — di scoreboard, di history rank, di monthly staircase, di whole yield curve maturity by maturity, di eleven sectors, di options tape month-by-month evolution, and di notable single names of di half. Every number here na stored query result; expand any panel for di SQL.

The half on the board

QueryH1 2026: half, Q1, and Q2 returns for the four index ETFs — dem compute am for one query
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-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 h1_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)
         / 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-06-30') AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959), 2) AS h1_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

The two-quarter split na di shape wey define di half: SPY -5.2% then 14.1%; QQQ -6.9% then 26.5%. IWM be di quiet leader for di half at 21.3% — small caps beat every large-cap index across di six months. Di quarter detail dey inside di Q2 recap. One note on di arithmetic of halves: SPY 8.8% for di six months dey far less than Q2 alone, because di half join one drawdown plus one recovery — di compounding of -5.2% and 14.1% land where di table talk, and any 'first-half return' headline quietly carry that whole round trip inside.

The half wey dey face every first half for the tape

8.8% half dey unusual? The panels wey dey below dey recalculate the SAME first-half return — from first normal-time opening to last normal-time closing, January reach June — for every year of minute-by-minute record, inside one query per ETF pair with same calculation for every year, then dem rank this half against the rest. The minute tape first date, dem verify am for the method wey dey below; the first complete first half wey dey on file na the one the receipts name.

QueryEvery first half wey don pass for tape: SPY and QQQ, dem recompute am same way by year (per-year session counts dey show)
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 h1_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')) <= 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 >= 100
ORDER BY y ASC, ticker ASC
QueryThe rank receipts: this half versus every one wey don pass before (SPY and QQQ; rank 1 na best; self-excluded)
The exact SQL behind every number
SELECT ticker,
       round(anyIf(ret, y = 2026), 1) AS h1_2026_pct,
       arrayCount(x -> x > anyIf(ret, y = 2026), groupArrayIf(ret, y != 2026)) + 1 AS rank_best,
       count() AS halves_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')) <= 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 >= 100
)
GROUP BY ticker
ORDER BY ticker ASC

SPY 8.8% dey rank 6 out of 23 first halves since 2004 (rank 1 = the best) — upper third, no be record. QQQ 18.7% dey rank 3 out of 16 comparable halves — the QQQ series no too long — the fund don trade under another root symbol for some years — and any year wey get less than one hundred first-half sessions for one symbol, dem remove am through session-count guard wey dey the table, so you fit see the ones wey dem remove, dem no hide am.

QueryEvery first half for tape: DIA and IWM, same arithmetic
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 h1_return_pct
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('DIA', 'IWM')
  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')) <= 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 >= 100
ORDER BY y ASC, ticker ASC
QueryThe rank receipts: DIA and IWM versus every first half wey don pass (rank 1 na best; self-excluded)
The exact SQL behind every number
SELECT ticker,
       round(anyIf(ret, y = 2026), 1) AS h1_2026_pct,
       arrayCount(x -> x > anyIf(ret, y = 2026), groupArrayIf(ret, y != 2026)) + 1 AS rank_best,
       count() AS halves_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 ('DIA', 'IWM')
      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')) <= 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 >= 100
)
GROUP BY ticker
ORDER BY ticker ASC

The small-cap row na the headline for the half: IWM dey rank 1 out of 23 first halves since 2004 — the best one wey don dey record for the fund, pass every recovery half wey dey the table. DIA 8.4% dey rank 4 out of 23. Basis, as e be: normal-time open-to-close inside each calendar first half, same calculation for every year, upper bound dem pin am for the end of this period so the comparison group no go just dey grow when dem add later years.

Month by month

QueryThe half, month by month (SPY and QQQ, dem recompute am same way inside one query)
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(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) / argMinIf(toFloat64(open), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) - 1) * 100, 1) AS month_return_pct,
    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 month_close
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ')
  AND window_start >= toDateTime('2026-01-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')
GROUP BY period_start, ticker
ORDER BY period_start, ticker

The staircase dey climb small, come down, down, sharp sharp up, up, small down: April be di half own pivot (SPY 9.9%, QQQ 14.8% — QQQ own best month of di six), and June close di half quietly for -1.2% inside SPY. June own recap, plus di breadth split wey dey behind dat quiet number, dey here. If you read di table month by month: di year open nearly flat (SPY 0.9% for January), slide through February (-0.5%) and March (-4.2%), then April reverse di whole run inside one single month, May extend am (4.9%), and June cool down small. Three out of six months be negative — coin-flip month count for half wey finish solidly positive, wey be di usual shape of equity halves and e worth to remember anytime one single month dey read like verdict.

Rates: di whole curve, maturity by maturity

Treasury curve spend di half twisting, no be shifting. Di very front end — di maturities wey dey track policy pass close — barely move; two-year rise two-thirds of percentage point; thirty-year barely notice. Di panel lay di whole curve out, start of half against end.

QuerySeven maturities: yield when the half start vs when e end, plus the change
The exact SQL behind every number
SELECT maturity, start_pct, end_pct, round((end_pct - start_pct) * 100, 0) AS change_bp FROM (
    SELECT '1-month' AS maturity, (SELECT round(argMin(yield_1_month, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_1_month)) AS start_pct, (SELECT round(argMax(yield_1_month, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_1_month)) AS end_pct, 1 AS o
    UNION ALL SELECT '3-month', (SELECT round(argMin(yield_3_month, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_3_month)), (SELECT round(argMax(yield_3_month, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_3_month)), 2
    UNION ALL SELECT '1-year', (SELECT round(argMin(yield_1_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_1_year)), (SELECT round(argMax(yield_1_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_1_year)), 3
    UNION ALL SELECT '2-year', (SELECT round(argMin(yield_2_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_2_year)), (SELECT round(argMax(yield_2_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_2_year)), 4
    UNION ALL SELECT '5-year', (SELECT round(argMin(yield_5_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_5_year)), (SELECT round(argMax(yield_5_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_5_year)), 5
    UNION ALL SELECT '10-year', (SELECT round(argMin(yield_10_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_10_year)), (SELECT round(argMax(yield_10_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_10_year)), 6
    UNION ALL SELECT '30-year', (SELECT round(argMin(yield_30_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_30_year)), (SELECT round(argMax(yield_30_year, date), 2) FROM global_markets.treasury_yields WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30') AND isNotNull(yield_30_year)), 7
) ORDER BY o

Read di change column top to bottom and di twist go clear: 1-month move -2 basis points for six months while 2-year rise 67 and 30-year just 5. Parallel shift dey move every maturity together; dis half move di middle of di curve and pin both ends — short rates anchor near policy, long rates barely reprice.

Wetin e do to di slope

QuerySlope receipts: start, end, minimum — the spread never invert this half
The exact SQL behind every number
SELECT
    round((argMin(yield_10_year - yield_2_year, date)) * 100, 0) AS start_bp,
    round((argMax(yield_10_year - yield_2_year, date)) * 100, 0) AS end_bp,
    round(min((yield_10_year - yield_2_year)) * 100, 0) AS min_bp,
    count() AS prints
FROM global_markets.treasury_yields
WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30')
  AND isNotNull(yield_10_year) AND isNotNull(yield_2_year)

Short end dey rise pass long end na wetin flattening be: di 2s10s spread grind down from 72 to 30 basis points across 124 prints — and di lowest print for di half na 27, so di curve flatten without ever invert. Weda flatter curve "mean" anytin about wetin go come next na forecast, and dis table no dey make forecast.

QueryThe 2s10s spread, every print wey the half get
The exact SQL behind every number
SELECT toString(date) AS d, round((yield_10_year - yield_2_year) * 100, 0) AS spread_2s10s_bp
FROM global_markets.treasury_yields
WHERE date >= toDate('2026-01-01') AND date <= toDate('2026-06-30')
  AND isNotNull(yield_10_year) AND isNotNull(yield_2_year)
ORDER BY date

Di daily series show say di flattening no be one event but grind — 124 prints dey step down through di half, with di spread start di year at 72 basis points and end at 30.

Sectors: the dispersion the index hid

Which parts of the market actually make the half money? The eleven SPDR sector ETFs, half and quarter split, one query.

QueryThe eleven sector ETFs: H1 2026 return, Q1 and Q2 split, plus H1 dollar volume
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-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 h1_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)
         / 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(sumIf(toFloat64(close) * toFloat64(volume), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) / 1e9, 1) AS h1_dollar_bn
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('XLB', 'XLC', 'XLE', 'XLF', 'XLI', 'XLK', 'XLP', 'XLRE', 'XLU', 'XLV', 'XLY')
  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

The dispersion na the story: technology (XLK) finish the half up 30.8% — with a 42% second quarter — while communication services (XLC) finish -9.3%, one spread of about forty percentage points between the best sector and the worst sector inside six months. To own "the market" na to own both ends of that one. Energy na the panel cautionary tale about long windows: XLE show one healthy 18.8% half — dem build am from one 36.9% FIRST quarter wey -11% second follow. E be the market strongest large sector for the quarter wey the indexes fall, and among the weakest for the quarter wey dem recover. Financials (XLF) run the same movie backwards: -9.9% for Q1, then 7.7% for Q2. One single H1 number dey flatten both round trips; the quarter split na the honest view.

Wetin e take trade each sector

Return tables dey everywhere; quote tapes no dey so. This panel dey measure each sector ETF median bid-ask spread across one full representative session — 123 sessions inside di half, and June 29 na di one dem measure here, dem label am as such — from every NBBO update that day.

QueryMedian quoted spread by sector ETF, one representative session (June 29, 2026), regular hours
The exact SQL behind every number
SELECT ticker,
    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), 2) 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 IN ('XLB', 'XLC', 'XLE', 'XLF', 'XLI', 'XLK', 'XLP', 'XLRE', 'XLU', 'XLV', 'XLY')
  AND sip_timestamp >= toDateTime64('2026-06-29 13:30:00', 9) AND sip_timestamp < toDateTime64('2026-06-29 20:00:00', 9)
GROUP BY ticker
ORDER BY med_spread_bps

Every one of the eleven quotes dey at few basis points or less — di cheapest na 0.62 bps (XLV), di widest na 2.23 bps (XLRE) — which na di practical meaning of "sector ETFs dey liquid": di cost to enter any sector view dey measured in hundredths of a percent. For scale against individual stocks, the June 29 deep-dive dey measure one thin name spread at hundreds of times these levels, on the same session.

Breadth across the half

Breadth for half scale dey come in two grains here. The coarse grain: SPY log 64 up sessions against 59 down across the half's 123 — e be like say na almost coin flip of days underneath one solidly positive half, na so one grind higher dey look from the daily tape. The fine grain — every ticker wey dem count — dey live for month scale: June advancer/decliner split dey inside the June recap (more tickers fall pass the ones wey rise there), and the quarter-boundary count dey inside the Q2 recap. One single six-month whole-market advancer scan pass the query budget wey this page dey generate under; that bound na im dem disclose here instead of to narrow am quietly.

QuerySPY sessions wey go up vs ones wey go down across the half, 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-01-01 00:00:00')
      AND window_start < toDateTime('2026-07-01 00:00:00')
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY d
)

Di options tape, month by month

Wetin only one half-year page fit show: how di options tape itself don change. Each panel wey dey below na one whole-tape scan of one month — total contract volume, and di share wey trade for im own expiry day (0DTE). Di story wey dem measure: volume run from 1316.1 million contracts for January reach 1477.9 million for June — di half-year busiest options month — and di same-day-expiry share climb from 26.5% for January reach 34.3% for June, di highest month of di half. One third of June options volume na contracts wey expire that same day.

QueryJanuary: 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-01-01 00:00:00')
  AND window_start < toDateTime('2026-02-01 00:00:00')
QueryFebruary: 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-02-01 00:00:00')
  AND window_start < toDateTime('2026-03-01 00:00:00')
QueryMarch: 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-03-01 00:00:00')
  AND window_start < toDateTime('2026-04-01 00:00:00')
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 cost to trade, dem sample am evri month

QuerySPY median quoted spread for one labeled sample session per month (second Wednesdays), every NBBO update
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-01-14 14:30:00', 9) AND sip_timestamp < toDateTime64('2026-01-14 21:00:00', 9))
    OR (sip_timestamp >= toDateTime64('2026-02-11 14:30:00', 9) AND sip_timestamp < toDateTime64('2026-02-11 21:00:00', 9))
    OR (sip_timestamp >= toDateTime64('2026-03-11 13:30:00', 9) AND sip_timestamp < toDateTime64('2026-03-11 20:00:00', 9))
    OR (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 median quoted spread for di second Wednesday of evri month: 0.145 bps for January sample, 0.144 for February one, e rough double dat one through di spring samples, and 0.409 bps for June sample — na di widest of all di six. Basis, make e clear: dis na single labeled sample sessions (di second Wednesday of evri month), no be month-wide medians — one six-month whole-quote-tape scan pass di query budget for dis page. Di sample dates dey inside di panel, and invalid quotes dem dey count dia, dem no dey drop dem quietly.

The half's notable names

Measured notability, not editorial pick: the set below is the top of the quarter-scale dollar-volume leaderboard, recomputed here so this page carries its own receipt, and each name that earned a deep-dive links to it.

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

MU out-traded SPY across the second quarter ($2047.5 billion against $2032 billion) — the same receipt the Q2 recap carries. For the six single names in that leader set, the half:

QueryThe leader set half and Q2 returns plus half dollar volume — one ticker-filtered query
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)
         / argMinIf(toFloat64(open), window_start, (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) - 1) * 100, 1) AS h1_return_pct,
    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)
         / argMinIf(toFloat64(open), window_start, window_start >= toDateTime('2026-04-01 00:00:00') 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(sumIf(toFloat64(close) * toFloat64(volume), (toHour(toTimeZone(window_start, 'America/New_York')) * 60 + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959) / 1e9, 1) AS h1_dollar_bn
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('MU', 'NVDA', 'TSLA', 'SNDK', 'AMD', 'INTC')
  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

The memory-and-semiconductor complex owns the half: SNDK returned 830.1% in six months, MU 290% — its June wey dem dissect am tick by tick dey here — and INTC 269.3%. NVDA, the set's biggest tape at $3287.9 billion of half-year turnover, returned 5.2% — single digits while the memory names multiplied; its June deep-dive carry that divergence session by session. TSLA na the set's one decliner at -8.2%. The June mega-listing trades under a reused symbol and dem exclude am from leader scans pending entity verification — its first month get its own receipt-by-receipt post. Basis: the leader SET dem choose am by Q2 regular-hours dollar volume (the scan above); the return columns na the half's, first regular-hours open to last regular-hours close.

The half's IPO wave, month by month

QueryNew listings per month across the half
The exact SQL behind every number
SELECT toString(toStartOfMonth(listing_date)) AS m, count() AS listings
FROM global_markets.stocks_ipos
WHERE listing_date >= toDate('2026-01-01') AND listing_date <= toDate('2026-06-30')
GROUP BY m
ORDER BY m

The listing wave don front-load and e no even: February na the half's busiest month with 47 listings, and March na the quietest one with 17 — the famous June mega-listing show for month wey otherwise get ordinary volume (35 listings).

The half's calendar

QueryH1 2026 corporate calendar (all three month-end filing-index gaps dem disclose)
The exact SQL behind every number
SELECT
    (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= toDate('2026-01-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-01-01') AND execution_date <= toDate('2026-06-30')) AS splits,
    (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date >= toDate('2026-01-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-01-01') AND filing_date <= toDate('2026-06-30')) AS h1_filings,
    (SELECT uniqExact(accession_number) FROM global_markets.stocks_sec_edgar_index WHERE filing_date = toDate('2026-03-31')) AS filings_mar31,
    (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

205 companies come public for the half — one listing wave wey biggest June debut get im own receipt-by-receipt post — plus 830 splits and 28356 ex-dividend events. For scale, e mean more than one new listing per trading session across the half. The filing total carry one triple disclosure: the SEC index dey near-empty for every month-end of the half wey last calendar day be weekday (55 filings wey dem index for March 31, 34 for April 30, and 31 for June 30, against thousands for neighboring days), so half-year filing counts dey understated until the feed backfill — the month-end gap get im own diagnostic note.

Di session receipt

Query123 sessions for the half, dem verify am from the tape
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-01-01 00:00:00') AND window_start < toDateTime('2026-07-01 00:00:00')) AS h1_sessions

Data notes

Full data notes
  • Short interest stop for June 15 settlement as of when dem generate am (end-of-June print never come yet — full disclosure dey inside June recap; this post go regenerate when e land).
  • Three 2026 month-end filing-index days near empty (March 31, April 30, June 30 — every month-end of the half wey last calendar day be weekday); dem disclose am inline with the counts, dem diagnose am inside the month-end gap note. Last-year month-ends carry thousands of filings, so na feed gap, no be filing holiday.
  • The leaderboard for half scale get bound: six-month market-wide scan pass the query budget wey this page dey generate under, so dem choose the notable-names set from the quarter-scale leader scan wey dey inline, and dem compute the half returns for that fixed set — dem state the basis where dem use am. The Q2 and June recaps carry the full leader tables for their own scales.
  • Whole-market breadth for half scale get bound too — dem show the day-grain split; the ticker-grain counts dey for month and quarter scale, dem link am inside the breadth section.
  • The QQQ history series no reach as SPY own: the fund trade under different root symbol for some years; the session-count guard dey exclude those years visibly (per-year session counts dey inside the table).
  • The six options panels na whole-tape monthly scans and the spread trend sample one labeled session per month — both dey run on the batch path; dem state sample bases inline.
  • Microstructure dey inside the June 29 deep-dive.

How we do am

  • The time wey dey here na January 1 – June 30, 2026 — 123 sessions, wey we don verify from the bars wey we see. Returns na from first regular-hours open to last regular-hours close inside each window, all computed inside the same queries wey dey show.
  • Historical comparisons, we dey recompute am LIVE against the full minute history for the time wey we dey generate — we never read am from stored values. Depth verification: the earliest minute-bar date inside the delayed view wey we use here match the base table (September 2003), we check am when we dey write, so the first complete first half wey dey on file na 2004's. Every history query dey pin its upper bound for this period end (the comparison set no fit grow when later years enter) and we dey apply minimum-session guard with per-year session counts wey dey show.
  • Timestamps dey stored for UTC with raw UTC bounds. Multi-year history blocks dey filter regular hours on the Eastern wall clock (9:30–15:59, we dey convert per row — na the only DST-safe convention across decades); single-window blocks for EDT-only ranges dey use the equivalent raw-UTC band. Both conventions dey state here.
  • Generation dey run through the gated read-only path; the public page never dey query live. Warehouse state as of July 5, 2026.

This na the first edition of the standing semiannual recap; the full-year edition go follow for January. Quarter detail: Q2 2026. The macro releases wey the half trade against dey inside the macro picture.