Strasmore Research
Learn Matt ConnorBy Matt Connor

Why Relative Volume Differs Between Platforms

Relative volume rarely agrees across platforms. One AAPL session computed five ways from the same bars shows why readings split most in the first hour.

Relative volume differs between platforms for a plain reason: no two of them compute the same ratio. Each divides today's volume by an average of past volume, but each picks its own basis (the whole day so far, or only the volume printed by this clock minute on past days), its own lookback, and its own set of bars. Below, one AAPL session runs through five of those definitions from the same minute bars, and at 10:30 a.m. the readings differ by multiples.

Why relative volume differs between platforms

Relative volume, or RVOL, is volume now divided by normal volume. Every disagreement between two platforms hides inside the word normal, in one of four choices.

  • The basis. A full-day basis divides the volume printed so far today by the average of complete past sessions. A same-time basis divides it by the average volume that past sessions had printed by this same minute. At 4:00 p.m. the two are identical. At 9:45 a.m. they are not close.
  • The lookback. Ten sessions, twenty, fifty, or about three months (63 sessions). A short lookback tracks the stock's recent behaviour; a long one dilutes it.
  • Which bars count. Pre-market prints, after-hours prints, half-days and the closing auction each sit inside or outside the ratio depending on the platform's session template.
  • The clock. A screener on delayed data compares a 15-minute-old cumulative volume with an average built for the current minute.

The relative volume guide covers what the ratio is for. This post is about why two screens showing 0.4 and 1.2 for the same stock at the same minute can both be arithmetically right, the same way RSI differs between platforms: a shared name, an unshared formula.

What each platform says it computes

Where a platform documents its definition it is quoted or paraphrased here; where it does not, that is said rather than guessed.

Finviz publishes one line on its screener field:

"Ratio between current volume and 3-month average value, intraday adjusted." Finviz screener help, Relative Volume filter, as read on September 16, 2026.

The three-month lookback is documented. The intraday adjustment is not: the help page does not say how a partial day is scaled against full days, so the mid-session figure cannot be reproduced from outside.

