Strasmore Research
Deep Dives Matt ConnorBy Matt Connor

Why Trading Volume Dies at Midday: U-Shape

Trading volume dies at midday in a U shaped curve: heavy at the open and close, thin from 11:30 to 14:00 ET. See the intraday profile and what it costs.

Trading volume dies at midday in a pattern the market repeats nearly every session: the U shaped intraday curve. Share volume and price movement both peak in the first half hour and the last half hour of the regular session, then sag to their thinnest point between roughly 11:30 a.m. and 2:00 p.m. ET. The shape holds across index funds and single stocks, and it holds year after year. It also sets what an order costs to fill at different hours of the day.

The subject here is the whole session's shape. For the opening minutes on their own, and the mechanics of a quote that starts wide and tightens through the morning, see why spreads widen at the open.

What does the intraday volume curve look like?

Split the 9:30 a.m. to 4:00 p.m. regular session into thirteen half hour buckets, add up the shares traded in each, then convert every bucket to its share of the day. If volume were spread evenly, each bucket would hold about 7.7% of it. The panel below runs that count for SPY, the largest S&P 500 tracking fund, and for AAPL over the same window of past sessions.

QueryShare of regular-session volume by half hour, SPY and AAPL
The exact SQL behind every number
WITH session_minutes AS
(
    SELECT
        ticker,
        formatDateTime(
            toStartOfInterval(toTimeZone(window_start, 'America/New_York'), INTERVAL 30 MINUTE),
            '%H:%i')                                                   AS et_time,
        toFloat64(volume)                                              AS share_volume
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'AAPL')
      AND window_start >= '2025-01-02 00:00:00'
      AND window_start <  '2026-07-01 00:00:00'
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
),
by_bucket AS
(
    SELECT
        ticker,
        et_time,
        sum(share_volume) AS bucket_volume
    FROM session_minutes
    GROUP BY ticker, et_time
),
ticker_totals AS
(
    SELECT
        ticker,
        sum(bucket_volume) AS ticker_volume
    FROM by_bucket
    GROUP BY ticker
)
SELECT
    b.et_time                                                          AS et_time,
    round(100 * sumIf(b.bucket_volume, b.ticker = 'SPY')
          / max(if(t.ticker = 'SPY', t.ticker_volume, 0)), 2)          AS spy_pct,
    round(100 * sumIf(b.bucket_volume, b.ticker = 'AAPL')
          / max(if(t.ticker = 'AAPL', t.ticker_volume, 0)), 2)         AS aapl_pct
FROM by_bucket AS b
INNER JOIN ticker_totals AS t ON t.ticker = b.ticker
GROUP BY b.et_time
ORDER BY b.et_time
Run this yourself

Read it left to right and the U draws itself. The opening bucket takes 12.15% of SPY's regular-session volume. Trade then drains out of the middle: by the 12:30 bucket the share is down to 5.16%. The last bucket before the bell, 15:30 to 4:00 p.m., takes 18.26% of continuous trading, one of 13 slices in the session, and the closing auction prints on top of that. AAPL traces the same curve with its own tilt, ending at 14.4% in that closing half hour.

Does volatility follow the same U shape?

It does, and that is the part that reaches your fills. Realised volatility, meaning how far price actually travels inside a bar rather than what an option implies about the future, traces the same profile. This panel measures the average high to low range of every one minute SPY bar in basis points (one basis point is one hundredth of one percent), grouped into the same half hour buckets.

QueryAverage one-minute SPY range in basis points, by half hour
The exact SQL behind every number
WITH minute_bars AS
(
    SELECT
        formatDateTime(
            toStartOfInterval(toTimeZone(window_start, 'America/New_York'), INTERVAL 30 MINUTE),
            '%H:%i')                                                   AS et_time,
        10000 * (toFloat64(high) - toFloat64(low)) / toFloat64(close)  AS range_bps,
        toUInt64(toUnixTimestamp(window_start))                        AS det
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2025-01-02 00:00:00'
      AND window_start <  '2026-07-01 00:00:00'
      AND close > 0
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) >= 570
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) < 960
)
SELECT
    et_time                                              AS et_time,
    round(avg(range_bps), 2)                             AS avg_range_bps,
    round(quantileDeterministic(0.5)(range_bps, det), 2) AS median_range_bps
FROM minute_bars
GROUP BY et_time
HAVING count() > 0
ORDER BY et_time
Run this yourself

Minutes in the opening bucket cover an average range of 7.54 basis points. The 12:30 bucket averages 4.66. The median line sits under the average in every bucket, the signature of a long right tail: most minutes are quiet, a handful are not.

Why does trading volume die at midday?

Three mechanics stack up across the session.

Overnight information meets the tape at the open

