TradingView indicator guides

Percentage Volume Oscillator: Formula, Histogram and Limits

The Percentage Volume Oscillator compares a fast volume EMA with a slow volume EMA. Its percentage scale removes a constant volume-unit multiplier. It does not identify buying or selling, and a positive PVO does not require the current volume bar to exceed either average.

Inspect the Pine Script

Three outputs, three different comparisons

F = EMA(volume, fast) L = EMA(volume, slow) PVO = 100 × (F − L) / L Signal = EMA(PVO, signalLength) Histogram = PVO − Signal

The denominator is the slow EMA, not whichever average currently has the larger numeric value. With a positive denominator, PVO above zero means F exceeds L. A positive histogram instead means PVO exceeds its own signal. It can occur while PVO is negative: PVO −2 and signal −3 produce a histogram of +1 percentage point.

The StockCharts PVO reference documents the 12/26/9 formula. These defaults describe a calculation, not a demonstrated optimum. The supplied code uses fixed 12/26/9 arguments; it has no length inputs in Settings.

A volume spike does not stay positive forever

For a short calculation, use EMA lengths 3 and 5 and signal length 3. Start from a long constant-volume history of 100, so both volume EMAs equal 100 and PVO and signal equal zero. These are synthetic inputs, not a backtest; the copyable script below retains 12/26/9.

Next volumeFast EMASlow EMAPVOSignalHistogram
200150133.33333312.5000006.2500006.250000
100125122.2222222.2727274.261364−1.988636
100112.5114.814815−2.0161291.122617−3.138746

On the second row, current volume is below both EMAs, yet PVO remains positive. The histogram is already negative. On the third row, volume is unchanged from the preceding bar but PVO falls below zero. Thus neither the PVO sign nor histogram sign is a direct label for the latest bar's volume change.

The same volume sequence can accompany rising, falling or flat prices: price is absent from this formula. A histogram color therefore cannot identify buying pressure, selling pressure or the direction of the next price move.

Read the four colors exactly

For histogram H, the code uses dark green when H is nonnegative and greater than its prior value; otherwise nonnegative H is light green. Negative H is light red when it rises toward zero and dark red otherwise. An unchanged positive histogram is light green; an unchanged negative histogram is dark red. Zero belongs to the nonnegative branch, although a zero-height column may not be visible.

These colors describe histogram level and change, not a calibrated acceleration or a trade instruction. For example, H rising from −4 to −2 remains below zero but uses light red. A positive histogram shrinking from 4 to 2 uses light green. Missing preceding H does not establish a rising comparison.

The script has no crossover markers, alert conditions, divergence detector, multi-timeframe request or orders. Its curves and colors can update on an open bar; it does not wait for bar-close confirmation.

Normalization does not make feeds interchangeable

Multiplying every volume value by a positive constant multiplies both volume EMAs by that constant, leaving PVO unchanged. Adding a constant is different: F = 120 and L = 100 give 20%, while F = 220 and L = 200 give 10%. The normalized number is a relative EMA gap, not a percentile, liquidity score or risk measure.

Check what the feed calls volume. TradingView documents unavailable volume and tick-volume cases in its volume-data guidance. A crypto venue is not automatically a complete market-wide tape. Comparing different session lengths or an unfinished bar with a completed bar can also change the meaning of the comparison.

The supplied helper applies built-in EMAs directly to volume. Missing source values follow the built-in EMA's skip-missing behavior; they are not filled with zero. A reported zero is numeric and affects the averages. A zero slow EMA makes the ratio undefined. The code does not flag missing feed coverage, reset after every gap or enforce a minimum positive denominator.

Lengths describe smoothing, not a finite data window

An EMA retains diminishing influence from older observations. A 12-bar EMA is not an exact rolling sum of the last 12 bars. On a five-minute chart, twelve bar intervals span an hour, but the EMA can still reflect earlier data. Changing both lengths while preserving their ratio also changes the recurrence coefficients and output.

To compare a modified script, hold the symbol, feed, session, chart interval and loaded history fixed. Check F and L before the ratio, then signal and histogram. Test a constant positive-volume sequence, a single spike, repeated zeros and a missing-value gap. Do not adjust parameters merely to hide an implementation mismatch.

OBV signs volume using close-to-close direction; PVO does not use close at all. CMF weights volume by the close's position within a bar's range. None of these constructions identifies institutional orders. Any proposed price-breakout filter needs its own event definition, execution costs and out-of-sample test; this page reports no measured win rate or return.

Pine Script v6: implementation and limits

