OBV (On-Balance Volume)
A cumulative signed-volume series: each candle adds its volume on an up close, subtracts on a down close, and leaves the running total unchanged on a flat close. The shape of the OBV curve, not its absolute level, is what carries information.
Quick reference
| Item | Value |
|---|---|
| Family | Volume |
| Input type | Candle (uses close and volume) |
| Output type | f64 |
| Output range | unbounded (signed, integer-of-volume in spirit) |
| Default parameters | none |
| Warmup period | 1 |
| Interpretation | divergence vs price signals accumulation / distribution |
Formula
For each candle t > 0 (after the seed):
if close_t > close_{t-1}: OBV_t = OBV_{t-1} + volume_t
if close_t < close_{t-1}: OBV_t = OBV_{t-1} - volume_t
if close_t == close_{t-1}: OBV_t = OBV_{t-1}The first candle initialises the running total to 0.0 and emits that value (crates/wickra-core/src/indicators/obv.rs:42-55).
Parameters
Obv::new() takes no parameters. Python: wickra.OBV(). Node: new w.OBV().
Inputs / Outputs
use wickra::{Indicator, Obv, Candle};
// Obv: Input = Candle, Output = f64
const _: fn(&mut Obv, Candle) -> Option<f64> = <Obv as Indicator>::update;- Python streaming. Accepts a 6-tuple or dict candle; returns
float | None. - Python batch.
OBV.batch(close, volume)takes two equal-length 1-Dnumpy.ndarraycolumns and returns a 1-Dnp.ndarray. The first value is0.0, neverNaN. - Node streaming. Not exposed; the Node binding ships only
batchforOBV. - Node batch.
obv.batch(close, volume)returnsArray<number>of the same length.
Warmup
warmup_period() == 1. The very first candle emits 0.0 by convention (the "baseline" — there is no prior close to compare against, so the indicator starts the running total at zero). Every subsequent candle emits the updated cumulative total.
Edge cases
- First bar. Always emits
0.0(pinned testfirst_candle_baseline_zero). This is the canonical OBV convention used by Granville's original formulation. - Equal closes. A candle with
close_t == close_{t-1}does not change the running total — the volume is discarded. (obv.rs:46-50). - Down close. Subtracts the bar's volume, so OBV can go strongly negative on a sustained downtrend; that is expected and meaningful.
- Zero volume. A zero-volume bar adds or subtracts
0, so OBV is unchanged regardless of close direction. - NaN / infinity.
Candle::newrejects non-finite OHLCV values before they reach OBV. - Reset.
reset()zeroes the running total and clears thehas_emitted/prev_closestate.
Examples
Rust
use wickra::{BatchExt, Candle, Indicator, Obv};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, 0)?, // baseline -> 0
Candle::new(10.0, 11.0, 10.0, 11.0, 20.0, 0)?, // up -> +20
Candle::new(11.0, 11.0, 10.5, 10.5, 30.0, 0)?, // down -> -30
Candle::new(10.5, 10.5, 10.5, 10.5, 40.0, 0)?, // flat -> 0
Candle::new(10.5, 12.0, 10.5, 12.0, 10.0, 0)?, // up -> +10
];
let mut obv = Obv::new();
println!("{:?}", obv.batch(&candles));
Ok(())
}Output:
[Some(0.0), Some(20.0), Some(-10.0), Some(-10.0), Some(0.0)]Hand check: baseline 0, then 0 + 20 = 20, then 20 - 30 = -10, then -10 (flat close discards the 40), then -10 + 10 = 0.
Python
import numpy as np
import wickra as ta
obv = ta.OBV()
c = np.array([10.0, 11.0, 10.5, 10.5, 12.0])
v = np.array([100.0, 20.0, 30.0, 40.0, 10.0])
print(obv.batch(c, v))Output:
[ 0. 20. -10. -10. 0.]Node
const w = require('wickra');
const obv = new w.OBV();
console.log(obv.batch(
[10, 11, 10.5, 10.5, 12],
[100, 20, 30, 40, 10],
));Output:
[ 0, 20, -10, -10, 0 ]Interpretation
- Divergence is the signal. OBV's absolute level depends entirely on where the series started and is therefore meaningless on its own. The interpretable signal is the shape of OBV relative to price: a new price high without a new OBV high (bearish divergence) suggests the rally is not being confirmed by accumulating buy volume, and vice versa.
- Trend confirmation. A rising OBV that tracks a rising price is confirmation of the trend; a flattening OBV under a still-rising price is the canonical warning of distribution.
- Smoothing. Many traders apply an SMA or EMA to OBV (e.g. 20-period SMA) and treat crossings of that smoothed line as buy/sell triggers.
Common pitfalls
- Absolute value is arbitrary. Comparing OBV values across different start times or different instruments is meaningless — only slopes, divergences, and crossings of derived smoothers carry signal.
- Flat closes discard volume. A candle that closes exactly at the previous close contributes nothing to OBV no matter how heavy its volume. Some practitioners prefer A/D-style alternatives (e.g. Chaikin Money Flow) that distribute the volume according to where in the bar's range the close landed, precisely to avoid this discontinuity.
References
- Joseph Granville, Granville's New Strategy of Daily Stock Market Timing for Maximum Profit, Prentice-Hall, 1976. The OBV construction was first popularised in Granville's earlier 1963 work and refined in his subsequent books.