Klinger Volume Oscillator (KVO)

Parameters: short_period = 2 | long_period = 5

Overview

The Klinger Volume Oscillator reveals the true force behind price movements by converting raw volume into directional Volume Force that accounts for the relationship between closing prices and the daily trading range, then comparing short and long term trends in this force to identify accumulation and distribution patterns. KVO calculates a cumulative measure that tracks whether each day's typical price moves higher or lower than the previous day, then multiplies this trend direction by volume weighted according to the relationship between price movement and trading range. The difference between a fast and slow exponential moving average of this Volume Force creates an oscillator that swings above zero during accumulation phases when smart money builds positions, and below zero during distribution when institutions unload holdings. Traders watch for KVO crossing above zero as confirmation that buying pressure dominates selling pressure, particularly powerful when price breaks resistance with the oscillator already positive. Divergences between KVO and price provide early warning signals, as rising KVO during price declines reveals hidden accumulation before reversals, while falling KVO during rallies exposes distribution beneath surface strength. The indicator excels at confirming breakouts by verifying that volume flow supports the price move, distinguishing genuine breakouts backed by institutional participation from false moves driven by retail speculation alone.

Implementation Examples

Compute KVO from OHLCV slices or candle data:

use vectorta::indicators::kvo::{kvo, KvoInput, KvoParams};
use vectorta::utilities::data_loader::{Candles, read_candles_from_csv};

// From OHLCV slices
let high   = vec![/* f64 */];
let low    = vec![/* f64 */];
let close  = vec![/* f64 */];
let volume = vec![/* f64 */];
let params = KvoParams { short_period: Some(2), long_period: Some(5) };
let input  = KvoInput::from_slices(&high, &low, &close, &volume, params);
let out    = kvo(&input)?; // out.values: Vec<f64>

// From Candles with defaults (short=2, long=5)
let candles: Candles = read_candles_from_csv("data/ohlcv.csv")?;
let input = KvoInput::with_default_candles(&candles);
let out   = kvo(&input)?;

API Reference

Input Methods
// From Candles
KvoInput::from_candles(&Candles, KvoParams) -> KvoInput

// From OHLCV slices
KvoInput::from_slices(&[f64], &[f64], &[f64], &[f64], KvoParams) -> KvoInput

// With default parameters (short=2, long=5)
KvoInput::with_default_candles(&Candles) -> KvoInput
Parameters Structure
pub struct KvoParams {
    pub short_period: Option<usize>, // Default: 2
    pub long_period:  Option<usize>, // Default: 5
}
Output Structure
pub struct KvoOutput {
    pub values: Vec<f64>, // Oscillator values: EMA_short(VF) - EMA_long(VF)
}
Validation, Warmup & NaNs
  • short_period ≥ 1 and long_period ≥ short_period; else KvoError::InvalidPeriod { short, long }.
  • All of high/low/close/volume must be non-empty; otherwise KvoError::EmptyData.
  • Find first index where all four inputs are finite. If fewer than 2 valid points thereafter: KvoError::NotEnoughValidData { valid }.
  • Outputs at indices 0..=first_valid_idx are NaN. First computed value appears at first_valid_idx + 1.
  • If no finite inputs exist: KvoError::AllValuesNaN.
  • Zero‑copy APIs validate destination length and may return KvoError::OutputLenMismatch { got, expected }.
Error Handling
use vectorta::indicators::kvo::{kvo, KvoError};

match kvo(&input) {
    Ok(output) => consume(output.values),
    Err(KvoError::EmptyData) => eprintln!("OHLCV data is empty"),
    Err(KvoError::InvalidPeriod { short, long }) =>
        eprintln!("Invalid periods: short={}, long={}", short, long),
    Err(KvoError::NotEnoughValidData { valid }) =>
        eprintln!("Not enough valid points after first finite index: {}", valid),
    Err(KvoError::AllValuesNaN) => eprintln!("All inputs are NaN"),
    Err(KvoError::OutputLenMismatch { got, expected }) =>
        eprintln!("Output buffer length {} != {}", got, expected),
}

Python Bindings

Basic Usage

Compute KVO from NumPy arrays (defaults: short=2, long=5):

import numpy as np
from vectorta import kvo

# OHLCV as NumPy arrays
high   = np.array([...], dtype=np.float64)
low    = np.array([...], dtype=np.float64)
close  = np.array([...], dtype=np.float64)
volume = np.array([...], dtype=np.float64)

# Defaults
vals = kvo(high, low, close, volume)

# Custom params and kernel
vals = kvo(high, low, close, volume, short_period=3, long_period=10, kernel="auto")
print(vals.shape)  # same length as inputs
Streaming Real-time Updates
from vectorta import KvoStream

stream = KvoStream(short_period=2, long_period=5)
for h, l, c, v in ohlcv_iter:
    val = stream.update(h, l, c, v)
    if val is not None:
        use(val)
Batch Parameter Optimization
import numpy as np
from vectorta import kvo_batch

high, low, close, volume = ...  # NumPy arrays
results = kvo_batch(
    high, low, close, volume,
    short_range=(2, 6, 1),
    long_range=(5, 10, 1),
    kernel="auto"
)

# results is a dict with 'values' [rows x cols], 'shorts', 'longs'
vals_2d = results['values']
shorts  = results['shorts']
longs   = results['longs']
CUDA Acceleration

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

# Coming soon: CUDA-accelerated KVO calculations
# See other indicators for CUDA patterns once available.

JavaScript/WASM Bindings

Basic Usage

Compute KVO using the WASM package:

import { kvo_js } from 'vectorta-wasm';

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

const values = kvo_js(high, low, close, volume, 2, 5);
Memory-Efficient Operations

Zero-copy into pre-allocated buffers:

import { kvo_alloc, kvo_free, kvo_into, memory } from 'vectorta-wasm';

const len = high.length;
const hPtr = kvo_alloc(len), lPtr = kvo_alloc(len), cPtr = kvo_alloc(len), vPtr = kvo_alloc(len);
const oPtr = kvo_alloc(len);

new Float64Array(memory.buffer, hPtr, len).set(high);
new Float64Array(memory.buffer, lPtr, len).set(low);
new Float64Array(memory.buffer, cPtr, len).set(close);
new Float64Array(memory.buffer, vPtr, len).set(volume);

// kvo_into(high_ptr, low_ptr, close_ptr, volume_ptr, out_ptr, len, short, long)
kvo_into(hPtr, lPtr, cPtr, vPtr, oPtr, len, 2, 5);
const out = new Float64Array(memory.buffer, oPtr, len).slice();

[hPtr, lPtr, cPtr, vPtr, oPtr].forEach(p => kvo_free(p, len));
Batch Processing
import { kvo_batch_js } from 'vectorta-wasm';

const cfg = { short_period_range: [2, 6, 1], long_period_range: [5, 10, 1] };
const result = kvo_batch_js(high, low, close, volume, cfg);

// result: { values: number[], combos: {short_period?: number, long_period?: number}[], rows: number, cols: number }
// values is a flat row-major array (rows x cols)
console.log(result.rows, result.cols, result.combos.length);

Performance Analysis

Comparison:
View:

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

Loading chart...

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

Related Indicators