What Is Relative Volume (RVOL)? Measured on Real Data
Relative volume (RVOL) compares a stock's volume to its own 20-day average. See the intraday U-shape, the time-of-day fix, and measured RVOL percentiles.
Relative volume (RVOL) measures how busy a stock is trading compared with its own recent normal: the session's share volume divided by the stock's average daily volume, usually taken over the trailing 20 sessions. An RVOL of 1 is an ordinary day, 2 means twice the usual shares changed hands, and 0.5 marks a session running at half speed. Every figure on this page is measured from minute-level US market data, with the exact query attached to each panel.
What is relative volume? A plain-English definition
Raw volume means little on its own: five million shares is a dead-quiet day for a mega-cap and a once-a-year event for a small-cap. Relative volume makes the comparison to the stock's own normal explicit:
RVOL = the day's volume ÷ the stock's average daily volume (typically the last 20 sessions)
A hypothetical example: a stock that averages 10 million shares a day and prints 25 million by the close finished at an RVOL of 2.5. (A print is a trade reported to the consolidated tape — the market-wide record of executions — and "the tape" is trader shorthand for that record.) The 20-session lookback is a convention — platforms also use 10, 30, or 50 days; it is roughly one calendar month of trading. The window also inherits the exchange calendar's quirks: a holiday half-day adds only a fraction of a normal session's volume to the average, and market holidays and early closes maps where those land.
Why do traders watch relative volume?
RVOL is a participation gauge, not a direction gauge. High-RVOL days cluster around identifiable events — earnings, index additions, headline news. A price move printed on heavy volume had a crowd behind it; the same move at an RVOL of 0.3 can be the work of a handful of orders. Activity also travels with tradability: across stocks, the most thinly traded names quote the widest bid-ask spreads, and for any one stock, quotes tend to sit somewhat wider on its quietest sessions than on its busiest. (Volume also underpins VWAP, the volume-weighted average price — the other big volume-based reference.)
How to calculate relative volume — and the time-of-day trap
The full-day calculation is one division: the session's total volume ÷ the 20-session average daily volume. It is only honest after the close. The trap is running it mid-session: at 10:30 a.m. the running total is naturally a fraction of any full-day figure, so even a violent morning reads as sleepy. Intraday volume follows a U-shape across the clock, and any intraday RVOL has to start from that shape.
Below is the shape, measured on SPY, the S&P 500 ETF: median shares traded per minute in each 30-minute clock bucket over the last 30 days of completed sessions, extended hours included (4:00 a.m. to 8:00 p.m. Eastern Time).
The exact SQL behind every number
SELECT formatDateTime(toStartOfInterval(toTimeZone(window_start, 'America/New_York'), INTERVAL 30 MINUTE), '%H:%i') AS et_time,
round(quantileDeterministic(0.5)(toFloat64(volume), toUInt64(toUnixTimestamp(window_start))) / 1000, 1) AS median_minute_volume_k
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= now() - INTERVAL 30 DAY
AND toDate(toTimeZone(window_start, 'America/New_York')) < toDate(toTimeZone(now(), 'America/New_York'))
GROUP BY et_time
HAVING et_time >= '04:00' AND et_time < '20:00'
ORDER BY et_timeThe 9:30 bucket — the first half hour of regular trading — ran a median of 150.9 thousand shares per minute, against 4.3 thousand in the 9:00 premarket bucket just before it. The pace thins to 51.8 thousand in the 12:30 bucket, then builds into the close: 218 thousand in the 3:30 bucket, the run-up to the closing auction and well above the lunchtime trough. After the 4:00 p.m. bell the per-minute pace falls off a cliff into after-hours trading.
Intraday relative volume: adjust for time of day
The fix changes the denominator: compare today's running total against the average running total at the same clock time. That requires knowing the share of a normal day's volume already complete at each point on the clock — measured here on SPY's last 20 completed sessions:
The exact SQL behind every number
WITH per_min AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
formatDateTime(toTimeZone(window_start, 'America/New_York'), '%H:%i') AS et_min,
sum(toFloat64(volume)) AS v
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'SPY'
AND window_start >= now() - INTERVAL 45 DAY
AND toDate(toTimeZone(window_start, 'America/New_York')) < toDate(toTimeZone(now(), 'America/New_York'))
GROUP BY et_date, et_min
),
last20 AS (
SELECT DISTINCT et_date FROM per_min ORDER BY et_date DESC LIMIT 20
)
SELECT checkpoint AS et_checkpoint,
round(avg(share) * 100, 1) AS avg_pct_of_day_volume_done
FROM (
SELECT et_date,
checkpoint,
sumIf(v, et_min < checkpoint) / sum(v) AS share
FROM (
SELECT et_date, et_min, v, arrayJoin(['10:00', '12:00', '14:00', '15:30', '16:00']) AS checkpoint
FROM per_min
WHERE et_date IN (SELECT et_date FROM last20)
)
GROUP BY et_date, checkpoint
)
GROUP BY checkpoint
ORDER BY checkpointBy 10:00 a.m., half an hour into regular trading, SPY has typically printed just 11.4% of its eventual full-day volume; by noon 33%, by 2:00 p.m. 49.1%, by 3:30 p.m. still only 65.9%. Even at the 4:00 p.m. closing bell the total stands at 84.1% — everything else is reported at or after 16:00, and most of it lands in the first minutes after the bell, when the closing auction and other end-of-day trades hit the tape; the after-hours session itself is far thinner. That squares with the cliff in the chart above: per-minute pace collapses at the bell even while a meaningful slice of the day's total is still being reported. A time-adjusted morning RVOL divides today's cumulative volume by the 20-day average multiplied by the checkpoint share. One caveat: this schedule is SPY's — each stock keeps its own clock, and the worked example below measures one name's.
A worked example: a real heavy-volume session (MU)
June 2026 was a heavy month for Micron — the MU June 2026 deep-dive walks the full tape. Here is that month's single biggest-volume session, with the RVOL math done both ways:
The exact SQL behind every number
WITH mu_daily AS (
SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
sum(toFloat64(volume)) AS day_volume,
sumIf(toFloat64(volume), formatDateTime(toTimeZone(window_start, 'America/New_York'), '%H:%i') < '10:30') AS vol_by_1030
FROM global_markets.delayed_stocks_minute_aggs
WHERE ticker = 'MU'
AND window_start >= toDateTime('2026-04-15 00:00:00', 'America/New_York')
AND window_start < toDateTime('2026-07-01 00:00:00', 'America/New_York')
GROUP BY et_date
HAVING day_volume > 1000000
),
biggest AS (
SELECT et_date, day_volume, vol_by_1030
FROM mu_daily
WHERE et_date >= toDate('2026-06-01') AND et_date <= toDate('2026-06-30')
ORDER BY day_volume DESC
LIMIT 1
),
trailing AS (
SELECT sum(day_volume) / 20 AS adv, sum(vol_by_1030) / 20 AS avg_by_1030
FROM (
SELECT day_volume, vol_by_1030
FROM mu_daily
WHERE et_date < (SELECT et_date FROM biggest)
ORDER BY et_date DESC
LIMIT 20
)
)
SELECT formatDateTime((SELECT et_date FROM biggest), '%Y-%m-%d') AS session_date,
round((SELECT vol_by_1030 FROM biggest) / 1e6, 1) AS vol_by_1030_m,
round((SELECT avg_by_1030 FROM trailing) / 1e6, 1) AS avg_vol_by_1030_m,
round((SELECT vol_by_1030 FROM biggest) / (SELECT avg_by_1030 FROM trailing), 1) AS rvol_1030_adjusted,
round((SELECT vol_by_1030 FROM biggest) / (SELECT adv FROM trailing), 2) AS rvol_1030_naive,
round((SELECT day_volume FROM biggest) / 1e6, 1) AS session_volume_m,
round((SELECT adv FROM trailing) / 1e6, 1) AS trailing_adv_m,
round((SELECT day_volume FROM biggest) / (SELECT adv FROM trailing), 1) AS rvol_full_dayMU's largest session of the month landed on 2026-06-25. By 10:30 a.m. that day, 32.8 million shares had traded, against an average of 17.3 million by the same clock time over the prior 20 sessions — a time-adjusted RVOL of 1.9. Divide that morning total by the full-day average of 51 million instead and the naive reading is 0.64: a below-average day, apparently, in a morning running at close to twice its usual pace. The session finished at 77.2 million shares, a full-day RVOL of 1.5.
That closing figure can look modest next to screenshots of ten-times-average movers — whether it truly is modest is a question for a distribution, measured next.
What counts as high relative volume?
Trading forums float thresholds like "RVOL above 2 is high." Here is the measured version: full-day RVOL for every US-listed stock and ETF with a 20-day average daily volume above 5 million shares (and a full 20 prior sessions of history), percentiled across the group for the most recent completed session in our data. One note on the label: the screen counts shares, not dollars, so this high-volume group runs from mega-caps and ETFs down to cheap, high-churn small-caps — high share volume is not the same thing as easy tradability.
The exact SQL behind every number
WITH daily AS (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
toFloat64(sum(volume)) AS day_volume
FROM global_markets.delayed_stocks_minute_aggs
WHERE window_start >= now() - INTERVAL 40 DAY
AND toDate(toTimeZone(window_start, 'America/New_York')) < toDate(toTimeZone(now(), 'America/New_York'))
GROUP BY ticker, et_date
),
spy_days AS (
SELECT et_date,
day_volume,
avg(day_volume) OVER (ORDER BY et_date ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS prior_avg
FROM daily
WHERE ticker = 'SPY' AND day_volume > 10000000
),
sessions AS (
SELECT et_date FROM spy_days
),
latest AS (
SELECT max(et_date) AS d FROM spy_days WHERE prior_avg > 0 AND day_volume >= 0.6 * prior_avg
),
prior20 AS (
SELECT et_date FROM sessions WHERE et_date < (SELECT d FROM latest) ORDER BY et_date DESC LIMIT 20
),
rvols AS (
SELECT ticker, day_vol / adv20 AS rvol
FROM (
SELECT ticker,
sumIf(day_volume, et_date = (SELECT d FROM latest)) AS day_vol,
sumIf(day_volume, et_date IN (SELECT et_date FROM prior20)) / 20 AS adv20,
countIf(day_volume > 0 AND et_date IN (SELECT et_date FROM prior20)) AS sessions_traded
FROM daily
WHERE ticker NOT IN ('SPCX')
GROUP BY ticker
HAVING adv20 > 5000000 AND day_vol > 0 AND sessions_traded = 20
)
)
SELECT pair.1 AS percentile,
round(pair.2, 2) AS rvol
FROM (
SELECT arrayJoin(arrayZip(['p10', 'p25', 'p50 (median)', 'p75', 'p90', 'p99'], quantilesExact(0.1, 0.25, 0.5, 0.75, 0.9, 0.99)(rvol))) AS pair
FROM rvols
)
ORDER BY percentileThe median name in this high-volume group traded at 0.87 times its own 20-day average. Nine names in ten came in below 1.54, and only one in a hundred cleared 3.04. Set MU's session from the previous section against this table and it stops looking modest — even busy names spend most days near their own average; multiples of it are rare.
The bottom tail matters too: the 10th percentile sat at 0.09. Readings that low usually mark a stock whose 20-day average still carries one enormous session — after a spike, the inflated denominator holds RVOL down for weeks. The window has a memory, in both directions.
One caveat: this is a single session's cross-section — percentiles drift day to day, and market-wide event days (index rebalances, option expirations) can lift the whole curve.
Which stocks had the highest relative volume?
Same universe, ranked: the ten highest full-day RVOL readings of the 2026-07-02 session among names averaging over 5 million shares a day — the board a screener would flag as unusual volume. The screen demands a full 20 prior sessions — recent listings have no meaningful 20-day average and would otherwise fill the board with artifacts.
The exact SQL behind every number
WITH daily AS (
SELECT ticker,
toDate(toTimeZone(window_start, 'America/New_York')) AS et_date,
toFloat64(sum(volume)) AS day_volume
FROM global_markets.delayed_stocks_minute_aggs
WHERE window_start >= now() - INTERVAL 40 DAY
AND toDate(toTimeZone(window_start, 'America/New_York')) < toDate(toTimeZone(now(), 'America/New_York'))
GROUP BY ticker, et_date
),
spy_days AS (
SELECT et_date,
day_volume,
avg(day_volume) OVER (ORDER BY et_date ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS prior_avg
FROM daily
WHERE ticker = 'SPY' AND day_volume > 10000000
),
sessions AS (
SELECT et_date FROM spy_days
),
latest AS (
SELECT max(et_date) AS d FROM spy_days WHERE prior_avg > 0 AND day_volume >= 0.6 * prior_avg
),
prior20 AS (
SELECT et_date FROM sessions WHERE et_date < (SELECT d FROM latest) ORDER BY et_date DESC LIMIT 20
)
SELECT ticker,
formatDateTime((SELECT d FROM latest), '%Y-%m-%d') AS session_date,
round(day_vol / 1e6, 1) AS session_volume_m,
round(adv20 / 1e6, 1) AS adv_20d_m,
round(day_vol / adv20, 1) AS rvol
FROM (
SELECT ticker,
sumIf(day_volume, et_date = (SELECT d FROM latest)) AS day_vol,
sumIf(day_volume, et_date IN (SELECT et_date FROM prior20)) / 20 AS adv20,
countIf(day_volume > 0 AND et_date IN (SELECT et_date FROM prior20)) AS sessions_traded
FROM daily
WHERE ticker NOT IN ('SPCX')
GROUP BY ticker
HAVING adv20 > 5000000 AND day_vol > 0 AND sessions_traded = 20
)
ORDER BY rvol DESC, ticker
LIMIT 10The top of the board, LIMN, printed 524.8 million shares against a 20-day average of 7.2 million — an RVOL of 72.5. Every row cleared its own average. Boards like this tilt away from household mega-caps — doubling an already-enormous average takes an extraordinary number of shares, while a smaller name can multiply its tape in one session.
Data notes: session selection and exclusions
Both ranked tables use the same universe and the same session rule. A candidate session only counts as complete once SPY's tape for that date reaches at least 60% of SPY's own prior-20-session average volume — the newest day's data arrives with a lag, and this check keeps a partially loaded day from being read as a finished session. Both tables also exclude one symbol the exchanges recently reassigned to a new listing: vendor feeds carry two different companies' history under that ticker, so its 20-day average is not a meaningful baseline. The exclusion list in the SQL is kept in sync with our symbol-identity checks at every refresh; any newly flagged reused symbol is added the same way.
FAQ
What is a good RVOL number?
There is no official threshold. Measured across high-volume US stocks on the most recent completed session in our data, the median name ran at 0.87 times its 20-day average, the 90th percentile was 1.54, and the 99th was 3.04.
What does an RVOL of 2 mean?
The stock has traded twice its average volume for the measured window, usually the trailing 20 sessions. The clock matters: 2 against a same-time-of-day average is a strong mid-session reading; 2 at the close marks a day at double the normal pace.
How do you calculate relative volume intraday?
Divide today's cumulative volume by the average cumulative volume at the same clock time over your lookback window. Dividing a mid-morning total by a full-day average understates the reading badly — by 10:00 a.m., SPY has typically completed only 11.4% of its eventual day volume.
Does relative volume include premarket and after-hours trading?
Platform conventions differ; this page counts every minute bar in the Eastern Time calendar day, extended hours included. For SPY the premarket contribution is visibly small — a median of 4.3 thousand shares a minute in the 9:00 bucket versus 150.9 thousand just after the open.
Is high relative volume bullish or bearish?
Neither. RVOL measures participation, not direction — heavy tape shows up on a stock's best and worst days alike. High-RVOL sessions have historically coincided with earnings, index changes, and headline news; the number says the crowd showed up, not which way it leans.
Every panel above carries the exact SQL that produced it — open one, swap in your own ticker, and measure RVOL for the names you actually trade on the Strasmore terminal.