Volume Spread Analysis (VSA), the framework popularised by Tom Williams out of Richard Wyckoff’s work, rests on one deceptively simple question: is the effort matching the result?
Effort is the volume traded on a bar. Result is how far price actually travelled, the spread of the bar. When a lot of effort produces little result, someone is absorbing the move. When little effort produces a large result, the path of least resistance is wide open. Everything else in VSA is vocabulary built on top of that comparison.
This is a ProBuilder port of TradingIQ’s “Volume Spread Analysis IQ”. The original is a single TradingView indicator that draws both in a sub-panel and, via force_overlay, on the price chart. ProRealTime has no force_overlay, so the port is delivered as two indicators that work together:
Both share the same classification engine. You can run either on its own, or both at once for the full picture.
A raw volume number is meaningless on its own: ten thousand contracts is huge on one instrument and trivial on another. The fix is to rank it. For every bar the indicator looks back over a rolling window of 100 bars and asks what fraction of those bars had a volume less than or equal to this one:
volRank = (count of last 100 bars with volume <= current volume) / 100 * 100
That gives a percentile from 0 to 100. The same operation is applied to the spread (the bar range, high - low) to get spreadRank. Effort is the volume percentile, Result is the spread percentile.
Each percentile is bucketed into a decile from 1 to 10: above 90 becomes 9, above 80 becomes 8, and the very top (the current bar is the heaviest or widest in the window) maps to 10. Effort and Result now share a small integer scale and can be compared at a glance. Effort 10 is climactic volume, 1 is near-dead activity; Result 10 is an ultra-wide bar, 1 an ultra-narrow one.
Once both are on the same scale, the gap carries the VSA message:
The panel marks this explicitly: when the two deciles differ by 3 or more it drops a coloured dot on the zero line, and two moving averages (one over Effort, one over Result) show the regime rather than the bar-to-bar flicker.
The signals piece adds one more layer: a running strength-versus-weakness score. The original builds it from a long chain of string-labelled states, but that score only ever receives points from six of those states, and those six collapse cleanly into four bar types (strong or moderate up, strong or moderate down) crossed with the effort level:
bull points: high effort + strong up = 3, normal + (strong or moderate up) = 2, low effort + strong up = 1
bear points: high effort + strong dn = 3, normal + (strong or moderate dn) = 2, low effort + strong dn = 1
Everything else scores zero. Summing these over a lookback window, with a small trend bonus, gives a netScore that drives the candle colour (red for net weakness, teal for net strength) and gates two of the patterns. This reduction is exact, not an approximation, which is what makes the port faithful without dragging the string machinery into ProBuilder.
Shared by both parts:
lookback = 100: the rolling window for the volume and spread percentile ranks.Effort vs Result panel:
maLength = 14: length of the Effort and Result moving averages.maType = 0: 0 = SMA, 1 = EMA, 2 = WMA, 3 = Wilder (RMA). The original defaults to SMA.VSA Signals:
bgLookback = 10: how many bars the background score sums over. Larger is steadier.bgThresh = 2.0: how strong the net score must be before the background counts as strong or weak (this gates Upthrust and Shakeout).pivLen = 3: pivot strength of the internal trend proxy that replaces the original’s library ZigZag.colorCandles = 1: set to 0 to keep native candle colours and show only the pattern marks.
//--------------------------------------------
// PRC_Volume Spread Analysis IQ - Effort vs Result (by TradingIQ)
// version = 0
// 07.07.2026
// Iván González @ www.prorealcode.com
// Sharing ProRealTime knowledge
//--------------------------------------------
// --- Inputs ---
lookback = 100 // Percentile window (bars)
maLength = 14 // MA length for Effort / Result
maType = 0 // 0=SMA, 1=EMA, 2=WMA, ...
//--------------------------------------------
// --- Effort (volume) and Result (spread) percentile rank over lookback ---
myLb = min(lookback - 1, barindex)
denom = myLb + 1
barSpread = high - low
volCount = 0
sprCount = 0
FOR i = 0 TO myLb DO
IF volume[i] <= volume THEN
volCount = volCount + 1
ENDIF
IF (high[i] - low[i]) <= barSpread THEN
sprCount = sprCount + 1
ENDIF
NEXT
volRank = volCount * 100 / denom
spreadRank = sprCount * 100 / denom
//--------------------------------------------
// --- Map percentile 0..100 to 1..10 deciles ---
IF volRank >= 100 THEN
effortRankEasy = 10
ELSIF volRank > 90 THEN
effortRankEasy = 9
ELSIF volRank > 80 THEN
effortRankEasy = 8
ELSIF volRank > 70 THEN
effortRankEasy = 7
ELSIF volRank > 60 THEN
effortRankEasy = 6
ELSIF volRank > 50 THEN
effortRankEasy = 5
ELSIF volRank > 40 THEN
effortRankEasy = 4
ELSIF volRank > 30 THEN
effortRankEasy = 3
ELSIF volRank > 20 THEN
effortRankEasy = 2
ELSE
effortRankEasy = 1
ENDIF
IF spreadRank >= 100 THEN
spreadRankEasy = 10
ELSIF spreadRank > 90 THEN
spreadRankEasy = 9
ELSIF spreadRank > 80 THEN
spreadRankEasy = 8
ELSIF spreadRank > 70 THEN
spreadRankEasy = 7
ELSIF spreadRank > 60 THEN
spreadRankEasy = 6
ELSIF spreadRank > 50 THEN
spreadRankEasy = 5
ELSIF spreadRank > 40 THEN
spreadRankEasy = 4
ELSIF spreadRank > 30 THEN
spreadRankEasy = 3
ELSIF spreadRank > 20 THEN
spreadRankEasy = 2
ELSE
spreadRankEasy = 1
ENDIF
//--------------------------------------------
// --- Moving averages of Effort and Result ---
effortMA = average[maLength, maType](effortRankEasy)
resultMA = average[maLength, maType](spreadRankEasy)
//--------------------------------------------
// --- Effort vs Result divergence (gap >= 3 deciles) ---
gap = abs(spreadRankEasy - effortRankEasy)
IF gap >= 3 THEN
divMark = 0
IF spreadRankEasy > effortRankEasy THEN
dR = 250
dG = 161
dB = 164
ELSE
dR = 91
dG = 156
dB = 246
ENDIF
ELSE
divMark = undefined
dR = 0
dG = 0
dB = 0
ENDIF
//--------------------------------------------
RETURN effortRankEasy AS "Effort" coloured(255, 158, 249) style(point), spreadRankEasy AS "Result" coloured(233, 226, 255) style(point), effortMA AS "Effort MA" coloured(255, 158, 249) style(line), resultMA AS "Result MA" coloured(233, 226, 255) style(line), divMark AS "Divergence" coloured(dR, dG, dB) style(point)
//--------------------------------------------
// PRC_Volume Spread Analysis IQ - Signals (by TradingIQ)
// version = 0
// 07.07.2026
// Iván González @ www.prorealcode.com
// Sharing ProRealTime knowledge
//--------------------------------------------
// --- Inputs ---
lookback = 100 // percentile window (bars)
bgLookback = 10 // VSA background window (Medium)
bgThresh = 2.0 // background strong/weak threshold
pivLen = 3 // trend zigzag pivot strength
colorCandles = 1 // 1 = recolor candles by background bias
// --- Percentile thresholds (Standard sense) ---
highVolMin = 70
lowVolMax = 30
spreadWideMin = 70
spreadNormalMin = 30
closeThresh = 0.60
rejWickThresh = 0.45
rejBodyMax = 0.45
//--------------------------------------------
// --- Percentile ranks: volume = effort, spread = result ---
myLb = min(lookback - 1, barindex)
denom = myLb + 1
barSpread = high - low
IF barSpread = 0 THEN
barSpread = 0.0000001
ENDIF
volCount = 0
sprCount = 0
FOR i = 0 TO myLb DO
IF volume[i] <= volume THEN
volCount = volCount + 1
ENDIF
IF (high[i] - low[i]) <= barSpread THEN
sprCount = sprCount + 1
ENDIF
NEXT
volRank = volCount * 100 / denom
spreadRank = sprCount * 100 / denom
//--------------------------------------------
// --- Effort class: high / normal / low ---
effortHigh = volRank >= highVolMin
effortLow = volRank < lowVolMax
effortNormal = effortHigh = 0 AND effortLow = 0
//--------------------------------------------
// --- Spread class flags ---
spreadWideOrUltra = spreadRank >= spreadWideMin
spreadNormalExact = spreadRank >= spreadNormalMin AND spreadRank < spreadWideMin
narrowSpread = spreadRank < spreadNormalMin
spreadOk = spreadRank >= spreadNormalMin
//--------------------------------------------
// --- Close position: high / mid / low ---
R = ((close - low) - (high - close)) / barSpread
closeHigh = R >= closeThresh
closeLow = R <= (0 - closeThresh)
closeMid = closeHigh = 0 AND closeLow = 0
//--------------------------------------------
// --- Bar direction ---
upBar = close > close[1]
downBar = close < close[1]
//--------------------------------------------
// --- Rejection shapes (for upthrust / shakeout) ---
bodyv = abs(close - open)
upWick = high - max(open, close)
loWick = min(open, close) - low
bodyRatio = bodyv / barSpread
upWickRatio = upWick / barSpread
loWickRatio = loWick / barSpread
upRej = upWickRatio >= rejWickThresh AND upWickRatio > loWickRatio AND bodyRatio <= rejBodyMax
lowRej = loWickRatio >= rejWickThresh AND loWickRatio > upWickRatio AND bodyRatio <= rejBodyMax
//--------------------------------------------
// --- Trend proxy: pivot zigzag direction (replaces IQ.IQZZsimple) ---
pivRange = 2 * pivLen + 1
isPH = 0
isPL = 0
IF barindex >= pivRange THEN
IF high[pivLen] = highest[pivRange](high) THEN
isPH = 1
ENDIF
IF low[pivLen] = lowest[pivRange](low) THEN
isPL = 1
ENDIF
ENDIF
once direction = 0
IF isPL THEN
direction = 1
ENDIF
IF isPH THEN
direction = -1
ENDIF
//--------------------------------------------
// --- Result sub-cases feeding the background score (exact reduction) ---
strongUp = upBar AND closeHigh AND spreadWideOrUltra
modUp = upBar AND closeHigh AND spreadNormalExact
strongDn = downBar AND closeLow AND spreadWideOrUltra
modDn = downBar AND closeLow AND spreadNormalExact
//--------------------------------------------
// --- Bull / bear score per bar ---
bullScore = 0
IF effortHigh AND strongUp THEN
bullScore = 3
ELSIF effortLow AND strongUp THEN
bullScore = 1
ELSIF effortNormal AND (strongUp OR modUp) THEN
bullScore = 2
ENDIF
bearScore = 0
IF effortHigh AND strongDn THEN
bearScore = 3
ELSIF effortLow AND strongDn THEN
bearScore = 1
ELSIF effortNormal AND (strongDn OR modDn) THEN
bearScore = 2
ENDIF
//--------------------------------------------
// --- Background net score over bgLookback + trend rule ---
strengthScore = summation[bgLookback](bullScore)
weaknessScore = summation[bgLookback](bearScore)
IF direction = 1 THEN
strengthScore = strengthScore + 5
ELSE
weaknessScore = weaknessScore + 5
ENDIF
netScore = strengthScore - weaknessScore
strongBg = netScore > bgThresh
weakBg = netScore < (0 - bgThresh)
//--------------------------------------------
// --- Stopping volume climax timer ---
logret = log(close / close[3])
meanlr = average[20](logret)
stdlr = std[20](logret)
IF stdlr = 0 THEN
stdlr = 0.0000001
ENDIF
zmove = (logret - meanlr) / stdlr
once svBar = -1000
IF zmove <= -2 THEN
svBar = barindex
ENDIF
stopVolActive = barindex - svBar <= 5
//--------------------------------------------
// --- VSA pattern signals ---
volLessP2 = volume < volume[1] AND volume < volume[2]
lowVolVsa = effortLow OR volLessP2
noDemand = upBar AND (lowVolVsa OR volLessP2) AND narrowSpread AND closeMid
noSupply = downBar AND (lowVolVsa OR volLessP2) AND narrowSpread AND closeMid
noDemandConfirmed = noDemand[1] AND close < close[1]
noSupplyConfirmed = noSupply[1] AND close > close[1]
upthrust = upRej AND closeMid AND spreadOk AND weakBg
shakeout = lowRej AND (closeMid OR closeHigh) AND spreadOk AND strongBg
upthrustSig = upthrust[1] AND downBar AND close < close[1]
shakeoutSig = shakeout[1] AND upBar AND close > close[1]
stoppingVolume = downBar AND effortHigh AND (narrowSpread OR spreadNormalExact) AND closeMid AND stopVolActive
//--------------------------------------------
// --- Drawing (overlay) ---
atr14 = averagetruerange[14](close)
off = atr14 * 0.5
// Recolor candles by background bias (red -> teal gradient on netScore)
IF colorCandles = 1 THEN
tcol = (netScore + 10) / 20
IF tcol > 1 THEN
tcol = 1
ENDIF
IF tcol < 0 THEN
tcol = 0
ENDIF
cr = round(255 + tcol * (85 - 255))
cg = round(116 + tcol * (255 - 116))
cb = round(116 + tcol * (218 - 116))
drawcandle(open, high, low, close) coloured(cr, cg, cb)
ENDIF
// Signal markers (one per event, on the signalling bar)
IF noDemandConfirmed THEN
drawtext("ND", barindex - 1, high[1] + off) coloured(255, 116, 116)
ENDIF
IF noSupplyConfirmed THEN
drawtext("NS", barindex - 1, low[1] - off) coloured(85, 255, 218)
ENDIF
IF upthrustSig THEN
drawtext("UT", barindex - 1, high[1] + off) coloured(255, 116, 116)
ENDIF
IF shakeoutSig THEN
drawtext("SO", barindex - 1, low[1] - off) coloured(85, 255, 218)
ENDIF
IF stoppingVolume THEN
IF strongBg THEN
stvR = 85
stvG = 255
stvB = 218
ELSE
stvR = 255
stvG = 116
stvB = 116
ENDIF
drawtext("STV", barindex, low - off) coloured(stvR, stvG, stvB)
ENDIF
//--------------------------------------------
RETURN