Company news, regulatory filings, overseas sessions, and economic releases accumulate while US exchanges are shut, seventeen and a half hours of every weekday plus the whole weekend. All of it arrives in one place: the opening auction and the minutes just after it. Positions re-decided overnight get traded there. By late morning that backlog has largely printed, and the arrival rate of genuinely new information slows for a few hours.

The closing auction pulls flow to the end of the day

The closing price is the official mark of the day. Index funds track it, mutual fund NAVs are struck on it, futures and options settlements reference it, and performance is measured against it, so an order whose job is to match the close belongs in the closing auction rather than at 12:15 p.m. Rebalances, ETF creation and redemption baskets, benchmark hedges, and end of day risk trims all queue for that one moment. The flow is scheduled rather than opportunistic, and it lands in the same half hour every session.

Liquidity providers thin out over the lunch window

Market makers, the firms quoting both sides of a stock continuously, size their quotes to the flow they expect. Order arrival slows between roughly 11:30 and 2:00, and quoted size shrinks alongside it. Fewer resting shares at the inside price means a given order walks further up or down the book. On a name like SPY the spread often holds at a penny through the middle of the day while the depth behind that penny thins, which shows up as slippage rather than as a wider quote. The London equity close lands around 11:30 a.m. ET and removes a set of participants that was active earlier.

Is the U shape the same for every stock?

The tilt varies from name to name. The shape does not. This panel scores each window against an average minute of that name's own session: 1.00 means the window traded exactly its proportional share, 2.00 means double.

QueryOpen, midday and close volume against an average session minute
The exact SQL behind every number
WITH session_minutes AS
(
    SELECT
        ticker,
        toHour(toTimeZone(window_start, 'America/New_York')) * 60
          + toMinute(toTimeZone(window_start, 'America/New_York'))     AS et_minute,
        toFloat64(volume)                                              AS share_volume
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY', 'QQQ', 'AAPL', 'MSFT', 'NVDA', 'KO')
      AND window_start >= '2025-01-02 00:00:00'
      AND window_start <  '2026-07-01 00:00:00'
)
SELECT
    ticker                                                             AS ticker,
    round((sumIf(share_volume, et_minute >= 570 AND et_minute < 600)
           / countIf(et_minute >= 570 AND et_minute < 600))
          / (sum(share_volume) / count()), 2)                          AS open30_index,
    round((sumIf(share_volume, et_minute >= 690 AND et_minute < 840)
           / countIf(et_minute >= 690 AND et_minute < 840))
          / (sum(share_volume) / count()), 2)                          AS midday_index,
    round((sumIf(share_volume, et_minute >= 930 AND et_minute < 960)
           / countIf(et_minute >= 930 AND et_minute < 960))
          / (sum(share_volume) / count()), 2)                          AS close30_index
FROM session_minutes
WHERE et_minute >= 570 AND et_minute < 960
GROUP BY ticker
HAVING countIf(et_minute >= 570 AND et_minute < 600) > 0
   AND countIf(et_minute >= 690 AND et_minute < 840) > 0
   AND countIf(et_minute >= 930 AND et_minute < 960) > 0
ORDER BY close30_index DESC
Run this yourself

KO runs the heaviest close on the panel at 2.58 times an average minute, against 1.38 times for NVDA. The middle of the session runs light in both, at 0.65 and 0.72 times an average minute over the 11:30 to 2:00 stretch. Index products often lean harder on the close than single names do. A stock with a heavy retail following leans a little harder on the open.

Has the intraday volume curve changed over the years?

Same scoring, now year by year on SPY.

QuerySPY open, midday and close volume against an average minute, by year
The exact SQL behind every number
WITH session_minutes AS
(
    SELECT
        toYear(toTimeZone(window_start, 'America/New_York'))            AS yr,
        toHour(toTimeZone(window_start, 'America/New_York')) * 60
          + toMinute(toTimeZone(window_start, 'America/New_York'))     AS et_minute,
        toFloat64(volume)                                              AS share_volume
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND window_start >= '2019-01-01 00:00:00'
      AND window_start <  '2026-07-01 00:00:00'
)
SELECT
    toString(yr)                                                       AS year,
    round((sumIf(share_volume, et_minute >= 570 AND et_minute < 600)
           / countIf(et_minute >= 570 AND et_minute < 600))
          / (sum(share_volume) / count()), 2)                          AS open30_index,
    round((sumIf(share_volume, et_minute >= 690 AND et_minute < 840)
           / countIf(et_minute >= 690 AND et_minute < 840))
          / (sum(share_volume) / count()), 2)                          AS midday_index,
    round((sumIf(share_volume, et_minute >= 930 AND et_minute < 960)
           / countIf(et_minute >= 930 AND et_minute < 960))
          / (sum(share_volume) / count()), 2)                          AS close30_index
