Volume Price Confirmation Indicator (VPCI)

Parameters: short_range = 5 | long_range = 25

Overview

Buff Dormeier developed the Volume Price Confirmation Indicator to reveal the proportional imbalances between price trends and volume-weighted price trends, winning the prestigious Charles H. Dow Award in 2007 for this innovative work. The indicator measures the intrinsic relationship between price and volume by comparing volume-weighted moving averages against simple moving averages over identical periods, exposing whether price movements have proper volume support. When VPCI rises, it indicates that volume confirms the price trend as traders participate with conviction, while declining VPCI values suggest weakening participation that often precedes trend reversals. Dormeier built VPCI on his earlier work with volume-weighted moving averages from the 1990s, believing that volume represents the force of investor conviction and typically leads price action. The indicator excels at identifying divergences where price continues trending while volume support weakens, providing advance warning of potential exhaustion points. By incorporating both the main VPCI line and its smoothed counterpart VPCIS, traders can filter out noise while maintaining sensitivity to genuine volume-price relationships that drive sustainable market moves.

Implementation Examples

Compute VPCI and VPCIS from slices or candles:

use vector_ta::indicators::vpci::{vpci, VpciInput, VpciParams};
use vector_ta::utilities::data_loader::{Candles, read_candles_from_csv};

// From close + volume slices
let close = vec![101.0, 102.0, 100.5, 103.0, 104.0];
let volume = vec![2000.0, 2500.0, 1800.0, 3000.0, 3200.0];
let params = VpciParams { short_range: Some(5), long_range: Some(25) }; // defaults 5/25
let input = VpciInput::from_slices(&close, &volume, params);
let out = vpci(&input)?; // out.vpci, out.vpcis

// From Candles with defaults (sources: "close" and "volume")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = VpciInput::with_default_candles(&candles);
let out = vpci(&input)?;

// Access values
for (v, s) in out.vpci.iter().zip(out.vpcis.iter()) {
    println!("VPCI: {v}, VPCIS: {s}");
}

API Reference

Input Methods
// From close + volume slices
VpciInput::from_slices(&[f64], &[f64], VpciParams) -> VpciInput

// From candles with custom sources
VpciInput::from_candles(&Candles, &str /*close_source*/, &str /*volume_source*/, VpciParams) -> VpciInput

// From candles with defaults ("close"/"volume", short=5, long=25)
VpciInput::with_default_candles(&Candles) -> VpciInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct VpciParams {
    pub short_range: Option<usize>, // Default: 5
    pub long_range: Option<usize>,  // Default: 25
}
Output Structure
#[derive(Debug, Clone)]
pub struct VpciOutput {
    pub vpci: Vec<f64>,  // main oscillator
    pub vpcis: Vec<f64>, // short-term, volume-weighted smoothing
}
Validation, Warmup & NaNs
  • close.len() == volume.len() or VpciError::MismatchedInputLengths.
  • short_range > 0, long_range > 0, and short_range ≤ long_range; otherwise VpciError::InvalidRange.
  • If short_range or long_range exceeds input length, VpciError::InvalidRange.
  • Warmup: first valid index is the first point where both close and volume are finite; outputs before first + long_range − 1 are NaN.
  • Zero denominators produce NaN (e.g., VWMA when volume sum is 0 over a window).
  • If not enough valid data after the first finite pair (len − first < long_range), VpciError::NotEnoughValidData.
  • Streaming: VpciStream::update returns None until count ≥ long_range; then returns (vpci, vpcis) for the newest point.
Error Handling
use vector_ta::indicators::vpci::{vpci, VpciError};

match vpci(&input) {
    Ok(output) => process(output.vpci, output.vpcis),
    Err(VpciError::AllValuesNaN) =>
        eprintln!("All close or volume values are NaN"),
    Err(VpciError::InvalidRange { period, data_len }) =>
        eprintln!("Invalid range: period = {period}, data length = {data_len}"),
    Err(VpciError::NotEnoughValidData { needed, valid }) =>
        eprintln!("Need {needed} data points after first valid; only {valid}"),
    Err(VpciError::SmaError(e)) =>
        eprintln!("SMA error: {e}"),
    Err(VpciError::MismatchedInputLengths { close_len, volume_len }) =>
        eprintln!("close len {close_len} != volume len {volume_len}"),
    Err(VpciError::MismatchedOutputLengths { vpci_len, vpcis_len, data_len }) =>
        eprintln!("output lens (vpci={vpci_len}, vpcis={vpcis_len}) must equal data_len={data_len}"),
    Err(VpciError::KernelNotAvailable) =>
        eprintln!("Requested kernel not available for this build/CPU"),
}

Python Bindings

Basic Usage

Compute VPCI and VPCIS from NumPy arrays:

import numpy as np
from vector_ta import vpci

close = np.array([101.0, 102.0, 100.5, 103.0, 104.0], dtype=float)
volume = np.array([2000.0, 2500.0, 1800.0, 3000.0, 3200.0], dtype=float)

# Defaults are short=5, long=25; specify explicitly if desired
vpci_vals, vpcis_vals = vpci(close, volume, short_range=5, long_range=25)

