Negative Volume Index (NVI)

Overview

The Negative Volume Index (NVI) tracks price action during periods of declining volume, operating on the principle that informed traders often act during quieter market sessions. The indicator accumulates price percentage changes only when current volume falls below the previous bar; when volume increases or holds steady, NVI remains flat. Starting from a baseline of 1000.0 at the first valid observation, the series builds a cumulative record of moves made under lighter participation. Technical analysts monitor NVI trends and moving averages to identify smart money activity, as sustained NVI advances during low volume periods often precede broader market rallies. The indicator pairs naturally with the Positive Volume Index to provide a complete picture of volume-stratified price behavior.

Implementation Examples

Get started with NVI using slices or candles:

use vectorta::indicators::nvi::{nvi, NviInput, NviParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From slices (close, volume)
let close = vec![100.0, 101.0, 100.5, 102.0];
let volume = vec![1_000.0, 900.0, 950.0, 800.0];
let input = NviInput::from_slices(&close, &volume, NviParams);
let result = nvi(&input)?;

// From candles with default source ("close")
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = NviInput::with_default_candles(&candles);
let result = nvi(&input)?;

// Or specify a different close source explicitly
let input = NviInput::from_candles(&candles, "close", NviParams);
let result = nvi(&input)?;

// Access values
for v in result.values { println!("NVI: {}", v); }

API Reference

Input Methods
// From slices (close, volume)
NviInput::from_slices(&[f64], &[f64], NviParams) -> NviInput

// From candles with custom close source
NviInput::from_candles(&Candles, &str, NviParams) -> NviInput

// From candles with defaults (source="close")
NviInput::with_default_candles(&Candles) -> NviInput
Parameters Structure
pub struct NviParams; // no parameters
Output Structure
pub struct NviOutput {
    pub values: Vec<f64>, // NVI values (seeded at 1000.0, NaN prefix)
}
Validation, Warmup & NaNs
  • Requires matching-length close and volume series; otherwise NviError::MismatchedLength.
  • Indices before the first finite pair are NaN. The first valid output is 1000.0.
  • Needs at least 2 valid points after the first finite pair; else NviError::NotEnoughValidData.
  • Updates only when volume[t] < volume[t−1]; otherwise the value is carried forward unchanged.
  • nvi_into_slice: destination length must equal inputs; else NviError::DestinationLengthMismatch.
Error Handling
use vectorta::indicators::nvi::{nvi, NviError};

match nvi(&input) {
    Ok(output) => process(output.values),
    Err(NviError::EmptyData) => println!("Input data is empty"),
    Err(NviError::AllCloseValuesNaN) => println!("All close values are NaN"),
    Err(NviError::AllVolumeValuesNaN) => println!("All volume values are NaN"),
    Err(NviError::NotEnoughValidData { needed, valid }) =>
        println!("Need {} data points after first valid, only {}", needed, valid),
    Err(NviError::MismatchedLength { close_len, volume_len }) =>
        println!("Length mismatch: close={}, volume={}", close_len, volume_len),
    Err(NviError::DestinationLengthMismatch { dst_len, close_len, volume_len }) =>
        println!("Output len {} != inputs ({}, {})", dst_len, close_len, volume_len),
    Err(e) => println!("NVI error: {}", e),
}

Python Bindings

Basic Usage

Calculate NVI from NumPy arrays (kernel optional):

import numpy as np
from vectorta import nvi

close = np.array([100.0, 101.0, 100.5, 102.0], dtype=np.float64)
volume = np.array([1000.0, 900.0, 950.0, 800.0], dtype=np.float64)

# Auto-select kernel ("auto", "scalar", "avx2", "avx512")
values = nvi(close, volume, kernel="auto")
print(values)  # NumPy array, same length as inputs
Streaming Real-time Updates

Update with live close/volume pairs:

from vectorta import NviStream

stream = NviStream()
for c, v in zip(close, volume):
    val = stream.update(float(c), float(v))
    if val is not None:
        print("NVI:", val)  # 1000.0 at the first valid update
Batch Processing

Compute the single-row NVI batch efficiently:

import numpy as np
from vectorta import nvi_batch

close = np.asarray(close, dtype=np.float64)
volume = np.asarray(volume, dtype=np.float64)

res = nvi_batch(close, volume, kernel="auto")
print(res["values"].shape)  # (1, len(close))
print(res["rows"], res["cols"])
CUDA Acceleration

CUDA support for NVI is currently under development. The API will follow the same pattern as other CUDA-enabled indicators.

# Coming soon: CUDA-accelerated NVI calculations

JavaScript/WASM Bindings

Basic Usage

Calculate NVI in JavaScript/TypeScript:

import { nvi_js } from 'vectorta-wasm';

const close = new Float64Array([100.0, 101.0, 100.5, 102.0]);
const volume = new Float64Array([1000.0, 900.0, 950.0, 800.0]);

const nviValues = nvi_js(close, volume); // Float64Array
console.log('NVI values:', nviValues);
Memory-Efficient Operations

Use zero-copy operations for large datasets:

import { nvi_alloc, nvi_free, nvi_into, memory } from 'vectorta-wasm';

const close = new Float64Array([/* ... */]);
const volume = new Float64Array([/* ... */]);
const len = close.length;

// Allocate WASM memory
const closePtr = nvi_alloc(len);
const volumePtr = nvi_alloc(len);
const outPtr = nvi_alloc(len);

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

// Compute NVI directly into the output buffer
nvi_into(closePtr, volumePtr, outPtr, len);

// Read results (slice to copy out of WASM memory)
const values = new Float64Array(memory.buffer, outPtr, len).slice();

// Free WASM memory
nvi_free(closePtr, len);
nvi_free(volumePtr, len);
nvi_free(outPtr, len);

console.log('NVI values:', values);
Batch Processing

Compute the single-row batch using pointer API:

import { nvi_alloc, nvi_free, nvi_batch_into, memory } from 'vectorta-wasm';

const len = close.length;
const closePtr = nvi_alloc(len);
const volumePtr = nvi_alloc(len);
const outPtr = nvi_alloc(len);

new Float64Array(memory.buffer, closePtr, len).set(close);
new Float64Array(memory.buffer, volumePtr, len).set(volume);

// Returns number of rows (1); writes values into outPtr
const rows = nvi_batch_into(closePtr, volumePtr, outPtr, len);
const values = new Float64Array(memory.buffer, outPtr, len).slice();

console.log(rows); // 1
console.log(values.length === len);

nvi_free(closePtr, len);
nvi_free(volumePtr, len);
nvi_free(outPtr, len);

Performance Analysis

Comparison:
View:

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

Loading chart...

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

Related Indicators