FROM session_minutes
WHERE et_minute >= 570 AND et_minute < 960
GROUP BY yr
HAVING countIf(et_minute >= 570 AND et_minute < 600) > 0
   AND countIf(et_minute >= 690 AND et_minute < 840) > 0
   AND countIf(et_minute >= 930 AND et_minute < 960) > 0
ORDER BY yr
Run this yourself

Across the 8 calendar years drawn, the opening half hour clears the 1.00 line at both ends of the range: 1.5 times an average minute in 2019, and 1.62 times in 2026. The midday stretch sits under it in both, at 0.68 and 0.69. The final half hour reads 2.41 times in 2019 and 2.34 times in 2026. Assets that mark against the closing print have grown over the same span.

See the curve with thirteen numbers

The shape is simple enough to inspect in a table you type yourself. Save this as ushape.py and run python3 ushape.py. It uses nothing outside the Python standard library. The volumes in it are invented round numbers for one imaginary session, not a quote of any real day, and only the shape matters.

# Half-hour share volumes for one imaginary session. Invented numbers.
buckets = [
    ('09:30', 4_200_000),
    ('10:00', 2_600_000),
    ('10:30', 1_900_000),
    ('11:00', 1_500_000),
    ('11:30', 1_200_000),
    ('12:00', 1_050_000),
    ('12:30',   900_000),
    ('13:00', 1_000_000),
    ('13:30', 1_250_000),
    ('14:00', 1_500_000),
    ('14:30', 1_950_000),
    ('15:00', 2_800_000),
    ('15:30', 5_100_000),
]

total = sum(shares for _, shares in buckets)
trough_label, trough_shares = min(buckets, key=lambda row: row[1])

for label, shares in buckets:
    pct = 100.0 * shares / total
    mark = '  <-- trough' if label == trough_label else ''
    print(f'{label}  {shares:>9,}  {pct:5.1f}%  {"#" * round(pct)}{mark}')

print()
print(f'session total  {total:>9,} shares in {len(buckets)} half-hour buckets')
print(f'trough bucket  {trough_label} at {100.0 * trough_shares / total:.1f}% of the day')
print(f'ends vs trough {(buckets[0][1] + buckets[-1][1]) / trough_shares:.1f}x')

The output prints each bucket's percent of the day, a crude bar of hashes, and a flag on the lightest bucket. Swap in the buckets of a session you care about and the same arithmetic shows where that day's liquidity actually sat. Comparing one bucket against what is normal for that bucket is relative volume in miniature.

What the U shape does to execution

Executions inherit the curve. A market order at 12:40 p.m. meets a thinner book than the same order at 3:40 p.m., and pays for it in slippage rather than in the quoted spread. Two consequences follow for anyone measuring their own fills.

  • A VWAP benchmark is volume weighted, so the two ends of the day dominate it. Beating VWAP is mostly a question of how the open and the close were handled.
  • Scheduled algorithms trade in proportion to the expected curve, and their fills cluster into the last half hour alongside everyone else's.

Quarterly expiration sessions bend the curve further toward the close. Those dates sit in the triple witching calendar.

Why signals fitted on morning data fade after lunch

Here is the part that catches quantitative work. A model fitted on minute bars over the full session is fitted mostly on the loud parts of it. Weight every minute equally and the sample is still dominated by high volume minutes, since those minutes carry most of the price formation and most of the fills a backtest assumes it can get.

A signal estimated that way inherits the microstructure of those minutes: tight quotes and deep books. Run it at 12:45 p.m. and the conditions on the screen are not the conditions it was fitted under. Costs that rounded to nothing in the fitted sample are large in the thin one, and a measured edge of a few basis points disappears into them.

Sample weighting is one mechanical contributor among several. This is an observation about what a fitted sample contains, not a verdict on any particular model. Fitting by time of day bucket, or scoring performance against the curve instead of against the clock, is how practitioners usually handle the shape.

FAQ

What time of day is trading volume lowest?

Volume is thinnest in the middle of the regular session, roughly 11:30 a.m. to 2:00 p.m. ET, and the single lightest half hour usually falls between noon and 1:00 p.m. The panels above measure each window against an average minute of the same session.

Why is trading volume highest at the open and the close?

Overnight information meets the tape in the opening minutes, and the closing auction is the one moment where the official closing price is set, so orders that have to match that price gather there. Both ends of the day collect flow that has nowhere else to go.

Does the midday lull widen spreads?

On very liquid names the quoted spread often holds at a penny through the middle of the day while the size available at that penny shrinks, so the cost lands as slippage on larger orders. On thinner names the quote itself widens.

Is the U shape found outside US equities?

A version of it appears in most continuous auction equity markets that open and close with an auction, though the tilt differs by market. The US close is unusually heavy, alongside the size of the funds that mark against the closing print.


Every panel here ships with the exact SQL underneath it, expand one to see how the buckets were counted. To draw the same curve for a name you follow, ask for it in plain English on the Strasmore terminal.