Chaikin's Volatility (CVI)

Parameters: period = 10

Overview

Chaikin's Volatility (CVI), developed by Marc Chaikin, measures the rate of change in trading range to identify volatility expansion and contraction patterns. The indicator calculates an exponential moving average of the daily trading range (high minus low), then compares the current EMA value to its value from the same number of periods ago. This percentage change reveals whether volatility is increasing or decreasing relative to recent history. The formula essentially tracks how quickly the average trading range is expanding or contracting over time.

CVI oscillates above and below zero, with positive values indicating expanding volatility and negative values showing volatility contraction. Sharp increases in CVI often occur at market bottoms when fear drives wider trading ranges. Conversely, CVI typically declines during quiet accumulation phases or market tops when complacency reduces daily price swings. The indicator reaches extreme positive readings during panic selling or volatile corrections. Extreme negative readings suggest unusually quiet market conditions that often precede significant moves.

Traders use CVI to anticipate potential breakouts and adjust their strategies to current volatility conditions. Rising CVI values warn of increasing market uncertainty, prompting traders to widen stops or reduce position sizes. Declining CVI suggests consolidation periods where range trading strategies may work better than trend following approaches. Many traders combine CVI with price patterns, watching for volatility expansion to confirm breakouts from consolidation areas. The indicator also helps options traders time their strategies, as expanding volatility typically increases option premiums while contracting volatility creates opportunities for selling premium.

Implementation Examples

Get started with CVI in a few lines:

use vectorta::indicators::cvi::{cvi, CviInput, CviParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// Using with high/low slices
let high = vec![10.0, 12.0, 11.5, 13.0, 12.8];
let low  = vec![ 9.0, 10.5, 10.8, 12.1, 11.9];
let params = CviParams { period: Some(10) };
let input = CviInput::from_slices(&high, &low, params);
let result = cvi(&input)?;

// Using with Candles (defaults: period=10)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = CviInput::with_default_candles(&candles);
let result = cvi(&input)?;

// Access CVI values (percentage change)
for value in result.values {
    println!("CVI: {}", value);
}

API Reference

Input Methods
// From high/low slices
CviInput::from_slices(&[f64], &[f64], CviParams) -> CviInput

// From candles with custom params
CviInput::from_candles(&Candles, CviParams) -> CviInput

// From candles with default params (period=10)
CviInput::with_default_candles(&Candles) -> CviInput
Parameters Structure
#[derive(Debug, Clone)]
pub struct CviParams {
    pub period: Option<usize>, // Default: 10
}
Output Structure
#[derive(Debug, Clone)]
pub struct CviOutput {
    pub values: Vec<f64>, // CVI values (% change of EMA(range) vs lagged)
}
Validation, Warmup & NaNs
  • high.len() == low.len() and both non-empty; otherwise CviError::EmptyData.
  • period > 0 and period ≤ len; otherwise CviError::InvalidPeriod.
  • Finds the first index where both inputs are finite; if none: CviError::AllValuesNaN.
  • Needs at least 2*period - 1 valid points after the first finite value; else CviError::NotEnoughValidData.
  • Outputs are NaN up to index first_valid_idx + 2*period - 1.
  • Streaming: CviStream::update returns None until its internal lag buffer fills, then returns Some(f64).
Error Handling
use vectorta::indicators::cvi::{cvi, CviError, CviInput, CviParams};

let input = CviInput::from_slices(&high, &low, CviParams { period: Some(10) });
match cvi(&input) {
    Ok(output) => handle(output.values),
    Err(CviError::EmptyData) => eprintln!("no data or length mismatch"),
    Err(CviError::InvalidPeriod { period, data_len }) => {
        eprintln!("invalid period: {} (len={})", period, data_len)
    }
    Err(CviError::NotEnoughValidData { needed, valid }) => {
        eprintln!("need {} valid points, only {}", needed, valid)
    }
    Err(CviError::AllValuesNaN) => eprintln!("all inputs are NaN"),
}

Python Bindings

Basic Usage

Calculate CVI from NumPy arrays of highs and lows:

import numpy as np
from vectorta import cvi

high = np.array([10.0, 12.0, 11.5, 13.0, 12.8])
low  = np.array([ 9.0, 10.5, 10.8, 12.1, 11.9])

# Required: period
values = cvi(high, low, period=10, kernel="auto")
print("CVI:", values)
Streaming Real-time Updates

Stream per-candle updates using the first candle to seed state:

from vectorta import CviStream

stream = CviStream(period=10, initial_high=first_high, initial_low=first_low)

for (h, l) in hl_feed:
    val = stream.update(h, l)
    if val is not None:
        print("CVI:", val)
Batch Parameter Sweep

Test many period settings and compare results:

import numpy as np
from vectorta import cvi_batch

high = np.array([...])
low  = np.array([...])

res = cvi_batch(high, low, period_range=(5, 20, 5), kernel="auto")

# Results
values  = res["values"]      # shape: (num_periods, len(series))
periods = res["periods"]     # list of periods tested
print(values.shape, periods)
CUDA Acceleration

CUDA support for CVI is coming soon. The API will follow the same pattern as other CUDA-enabled indicators.

# Coming soon: CUDA-accelerated CVI calculations
# Pattern will mirror other CUDA-enabled indicators in this library.

JavaScript/WASM Bindings

Basic Usage

Calculate CVI in JavaScript/TypeScript:

import { cvi_js } from 'vectorta-wasm';

const high = new Float64Array([10.0, 12.0, 11.5, 13.0, 12.8]);
const low  = new Float64Array([ 9.0, 10.5, 10.8, 12.1, 11.9]);

// Calculate CVI with period=10
const values = cvi_js(high, low, 10);
console.log('CVI:', values);
Memory-Efficient Operations

Use zero-copy operations for large datasets:

import { cvi_alloc, cvi_free, cvi_into, memory } from 'vectorta-wasm';

const high = new Float64Array([/* ... */]);
const low  = new Float64Array([/* ... */]);
const len = high.length;

const highPtr = cvi_alloc(len);
const lowPtr  = cvi_alloc(len);
const outPtr  = cvi_alloc(len);

new Float64Array(memory.buffer, highPtr, len).set(high);
new Float64Array(memory.buffer, lowPtr,  len).set(low);

// Write results directly into outPtr
cvi_into(highPtr, lowPtr, outPtr, len, 10);

const values = new Float64Array(memory.buffer, outPtr, len).slice();

// Free allocated memory
cvi_free(highPtr, len);
cvi_free(lowPtr,  len);
cvi_free(outPtr,  len);
Batch Processing

Test multiple periods efficiently:

import { cvi_batch } from 'vectorta-wasm';

const high = new Float64Array([/* ... */]);
const low  = new Float64Array([/* ... */]);

// Unified API: returns values, combos, rows, cols
const cfg = { period_range: [5, 20, 5] } as const;
const out = cvi_batch(high, low, cfg);
console.log(out.rows, out.cols);
console.log(out.combos); // Array of { period }

`}

Performance Analysis

Comparison:
View:

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

Loading chart...

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

Related Indicators