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 vectorta::indicators::pvi::{pvi, PviInput, PviParams};
use vectorta::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 vectorta::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 vectorta 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 vectorta 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 vectorta 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 support for PVI is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.
# Coming soon: CUDA-accelerated PVI calculations
# Example signatures will mirror other CUDA-enabled indicators in this project. 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); 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-05
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