Positive Volume Index (PVI)
Overview
The Positive Volume Index (PVI) isolates price movements occurring during periods of expanding volume, reflecting the theory that retail crowd activity dominates high-volume sessions. Starting from an initial value of 1000, the indicator multiplies forward by the price percentage change only when current volume exceeds the previous bar; during stable or declining volume periods, PVI holds its prior value unchanged. This selective accumulation creates a cumulative series that captures the directional bias of crowd-driven price action. Market technicians compare PVI trends against the Negative Volume Index to distinguish between informed professional behavior during quiet periods and emotional retail participation during active sessions. When PVI rises while volume expands, it confirms broad market participation supporting the trend; conversely, a declining PVI during high volume suggests distribution by the retail crowd. Many analysts apply moving averages to PVI for cleaner trend signals and market regime identification.
Implementation Examples
Compute PVI from close/volume slices or candles:
use vector_ta::indicators::pvi::{pvi, PviInput, PviParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};
// Using slices (close and volume)
let close = vec![100.0, 102.0, 101.0, 103.0];
let volume = vec![500.0, 600.0, 580.0, 700.0];
let params = PviParams { initial_value: Some(1000.0) }; // default is 1000.0
let input = PviInput::from_slices(&close, &volume, params);
let out = pvi(&input)?;
// Using candles with defaults (close/volume sources, initial_value=1000)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = PviInput::with_default_candles(&candles);
let out = pvi(&input)?;
// Access values
for v in out.values { println!("PVI: {}", v); } API Reference
Input Methods ▼
// From slices (close, volume)
PviInput::from_slices(&[f64], &[f64], PviParams) -> PviInput
// From candles with custom sources
PviInput::from_candles(&Candles, &str, &str, PviParams) -> PviInput
// From candles with defaults (close/volume, initial_value=1000)
PviInput::with_default_candles(&Candles) -> PviInput Parameters Structure ▼
#[derive(Debug, Clone)]
pub struct PviParams {
pub initial_value: Option<f64>, // Default: 1000.0
} Output Structure ▼
#[derive(Debug, Clone)]
pub struct PviOutput {
pub values: Vec<f64>, // PVI values aligned to input length
} Validation, Warmup & NaNs ▼
close.len() == volume.len()and neither input is empty; elsePviError::MismatchedLengthorPviError::EmptyData.- Finds the first index where both
closeandvolumeare finite; if none,PviError::AllValuesNaN. - Requires at least
2valid points from the first finite index; otherwisePviError::NotEnoughValidData. - Warmup: indices before the first finite pair are
NaN; the first valid output equalsinitial_value. - NaN handling: if current or previous close/volume is
NaN, the output at that step isNaN(no imputation). - Update rule: when volume increases vs. previous valid volume, apply percent change; otherwise carry forward the previous PVI.
Error Handling ▼
use vector_ta::indicators::pvi::{pvi, PviError};
match pvi(&input) {
Ok(output) => process(output.values),
Err(PviError::EmptyData) => eprintln!("pvi: Empty data provided."),
Err(PviError::AllValuesNaN) => eprintln!("pvi: All values are NaN."),
Err(PviError::MismatchedLength) => eprintln!("pvi: Close and volume length mismatch."),
Err(PviError::NotEnoughValidData) => eprintln!("pvi: Not enough valid data (need ≥ 2 valid points)."),
} Python Bindings
Basic Usage ▼
Calculate PVI from NumPy arrays of close and volume (default initial_value=1000.0):
import numpy as np
from vector_ta import pvi
close = np.array([100.0, 102.0, 101.0, 103.0])
volume = np.array([500.0, 600.0, 580.0, 700.0])
# Defaults
vals = pvi(close, volume)
# Custom initial value and kernel
vals = pvi(close, volume, initial_value=1000.0, kernel="auto")
print(vals) # NumPy array Streaming Real-time Updates ▼
Process real-time close/volume:
from vector_ta import PviStream
stream = PviStream(initial_value=1000.0)
for close, volume in feed:
val = stream.update(close, volume)
if val is not None:
handle(val) Batch Parameter Sweep ▼
Evaluate multiple initial values efficiently:
import numpy as np
from vector_ta import pvi_batch
close = np.asarray([...], dtype=float)
volume = np.asarray([...], dtype=float)
# (start, end, step)
cfg = (900.0, 1100.0, 50.0)
res = pvi_batch(close, volume, initial_value_range=cfg, kernel="auto")
print(res["values"].shape) # (num_rows, len(close))
print(res["initial_values"]) # tested initial_value per row CUDA Acceleration ▼
CUDA helpers are available when the Python package is built with CUDA support. Inputs must be float32; outputs are device arrays (DLPack / __cuda_array_interface__ compatible).
import numpy as np
from vector_ta import pvi_cuda_batch_dev, pvi_cuda_many_series_one_param_dev
# One series (float32)
close = np.asarray(load_close(), dtype=np.float32)
volume = np.asarray(load_volume(), dtype=np.float32)
initial_values = np.asarray(load_series(), dtype=np.float32)
dev = pvi_cuda_batch_dev(
close=close,
volume=volume,
initial_values=initial_values,
device_id=0,
)
# Many series (time-major)
close_tm = np.asarray(load_close_time_major_matrix(), dtype=np.float32)
rows, cols = close_tm.shape
close_tm = close_tm.ravel()
volume_tm = np.asarray(load_volume_time_major_matrix(), dtype=np.float32)
volume_tm = volume_tm.ravel()
dev_tm = pvi_cuda_many_series_one_param_dev(
close_tm=close_tm,
volume_tm=volume_tm,
cols=cols,
rows=rows,
initial_value=1.0,
device_id=0,
) JavaScript/WASM Bindings
Basic PVI Calculation ▼
import { pvi_js } from 'vectorta-wasm';
const close = new Float64Array([100, 102, 101, 103]);
const volume = new Float64Array([500, 600, 580, 700]);
const values = pvi_js(close, volume, 1000.0);
console.log(values); Memory-Efficient Operations ▼
Use zero-copy operations for better performance with large datasets:
import { pvi_alloc, pvi_free, pvi_into, memory } from 'vectorta-wasm';
const close = new Float64Array([/* close data */]);
const volume = new Float64Array([/* volume data */]);
const length = close.length;
// Allocate WASM memory
const closePtr = pvi_alloc(length);
const volumePtr = pvi_alloc(length);
const outPtr = pvi_alloc(length);
// Copy inputs
new Float64Array(memory.buffer, closePtr, length).set(close);
new Float64Array(memory.buffer, volumePtr, length).set(volume);
// Compute directly into WASM memory
pvi_into(closePtr, volumePtr, outPtr, length, 1000.0);
// Read results
const pviValues = new Float64Array(memory.buffer, outPtr, length).slice();
// Free when done
pvi_free(closePtr, length);
pvi_free(volumePtr, length);
pvi_free(outPtr, length);
console.log('PVI values:', pviValues); Batch Processing ▼
Test multiple initial values efficiently:
import { pvi_batch_js } from 'vectorta-wasm';
const close = new Float64Array([/* close data */]);
const volume = new Float64Array([/* volume data */]);
// Define parameter sweep
const config = { initial_value_range: [900.0, 1100.0, 50.0] };
const result = pvi_batch_js(close, volume, config);
// result has: { values, combos, rows, cols }
console.log(result.rows, result.cols);
// First row values
const firstRow = result.values.slice(0, close.length); CUDA Bindings (Rust)
use vector_ta::cuda::CudaPvi;
let cuda = CudaPvi::new(0)?;
let close: [f32] = /* ... */;
let volume: [f32] = /* ... */;
let initial_values: [f32] = /* ... */;
let out = cuda.pvi_batch_dev(&close, &volume, &initial_values)?;
let _ = out; Performance Analysis
Across sizes, Rust CPU runs about 1.03× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-08
Related Indicators
Accumulation/Distribution
Technical analysis indicator
Accumulation/Distribution Oscillator
Technical analysis indicator
Balance of Power
Technical analysis indicator
Chaikin Flow Oscillator
Technical analysis indicator
Elder Force Index
Technical analysis indicator
Ease of Movement
Technical analysis indicator