TradingView documents two things. Its field called Relative Volume is volume divided by a simple moving average of the previous 10 bars, excluding the current one. Its indicator called Relative Volume at Time averages volume at the same time offset across a set number of past periods (10 days in TradingView's worked description) and has a calculation-mode input:

"Cumulative (default): the indicator uses the total volume accumulated since the last period anchor. Regular: the indicator uses the non-cumulative volume at the time offset." TradingView Help Center, Relative Volume at Time, as read on September 16, 2026.

The cumulative, same-time version is the one built below.

thinkorswim documents a study named RelativeVolumeStDev: volume divided by its simple moving average, expressed in standard deviations over a length set in bars, with a spike flagged above 2.0. A time-of-day-matched cumulative ratio is not among its documented built-in studies; community thinkScript versions exist and set their own lookbacks.

Only one of these documented formulas, TradingView's Relative Volume at Time, compares like clock time with like.

The same session against four lookbacks

Start with the version every end-of-day screener shares in shape: a completed session's volume over an average of completed sessions, where the only free choice is the lookback. The panel takes AAPL's session of Thursday, September 10, 2026, and divides its consolidated daily volume by the average of the 10, 20, 50 and 63 sessions before it.

QueryOne AAPL session, four lookbacks: full-day relative volume from daily bars
lookbacksession_volume_millionsaverage_volume_millionsrvol_ratio
10 sessions7041.11.7
20 sessions7040.61.72
50 sessions7048.61.44
63 sessions (about 3 months)7053.41.31
The exact SQL behind every number
WITH toDate('2026-09-10') AS session_day
SELECT
    lookback,
    round(session_volume / 1e6, 1)            AS session_volume_millions,
    round(average_volume / 1e6, 1)            AS average_volume_millions,
    round(session_volume / average_volume, 2) AS rvol_ratio
FROM
(
    SELECT
        date,
        vol                                                                     AS session_volume,
        avg(vol) OVER (ORDER BY date ROWS BETWEEN 10 PRECEDING AND 1 PRECEDING) AS avg10,
        avg(vol) OVER (ORDER BY date ROWS BETWEEN 20 PRECEDING AND 1 PRECEDING) AS avg20,
        avg(vol) OVER (ORDER BY date ROWS BETWEEN 50 PRECEDING AND 1 PRECEDING) AS avg50,
        avg(vol) OVER (ORDER BY date ROWS BETWEEN 63 PRECEDING AND 1 PRECEDING) AS avg63,
        count()  OVER (ORDER BY date ROWS BETWEEN 63 PRECEDING AND 1 PRECEDING) AS prior_sessions
    FROM
    (
        SELECT date, max(toFloat64(volume)) AS vol
        FROM global_markets.stocks_daily_aggs
        WHERE ticker = 'AAPL'
          AND date >= session_day - 110
          AND date <= session_day
        GROUP BY date
    )
)
ARRAY JOIN
    ['10 sessions', '20 sessions', '50 sessions', '63 sessions (about 3 months)'] AS lookback,
    [10, 20, 50, 63]                                                                 AS n,
    [avg10, avg20, avg50, avg63]                                                     AS average_volume
WHERE date = session_day
  AND prior_sessions >= 63
  AND session_volume > 0
ORDER BY n
Run this yourself

The session printed 70 million shares. Against the prior 10 sessions that is a relative volume of 1.7; against the prior 63, roughly the three-month window Finviz names, it is 1.31, with the 50-session reading at 1.44. Nothing about the day changed between those numbers. Only the definition of normal did, from a 41.1 million-share average over ten sessions to 53.4 million over sixty-three. When a stock has been quieter or busier lately than it was three months ago, short and long lookbacks disagree by exactly that drift, which is why the average daily volume window matters before the ratio is ever computed.

Why the gap is widest in the first hour

The lookback nudges the reading. The basis moves it by multiples, and only during the session. The next panel adds up AAPL's one-minute bars for the same September 10 session at 14 clock checkpoints, keeping bars whose Eastern clock time runs from 9:30 a.m. through the 4:00 p.m. bar that carries the closing auction print, alongside the average cumulative volume at each checkpoint over the prior 10 sessions.

QueryHow the session's volume piled up against the prior 10 sessions, checkpoint by checkpoint
et_timesession_cumulative_millionstypical_cumulative_millionstypical_share_of_session_pct
09:458.32.78.5
10:0011.14.514.1
10:3014.57.523.8
11:001910.131.9
11:3023.412.439.2
12:0029.214.545.7
12:3032.816.351.4
13:0038.217.755.8
13:3041.219.561.5
14:0045.32166.4
14:304822.972.4
15:0050.325.179.3
15:3053.426.985.1
16:0059.331.7100
The exact SQL behind every number
WITH
    toDate('2026-09-10') AS session_day,
    [585, 600, 630, 660, 690, 720, 750, 780, 810, 840, 870, 900, 930, 960] AS checkpoints
SELECT
    formatDateTime(toDateTime(session_day, 'UTC') + checkpoints[i] * 60, '%H:%i', 'UTC') AS et_time,
    round(session_cums[i] / 1e6, 1)                                                        AS session_cumulative_millions,
    round(typical_cums[i] / 1e6, 1)                                                        AS typical_cumulative_millions,
    round(100 * typical_cums[i] / typical_cums[14], 1)                                     AS typical_share_of_session_pct
FROM
(
    SELECT
        arrayJoin(arrayEnumerate(checkpoints)) AS i,
        session_cums,
        arrayMap(k -> arrayAvg(x -> arrayElement(tupleElement(x, 2), k), arraySlice(prior, 1, 10)),
                 arrayEnumerate(checkpoints)) AS typical_cums
    FROM
    (
        SELECT
            anyIf(cums, d = session_day)                                                        AS session_cums,
            arrayReverseSort(x -> tupleElement(x, 1), groupArrayIf((d, cums), d < session_day)) AS prior
        FROM
        (
            SELECT
                d,
                arrayMap(cp -> arraySum(x -> if(tupleElement(x, 1) <= cp, tupleElement(x, 2), 0), mv),
                         checkpoints) AS cums
            FROM
            (
                SELECT d, groupArray((minute_of_day, vol)) AS mv
                FROM
                (
                    SELECT
                        toDate(toTimeZone(window_start, 'America/New_York'))      AS d,
                        toHour(toTimeZone(window_start, 'America/New_York')) * 60
                          + toMinute(toTimeZone(window_start, 'America/New_York')) AS minute_of_day,
                        max(toFloat64(volume))                                     AS vol
                    FROM global_markets.delayed_stocks_minute_aggs
                    WHERE ticker = 'AAPL'
                      AND window_start >= toDateTime(session_day - 20, 'America/New_York')
                      AND window_start <  toDateTime(session_day + 1, 'America/New_York')
                    GROUP BY d, minute_of_day
                    HAVING minute_of_day >= 570 AND minute_of_day <= 960
                )
                GROUP BY d
            )
        )
        HAVING length(session_cums) = 14 AND length(prior) >= 10
    )
)
ORDER BY i
Run this yourself

A typical AAPL session in this window had printed 8.5% of its regular-session volume by 9:45 a.m. and 23.8% by 10:30 a.m., on its way to 31.7 million shares at the close, where the share reaches 100% by definition. Volume front-loads: a large share of the session prints in the first hour, the pace flattens through lunch (the shape in why trading volume dies at midday), and the closing auction adds a final block at 4:00 p.m.

Now divide the session's cumulative volume two ways. The same-time basis divides it by the typical cumulative volume at that checkpoint. The full-day basis divides it by the typical full-session total, which is what a screener effectively does when it sets volume so far against a plain average daily volume. Both use the identical 10-session lookback, so the basis is the only difference.

QuerySame session, same 10-session lookback: same-time basis vs full-day basis through the day
et_timesame_time_basis_ratiofull_day_basis_ratio
09:453.10.26
10:002.480.35
10:301.920.46
11:001.880.6
11:301.880.74
12:002.020.92
12:302.011.04
13:002.161.21
13:302.121.3
14:002.161.43
14:302.091.51
15:0021.59
15:301.981.69
16:001.871.87
The exact SQL behind every number
WITH
    toDate('2026-09-10') AS session_day,
    [585, 600, 630, 660, 690, 720, 750, 780, 810, 840, 870, 900, 930, 960] AS checkpoints
SELECT
    formatDateTime(toDateTime(session_day, 'UTC') + checkpoints[i] * 60, '%H:%i', 'UTC') AS et_time,
    round(session_cums[i] / typical_cums[i], 2)                                            AS same_time_basis_ratio,
    round(session_cums[i] / typical_cums[14], 2)                                           AS full_day_basis_ratio
FROM
(
    SELECT
        arrayJoin(arrayEnumerate(checkpoints)) AS i,
        session_cums,
        arrayMap(k -> arrayAvg(x -> arrayElement(tupleElement(x, 2), k), arraySlice(prior, 1, 10)),
                 arrayEnumerate(checkpoints)) AS typical_cums
    FROM
    (
        SELECT
            anyIf(cums, d = session_day)                                                        AS session_cums,
            arrayReverseSort(x -> tupleElement(x, 1), groupArrayIf((d, cums), d < session_day)) AS prior
        FROM
        (
            SELECT
                d,
                arrayMap(cp -> arraySum(x -> if(tupleElement(x, 1) <= cp, tupleElement(x, 2), 0), mv),
                         checkpoints) AS cums
            FROM
            (
                SELECT d, groupArray((minute_of_day, vol)) AS mv
                FROM
                (
                    SELECT
                        toDate(toTimeZone(window_start, 'America/New_York'))      AS d,
                        toHour(toTimeZone(window_start, 'America/New_York')) * 60
                          + toMinute(toTimeZone(window_start, 'America/New_York')) AS minute_of_day,
                        max(toFloat64(volume))                                     AS vol
                    FROM global_markets.delayed_stocks_minute_aggs
                    WHERE ticker = 'AAPL'
                      AND window_start >= toDateTime(session_day - 20, 'America/New_York')
                      AND window_start <  toDateTime(session_day + 1, 'America/New_York')
                    GROUP BY d, minute_of_day
                    HAVING minute_of_day >= 570 AND minute_of_day <= 960
                )
                GROUP BY d
            )
        )
        HAVING length(session_cums) = 14 AND length(prior) >= 10
    )
)
ORDER BY i
Run this yourself

At 9:45 a.m. the same-time reading was 3.1 and the full-day reading was 0.26. At 10:30 a.m. they were 1.92 and 0.46. At the close both lines meet at 1.87, by construction: once the session is complete, volume by this minute and volume for the day are the same number.

The full-day line measures what fraction of an ordinary day has printed so far, multiplied by the actual relative volume; it says little about how busy the open was. Early in the session that fraction is small, so the line starts near zero and climbs toward its end-of-day value whatever the stock is doing. The gap between the two definitions is largest when the fraction is smallest, the first hour, and it narrows through the afternoon.

The five readings side by side

The last panel puts every definition on one set of rows at two moments, 10:30 a.m. ET and the 4:00 p.m. close, all from the same regular-session bars: full-day rows at three lookbacks and same-time rows at two, from 10 sessions up to 63.

QueryFive relative-volume definitions on one AAPL session, at 10:30 a.m. ET and at the close
definitionat_10_30_et_ratioat_close_ratio
Full-day basis, 63-session (3-month) average0.381.54
Full-day basis, 50-session average0.41.62
Full-day basis, 10-session average0.461.87
Same-time basis, 50-session average1.521.62
Same-time basis, 10-session average1.921.87
The exact SQL behind every number
WITH toDate('2026-09-10') AS session_day
SELECT
    tupleElement(r, 1)           AS definition,
    round(tupleElement(r, 2), 2) AS at_10_30_et_ratio,
    round(tupleElement(r, 3), 2) AS at_close_ratio
FROM
(
    SELECT
        arrayJoin([
            ('Full-day basis, 63-session (3-month) average', session_1030 / avg63_full, session_close / avg63_full, 1),
            ('Full-day basis, 50-session average',           session_1030 / avg50_full, session_close / avg50_full, 2),
            ('Full-day basis, 10-session average',           session_1030 / avg10_full, session_close / avg10_full, 3),
            ('Same-time basis, 50-session average',          session_1030 / avg50_same, session_close / avg50_full, 4),
            ('Same-time basis, 10-session average',          session_1030 / avg10_same, session_close / avg10_full, 5)
        ]) AS r
    FROM
    (
        SELECT
            anyIf(cum_1030, d = session_day)                                                                    AS session_1030,
            anyIf(cum_close, d = session_day)                                                                   AS session_close,
            arrayReverseSort(x -> tupleElement(x, 1), groupArrayIf((d, cum_1030, cum_close), d < session_day)) AS prior,
            arrayAvg(x -> tupleElement(x, 3), arraySlice(prior, 1, 63))                                        AS avg63_full,
            arrayAvg(x -> tupleElement(x, 3), arraySlice(prior, 1, 50))                                        AS avg50_full,
            arrayAvg(x -> tupleElement(x, 3), arraySlice(prior, 1, 10))                                        AS avg10_full,
            arrayAvg(x -> tupleElement(x, 2), arraySlice(prior, 1, 50))                                        AS avg50_same,
            arrayAvg(x -> tupleElement(x, 2), arraySlice(prior, 1, 10))                                        AS avg10_same
        FROM
        (
            SELECT
                d,
                sumIf(vol, minute_of_day <= 630) AS cum_1030,
                sum(vol)                          AS cum_close
            FROM
            (
                SELECT
                    toDate(toTimeZone(window_start, 'America/New_York'))      AS d,
                    toHour(toTimeZone(window_start, 'America/New_York')) * 60
                      + toMinute(toTimeZone(window_start, 'America/New_York')) AS minute_of_day,
                    max(toFloat64(volume))                                     AS vol
                FROM global_markets.delayed_stocks_minute_aggs
                WHERE ticker = 'AAPL'
                  AND window_start >= toDateTime(session_day - 100, 'America/New_York')
                  AND window_start <  toDateTime(session_day + 1, 'America/New_York')
                GROUP BY d, minute_of_day
                HAVING minute_of_day >= 570 AND minute_of_day <= 960
            )
            GROUP BY d
        )
        HAVING length(prior) >= 63 AND session_close > 0
    )
)
ORDER BY tupleElement(r, 4)
Run this yourself

At 10:30 a.m. the screener-style row, full-day basis against 63 sessions, read 0.38. The chart-style row, same-time basis against 10 sessions, read 1.92 at the same minute. By the close the same two rows read 1.54 and 1.87, and what remains between them is only the lookback: each same-time row collapses onto its full-day twin the moment the session ends. Down the 10:30 column the basis dominates; down the close column only the lookback is left. The 10-session full-day row here will not match the lookback panel exactly: that panel uses consolidated daily volume, extended hours included, while these rows use regular-session bars only.

Which definition fits which job

A rule that survives the table: match the basis to the moment, and the lookback to the question.

Pre-market gap screening happens before any regular-session bar exists, so a same-time basis has nothing to compare against unless the platform keeps a pre-market-specific average, and none of the documented definitions above describes one. A full-day basis over a long lookback is the practical form here, with one caveat: the number is not comparable with 1.0. It ranks candidates against each other, and its meaning shifts minute by minute as more of the day fills in.

Intraday confirmation, asking whether a move at 10:15 a.m. is being met with unusual participation right now, is the job for the same-time basis over a short lookback of 10 to 20 sessions. Its reading is comparable with 1.0 at any minute, so a threshold set at the open still means the same thing at lunch.

After the close, the basis no longer matters and the only decision left is the lookback. A 10-session average answers "unusual versus the last two weeks"; a 63-session average answers "unusual versus the quarter". The weekly unusual volume screen uses the completed-session form for that reason.

Whichever definition a platform uses, the reading is only interpretable next to its formula. A screen that shows RVOL without its basis and lookback cannot be read against 1.0, or against another platform.

FAQ

Why is relative volume different on Finviz and TradingView?

Finviz documents a three-month average with an unpublished intraday adjustment. TradingView's Relative Volume field documents a 10-bar simple moving average, and its Relative Volume at Time indicator compares cumulative volume with the same clock time over past sessions. Different lookbacks, and during the session a different basis, so the two rarely print the same figure for the same stock.

What is a good relative volume number?

It depends on the definition. A same-time reading is comparable with 1.0 at any minute: 2.0 means twice the volume a typical session had printed by this time. A full-day reading starts near zero at the open and rises through the day, so 0.4 at 10:00 a.m. can be an ordinary open. Any threshold has to be set against the specific formula the screen uses.

Does relative volume include pre-market volume?

It depends on the platform's session template. Consolidated daily volume, the kind a full-day screener averages, includes pre-market and after-hours prints. A chart set to regular hours excludes them from both sides of the ratio. The intraday panels above use regular-session one-minute bars only, including the 4:00 p.m. bar where the closing auction prints.

Why does relative volume look low right after the open?

On a full-day basis it always does: only a small fraction of a typical day has printed by 9:45 a.m., so volume so far divided by a whole normal day is small even on a busy open. A same-time basis removes that effect by comparing the first fifteen minutes with the first fifteen minutes of past sessions.


Every panel above carries its SQL beneath it; swap the ticker or the session date and the same arithmetic runs on any name. To compute relative volume on a definition of your own, ask the question in plain English on the Strasmore terminal.

#relative volume#rvol#finviz#thinkorswim#tradingview#intraday volume