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; else PviError::MismatchedLength or PviError::EmptyData.
  • Finds the first index where both close and volume are finite; if none, PviError::AllValuesNaN.
  • Requires at least 2 valid points from the first finite index; otherwise PviError::NotEnoughValidData.
  • Warmup: indices before the first finite pair are NaN; the first valid output equals initial_value.
  • NaN handling: if current or previous close/volume is NaN, the output at that step is NaN (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

Comparison:
View:

Across sizes, Rust CPU runs about 1.03× faster than Tulip C in this benchmark.

Loading chart...

AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-05

Related Indicators