Strasmore Research
Market Recap Matt ConnorBy Matt Connor · Updated 2026-07-26

Market Recap: June 30, 2026, The Day in Numbers

The quarter's last session in numbers: a technology-led index gain over a mostly red tape, the memory complex on top, and a same-day options tape at the close.

Tuesday, June 30, 2026, the quarter's last session, was a technology-led up day on a mostly red market: QQQ gained 1.63% and SPY 0.73%, yet only 4 of the 10 S&P sector funds closed higher (3,349 liquid names up, 3,033 down). The prior session: the June 29 recap. Every number below is read from a stored query, expand any panel for the SQL.

The scoreboard

Every change compares June 30's last regular-session minute bar with Monday June 29's.

QuerySPY / QQQ / DIA / IWM: June 30 vs the June 29 close, regular hours
The exact SQL behind every number
WITH prior AS (
    SELECT ticker, argMax(close, window_start) AS prior_close
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'DIA', 'IWM')
      AND window_start >= '2026-06-29 13:30:00' AND window_start < '2026-06-29 20:00:00'
    GROUP BY ticker
),
sess AS (
    SELECT ticker,
           argMin(open, window_start) AS day_open,
           argMax(close, window_start) AS day_close,
           max(high) AS day_high,
           min(low) AS day_low,
           round(toFloat64(sum(volume)) / 1e6, 1) AS shares_traded_m
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'DIA', 'IWM')
      AND window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'
    GROUP BY ticker
)
SELECT
    s.ticker AS ticker,
    round(toFloat64(p.prior_close), 2) AS prior_close,
    round(toFloat64(s.day_open), 2) AS day_open,
    round(toFloat64(s.day_close), 2) AS day_close,
    round((toFloat64(s.day_close) / toFloat64(p.prior_close) - 1) * 100, 2) AS pct_change,
    round(toFloat64(s.day_high), 2) AS day_high,
    round(toFloat64(s.day_low), 2) AS day_low,
    s.shares_traded_m AS shares_traded_m
FROM sess s
JOIN prior p ON s.ticker = p.ticker
ORDER BY s.ticker
Run this yourself

SPY opened at $741.29 and finished at $746.32, near its $748.02 high. QQQ's 1.63% against DIA's 0.12% is the shape of the day: growth ran, blue chips barely moved, IWM 0.49%.

The quarter this day closed

June 30 closed the last of 62 sessions in the second quarter, and the day's tilt was the quarter's in miniature.

QueryQ2 2026 end to end: each index ETF from the April 1 open to the June 30 close, regular hours
The exact SQL behind every number
WITH q2 AS (
    SELECT ticker,
           toDate(toTimeZone(window_start, 'America/New_York')) AS d,
           argMin(toFloat64(open), window_start) AS d_open,
           argMax(toFloat64(close), window_start) AS d_close
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'DIA', 'IWM')
      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 ticker, d
)
SELECT
    ticker,
    toString(min(d)) AS quarter_first_session,
    count() AS quarter_sessions,
    round(argMin(d_open, d), 2) AS quarter_open,
    round(argMax(d_close, d), 2) AS quarter_close,
    round((argMax(d_close, d) / argMin(d_open, d) - 1) * 100, 2) AS quarter_pct,
    countIf(d_close > d_open) AS up_sessions
FROM q2
GROUP BY ticker
ORDER BY quarter_pct DESC, ticker ASC
Run this yourself

April 1 open to June 30 close: QQQ +26.53%, IWM 20.21%, SPY 14.13%, DIA 12.08%. QQQ topped the quarter and June 30 itself, though the middle of the board swapped: SPY edged IWM on the day, IWM led SPY across the quarter. QQQ closed higher on 38 of the 62 sessions (the Q2 recap runs it panel by panel).

Was the day unusual?

QuerySPY's open-to-close move ranked against the trailing month of sessions (rank 1 = biggest absolute move)
The exact SQL behind every number
SELECT round(anyIf(oc_pct, d = toDate('2026-06-30')), 2) AS day_move_pct,
       arrayCount(x -> x > abs(anyIf(oc_pct, d = toDate('2026-06-30'))), groupArrayIf(abs(oc_pct), d != toDate('2026-06-30'))) + 1 AS abs_move_rank,
       count() AS sessions_compared,
       toString(min(d)) AS first_session
FROM (
    SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
           (argMax(toFloat64(close), window_start) / argMin(toFloat64(open), window_start) - 1) * 100 AS oc_pct
    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')
      AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
    GROUP BY d
)
Run this yourself

