Klinger Volume Oscillator (KVO)
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 vector_ta::indicators::kvo::{kvo, KvoInput, KvoParams};
use vector_ta::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 ≥ 1andlong_period ≥ short_period; elseKvoError::InvalidPeriod { short, long }.- All of
high/low/close/volumemust be non-empty; otherwiseKvoError::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_idxareNaN. First computed value appears atfirst_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 vector_ta::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 vector_ta 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 vector_ta 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 vector_ta 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 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 kvo_cuda_batch_dev, kvo_cuda_many_series_one_param_dev
# One series (float32)
high_f32 = np.asarray(load_high(), dtype=np.float32)
low_f32 = np.asarray(load_low(), dtype=np.float32)
close_f32 = np.asarray(load_close(), dtype=np.float32)
volume_f32 = np.asarray(load_volume(), dtype=np.float32)
dev = kvo_cuda_batch_dev(
high_f32=high_f32,
low_f32=low_f32,
close_f32=close_f32,
volume_f32=volume_f32,
short_range=(2, 20, 2),
long_range=(2, 20, 2),
device_id=0,
)
# Many series (time-major)
high_tm_f32 = np.asarray(load_high_time_major_matrix(), dtype=np.float32)
rows, cols = high_tm_f32.shape
high_tm_f32 = high_tm_f32.ravel()
low_tm_f32 = np.asarray(load_low_time_major_matrix(), dtype=np.float32)
low_tm_f32 = low_tm_f32.ravel()
close_tm_f32 = np.asarray(load_close_time_major_matrix(), dtype=np.float32)
close_tm_f32 = close_tm_f32.ravel()
volume_tm_f32 = np.asarray(load_volume_time_major_matrix(), dtype=np.float32)
volume_tm_f32 = volume_tm_f32.ravel()
dev_tm = kvo_cuda_many_series_one_param_dev(
high_tm_f32=high_tm_f32,
low_tm_f32=low_tm_f32,
close_tm_f32=close_tm_f32,
volume_tm_f32=volume_tm_f32,
cols=cols,
rows=rows,
short_period=14,
long_period=14,
device_id=0,
) 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); CUDA Bindings (Rust)
use vector_ta::cuda::CudaKvo;
use vector_ta::indicators::kvo::KvoBatchRange;
let cuda = CudaKvo::new(0)?;
let high: [f32] = /* ... */;
let low: [f32] = /* ... */;
let close: [f32] = /* ... */;
let volume: [f32] = /* ... */;
let sweep = KvoBatchRange::default();
let out = cuda.kvo_batch_dev(&high, &low, &close, &volume, &sweep)?;
let _ = out; Performance Analysis
Across sizes, Rust CPU runs about 1.10× faster than Tulip C in this benchmark.
AMD Ryzen 9 9950X (CPU) | NVIDIA RTX 4090 (GPU) | Benchmarks: 2026-01-08
Related Indicators
Accumulation/Distribution
Technical analysis indicator
Accumulation/Distribution Oscillator
Technical analysis indicator
Balance of Power
Technical analysis indicator
Chaikin Flow Oscillator
Technical analysis indicator
Elder Force Index
Technical analysis indicator
Ease of Movement
Technical analysis indicator