The calculation and plots retain the fixed 12/26/9 example. Its time-range parsing helper is unused and does not filter the volume series. Compare the three plotted values on closed bars before using them in another script.

Pine Script v6
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pineify

//======================================================================//
//                    ____  _            _  __                          //
//                   |  _ \(_)_ __   ___(_)/ _|_   _                    //
//                   | |_) | | '_ \ / _ \ | |_| | | |                   //
//                   |  __/| | | | |  __/ |  _| |_| |                   //
//                   |_|   |_|_| |_|\___|_|_|  \__, |                   //
//                                             |___/                    //
//======================================================================//

//@version=6
indicator(title="Percentage Volume Oscillator", overlay=false, max_labels_count=500)

// Common helper functions for time range filtering
p_comm_time_range_to_unix_time(string time_range, int date_time = time, string timezone = syminfo.timezone) =>
    int start_unix_time = na
    int end_unix_time = na
    int start_time_hour = na
    int start_time_minute = na
    int end_time_hour = na
    int end_time_minute = na
    if str.length(time_range) == 11
        start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
        start_time_minute := math.floor(str.tonumber(str.substring(time_range, 3, 5)))
        end_time_hour := math.floor(str.tonumber(str.substring(time_range, 6, 8)))
        end_time_minute := math.floor(str.tonumber(str.substring(time_range, 9, 11)))
    else if str.length(time_range) == 9
        start_time_hour := math.floor(str.tonumber(str.substring(time_range, 0, 2)))
        start_time_minute := math.floor(str.tonumber(str.substring(time_range, 2, 4)))
        end_time_hour := math.floor(str.tonumber(str.substring(time_range, 5, 7)))
        end_time_minute := math.floor(str.tonumber(str.substring(time_range, 7, 9)))
    start_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), start_time_hour, start_time_minute, 0)
    end_unix_time := timestamp(timezone, year(date_time, timezone), month(date_time, timezone), dayofmonth(date_time, timezone), end_time_hour, end_time_minute, 0)
    [start_unix_time, end_unix_time]

// PVO Calculation
p_ta_pvo(simple int fast_length, simple int slow_length, simple int signal_length) =>
    pvo = 100 * (ta.ema(volume, fast_length) - ta.ema(volume, slow_length)) / ta.ema(volume, slow_length)
    signal = ta.ema(pvo, signal_length)
    diff = pvo - signal
    [pvo, signal, diff]

// Indicator call with default (12, 26, 9)
[p_ind_1_pvo, p_ind_1_signal, p_ind_1_diff] = p_ta_pvo(12, 26, 9)

// PVO Histogram with momentum-based coloring
p_ind_1_pvo_color = p_ind_1_diff >= 0 ? (p_ind_1_diff[1] < p_ind_1_diff ? #26A69A : #B2DFDB) : (p_ind_1_diff[1] < p_ind_1_diff ? #FFCDD2 : #EF5350)
plot(p_ind_1_diff, title="PVO - Hist", style=plot.style_columns, color=p_ind_1_pvo_color)
hline(0, title="PVO - Zero Line", linestyle=hline.style_dotted)
plot(p_ind_1_pvo, "PVO - PVO", color.rgb(0, 148, 255, 0), 1)
plot(p_ind_1_signal, "PVO - Signal", color.rgb(255, 106, 0, 0), 1)
Existing indicator chart preview
Existing visual preview. Verify exact values against the source; the image is not a performance result.

Specify a checkable adaptation in Pineify

Specify the lengths, missing-volume policy and whether any new alert must wait for bar close. Ask the Pine Script AI Coding Agent to expose fast and slow volume EMAs for a numerical check. Adding price rules, divergence detection or strategy orders requires separate code and validation.

Open Pine Script AI Coding Agent

Educational indicator analysis. No measured trading performance is established here. Syntax validation does not prove formula equivalence, execution assumptions or profitability.

Implementation questions

It means the fast volume EMA exceeds the slow volume EMA when the denominator is positive. Current volume can be below both averages, as the synthetic second row demonstrates.

No price input appears in PVO. A positive histogram only means PVO is above its signal; PVO itself can still be negative.

This copy uses fixed 12/26/9 arguments, with no input declarations. Edit those arguments or ask for explicit inputs, then verify the changed output. No universally best settings are established here.

No. It plots three series and a zero line. It contains no alertcondition, price-pivot comparison or strategy orders.

The built-in EMAs follow their missing-source rules, while numeric zero contributes to smoothing. A zero slow EMA makes the division undefined. The script does not certify feed completeness or replace missing observations with zero.