Intraday volume is not flat. The open and the close of a session are always busy, lunch hours are quiet, and a news release at a fixed time shows up every day at the same bar. Comparing the volume of a 10:30 bar with a moving average of the last 20 bars mixes all of that together, so a perfectly normal opening bar looks huge and a busy lunch bar looks small.
Volume by Time, by LuxAlgo, compares every bar only with the bars that opened at the same time of day in previous sessions. Each bar gets its own reference: what volume is normal at that exact time. On top of that, the reference is coloured by the direction that usually dominates at that time, so you can see at a glance whether a given hour tends to be driven by buyers or by sellers.
The indicator groups bars by their opening time (hours, minutes and seconds). For each time of day it keeps a history of volumes:
From that history it computes two numbers for the current bar:
The history can cover the whole chart or only the last N sessions (the Length parameter). With Length = 0 every past session is used.
The data box shows the volume, the reference and the number of sessions used for each bar.
The indicator is built for intraday charts. On daily or higher timeframes every bar has the same opening time, so the reference becomes a plain average of all bars.
//---------------------------------------------------------------
// PRC_Volume by Time
// version = 0
// 20.10.2026
// Iván González @ www.prorealcode.com
// Author: LuxAlgo
// Sharing ProRealTime knowledge
//--------------------------------------------------------------------//
// Settings
//--------------------------------------------------------------------//
avgType = 0 // Analysis type: 0 = Average, 1 = Median
lenDays = 0 // Length (days): values kept for each time of day, 0 = whole history
biDir = 0 // Bi-directional: 1 = bearish volume below zero, bullish volume above zero
// Average volume line colours
upR = 8
upG = 153
upB = 129
dnR = 242
dnG = 54
dnB = 69
// Volume colours (alpha 102 = 60% transparent)
upVR = 120
upVG = 123
upVB = 134
dnVR = 0
dnVG = 0
dnVB = 0
volA = 102
//--------------------------------------------------------------------//
// Volume of the bar, signed by its direction
//--------------------------------------------------------------------//
vol = volume
IF close > open THEN
sVol = vol
ELSE
sVol = 0 - vol
ENDIF
// Time of day of the bar, measured in seconds from the first bar of the session.
// opentime is in the platform time zone, not the exchange one: when the user's
// country and the exchange change to summer time on different dates, the whole
// session moves one hour on the user's clock. Elapsed time keeps the same bars
// together all year round.
hh = floor(opentime / 10000)
mm = floor(opentime / 100) - hh * 100
ss = opentime - floor(opentime / 100) * 100
todSec = hh * 3600 + mm * 60 + ss
ONCE startSec = -1
IF intradaybarindex = 0 AND barindex > 0 THEN
startSec = todSec
ENDIF
IF 0 > startSec THEN
// First session on the chart, usually loaded from the middle: its bars get
// keys of their own so they are not mixed with the rest of the sessions
tKey = 86400 + todSec
ELSE
tKey = todSec - startSec
IF 0 > tKey THEN
// Session that crosses midnight
tKey = tKey + 86400
ENDIF
ENDIF
//--------------------------------------------------------------------//
// Each bar is linked to the previous bar with the same time of day.
// Only pure assignments indexed by barindex: re-running the live bar on
// every tick rewrites the same values instead of adding them again.
//--------------------------------------------------------------------//
IF isset($lastB[tKey]) THEN
IF $lastB[tKey] = barindex THEN
prevB = $prevSame[barindex]
ELSE
prevB = $lastB[tKey]
ENDIF
ELSE
prevB = -1
ENDIF
$lastB[tKey] = barindex
$prevSame[barindex] = prevB
IF prevB < 0 THEN
// The first value of each time of day is stored unsigned, as in the original
$val[barindex] = vol
$cnt[barindex] = 1
$cumV[barindex] = vol
$cumA[barindex] = vol
$startB[barindex] = barindex
ELSE
$val[barindex] = sVol
$cnt[barindex] = $cnt[prevB] + 1
$cumV[barindex] = $cumV[prevB] + sVol
$cumA[barindex] = $cumA[prevB] + vol
$nextSame[prevB] = barindex
// Window full: the oldest value of this time of day leaves the window
IF lenDays > 0 AND $cnt[barindex] > lenDays THEN
// Read the start into a variable first: an array element used directly
// as the index of another array stops the indicator with an error
oldStart = $startB[prevB]
$startB[barindex] = $nextSame[oldStart]
ELSE
$startB[barindex] = $startB[prevB]
ENDIF
ENDIF
//--------------------------------------------------------------------//
// Average or median of the window (signed values and absolute values)
//--------------------------------------------------------------------//
sB = $startB[barindex]
winN = $cnt[barindex] - $cnt[sB] + 1
IF avgType = 0 THEN
rawAvg = ($cumV[barindex] - $cumV[sB] + $val[sB]) / winN
absAvg = ($cumA[barindex] - $cumA[sB] + abs($val[sB])) / winN
ELSE
FOR mPass = 1 TO 2 DO
// Copy the window: this bar and its predecessors at the same time of day
bW = barindex
FOR kW = 0 TO winN - 1 DO
IF mPass = 1 THEN
$wq[kW] = $val[bW]
ELSE
$wq[kW] = abs($val[bW])
ENDIF
bW = $prevSame[bW]
NEXT
// Wirth's selection: leaves the kSel-th smallest value at position kSel
kSel = floor(winN / 2)
loQ = 0
hiQ = winN - 1
WHILE loQ < hiQ DO
pvQ = $wq[kSel]
iQ = loQ
jQ2 = hiQ
WHILE iQ <= jQ2 DO
WHILE $wq[iQ] < pvQ DO
iQ = iQ + 1
WEND
WHILE pvQ < $wq[jQ2] DO
jQ2 = jQ2 - 1
WEND
IF iQ <= jQ2 THEN
tmpQ = $wq[iQ]
$wq[iQ] = $wq[jQ2]
$wq[jQ2] = tmpQ
iQ = iQ + 1
jQ2 = jQ2 - 1
ENDIF
WEND
IF jQ2 < kSel THEN
loQ = iQ
ENDIF
IF kSel < iQ THEN
hiQ = jQ2
ENDIF
WEND
medQ = $wq[kSel]
// Even count: average of the two middle values. Everything left of kSel is
// now smaller or equal, so the lower middle value is the largest of them.
IF winN MOD 2 = 0 THEN
lowQ = $wq[0]
IF kSel > 1 THEN
FOR kW = 1 TO kSel - 1 DO
lowQ = max(lowQ, $wq[kW])
NEXT
ENDIF
medQ = (medQ + lowQ) / 2
ENDIF
IF mPass = 1 THEN
rawAvg = medQ
ELSE
absAvg = medQ
ENDIF
NEXT
ENDIF
//--------------------------------------------------------------------//
// Colours and display
//--------------------------------------------------------------------//
IF rawAvg > 0 THEN
aR = upR
aG = upG
aB = upB
dirAvg = absAvg
ELSE
aR = dnR
aG = dnG
aB = dnB
dirAvg = 0 - absAvg
ENDIF
IF close > open THEN
vR = upVR
vG = upVG
vB = upVB
ELSE
vR = dnVR
vG = dnVG
vB = dnVB
ENDIF
IF biDir = 1 THEN
avgTop = dirAvg
volTop = sVol
ELSE
avgTop = absAvg
volTop = vol
ENDIF
// Volume: filled candle. Only one DRAWCANDLE per bar is displayed (the last
// one drawn), so the average volume is the coloured line of the RETURN
DRAWCANDLE(0, max(0, volTop), min(0, volTop), volTop) COLOURED(vR, vG, vB, volA) BORDERCOLOR(vR, vG, vB, volA)
// Volume and window length only for the data box (invisible lines)
RETURN volTop COLOURED(vR, vG, vB, 0) AS "Volume", avgTop COLOURED(aR, aG, aB, 255) STYLE(line, 2) AS "Average Volume", winN COLOURED(120, 123, 134, 0) AS "Avg Length"
Volume only means something against what is normal, and in intraday trading what is normal depends on the time of day. Volume by Time gives every bar its own benchmark and adds the dominant direction on top, which makes unusual participation much easier to spot.