On Balance Volume (OBV)

Overview

On Balance Volume (OBV) measures cumulative buying and selling pressure by adding volume on up days and subtracting it on down days. When closing price exceeds the previous close, the entire session volume contributes positively to the running total; conversely, declining closes subtract their full volume from the cumulative sum. Starting from zero at the first valid bar, OBV builds a continuous record of volume flow that reveals whether accumulation or distribution dominates. The absolute OBV value holds no inherent meaning; traders instead analyze the slope and direction of the line, watching for divergences between OBV trends and price movements. A rising OBV alongside rising prices confirms strength, while divergence often signals reversals. This volume-weighted momentum indicator remains one of the most widely used tools for validating price trends.

Implementation Examples

Compute OBV from closes and volumes or candles:

use vectorta::indicators::obv::{obv, ObvInput, ObvParams};
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![1200.0, 1500.0, 1800.0, 1700.0];
let input = ObvInput::from_slices(&close, &volume, ObvParams::default());
let result = obv(&input)?;

// From candles with defaults (source: close + volume)
let candles: Candles = read_candles_from_csv("data/sample.csv")?;
let input = ObvInput::with_default_candles(&candles);
let result = obv(&input)?;

// Access the OBV values
for value in result.values {
    println!("OBV: {}", value);
}

API Reference

Input Methods
// From candles (uses close + volume)
ObvInput::from_candles(&Candles, ObvParams) -> ObvInput

// From slices
ObvInput::from_slices(&[f64], &[f64], ObvParams) -> ObvInput

// From candles with default params
ObvInput::with_default_candles(&Candles) -> ObvInput
Parameters Structure
#[derive(Debug, Clone, Default)]
pub struct ObvParams; // No tunable parameters
Output Structure
#[derive(Debug, Clone)]
pub struct ObvOutput {
    pub values: Vec<f64>, // cumulative OBV
}
Validation, Warmup & NaNs
  • Errors on empty inputs (ObvError::EmptyData) or mismatched lengths (ObvError::DataLengthMismatch).
  • Skips leading NaNs: indices before the first bar where both close and volume are finite are NaN; the first valid OBV is 0.0.
  • Updates: if Closet > Closet-1 add Volumet; if lower subtract; if equal, OBV unchanged.
  • Streaming: returns None until the first valid update, then cumulative values thereafter.
Error Handling
use vectorta::indicators::obv::{obv, ObvError};

match obv(&input) {
    Ok(output) => process(output.values),
    Err(ObvError::EmptyData) => eprintln!("Input data is empty"),
    Err(ObvError::DataLengthMismatch { close_len, volume_len }) =>
        eprintln!("Length mismatch: close={}, volume={}", close_len, volume_len),
    Err(ObvError::AllValuesNaN) => eprintln!("All values are NaN"),
    Err(e) => eprintln!("OBV error: {}", e),
}

Python Bindings

Basic Usage

Compute OBV from NumPy arrays of close and volume:

import numpy as np
from vectorta import obv

close = np.array([100.0, 101.0, 100.5, 102.0], dtype=np.float64)
volume = np.array([1200.0, 1500.0, 1800.0, 1700.0], dtype=np.float64)

# Auto kernel (default)
values = obv(close, volume)

# Or specify a kernel explicitly (e.g., "avx2")
values = obv(close, volume, kernel="avx2")

print(values)  # numpy.ndarray of shape (len(close),)
Streaming Real-time Updates

Process close/volume ticks incrementally:

from vectorta import ObvStream

stream = ObvStream()
for c, v in feed:  # (float, float)
    obv_val = stream.update(c, v)
    if obv_val is not None:
        consume(obv_val)
Batch Processing

OBV has no parameters; batch returns a single row:

import numpy as np
from vectorta import obv_batch

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

result = obv_batch(close, volume, kernel="auto")
# result['values'].shape == (1, len(close))
print(result['values'])
CUDA Acceleration

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

# Coming soon: CUDA-accelerated OBV calculations

JavaScript/WASM Bindings

Basic Usage

Compute OBV from close and volume arrays:

import { obv_js } from 'vectorta-wasm';

const close = new Float64Array([100.0, 101.0, 100.5, 102.0]);
const volume = new Float64Array([1200.0, 1500.0, 1800.0, 1700.0]);

const values = obv_js(close, volume);
console.log('OBV values:', values);
Memory-Efficient Operations

Operate directly on WASM memory with zero extra allocations:

import { obv_alloc, obv_free, obv_into, memory } from 'vectorta-wasm';

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

// Allocate WASM memory for inputs and output
const closePtr = obv_alloc(len);
const volumePtr = obv_alloc(len);
const outPtr = obv_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 buffer
obv_into(closePtr, volumePtr, outPtr, len);

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

// Free allocated memory
obv_free(closePtr, len);
obv_free(volumePtr, len);
obv_free(outPtr, len);
Batch Processing

OBV batch returns a single row object with shape metadata:

import { obv_batch } from 'vectorta-wasm';

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

const { values, rows, cols } = obv_batch(close, volume);
console.log(rows, cols);
console.log(values);

Performance Analysis

Comparison:
View:

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

Loading chart...

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

Related Indicators