Strasmore Research
Market Recap Matt ConnorBy Matt Connor

Market Recap: Week of July 20, 2026

The week after July's monthly options expiration, measured: the index scoreboard, sector dispersion, weekly breadth, and where the tape's dollars went.

The week of July 20, 2026 was the first full week after July's monthly options expiration, and it ran 5 sessions with no closure inside it. SPY's close-over-close change on the week came to -0.59%, and measured open-to-close against the trailing year of weeks it ranked 43 of 53. Every number below is read from a stored query, and every window is pinned to explicit dates, so re-running the SQL returns these same figures.

The week on the board

Weekly changes run from Friday July 17's regular close to Friday July 24's. The last column carries the week before it, so the two sit side by side. Rows are alphabetical.

QuerySPY / QQQ / DIA / IWM: week of July 20 vs the July 17 close, with the prior week's change
The exact SQL behind every number
SELECT ticker,
    round(argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00'), 2) AS prior_week_close,
    round(argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-24 13:30:00' AND window_start < '2026-07-24 20:00:00'), 2) AS week_close,
    round((argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-24 13:30:00' AND window_start < '2026-07-24 20:00:00')
         / argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00') - 1) * 100, 2) AS week_change_pct,
    round((argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
         / argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-10 13:30:00' AND window_start < '2026-07-10 20:00:00') - 1) * 100, 2) AS prior_week_change_pct
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker IN ('SPY', 'QQQ', 'DIA', 'IWM')
  AND ((window_start >= '2026-07-10 13:30:00' AND window_start < '2026-07-10 20:00:00')
    OR (window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
    OR (window_start >= '2026-07-24 13:30:00' AND window_start < '2026-07-24 20:00:00'))
GROUP BY ticker
ORDER BY ticker

On the week: DIA moved -0.38%, IWM -0.98%, QQQ -1.59%, and SPY -0.59% to a $738.85 close. The prior week's readings, in the same alphabetical order, were -0.95%, -0.63%, -4.17%, and -1.55%. How far the four indexes sit apart is the first thing a scoreboard measures, and it is not always small.

The week against the trailing year

One week's number means little without the distribution behind it. This panel recomputes every trailing week on identical logic, open-to-close through the regular session, and ranks this one inside it.

QuerySPY's week in trailing-year context (open-to-close weekly returns)
The exact SQL behind every number
SELECT round(anyIf(ret, wk = toDate('2026-07-20')), 2) AS week_open_to_close_pct,
       arrayCount(x -> x > anyIf(ret, wk = toDate('2026-07-20')), groupArrayIf(ret, wk != toDate('2026-07-20'))) + 1 AS rank_best,
       count() AS weeks_compared,
       toString(min(wk)) AS first_week,
       anyIf(sessions_measured, wk = toDate('2026-07-20')) AS sessions_this_week
FROM (
    SELECT toStartOfWeek(toDate(toTimeZone(window_start, 'America/New_York')), 1) AS wk,
           uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions_measured,
           (argMax(toFloat64(close), window_start) / argMin(toFloat64(open), window_start) - 1) * 100 AS ret
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= toDateTime('2025-07-21 00:00:00')
      AND window_start < toDateTime('2026-07-25 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 wk
    HAVING sessions_measured >= 3
)

Measured that way, SPY returned -1.1% and ranked 43 of 53 weeks going back to 2025-07-21. Note the definition gap: this figure starts at the week's first regular-session open, the scoreboard above starts at the prior week's close. Both are labeled everywhere they appear.

Five sessions, one arc

QuerySPY by session: close, change, and share volume, July 20-24
The exact SQL behind every number
SELECT toString(d) AS date,
       round(c, 2) AS spy_close,
       round((c / prev_c - 1) * 100, 2) AS change_pct,
       round(shares_m, 1) AS spy_shares_m
FROM (
    SELECT d, c, shares_m,
           lagInFrame(c) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_c
    FROM (
        SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
               argMax(toFloat64(close), window_start) AS c,
               toFloat64(sum(volume)) / 1e6 AS shares_m
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
            OR (window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-24 20:00:00'))
          AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
        GROUP BY d
    )
)
WHERE d >= '2026-07-20'
ORDER BY date

Session by session, SPY's change read -0.15% Monday, 0.84% Tuesday, -0.12% Wednesday, -1.22% Thursday, and 0.08% Friday, finishing at $738.85 on 40.1 million shares. Monday's figure is measured against the July 17 close, the session immediately preceding the period.

Breadth, session by session

An index level is one number. Breadth counts how many stocks travelled with it, and it is the number that decides whether a green week was broad or narrow.

QueryAdvancers and decliners per session, names with $5M+ traded, July 20-24
The exact SQL behind every number
SELECT toString(d) AS date, advancers, decliners, unchanged, measured_names
FROM (
    SELECT d,
           countIf(c > prev_c) AS advancers,
           countIf(c < prev_c) AS decliners,
           countIf(c = prev_c) AS unchanged,
           count() AS measured_names
    FROM (
        SELECT d, c, dv,
               lagInFrame(c) OVER (PARTITION BY ticker ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS prev_c
        FROM (
            SELECT ticker,
                   toDate(toTimeZone(window_start, 'America/New_York')) AS d,
                   argMax(toFloat64(close), window_start) AS c,
                   sum(toFloat64(close) * toFloat64(volume)) AS dv
            FROM global_markets.delayed_stocks_minute_aggs
            WHERE ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
                OR (window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-24 20:00:00'))
              AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
            GROUP BY ticker, d
        )
    )
    WHERE d >= '2026-07-20' AND prev_c > 0 AND dv >= 5000000
    GROUP BY d
)
ORDER BY date

On Monday, 1268 of 3945 measured names rose and 2632 fell. By Friday the split read 2273 up against 1635 down, out of 3950. Names without a close on both sides of a session are excluded, which is why the measured count drifts a little from day to day.

The same question asked once, across the whole week:

QueryWeekly breadth: July 24 regular close vs July 17 regular close, names with $5M+ traded during the week
The exact SQL behind every number
SELECT
    countIf(cw > cp AND liquid) AS advancers,
    countIf(cw < cp AND liquid) AS decliners,
    countIf(cw = cp AND liquid) AS unchanged,
    countIf(liquid) AS measured_names,
    countIf(NOT liquid) AS dropped_by_liquidity_filter,
    round(100.0 * countIf(cw > cp AND liquid) / countIf(liquid), 1) AS advancer_pct
FROM (
    SELECT ticker, cp, cw, dv >= 5000000 AS liquid
    FROM (
        SELECT ticker,
               argMaxIf(toFloat64(close), window_start, window_start < '2026-07-18 00:00:00') AS cp,
               argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-24 13:30:00') AS cw,
               sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-07-20 13:30:00') AS dv
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
            OR (window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-25 00:00:00'))
          AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
        GROUP BY ticker
        HAVING cp > 0 AND cw > 0
    )
)

Across 6100 names clearing the five-million-dollar regular-hours turnover bar, 2437 closed above their July 17 close and 3630 closed below, an advancer share of 40%. The liquidity filter set aside a further 5099 names that traded under that bar during the week. They are counted here rather than quietly discarded.

The sector scoreboard

The eleven SPDR sector ETFs, July 17's close to July 24's, ranked best to worst.

QuerySector ETFs, full-week change: July 24 close vs July 17 close
The exact SQL behind every number
SELECT sector, week_pct, round(max(week_pct) OVER () - week_pct, 2) AS points_behind_best
FROM (
    SELECT multiIf(ticker = 'XLK', 'Technology', ticker = 'XLC', 'Communications', ticker = 'XLE', 'Energy',
                   ticker = 'XLF', 'Financials', ticker = 'XLI', 'Industrials', ticker = 'XLB', 'Materials',
                   ticker = 'XLP', 'Staples', ticker = 'XLRE', 'Real Estate', ticker = 'XLU', 'Utilities',
                   ticker = 'XLV', 'Health Care', 'Consumer Discretionary') AS sector,
           round((cw / cp - 1) * 100, 2) AS week_pct
    FROM (
        SELECT ticker,
               argMaxIf(toFloat64(close), window_start, window_start < '2026-07-18 00:00:00') AS cp,
               argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-24 13:30:00') AS cw
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker IN ('XLB', 'XLC', 'XLE', 'XLF', 'XLI', 'XLK', 'XLP', 'XLRE', 'XLU', 'XLV', 'XLY')
          AND ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
            OR (window_start >= '2026-07-24 13:30:00' AND window_start < '2026-07-24 20:00:00'))
        GROUP BY ticker
        HAVING cp > 0 AND cw > 0
    )
)
ORDER BY week_pct DESC

Energy sat at the top of the board at 3.33%. Consumer Discretionary sat at the bottom at -5.2%, 8.53 percentage points behind it. That gap is the week's sector dispersion, and it is worth reading on its own: a week where all eleven land within a point of each other is a very different market from one where the spread runs into double digits.

Where the dollars went

QueryTop names by regular-hours dollar volume, full week July 20-24
The exact SQL behind every number
SELECT ticker, week_dollar_bn, round(100 * week_dollar_bn / max(week_dollar_bn) OVER (), 1) AS pct_of_leader
FROM (
    SELECT ticker,
           round(sum(toFloat64(close) * toFloat64(volume)) / 1e9, 1) AS week_dollar_bn
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-24 20:00:00'
      AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
      AND ticker NOT IN ('SPCX')
    GROUP BY ticker
    ORDER BY week_dollar_bn DESC
    LIMIT 8
)
ORDER BY week_dollar_bn DESC

MU printed 158.1 billion dollars of regular-hours turnover across the five sessions, ahead of SPY at 133.9 billion and QQQ at 101.5 billion. Eighth on the board, AAPL, traded 34.5% of the leader's dollars. Turnover records where the market spent its week arguing, and it does not have to agree with the price. The per-name version of this measure is relative volume.

The week's biggest movers

Splits move the print without moving the value: a reverse split manufactures a fake four-digit gain, a forward split a fake collapse of the same size. The week carried 25 reverse splits and 11 forward splits, and both boards below exclude any name whose split executed between the July 17 close and the July 24 close, the exact interval the change is measured over. Both also require five million dollars of regular-hours turnover.

QueryWeekly movers: ten biggest gainers and decliners, regular-hours closes, $5M+ traded, splits excluded
The exact SQL behind every number
SELECT ticker, board, week_pct, week_dollar_m, sessions_traded,
       round(100 * week_dollar_m / max(week_dollar_m) OVER (), 1) AS pct_of_dollar_max
FROM (
    SELECT 'gainers' AS board, ticker, round((cw / cp - 1) * 100, 1) AS week_pct,
           round(dv / 1e6, 1) AS week_dollar_m, sessions_traded
    FROM (
        SELECT ticker,
               argMaxIf(toFloat64(close), window_start, window_start < '2026-07-18 00:00:00') AS cp,
               argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-24 13:30:00') AS cw,
               sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-07-20 13:30:00') AS dv,
               uniqExactIf(toDate(toTimeZone(window_start, 'America/New_York')), window_start >= '2026-07-20 13:30:00') AS sessions_traded
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker NOT IN ('SPCX')
          AND ticker NOT IN (SELECT ticker FROM global_markets.stocks_splits WHERE execution_date > '2026-07-17' AND execution_date <= '2026-07-24')
          AND ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
            OR (window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-25 00:00:00'))
          AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
        GROUP BY ticker
        HAVING cp > 0 AND cw > 0 AND dv >= 5000000
    )
    ORDER BY week_pct DESC
    LIMIT 10
    UNION ALL
    SELECT 'decliners' AS board, ticker, round((cw / cp - 1) * 100, 1) AS week_pct,
           round(dv / 1e6, 1) AS week_dollar_m, sessions_traded
    FROM (
        SELECT ticker,
               argMaxIf(toFloat64(close), window_start, window_start < '2026-07-18 00:00:00') AS cp,
               argMaxIf(toFloat64(close), window_start, window_start >= '2026-07-24 13:30:00') AS cw,
               sumIf(toFloat64(close) * toFloat64(volume), window_start >= '2026-07-20 13:30:00') AS dv,
               uniqExactIf(toDate(toTimeZone(window_start, 'America/New_York')), window_start >= '2026-07-20 13:30:00') AS sessions_traded
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker NOT IN ('SPCX')
          AND ticker NOT IN (SELECT ticker FROM global_markets.stocks_splits WHERE execution_date > '2026-07-17' AND execution_date <= '2026-07-24')
          AND ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
            OR (window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-25 00:00:00'))
          AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
        GROUP BY ticker
        HAVING cp > 0 AND cw > 0 AND dv >= 5000000
    )
    ORDER BY week_pct ASC
    LIMIT 10
)
ORDER BY board DESC, abs(week_pct) DESC

Making either board took a real move. The mildest gainer shown rose 92.3%, and the mildest decliner printed -47.8%. At the extremes, STAK moved 383.7% on 251.6 million dollars traded, and LBGJ moved -98.8% on 47.7 million. That decline sits close to the whole quoted value of the name, so the board carries its own receipt: LBGJ printed regular-hours bars on 5 of the five sessions, and no split executed against it between the two closes the board measures.

The quoted spread, session by session

Price is the headline. The cost of getting the trade done is the quoted spread, measured here on SPY one session at a time, straight from the raw NBBO tape rather than from any summary.

QuerySPY median quoted spread and NBBO updates per session, regular hours, July 20-24
The exact SQL behind every number
SELECT toString(toDate(sip_timestamp)) AS date,
       round(quantileExactIf(0.5)(
           10000 * (toFloat64(ask_price) - toFloat64(bid_price)) / ((toFloat64(ask_price) + toFloat64(bid_price)) / 2),
           bid_price > 0 AND ask_price > 0 AND ask_price >= bid_price), 2) AS median_spread_bps,
       round(count() / 1e6, 2) AS quote_updates_m,
       countIf(bid_price <= 0 OR ask_price <= 0) AS one_sided_quote_count,
       countIf(bid_price > ask_price AND bid_price > 0 AND ask_price > 0) AS crossed_quote_count
FROM global_markets.cache_stocks_quotes
WHERE ticker = 'SPY'
  AND sip_timestamp >= '2026-07-20 13:30:00'
  AND sip_timestamp < '2026-07-24 20:00:00'
  AND (toHour(sip_timestamp) * 60 + toMinute(sip_timestamp)) BETWEEN 810 AND 1199
GROUP BY toDate(sip_timestamp)
HAVING countIf(bid_price > 0 AND ask_price > 0 AND ask_price >= bid_price) > 0
ORDER BY toDate(sip_timestamp)

SPY's median quoted spread measured 0.27 basis points of mid on Monday and 0.27 on Friday, on 3.89 million and 4.4 million NBBO updates. The last two columns are the disclosure rather than the finding: 0 one-sided quotes and 1084 crossed quotes were counted out of Monday's median instead of being silently dropped. A crossed quote, bid above ask, is an ordinary artifact of a consolidated feed stitched together from many venues at nanosecond resolution.

The options week after monthly expiration

July's monthly expiration fell on the Friday before this week opened, so every contract trading in these five sessions is a survivor of it. Expiration timing sets the rhythm the same-day crowd trades to.

QueryOptions contracts, same-day share, and call share by session, July 20-24
The exact SQL behind every number
SELECT toString(toDate(sip_timestamp)) AS date,
       round(toFloat64(sum(size)) / 1e6, 1) AS contracts_m,
       round(100.0 * sumIf(size, substring(ticker, length(ticker) - 14, 6) = formatDateTime(toDate(sip_timestamp), '%y%m%d')) / sum(size), 1) AS pct_0dte,
       round(100.0 * sumIf(size, substring(ticker, length(ticker) - 8, 1) = 'C') / sum(size), 1) AS pct_call,
       round(100.0 * sumIf(size, substring(ticker, length(ticker) - 14, 6) = '260724') / sum(size), 1) AS pct_expiring_jul24
FROM global_markets.options_trades
WHERE sip_timestamp >= '2026-07-20 00:00:00' AND sip_timestamp < '2026-07-25 00:00:00'
GROUP BY toDate(sip_timestamp)
ORDER BY toDate(sip_timestamp)

Contract volume ran 64 million on Monday and 71.1 million on Friday. The same-day share, meaning contracts expiring on the session they trade, read 40.2% Monday against 49% Friday, the week's expiry session. Calls took 53.5% of Friday's contract volume. And the pull of that Friday was visible from the opening session: 15.1% of Monday's volume already sat in contracts dated to expire on it.

Rates across the week

QueryTreasury curve by session, July 17 print through July 24
The exact SQL behind every number
SELECT toString(date) AS date,
       round(toFloat64(yield_2_year), 2) AS yield_2y_pct,
       round(toFloat64(yield_10_year), 2) AS yield_10y_pct,
       round(toFloat64(yield_30_year), 2) AS yield_30y_pct,
       round((toFloat64(yield_10_year) - toFloat64(yield_2_year)) * 100) AS spread_2s10s_bp,
       round((toFloat64(yield_10_year) - (SELECT toFloat64(any(yield_10_year)) FROM global_markets.treasury_yields WHERE date = '2026-07-17')) * 100) AS chg_10y_from_prior_close_bp
FROM global_markets.treasury_yields
WHERE date >= '2026-07-17' AND date <= '2026-07-24'
ORDER BY date

Measured from the July 17 print, the ten-year moved 16 basis points to 4.71% by the last print on file, with the two-year at 4.37% and the thirty-year at 5.17%. The two-to-ten-year spread finished the series at 34 basis points. Treasury's file runs roughly a session behind the tape, so this panel carries 5 prints, each dated in the table.

The shorts: the daily files

FINRA publishes a daily short-sale volume file. Short volume is a gross daily flow rather than a position, and these files sometimes arrive truncated, so coverage gets measured before any ratio is quoted from them.

QueryDaily short-volume file coverage: tickers on file and short shares, July 20-24
The exact SQL behind every number
SELECT date, tickers_on_file, short_shares_bn,
       round(100.0 * tickers_on_file / max(tickers_on_file) OVER (), 1) AS pct_of_fullest_file
FROM (
    SELECT toString(date) AS date,
           uniqExact(ticker) AS tickers_on_file,
           round(sum(short_shares) / 1e9, 2) AS short_shares_bn
    FROM (
        SELECT date, ticker, max(short_volume) AS short_shares
        FROM global_markets.stocks_short_volume
        WHERE date >= '2026-07-20' AND date <= '2026-07-24'
        GROUP BY date, ticker
    )
    GROUP BY date
)
ORDER BY date

5 daily files are on hand for the week. The first holds 15098 tickers and 4.71 billion short-marked shares; the last holds 15062 tickers, or 99.7% of the fullest file's ticker count. Any per-name short-volume claim drawn from a file sitting well under its neighbors stays unverifiable until that file is refiled.

The calendar behind the week

QuerySEC filings by session and form type, July 20-24
The exact SQL behind every number
SELECT toString(filing_date) AS date,
       count() AS filings,
       countIf(form_type = '4') AS form4,
       countIf(form_type = '8-K') AS form8k,
       countIf(form_type = '424B2') AS f424b2
FROM global_markets.stocks_sec_edgar_index
WHERE filing_date >= '2026-07-20' AND filing_date <= '2026-07-24'
GROUP BY filing_date
ORDER BY date

The first session on file carried 3012 filings, 632 of them insider Form 4 reports and 166 of them 8-Ks. The last carried 3814, with 644 Form 4s and 223 8-Ks. 5 of the week's sessions have an EDGAR daily index on file; that index lands on its own schedule, which occasionally trails the tape.

QueryThe week's corporate calendar: dividends, splits, listings, news
The exact SQL behind every number
SELECT
    (SELECT count() FROM global_markets.stocks_sec_edgar_index WHERE filing_date >= '2026-07-20' AND filing_date <= '2026-07-24') AS filings_week,
    (SELECT uniqExact(filing_date) FROM global_markets.stocks_sec_edgar_index WHERE filing_date >= '2026-07-20' AND filing_date <= '2026-07-24') AS filing_days_on_file,
    (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= '2026-07-20' AND ex_dividend_date <= '2026-07-24') AS ex_dividends_week,
    (SELECT countIf(ticker IN ('AAPL', 'MSFT', 'JPM', 'KO', 'JNJ', 'XOM', 'CVX', 'PG', 'WMT', 'HD')) FROM global_markets.stocks_dividends WHERE ex_dividend_date >= '2026-07-20' AND ex_dividend_date <= '2026-07-24') AS household_ex_dividends_week,
    (SELECT countIf(toFloat64(split_from) > toFloat64(split_to)) FROM global_markets.stocks_splits WHERE execution_date >= '2026-07-20' AND execution_date <= '2026-07-24') AS reverse_splits_week,
    (SELECT countIf(toFloat64(split_to) > toFloat64(split_from)) FROM global_markets.stocks_splits WHERE execution_date >= '2026-07-20' AND execution_date <= '2026-07-24') AS forward_splits_week,
    (SELECT count() FROM global_markets.stocks_ipos WHERE listing_date >= '2026-07-20' AND listing_date <= '2026-07-24') AS listings_week,
    (SELECT count() FROM global_markets.stocks_news WHERE published_utc >= '2026-07-20 04:00:00' AND published_utc < '2026-07-25 04:00:00') AS news_week,
    (SELECT uniqExact(JSONExtractString(publisher, 'name')) FROM global_markets.stocks_news WHERE published_utc >= '2026-07-20 04:00:00' AND published_utc < '2026-07-25 04:00:00') AS news_publishers_week

The rest of the week's paperwork: 17106 SEC filings across 5 indexed days, 592 ex-dividend records, 1 of them from the ten household names this panel checks, 25 reverse and 11 forward splits, 4 new listings, and 1704 articles from 2 publishers. That last pair measures one feed's attention, not the world's media.

The sessions, verified

QuerySession verification: sessions, bars, holiday rows, up and down closes, next closure
The exact SQL behind every number
WITH spy AS (
    SELECT d, c, lagInFrame(c) OVER (ORDER BY d ASC ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS p
    FROM (
        SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS d,
               argMax(toFloat64(close), window_start) AS c
        FROM global_markets.delayed_stocks_minute_aggs
        WHERE ticker = 'SPY'
          AND ((window_start >= '2026-07-17 13:30:00' AND window_start < '2026-07-17 20:00:00')
            OR (window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-24 20:00:00'))
          AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199
        GROUP BY d
    )
)
SELECT
    uniqExact(toDate(toTimeZone(window_start, 'America/New_York'))) AS sessions_in_week,
    toString(min(toDate(toTimeZone(window_start, 'America/New_York')))) AS first_session,
    toString(max(toDate(toTimeZone(window_start, 'America/New_York')))) AS last_session,
    count() AS regular_bars_in_week,
    (SELECT count() FROM global_markets.stocks_market_holidays WHERE date >= '2026-07-20' AND date <= '2026-07-24') AS holiday_rows_in_week,
    (SELECT countIf(c > p) FROM spy WHERE d >= '2026-07-20') AS up_sessions,
    (SELECT countIf(c < p) FROM spy WHERE d >= '2026-07-20') AS down_sessions,
    (SELECT toString(min(date)) FROM global_markets.stocks_market_holidays WHERE date > '2026-07-24' AND date <= '2026-12-31' AND status = 'closed') AS next_closure_date,
    (SELECT argMin(name, date) FROM global_markets.stocks_market_holidays WHERE date > '2026-07-24' AND date <= '2026-12-31' AND status = 'closed') AS next_closure_name
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
  AND window_start >= '2026-07-20 13:30:00' AND window_start < '2026-07-24 20:00:00'
  AND (toHour(window_start) * 60 + toMinute(window_start)) BETWEEN 810 AND 1199

5 sessions, 2026-07-20 through 2026-07-24, with 0 holiday rows anywhere in the window. SPY printed 1950 regular-session minute bars across them. 2 sessions closed above the prior close and 3 closed below. The next scheduled closure is Labor Day on 2026-09-07.

On deck

The following week, read from our own tables:

QueryNext week on the calendar: closures, ex-dividends, splits, and the expiry pull
The exact SQL behind every number
SELECT
    (SELECT count() FROM global_markets.stocks_market_holidays WHERE date >= '2026-07-27' AND date <= '2026-07-31' AND status != 'open') AS closures_next_week,
    (SELECT count() FROM global_markets.stocks_dividends WHERE ex_dividend_date >= '2026-07-27' AND ex_dividend_date <= '2026-07-31') AS ex_dividends_next_week,
    (SELECT countIf(ticker IN ('AAPL', 'MSFT', 'JPM', 'KO', 'JNJ', 'XOM', 'CVX', 'PG', 'WMT', 'HD')) FROM global_markets.stocks_dividends WHERE ex_dividend_date >= '2026-07-27' AND ex_dividend_date <= '2026-07-31') AS household_ex_div_next_week,
    (SELECT count() FROM global_markets.stocks_splits WHERE execution_date >= '2026-07-27' AND execution_date <= '2026-07-31') AS splits_next_week,
    (SELECT round(100.0 * sumIf(size, substring(ticker, length(ticker) - 14, 6) = '260731') / sum(size), 1) FROM global_markets.options_trades WHERE sip_timestamp >= '2026-07-24 00:00:00' AND sip_timestamp < '2026-07-25 00:00:00') AS jul31_expiry_pct_of_friday_volume,
    (SELECT toString(max(settlement_date)) FROM global_markets.stocks_short_interest WHERE settlement_date <= '2026-07-24') AS latest_short_interest_settlement

The holiday table shows 0 closures in the following week. 368 ex-dividend records fall inside it, 0 from the household set, alongside 15 scheduled splits. Of Friday's options volume, 14.2% already sat in contracts expiring the following Friday. The most recent short-interest settlement date on file is 2026-07-15, a file that publishes on a lag long enough to have its own explainer.

FAQ

How did the stock market do the week of July 20, 2026?

SPY changed -0.59% close over close from July 17 to July 24, with QQQ at -1.59%, DIA at -0.38%, and IWM at -0.98%. Among liquid names, 2437 rose and 3630 fell.

Which sector led the week of July 20, 2026?

Energy, at 3.33%. The weakest of the eleven SPDR sector ETFs was Consumer Discretionary at -5.2%, 8.53 percentage points behind.

How many trading sessions were in the week of July 20, 2026?

5, running 2026-07-20 to 2026-07-24, with 0 holiday rows in the window. The next scheduled closure is Labor Day.

Which ticker traded the most dollars the week of July 20, 2026?

MU, at 158.1 billion dollars of regular-hours turnover, ahead of SPY at 133.9 billion.

Data notes

All stored timestamps are UTC; regular hours are 13:30-20:00 UTC per session, and week boundaries run from July 17's regular close to July 24's. Every close-over-close panel here reads the last regular-hours minute bar on each side of the comparison, so an extended-hours print can never set a weekly close. The trailing-year rank uses open-to-close weekly returns, so it will not match the close-over-close scoreboard; both definitions are labeled where they appear. Weekly breadth and the mover boards apply a five-million-dollar regular-hours turnover filter, and breadth publishes the count of names that filter set aside. Mover boards exclude any name whose split executed after the July 17 close and through July 24, the exact interval the change is measured over, which catches a split stamped to the intervening weekend as well as one stamped to a session; they do not exclude leveraged and inverse ETFs, which can dominate a board on a directional week. The bottom of the decliner board can sit close to a total loss of quoted value for the week, and that board publishes how many of the five sessions each name printed in. The SPY spread panel counts one-sided and crossed quotes rather than dropping them quietly, and the median is taken over the remaining valid quotes. Treasury's file runs about a session behind the tape and the EDGAR daily index lands on its own schedule, so the rates and filings panels report how many days they actually hold. No implied-volatility index appears here: those series are not licensed into this warehouse, so volatility is read off the tape through ranges, same-day options share, and quote behavior.

Methodology

  • Market data source: consolidated tape. delayed_stocks_minute_aggs for prices and volumes, cache_stocks_quotes for the NBBO panel, options_trades for the options week.
  • Close: the last regular-session minute bar of the session, never an assumed 16:00 print and never an extended-hours print.
  • Sessions: verified from the holiday table plus observed bars, never assumed from the calendar.
  • Time zone handling: WHERE clauses use raw UTC literals, and toTimeZone appears only in SELECT lists for ET labels.
  • Decimals: price, size, and volume columns are cast to Float64 before any division or product.
  • Deterministic aggregates: exact quantiles throughout, and every ordering or sign claim in prose is encoded as a sanity bound.
  • Warehouse as-of date: July 26, 2026.

Cross-links: the previous edition of this weekly series, what a bid-ask spread is, when options expire, short interest versus short volume, and the two-to-ten-year spread.

Every query above runs unchanged on the Strasmore terminal if you want to re-point a window at a different week.