# Optional: select CPU kernel ("auto", "scalar", "avx2", "avx512" — availability dependent)
vpci_vals, vpcis_vals = vpci(close, volume, short_range=5, long_range=25, kernel="avx2")

print("VPCI:", vpci_vals)
print("VPCIS:", vpcis_vals)
Streaming Real-time Updates

Process real-time (close, volume) ticks:

from vector_ta import VpciStream

stream = VpciStream(short_range=5, long_range=25)

for close_val, volume_val in feed:
    result = stream.update(close_val, volume_val)
    if result is not None:
        vpci_val, vpcis_val = result
        handle(vpci_val, vpcis_val)
Batch Parameter Optimization

Sweep short/long ranges and collect results:

import numpy as np
from vector_ta import vpci_batch

close = np.array([...], dtype=float)
volume = np.array([...], dtype=float)

results = vpci_batch(
    close,
    volume,
    short_range_tuple=(3, 10, 1),
    long_range_tuple=(15, 60, 5),
    kernel="auto"
)

# Results dict keys: 'vpci' [rows x cols], 'vpcis' [rows x cols],
#                    'short_ranges' [rows], 'long_ranges' [rows]
vpci_matrix = results['vpci']
vpcis_matrix = results['vpcis']
shorts = results['short_ranges']
longs = results['long_ranges']
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 vpci_cuda_batch_dev, vpci_cuda_many_series_one_param_dev

# One series (float32)
close_f32 = np.asarray(load_close(), dtype=np.float32)
volume_f32 = np.asarray(load_volume(), dtype=np.float32)

dev = vpci_cuda_batch_dev(
    close_f32=close_f32,
    volume_f32=volume_f32,
    short_range_tuple=(2, 20, 2),
    long_range_tuple=(2, 20, 2),
    device_id=0,
)

# Many series (time-major)
close_tm_f32 = np.asarray(load_close_time_major_matrix(), dtype=np.float32)
volume_tm_f32 = np.asarray(load_volume_time_major_matrix(), dtype=np.float32)

dev_tm = vpci_cuda_many_series_one_param_dev(
    close_tm_f32=close_tm_f32,
    volume_tm_f32=volume_tm_f32,
    short_range=14,
    long_range=14,
    device_id=0,
)

JavaScript/WASM Bindings

Basic Usage

Compute VPCI and VPCIS in JS/TS:

import { vpci_js } from 'vectorta-wasm';

const close = new Float64Array([101, 102, 100.5, 103, 104]);
const volume = new Float64Array([2000, 2500, 1800, 3000, 3200]);

const { vpci, vpcis } = vpci_js(close, volume, 5, 25) as { vpci: Float64Array, vpcis: Float64Array };
console.log('VPCI:', vpci);
console.log('VPCIS:', vpcis);
Memory-Efficient Operations

Use zero-copy operations for large datasets:

import { vpci_alloc, vpci_free, vpci_into, memory } from 'vectorta-wasm';

const len = close.length;
const closePtr = vpci_alloc(len);
const volumePtr = vpci_alloc(len);
const vpciPtr = vpci_alloc(len);
const vpcisPtr = vpci_alloc(len);

// Copy inputs into WASM memory
new Float64Array(memory.buffer, closePtr, len).set(close);
new Float64Array(memory.buffer, volumePtr, len).set(volume);

// Compute directly into output buffers
vpci_into(closePtr, volumePtr, vpciPtr, vpcisPtr, len, 5, 25);

// Read results (slice() to copy out)
const vpciOut = new Float64Array(memory.buffer, vpciPtr, len).slice();
const vpcisOut = new Float64Array(memory.buffer, vpcisPtr, len).slice();

// Free allocated memory
vpci_free(closePtr, len);
vpci_free(volumePtr, len);
vpci_free(vpciPtr, len);
vpci_free(vpcisPtr, len);
Batch Processing

Test multiple parameter combinations efficiently:

import { vpci_batch } from 'vectorta-wasm';

const cfg = { short_range: [3, 10, 1], long_range: [15, 60, 5] };
const res = vpci_batch(close, volume, cfg) as {
  vpci: Float64Array,
  vpcis: Float64Array,
  combos: Array<{ short_range?: number, long_range?: number }>,
  rows: number,
  cols: number
};

// Flattened output: row-major [rows * cols]
function row(slice: Float64Array, r: number, cols: number) {
  const start = r * cols; return slice.slice(start, start + cols);
}

// Example: first combo
const firstVpci = row(res.vpci, 0, res.cols);
const firstVpcis = row(res.vpcis, 0, res.cols);
// Parameters for that row
const firstParams = res.combos[0];

CUDA Bindings (Rust)

use vector_ta::cuda::CudaVpci;
use vector_ta::indicators::vpci::VpciBatchRange;

let cuda = CudaVpci::new(0)?;

let close_f32: [f32] = /* ... */;
let volume_f32: [f32] = /* ... */;
let sweep = VpciBatchRange::default();

let out = cuda.vpci_batch_dev(&close_f32, &volume_f32, &sweep)?;
let _ = out;

Performance Analysis

Comparison:
View:
Loading chart...

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

Related Indicators