How do I build a relative volume (or premarket volume) watchlist column in thinkorswim?
Updated August 27, 2026
Short answer
Divide today's volume by the average volume of the previous N days, offset by one bar so today doesn't pollute its own average, and plot the result: def avgVol = Average(volume, 20)[1]; plot RVol = volume / avgVol; Set the column's aggregation button to D so volume means daily volume, then color it with AssignBackgroundColor. The premarket version is harder because it needs an intraday aggregation, and intraday columns don't load enough history to average premarket volume over twenty days.
Set the column up first, because the aggregation button is where this goes wrong
Two ways in. In MarketWatch > Quotes, right-click any column header and pick Customize. Or from a sidebar watchlist, click the gear and pick Customize. Either way you get a long list of available columns. Scroll to the bottom and you’ll find entries called Custom 1 through Custom 19. Click the little script icon next to one, then the thinkScript Editor tab. Give it a name, paste, Apply, OK, then drag it into Current Set.
Now the part nearly everyone skips. In that same editor, next to the name field, there’s a small button that says D. That’s the aggregation period for this column, and it decides what your script even means. On a D column, volume is today’s daily volume. On a 5m column, volume is the volume of the most recent five minute bar. Same word, wildly different number, no error message either way. If your relative volume column is showing 0.03 for everything, that’s why.
For a normal RVOL column you want it left on D.
The script
# Relative volume watchlist column. Set the column aggregation button to D.
input length = 20;
# The [1] is the line people get wrong. Without it, today's volume is
# inside the average it's being compared to, which flattens the number.
def avgVol = Average(volume, length)[1];
plot RVol = volume / avgVol;
RVol.AssignValueColor(Color.BLACK);
AssignBackgroundColor(
if IsNaN(RVol) then Color.GRAY
else if RVol >= 3 then Color.DARK_GREEN
else if RVol >= 2 then Color.GREEN
else if RVol >= 1.5 then Color.LIGHT_GREEN
else if RVol >= 1 then Color.YELLOW
else Color.GRAY);
That’s the whole thing. A stock printing 2.4 has done 2.4 times its normal 20 day volume so far today. Early in the session the number is small for everybody, because it’s comparing a partial day against full days. That’s not a bug, it’s just what the ratio is. If you want it corrected for time of day you need a per-bar-of-day average, which is a much bigger script and doesn’t fit in a column without trouble.
Three things about that code worth saying out loud.
IsNaN(RVol) catches symbols with fewer than 21 days of history, new listings mostly. Without it you get an empty cell with a colour from whatever branch it fell into, which looks like a real reading.
Don’t write volume(period = AggregationPeriod.DAY). On a D column it’s redundant, and on any other column it’s a secondary aggregation, which watchlist columns flatly refuse. Half the RVOL columns floating around forums have that line in them and they only work because the person who posted it also had the column set to D. See the secondary aggregation period error for what that refusal actually looks like.
Decimals are controlled by the symbol’s tick size, not by you. thinkorswim renders plot values using the same number of decimals as tick size, so you’ll usually get two. If you want one, or you want a suffix like “x”, swap the plot for AddLabel(yes, AsText(RVol, NumberFormat.TWO_DECIMAL_PLACES) + "x"). The cost is that the column then sorts alphanumerically instead of numerically, so 10.4 sorts before 2.1. That trade catches people out constantly.
Colour choices are covered properly on the watchlist column colours page, including why the background colours but the text usually won’t.
The premarket version, and why it’s genuinely harder
Premarket volume is not a special data field. There’s no PreMarketVolume(). You have to accumulate it yourself from intraday bars, which means the column can’t be on D any more.
# Premarket volume column, in thousands.
# Set the column aggregation to an intraday period (5m works well) AND
# turn on the extended-hours option in that same aggregation popup.
input startTime = 0400;
input endTime = 0930;
def inPre = SecondsFromTime(startTime) >= 0 and SecondsTillTime(endTime) > 0;
def pmVol = if inPre and !inPre[1] then volume # first premarket bar, reset
else if inPre then pmVol[1] + volume # still premarket, add
else pmVol[1]; # after 9:30, hold the total
plot PMV = pmVol / 1000;
PMV.AssignValueColor(Color.BLACK);
AssignBackgroundColor(if PMV > 500 then Color.DARK_GREEN else Color.GRAY);
The else pmVol[1] branch is what keeps the number on screen at two in the afternoon instead of resetting to zero at the bell. It also means that before 4am the next morning, the column is still showing yesterday’s premarket total. Live with it or add a GetDay() check.
Now the honest part. Two things make this awkward and neither is your code’s fault.
First, extended hours has to be switched on for that column or every one of those bars simply doesn’t exist and you’ll get zero for everything. Click the aggregation button and look for the extended-hours option in the popup that opens. I’ve seen this reported consistently by people who have it working, but the wording moves around between platform versions, so find it on your own screen rather than trusting a screenshot from 2021.
Second, and this is the one nobody mentions: you can’t easily turn this into relative premarket volume. To say “this stock has done 4x its usual premarket volume” you’d need an average of premarket volume across the last N days, and that means reading N days of intraday bars. Intraday aggregations in a custom quote load a much shorter history than daily ones, so the bars you’d average over are frequently just not loaded. If you write it anyway, the loop that walks back through days is exactly the kind of thing that trips TooComplexException.
The workaround most people land on is two columns side by side. Raw premarket volume in one, ordinary daily RVOL in the other, and you eyeball both. Less elegant, works every day.
If the column comes up blank
Check the aggregation button before you check anything else. Then check that the script has a plot in it, because a script with only def lines produces an empty cell and colours nothing. Then hover the cell, because a red or grey cell with no number is usually an error and the tooltip will tell you which line. And if the number looks right on a chart but wrong in the column, that’s a scan and chart mismatch situation, same root cause, different symptom.