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.

graph TD A[Bar Executes] --> B[Read volume, open, close] B --> C[ta.sma volume smaLength] C --> D{volSma is na or zero?} D -- Yes --> E[relVol = na] D -- No --> F[relVol = volume divided by volSma] F --> G{relVol >= spikeMulti?} G -- Yes --> H[isSpike = true] G -- No --> I[isSpike = false] B --> J{close >= open?} J -- Yes --> K[isBullish = true] J -- No --> L[isBullish = false] H --> M{isBullish?} M -- Yes --> N[barColor = Lime] M -- No --> O[barColor = Red] I --> P{isBullish?} P -- Yes --> Q[barColor = Teal] P -- No --> R[barColor = Maroon] N --> S[plot volume histogram] O --> S Q --> S R --> S H --> T[plotshape at volume times 1.05] I --> U[plotshape series = na skip]

3. Indicator Architecture

The indicator is structured into four logical layers:

  1. Inputs: Lookback period and spike multiplier, both sourced from input.int() and input.float().
  2. Calculations: SMA of volume, relative volume ratio, and bar direction detection.
  3. Visualization: Color-coded volume histogram via plot() with plot.style_histogram, and spike markers via plotshape() with location.absolute.
  4. 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.
$$\text{relVol} = \frac{V_t}{\frac{1}{n}\sum_{i=0}^{n-1} V_{t-i}}$$

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() with location.absolute requires a numeric series argument — passing a bool directly causes CE10123. The correct pattern is isSpike ? volume * 1.05 : na, which provides a valid float coordinate for the marker.

Ideas for Further Development

  1. Volume-Weighted Average Price (VWAP) Integration: Add a VWAP line to the price chart (using force_overlay = true on a separate-pane indicator, or switching to overlay = true) to correlate volume spikes with VWAP deviations.
  2. 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.

Related Posts

Comments

Popular posts from this blog

Pine Script v6 plot() Function: Complete Guide to Lines, Histograms, and Circles

SuperTrend Indicator in Pine Script v6: Canonical Formula, Flip Logic, and Non-Repainting Signals