Open-to-close within the session, a different lens from the close-over-close scoreboard, SPY moved 0.68%, ranking 7 of 21 trailing sessions by absolute size. The index was calm; the tape underneath was not.

Breadth: a narrow positive edge

QueryAdvancers vs decliners among tickers with at least $1M traded on June 30
The exact SQL behind every number
WITH per_ticker AS (
    SELECT
        ticker,
        toFloat64(argMaxIf(close, window_start, window_start < '2026-06-30 00:00:00')) AS prior_close,
        toFloat64(argMaxIf(close, window_start, window_start >= '2026-06-30 00:00:00')) AS day_close,
        sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-06-30 00:00:00') AS day_dollar_volume
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE (window_start >= '2026-06-29 13:30:00' AND window_start < '2026-06-29 20:00:00')
       OR (window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00')
    GROUP BY ticker
)
SELECT
    countIf(day_close > prior_close AND day_dollar_volume >= 1000000) AS advancers,
    countIf(day_close < prior_close AND day_dollar_volume >= 1000000) AS decliners,
    countIf(day_close = prior_close AND day_dollar_volume >= 1000000) AS unchanged,
    countIf(day_dollar_volume >= 1000000) AS liquid_tickers,
    count() AS tickers_traded_both_sessions,
    count() - countIf(day_dollar_volume >= 1000000) AS dropped_by_liquidity_filter,
    round(100.0 * countIf(day_close > prior_close AND day_dollar_volume >= 1000000)
        / countIf(day_dollar_volume >= 1000000), 1) AS advancer_pct,
    countIf(ticker IN ('XLB', 'XLE', 'XLF', 'XLI', 'XLK', 'XLP', 'XLRE', 'XLU', 'XLV', 'XLY')) AS sector_funds_counted,
    countIf(ticker IN ('XLB', 'XLE', 'XLF', 'XLI', 'XLK', 'XLP', 'XLRE', 'XLU', 'XLV', 'XLY')
        AND day_close > prior_close) AS sector_funds_up
FROM per_ticker
WHERE prior_close > 0 AND day_close > 0
Run this yourself

3,349 advancers, 3,033 decliners, 88 unchanged: 51.8% of the liquid tape rose, a coin flip under a 1.63% growth-index print. The liquidity filter drops 5,055 of 11,525 dual-session tickers (under $1 million traded), counted here, never hidden.

Sector by sector: where the money actually went

Counting names one vote each hides where the money sat. The ten funds below each hold one slice of the S&P 500, and the spread between best and worst measures how uneven a session was.

QueryThe ten S&P sector funds on June 30: change vs Monday's close, and how far each sat behind the leader
The exact SQL behind every number
WITH per_name AS (
    SELECT
        ticker,
        toFloat64(argMaxIf(close, window_start, window_start < '2026-06-30 00:00:00')) AS prior_close,
        toFloat64(argMaxIf(close, window_start, window_start >= '2026-06-30 00:00:00')) AS day_close,
        round(sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-06-30 00:00:00') / 1e9, 2) AS day_dollar_bn
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('XLB', 'XLE', 'XLF', 'XLI', 'XLK', 'XLP', 'XLRE', 'XLU', 'XLV', 'XLY')
      AND ((window_start >= '2026-06-29 13:30:00' AND window_start < '2026-06-29 20:00:00')
        OR (window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'))
    GROUP BY ticker
)
SELECT
    ticker,
    round(prior_close, 2) AS prior_close,
    round(day_close, 2) AS day_close,
    round((day_close / prior_close - 1) * 100, 2) AS pct_chg,
    if(day_close > prior_close, 1, 0) AS is_up,
    round(max((day_close / prior_close - 1) * 100) OVER () - (day_close / prior_close - 1) * 100, 2) AS pts_behind_leader,
    day_dollar_bn
FROM per_name
ORDER BY pct_chg DESC, ticker ASC
Run this yourself

Technology (XLK) gained 2.69% and stood alone: industrials 1.34%, materials 0.36% and discretionary 0.15% were the only others green, and the board runs red from financials (-0.17%) down to utilities (-1.48%), staples (-1.53%) and real estate last at -2%. Best minus worst: 4.69 percentage points, 4 of 10 funds green. An index up less than a percent, a four-point spread underneath: one sector carried the day.

The day's biggest movers, market-wide

Breadth counts how many names rose; this shows which moved most, top and bottom five by percentage change among names trading $10 million or more.

QueryTop 5 gainers and top 5 decliners among names with $10M+ traded on June 30 (one reused-symbol listing excluded)
The exact SQL behind every number
WITH per_ticker AS (
    SELECT
        ticker,
        toFloat64(argMaxIf(close, window_start, window_start < '2026-06-30 00:00:00')) AS prior_close,
        toFloat64(argMaxIf(close, window_start, window_start >= '2026-06-30 00:00:00')) AS day_close,
        sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-06-30 00:00:00') AS day_dollar_volume
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE (window_start >= '2026-06-29 13:30:00' AND window_start < '2026-06-29 20:00:00')
       OR (window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00')
    GROUP BY ticker
    HAVING prior_close > 1 AND day_close > 0 AND day_dollar_volume >= 10000000 AND ticker NOT IN ('SPCX')
)
SELECT
    board,
    ticker,
    prior_close,
    day_close,
    pct_chg,
    round(abs(pct_chg), 2) AS abs_pct_chg,
    dollar_volume_m,
    if(ticker IN (SELECT ticker FROM global_markets.stocks_splits
                  WHERE execution_date >= '2026-06-29' AND execution_date <= '2026-06-30'), 1, 0) AS split_record_on_file
FROM (
    SELECT 'top gainers' AS board, ticker, round(prior_close, 2) AS prior_close, round(day_close, 2) AS day_close,
           round((day_close / prior_close - 1) * 100, 2) AS pct_chg, round(day_dollar_volume / 1e6, 0) AS dollar_volume_m
    FROM per_ticker
    ORDER BY (day_close / prior_close) DESC, ticker ASC
    LIMIT 5
    UNION ALL
    SELECT 'top decliners' AS board, ticker, round(prior_close, 2) AS prior_close, round(day_close, 2) AS day_close,
           round((day_close / prior_close - 1) * 100, 2) AS pct_chg, round(day_dollar_volume / 1e6, 0) AS dollar_volume_m
    FROM per_ticker
    ORDER BY (day_close / prior_close) ASC, ticker ASC
    LIMIT 5
)
ORDER BY if(board = 'top gainers', 0, 1) ASC, pct_chg DESC, ticker ASC
Run this yourself

Neither board looks like the index. JEM led at +256.76% on $141 million traded; SOC lost 55.89% on $306 million. ABVX rose 38.56% on $942 million, the heaviest turnover on either board; NVCT fell 35.62% and UNCY 39.09%. The split-record column keeps the board honest, a 1-for-10 reverse split prints as a +900% "gain" on an unadjusted tape, and every name here carries a 0: real prints.

Three rows are one trade twice: CRCA (-35.16%) and CRCG (-35.19%) fell while CRCD rose 35.47%, each close to twice the -17.53% move of the crypto-adjacent name below: leveraged single-stock wrappers.

The day's highlight: semiconductors into the quarter turn

Six names, one theme, very different outcomes, co-movement and magnitude only; the data does not say why.

QueryThe semiconductor and storage names: change vs Monday's close, range, and dollar volume
The exact SQL behind every number
WITH per_name AS (
    SELECT
        ticker,
        toFloat64(argMaxIf(close, window_start, window_start < '2026-06-30 00:00:00')) AS prior_close,
        toFloat64(argMaxIf(close, window_start, window_start >= '2026-06-30 00:00:00')) AS day_close,
        maxIf(toFloat64(high), window_start >= '2026-06-30 00:00:00') AS day_high,
        minIf(toFloat64(low), window_start >= '2026-06-30 00:00:00') AS day_low,
        argMinIf(window_start, toFloat64(low), window_start >= '2026-06-30 00:00:00') AS low_bar,
        argMaxIf(window_start, toFloat64(high), window_start >= '2026-06-30 00:00:00') AS high_bar,
        round(sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-06-30 00:00:00') / 1e9, 2) AS day_dollar_bn
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('AMD', 'INTC', 'MU', 'SNDK', 'TSM', 'WDC')
      AND ((window_start >= '2026-06-29 13:30:00' AND window_start < '2026-06-29 20:00:00')
        OR (window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'))
    GROUP BY ticker
)
SELECT
    ticker,
    round(prior_close, 2) AS prior_close,
    round(day_close, 2) AS day_close,
    round((day_close / prior_close - 1) * 100, 2) AS pct_chg,
    round(day_high, 2) AS day_high,
    formatDateTime(toTimeZone(high_bar, 'America/New_York'), '%H:%i') AS day_high_et,
    round(day_low, 2) AS day_low,
    formatDateTime(toTimeZone(low_bar, 'America/New_York'), '%H:%i') AS day_low_et,
    round((day_high / day_low - 1) * 100, 2) AS range_pct,
    day_dollar_bn
FROM per_name
ORDER BY ticker
Run this yourself

SanDisk jumped 10.79%, AMD 7.7%, Intel 5.93%, TSMC 4.86%, while MU, the tape's biggest ticket at $37.41 billion of turnover, closed just 0.52% higher and Western Digital (-2.01%) did not participate. This is the complex behind technology's 2.69% sector print (MU's June, tick by tick).

The counter-current ran through crypto-adjacent financials, against the green tape:

QueryCrypto-adjacent financials: change vs Monday's close, range, and dollar volume
The exact SQL behind every number
WITH per_name AS (
    SELECT
        ticker,
        toFloat64(argMaxIf(close, window_start, window_start < '2026-06-30 00:00:00')) AS prior_close,
        toFloat64(argMaxIf(close, window_start, window_start >= '2026-06-30 00:00:00')) AS day_close,
        maxIf(toFloat64(high), window_start >= '2026-06-30 00:00:00') AS day_high,
        minIf(toFloat64(low), window_start >= '2026-06-30 00:00:00') AS day_low,
        argMinIf(window_start, toFloat64(low), window_start >= '2026-06-30 00:00:00') AS low_bar,
        argMaxIf(window_start, toFloat64(high), window_start >= '2026-06-30 00:00:00') AS high_bar,
        round(sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-06-30 00:00:00') / 1e9, 2) AS day_dollar_bn
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('COIN', 'CRCL', 'HOOD', 'MSTR')
      AND ((window_start >= '2026-06-29 13:30:00' AND window_start < '2026-06-29 20:00:00')
        OR (window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'))
    GROUP BY ticker
)
SELECT
    ticker,
    round(prior_close, 2) AS prior_close,
    round(day_close, 2) AS day_close,
    round((day_close / prior_close - 1) * 100, 2) AS pct_chg,
    round(day_high, 2) AS day_high,
    formatDateTime(toTimeZone(high_bar, 'America/New_York'), '%H:%i') AS day_high_et,
    round(day_low, 2) AS day_low,
    formatDateTime(toTimeZone(low_bar, 'America/New_York'), '%H:%i') AS day_low_et,
    round((day_high / day_low - 1) * 100, 2) AS range_pct,
    day_dollar_bn
FROM per_name
ORDER BY ticker
Run this yourself

CRCL printed -17.53%, its $62.52 low arriving at 15:59 ET, the session's final minute, with MSTR -6.2%, COIN -3.61% and HOOD -1.53% alongside. Co-movement observed; cause not asserted.

What the news feed carried

A recap that refuses to assert causes owes the reader what was actually published that day.

QueryThe news feed on June 30: article count and the day's last headline for each name in this recap
The exact SQL behind every number
SELECT
    names.n AS ticker,
    countIf(has(tickers, names.n)) AS articles_today,
    if(countIf(has(tickers, names.n)) = 0, 'none',
       formatDateTime(toTimeZone(argMaxIf(published_utc, (published_utc, id), has(tickers, names.n)), 'America/New_York'), '%H:%i')) AS latest_et,
    if(countIf(has(tickers, names.n)) = 0, 'no article in our feed this session',
       JSONExtractString(argMaxIf(publisher, (published_utc, id), has(tickers, names.n)), 'name')) AS publisher_name,
    if(countIf(has(tickers, names.n)) = 0, 'no article in our feed this session',
       argMaxIf(title, (published_utc, id), has(tickers, names.n))) AS latest_headline
FROM global_markets.stocks_news
CROSS JOIN (SELECT arrayJoin(['NVDA', 'MU', 'SNDK', 'CRCL', 'SOC', 'ABVX', 'JEM']) AS n) AS names
WHERE published_utc >= '2026-06-30 04:00:00' AND published_utc < '2026-07-01 04:00:00'
GROUP BY names.n
ORDER BY articles_today DESC, ticker ASC
Run this yourself

The most-covered name was NVDA at 21 articles, not among the day's movers at all, its last piece of the session (16:28 ET, Investing.com) titled "These Stocks Could Benefit as the Robotaxi Race Heats Up". MU drew 7; SanDisk's lone article (The Motley Fool) is the only headline all day naming an actual move: "Why Sandisk Stock Is Skyrocketing Today".

The gap is the finding: the day's biggest decliner, its heaviest gainer and the crypto-adjacent name that fell hardest drew 0, 0 and 0 articles, zero apiece, inside a feed of 211 pieces. Coverage is not a proxy for price action.

Where the money traded

QueryVolume leaders two ways: top 6 by dollars traded, top 4 by shares traded (one reused-symbol listing excluded pending entity verification)
The exact SQL behind every number
SELECT ticker, leaderboard, dollar_volume_bn, if(dollar_volume_bn < 1, dollar_volume_m, NULL) AS dollar_value_m, shares_m,
    round(100 * if(leaderboard = 'by dollars traded', dollar_volume_bn, shares_m)
        / max(if(leaderboard = 'by dollars traded', dollar_volume_bn, shares_m)) OVER (PARTITION BY leaderboard), 1) AS pct_of_board_leader
FROM (
    SELECT
        'by dollars traded' AS leaderboard,
        ticker,
        round(sum(toFloat64(close) * toFloat64(volume)) / 1e9, 2) AS dollar_volume_bn,
        round(sum(toFloat64(close) * toFloat64(volume)) / 1e6, 0) AS dollar_volume_m,
        round(sum(toFloat64(volume)) / 1e6, 1) AS shares_m
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker
    ORDER BY dollar_volume_bn DESC
    LIMIT 6
    UNION ALL
    SELECT
        'by shares traded' AS leaderboard,
        ticker,
        round(sum(toFloat64(close) * toFloat64(volume)) / 1e9, 2) AS dollar_volume_bn,
        round(sum(toFloat64(close) * toFloat64(volume)) / 1e6, 0) AS dollar_volume_m,
        round(sum(toFloat64(volume)) / 1e6, 1) AS shares_m
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker
    ORDER BY shares_m DESC
    LIMIT 4
)
ORDER BY leaderboard ASC, if(leaderboard = 'by dollars traded', dollar_volume_bn, shares_m) DESC
Run this yourself

By dollars, MU towered over everything, index funds included: $37.41 billion against SPY's $31.84 billion, the second straight session it out-traded the flagship index fund (Monday's recap carries the first). The share board says something else: SOXS, a 3x-leveraged inverse semiconductor ETF (515.5 million shares), and two sub-dollar names worth a rounding error of MU's day, volume read two ways.

One name is held off both boards on purpose: a June listing whose ticker previously belonged to a different, unrelated company, so vendor feeds tag two entities with the same three letters (receipts here).

QueryShares traded per 30-minute bucket, regular hours (billions)
The exact SQL behind every number
SELECT
    formatDateTime(toStartOfInterval(toTimeZone(window_start, 'America/New_York'), INTERVAL 30 MINUTE), '%H:%i') AS et_time,
    round(sum(toFloat64(volume)) / 1e9, 2) AS shares_bn,
    round(100 * sum(toFloat64(volume)) / max(sum(toFloat64(volume))) OVER (), 1) AS pct_of_biggest_bucket
FROM global_markets.delayed_stocks_minute_aggs
WHERE window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'
GROUP BY et_time
ORDER BY et_time
Run this yourself

2.05 billion shares in the opening half hour, a 0.78 billion trough at 13:30, and the day's biggest bucket, 2.69 billion, into a quarter-end close, where the closing auction and its rebalancing flows print (the June 29 deep dive goes under the bars).

The options tape

QueryOne row for the whole options day: volume, same-day expiry, SPY's put/call skew by moneyness, the holiday-shifted week
The exact SQL behind every number
WITH
    (
        SELECT (any(underlying_symbol), any(toFloat64(strike_price)), any(option_type),
                any(toDateOrNull(concat('20', substring(ticker, length(ticker) - 14, 6)))),
                sum(size), count(), round(avg(toFloat64(price)), 3))
        FROM global_markets.options_trades
        WHERE sip_timestamp >= '2026-06-30 00:00:00' AND sip_timestamp < '2026-07-01 00:00:00'
        GROUP BY ticker
        ORDER BY sum(size) DESC
        LIMIT 1
    ) AS top_contract,
    (
        SELECT round(toFloat64(argMax(close, window_start)), 2)
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY' AND window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00'
    ) AS spy_close
SELECT
    round(count() / 1e6, 2) AS option_prints_m,
    round(toFloat64(sum(size)) / 1e6, 2) AS contracts_m,
    round(100.0 * sumIf(size, option_type = 'C') / sum(size), 1) AS call_pct_of_volume,
    round(100.0 * sumIf(size, substring(ticker, length(ticker) - 14, 6) = '260630') / sum(size), 1) AS same_day_expiry_pct,
    round(toFloat64(sumIf(size, substring(ticker, length(ticker) - 14, 6) = '260702')) / 1e6, 2) AS thu_jul2_expiry_contracts_m,
    countIf(substring(ticker, length(ticker) - 14, 6) = '260703') AS fri_jul3_expiry_prints,
    round(toFloat64(sumIf(size, underlying_symbol = 'SPY')) / 1e6, 2) AS spy_contracts_m,
    round(toFloat64(sumIf(size, underlying_symbol = 'QQQ')) / 1e6, 2) AS qqq_contracts_m,
    round(toFloat64(sumIf(size, underlying_symbol = 'SPY' AND option_type = 'P'))
        / toFloat64(sumIf(size, underlying_symbol = 'SPY' AND option_type = 'C')), 2) AS spy_put_call_ratio,
    round(toFloat64(sumIf(size, underlying_symbol = 'SPY' AND option_type = 'C'
        AND substring(ticker, length(ticker) - 14, 6) = '260630' AND toFloat64(strike_price) > spy_close)) / 1e6, 2) AS spy_0dte_otm_calls_m,
    round(toFloat64(sumIf(size, underlying_symbol = 'SPY' AND option_type = 'C'
        AND substring(ticker, length(ticker) - 14, 6) = '260630' AND toFloat64(strike_price) <= spy_close)) / 1e6, 2) AS spy_0dte_itm_calls_m,
    round(toFloat64(sumIf(size, underlying_symbol = 'SPY' AND option_type = 'P'
        AND substring(ticker, length(ticker) - 14, 6) = '260630' AND toFloat64(strike_price) < spy_close)) / 1e6, 2) AS spy_0dte_otm_puts_m,
    round(toFloat64(sumIf(size, underlying_symbol = 'SPY' AND option_type = 'P'
        AND substring(ticker, length(ticker) - 14, 6) = '260630' AND toFloat64(strike_price) >= spy_close)) / 1e6, 2) AS spy_0dte_itm_puts_m,
    top_contract.1 AS top_contract_underlying,
    top_contract.2 AS top_contract_strike,
    top_contract.3 AS top_contract_type,
    top_contract.4 AS top_contract_expiry,
    top_contract.5 AS top_contract_volume,
    round(top_contract.7, 3) AS top_contract_avg_price,
    round(top_contract.2 - spy_close, 2) AS top_strike_minus_spy_close
FROM global_markets.options_trades
WHERE sip_timestamp >= '2026-06-30 00:00:00' AND sip_timestamp < '2026-07-01 00:00:00'
Run this yourself

Options traded 61.95 million contracts across 10.06 million prints; calls took 57.3% of tape-wide volume, and 28% of it expired that Tuesday. SPY leaned the other way: 12.04 million contracts at a put/call ratio of 1.09, more puts than calls, on a day the fund rose.

Sorting SPY's same-day expiries against its $746.32 close makes the skew concrete: 3.33 million out-of-the-money puts (strikes below the close) and 2.28 million out-of-the-money calls, against 1.77 million in-the-money calls and 0.63 million in-the-money puts, downside strikes that expired worthless were the busiest bucket of SPY's 0DTE day. Nothing with a Friday, July 3 expiry code printed at all (0 prints, the market is closed that Friday); the Thursday weekly carried 10.52 million.

QueryThe six busiest option contracts of June 30, by contracts traded
The exact SQL behind every number
SELECT
    concat(underlying, ' $', toString(strike), ' ', if(typ = 'C', 'call', 'put')) AS contract,
    toString(expiry) AS expires,
    formatDateTimeInJodaSyntax(expiry, 'MMMM d, yyyy') AS expires_label,
    if(expiry = toDate('2026-06-30'), 1, 0) AS is_same_day,
    contracts,
    round(avg_px, 3) AS avg_premium,
    round(100 * contracts / max(contracts) OVER (), 1) AS pct_of_busiest
FROM (
    SELECT any(underlying_symbol) AS underlying,
           any(toFloat64(strike_price)) AS strike,
           any(option_type) AS typ,
           any(toDateOrNull(concat('20', substring(ticker, length(ticker) - 14, 6)))) AS expiry,
           sum(size) AS contracts,
           avg(toFloat64(price)) AS avg_px,
           any(ticker) AS occ
    FROM global_markets.options_trades
    WHERE sip_timestamp >= '2026-06-30 00:00:00' AND sip_timestamp < '2026-07-01 00:00:00'
    GROUP BY ticker
    ORDER BY contracts DESC, occ ASC
    LIMIT 6
)
ORDER BY contracts DESC, contract ASC
Run this yourself

The busiest contract anywhere was the same-day SPY $747 call, 821,361 contracts at an average premium of $0.641, finishing $0.68 above SPY's last regular bar, and worthless. The board behind it is the lesson: five of the six busiest contracts on the tape were same-day SPY strikes a dollar apart around the close (SPY $748 call next), each trading for cents. The exception, at 65.1% of the leader's volume, was KWEB $29 call expiring December 18, 2026, not a same-day contract: one six-month position among five afternoon lottery tickets.

Rates: the long end rose into the half's close

QueryThe Treasury curve, June 30 close vs June 29 (populated maturities only)
The exact SQL behind every number
SELECT
    t.1 AS curve_point,
    round(t.2, 2) AS jun30_yield_pct,
    round((t.2 - t.3) * 100) AS one_day_change_bp
FROM (
    SELECT arrayJoin([
        ('1 month',  toFloat64(d.yield_1_month),  toFloat64(p.yield_1_month)),
        ('3 month',  toFloat64(d.yield_3_month),  toFloat64(p.yield_3_month)),
        ('1 year',   toFloat64(d.yield_1_year),   toFloat64(p.yield_1_year)),
        ('2 year',   toFloat64(d.yield_2_year),   toFloat64(p.yield_2_year)),
        ('5 year',   toFloat64(d.yield_5_year),   toFloat64(p.yield_5_year)),
        ('10 year',  toFloat64(d.yield_10_year),  toFloat64(p.yield_10_year)),
        ('30 year',  toFloat64(d.yield_30_year),  toFloat64(p.yield_30_year)),
        ('2s10s spread', toFloat64(d.yield_10_year - d.yield_2_year), toFloat64(p.yield_10_year - p.yield_2_year))
    ]) AS t
    FROM (SELECT * FROM global_markets.treasury_yields WHERE date = '2026-06-30') AS d,
         (SELECT * FROM global_markets.treasury_yields WHERE date = '2026-06-29') AS p
)
Run this yourself

The long end backed up on the half's final day: the 10-year rose 6 bp to 4.44%, the 2-year 4 bp to 4.14%, and the 2s10s spread steepened 2 bp to 0.3 points. Long yields rose during a session whose weakest sector, real estate (-2%), and its third-weakest, utilities (-1.48%), are both classically rate-sensitive: a co-occurrence worth noticing, not a mechanism this data can prove (the H1 recap carries the half's curve).

The calendar behind the day

QueryJune 30's corporate calendar and information flow, in one row (the filing-index gap on display)
The exact SQL behind every number
WITH
    (
        SELECT (count(), uniqExact(publisher))
        FROM global_markets.stocks_news
        WHERE toDate(toTimeZone(published_utc, 'America/New_York')) = '2026-06-30'
    ) AS news,
    (
        SELECT (argMax(t, n), max(n))
        FROM (
            SELECT t, count() AS n
            FROM (
                SELECT arrayJoin(tickers) AS t
                FROM global_markets.stocks_news
                WHERE toDate(toTimeZone(published_utc, 'America/New_York')) = '2026-06-30'
            )
            WHERE t != 'SPCX'
            GROUP BY t
        )
    ) AS top_news
SELECT
    (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date = '2026-06-30') AS ex_dividend_records,
    (SELECT count() FROM global_markets.stocks_splits WHERE execution_date = '2026-06-30') AS splits_executed,
    (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date = '2026-06-30') AS ipos_listed,
    (SELECT uniqExact(accession_number) FROM global_markets.stocks_sec_edgar_index WHERE filing_date = '2026-06-30') AS sec_filings,
    (SELECT uniqExactIf(accession_number, form_type = '4') FROM global_markets.stocks_sec_edgar_index WHERE filing_date = '2026-06-30') AS insider_form4_filings,
    (SELECT uniqExactIf(accession_number, form_type = '8-K') FROM global_markets.stocks_sec_edgar_index WHERE filing_date = '2026-06-30') AS filings_8k,
    (SELECT arrayStringConcat(groupArray(concat(ticker, ' — ', issuer_name)), '; ') FROM (
        SELECT ticker, issuer_name FROM global_markets.stocks_ipos WHERE listing_date = '2026-06-30' ORDER BY ticker
    )) AS ipo_names,
    news.1 AS news_articles,
    news.2 AS news_publishers,
    top_news.1 AS most_covered_ticker,
    top_news.2 AS most_covered_articles,
    (SELECT uniqExact(accession_number) FROM global_markets.stocks_sec_edgar_index WHERE filing_date = toDate('2026-06-29')) AS filings_prior_session
Run this yourself

704 dividend records went ex-dividend on the quarter's last day, a quarter-end wave, most US companies paying quarterly, with 7 splits and 2 new listings (AACU — Ares Acquisition Corp III; OSPRU — Osprey Acquisition Corp. III). The filing column is the data-quality headline: the SEC index holds 31 filings for June 30, 0 insider Form 4s, 0 8-Ks, against 4439 the session before. A month-end pattern in the feed, diagnosed in the month-end gap note; any count including this day is understated until it backfills.

The session, verified

QuerySession check: SPY's observed minute-bar span
The exact SQL behind every number
SELECT
    formatDateTime(min(toTimeZone(window_start, 'America/New_York')), '%H:%i') AS first_spy_bar_et,
    formatDateTime(max(toTimeZone(window_start, 'America/New_York')), '%H:%i') AS last_spy_bar_et,
    count() AS spy_minute_bars,
    countIf(window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00') AS regular_session_bars,
    uniqExactIf(toDate(toTimeZone(window_start, 'America/New_York')), window_start >= '2026-06-30 13:30:00' AND window_start < '2026-06-30 20:00:00') AS day_sessions
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY' AND window_start >= '2026-06-30 00:00:00' AND window_start < '2026-07-01 00:00:00'
Run this yourself

SPY's bars run 04:00 to 19:59 New York time with exactly 390 regular-window bars, a complete session, verified from the tape (the exchange-calendar dataset carries only upcoming closures).

What the session adds up to

A 0.73% SPY print with 51.8% of names higher, 4 of 10 sector funds green and 4.69 points between best and worst is not a broad rally, it is one sector's rally wearing an index's clothes, its heaviest half-hour in a quarter-end auction. An index change is an average, and this one was carried by technology while most sectors fell: check the dispersion before trusting the arrow.

FAQ

How did the stock market do on June 30, 2026?

It closed higher: SPY gained 0.73% and QQQ 1.63% against June 29's close. Breadth was narrower, 51.8% of liquid names rose, 4 of 10 S&P sector funds green.

Which sector led the market on June 30, 2026?

Technology: XLK gained 2.69%, against 1.34% for industrials next and -2% for real estate last, 4.69 points between best and worst.

What were the biggest stock movers on June 30, 2026?

Among names trading $10 million or more, JEM gained the most (+256.76%) and SOC lost the most (55.89%). Neither carried a split record: both are real price moves, not corporate-action artifacts.

How did the stock market do in Q2 2026?

From the April 1 open to the June 30 close, across 62 sessions: QQQ +26.53%, IWM 20.21%, SPY 14.13%, DIA 12.08%, a growth-led quarter, ended by a growth-led day.

Why does so much volume trade in the last half hour?

The final half hour carried 2.69 billion shares against 2.05 billion at the open. The closing auction sets each stock's official closing price in one crossing, and index funds trade there to match it; quarter-end rebalancing concentrates the flow.

Data notes and method

  • The baskets. The sector board is the ten SPDR sector funds named in its panel, the same basket every session, so the dispersion number compares across days. The mover boards require $10 million of turnover and a prior close above $1, exclude the reused-symbol listing above, and carry a split-record column.
  • The caveats. Dollar volume is a per-minute proxy (close × volume per bar). The June 30 filing index is near-empty (the month-end gap note diagnoses it). News counts are one vendor feed: no article here is not proof nothing was published anywhere.
  • The method. One session (1, verified from observed bars); timestamps stored in UTC, converted to New York time in the queries; "close" is the last regular minute bar, not the auction print. Option expiries are re-parsed from the OCC ticker (the table's own column is broken); moneyness is measured against SPY's last regular bar. Panels run once, at authoring time, through the gated read-only path. Warehouse as of July 13, 2026.

Every panel is a stored query result, chart, table and SQL in one object. Paste any of them into the Strasmore terminal. Next session: July 1.