How to Build a Volume Indicator in Pine Script v6: Buying & Selling Pressure Analysis
Volume is one of the most fundamental data points in technical analysis — it tells you how much was traded, not just at what price. In this post, you will learn how to build a complete volume indicator in Pine Script v6 that color-codes bars by buying or selling pressure, detects volume spikes using a dynamic SMA baseline, and annotates those spikes directly on the chart using plotshape() with location.absolute.
1. What Is Volume and Why Does It Matter?
Volume represents the total number of contracts or shares exchanged during a given bar. When price moves on high volume, the move is considered more significant. When price moves on low volume, it may lack conviction. The key analytical concepts are:
- Buying Pressure: Close price is above the open price — bulls dominated the bar.
- Selling Pressure: Close price is below the open price — bears dominated the bar.
- Volume Spike: Current volume exceeds a rolling average by a configurable multiplier, signaling unusual activity.
2. Mathematical Foundation
The core calculation is a relative volume ratio:
$$\text{relVol} = \frac{\text{volume}}{\text{SMA}(\text{volume},\ n)}$$Where $n$ is the lookback period for the moving average. A value of $\text{relVol} > k$ (where $k$ is a user-defined spike threshold, e.g., 2.0) indicates a volume spike. The SMA is computed using ta.sma(), whose length parameter accepts an int value — and because ta.sma() natively supports dynamic (series) lengths, passing an input int (which is a weaker qualifier than series) is perfectly valid and satisfies the parameter requirement without any type mismatch.
3. Indicator Architecture
The indicator is structured into four logical layers:
- Inputs: Lookback period and spike multiplier, both sourced from
input.int()andinput.float(). - Calculations: SMA of volume, relative volume ratio, and bar direction detection.
- Visualization: Color-coded volume histogram via
plot()withplot.style_histogram, and spike markers viaplotshape()withlocation.absolute. - Reference Line: A constant SMA baseline drawn with
plot().
4. Full Pine Script v6 Code
The script below implements all four layers. Pay close attention to the plotshape() call: because we use location = location.absolute, the first argument must be a numeric price value (not a bool). We pass isSpike ? volume * 1.05 : na to position the marker just above the spike bar's volume column.
🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator(
title = "Volume Pressure Analyzer",
shorttitle = "VolPress",
overlay = false, // Runs in a separate pane — volume scale
max_bars_back = 500
)
// ─────────────────────────────────────────────
// SECTION 1: USER INPUTS
// ─────────────────────────────────────────────
// Lookback period for the volume SMA baseline.
// input.int() returns an `input int` qualifier.
// ta.sma() accepts a dynamic (series) int length, so `input int`
// satisfies the parameter with no type mismatch.
int smaLength = input.int(20, title = "SMA Length", minval = 2)
// Spike threshold: volume must exceed SMA × this multiplier.
float spikeMulti = input.float(2.0, title = "Spike Multiplier", minval = 1.0, step = 0.1)
// ─────────────────────────────────────────────
// SECTION 2: CORE CALCULATIONS
// ─────────────────────────────────────────────
// Rolling average of volume over `smaLength` bars.
// ta.sma() length parameter is int and supports dynamic (series) values;
// here we pass `input int` which is a weaker qualifier — fully legal.
float volSma = ta.sma(volume, smaLength)
// Relative volume: ratio of current volume to its rolling average.
// Guard against division by zero or na SMA on early bars.
float relVol = (not na(volSma) and volSma != 0.0) ? volume / volSma : na
// Detect buying pressure: close >= open on this bar.
bool isBullish = close >= open
// Detect a volume spike: relVol exceeds the user-defined multiplier.
bool isSpike = not na(relVol) and relVol >= spikeMulti
// ─────────────────────────────────────────────
// SECTION 3: COLOR LOGIC
// ─────────────────────────────────────────────
// Spike bars get a bright highlight; normal bars use standard bull/bear colors.
color barColor =
isSpike and isBullish ? color.new(color.lime, 0) :
isSpike and not isBullish ? color.new(color.red, 0) :
isBullish ? color.new(color.teal, 30) :
color.new(color.maroon, 30)
// ─────────────────────────────────────────────
// SECTION 4: PLOTTING
// ─────────────────────────────────────────────
// Plot the raw volume as a histogram, color-coded by pressure and spike status.
plot(
series = volume,
title = "Volume",
color = barColor,
style = plot.style_histogram,
linewidth = 4
)
// Plot the SMA baseline as a thin line for visual reference.
plot(
series = volSma,
title = "Volume SMA",
color = color.new(color.yellow, 20),
style = plot.style_line,
linewidth = 1
)
// ─────────────────────────────────────────────
// SECTION 5: SPIKE ANNOTATION
// ─────────────────────────────────────────────
// plotshape() with location.absolute requires a NUMERIC series, not a bool.
// We pass `volume * 1.05` when a spike is detected, or `na` otherwise.
// This positions the marker 5% above the top of the spike bar's column.
plotshape(
series = isSpike ? volume * 1.05 : na, // numeric price — required for location.absolute
title = "Volume Spike",
style = shape.labeldown,
location = location.absolute,
color = color.new(color.orange, 0),
text = "SPIKE",
textcolor = color.white,
size = size.small
)
5. Understanding the plotshape() Constraint with location.absolute
This is one of the most common compile errors beginners encounter. In Pine Script v6, plotshape() with location = location.absolute requires the series argument to be a numeric value (float or int), because the function needs an actual price coordinate to place the shape on the scale. Passing a bool directly causes CE10123 (cannot cast bool to float).
| Pattern | location Parameter | series Argument | Result |
|---|---|---|---|
plotshape(isSpike, location = location.abovebar) |
abovebar | bool ✅ | Legal — bool accepted |
plotshape(isSpike, location = location.absolute) |
absolute | bool ❌ | CE10123 — bool cannot cast to float |
plotshape(isSpike ? volume * 1.05 : na, location = location.absolute) |
absolute | float ✅ | Legal — numeric coordinate provided |
6. ta.sma() Length Parameter: Type and Qualifier Details
A common source of confusion is the distinction between a function's type and its qualifier. For ta.sma(), the length parameter is typed as int and supports dynamic (series) values — meaning you can pass a length that changes bar-by-bar. This is in contrast to functions like ta.ema() or ta.rsi(), which require a simple int (a value fixed at script initialization).
In our script, smaLength is declared as int smaLength = input.int(20, ...), which gives it an input int qualifier. Because the qualifier hierarchy runs const → input → simple → series (from weakest to strongest), an input int satisfies any parameter that accepts input, simple, or series int. Passing it to ta.sma() is therefore perfectly legal and matches the v6 Reference Manual exactly.
| Function | length Qualifier | Dynamic (series int) Length? | input int Length? |
|---|---|---|---|
ta.sma() |
series int | ✅ Yes | ✅ Yes |
ta.ema() |
simple int | ❌ No — compile error | ✅ Yes |
ta.rsi() |
simple int | ❌ No — compile error | ✅ Yes |
ta.atr() |
simple int | ❌ No — compile error | ✅ Yes |
7. Interpreting Volume Spikes in Context
A volume spike alone is not a signal — it is a context amplifier. The table below summarizes how to interpret spike bars in combination with price action:
| Spike Color | Bar Direction | Interpretation |
|---|---|---|
| 🟢 Lime | Bullish (close ≥ open) | Strong buying pressure — potential breakout or continuation |
| 🔴 Red | Bearish (close < open) | Strong selling pressure — potential breakdown or reversal |
| 🟩 Teal | Bullish, normal volume | Ordinary buying — no unusual activity |
| 🟫 Maroon | Bearish, normal volume | Ordinary selling — no unusual activity |
8. Relative Volume Formula Revisited
The relative volume ratio $\text{relVol}$ is dimensionless — it normalizes raw volume against its own historical average. This makes the spike threshold ($k$) universally applicable across different assets and timeframes, regardless of their absolute volume scale. For example:
- A stock trading 10 million shares/day and a crypto trading 50,000 BTC/day both use the same $k = 2.0$ threshold.
- $\text{relVol} = 1.0$ means volume is exactly at its 20-bar average.
- $\text{relVol} = 3.5$ means volume is 3.5× its average — a significant spike.
Where $V_t$ is the current bar's volume and $n$ is the SMA lookback period.
🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator("Relative Volume Spike Detector", overlay=true)
// Inputs
int volLen = input.int(20, "Volume Average Length", minval=1)
float spikeMulti = input.float(2.0, "Relative Volume Spike Multiplier", step=0.1)
// Relative Volume
float avgVol = ta.sma(volume, volLen)
float relVol = volume / avgVol
// Spike Detection
bool spike = relVol >= spikeMulti
// Plot signals
plotshape(
spike,
title="Volume Spike",
style=shape.triangleup,
location=location.belowbar,
color=color.lime,
size=size.small,
text="RVOL"
)
// Background highlight
bgcolor(spike ? color.new(color.green, 90) : na)
// Display relVol value
if spike
label.new(
bar_index,
low,
"relVol: " + str.tostring(relVol, "#.##"),
style=label.style_label_up,
color=color.green,
textcolor=color.white,
size=size.tiny
)
// Plot relVol in data window
plot(relVol, title="Relative Volume", color=color.orange, display=display.data_window)
plot(spikeMulti, title="Spike Threshold", color=color.red, display=display.data_window)
9. Conclusion
- Color-coded histograms provide an immediate visual distinction between buying pressure (teal/lime) and selling pressure (maroon/red), with spike bars highlighted in saturated colors.
- The relative volume ratio ($\text{relVol} = V / \text{SMA}(V, n)$) normalizes volume across assets and timeframes, making the spike threshold universally configurable via a single multiplier input.
- Using
plotshape()withlocation.absoluterequires a numeric series argument — passing abooldirectly causes CE10123. The correct pattern isisSpike ? volume * 1.05 : na, which provides a valid float coordinate for the marker.
Ideas for Further Development
- Volume-Weighted Average Price (VWAP) Integration: Add a VWAP line to the price chart (using
force_overlay = trueon a separate-pane indicator, or switching tooverlay = true) to correlate volume spikes with VWAP deviations. - Adaptive SMA Length: Since
ta.sma()accepts a dynamic (series int) length, you could compute the SMA period adaptively — for example, using a volatility measure like ATR to shorten the window during high-volatility regimes and lengthen it during quiet periods, creating a self-adjusting volume baseline.
Comments
Post a Comment