# 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 ```rust impl Indicator for Obv { type Input = Candle; type Output = f64; fn update(&mut self, candle: Candle) -> Option; fn warmup_period(&self) -> usize { 1 } } ``` - **Python streaming.** Accepts a 6-tuple or dict candle; returns `float | None`. - **Python batch.** `OBV.batch(close, volume)` takes two equal-length 1-D `numpy.ndarray` columns and returns a 1-D `np.ndarray`. The first value is `0.0`, never `NaN`. - **Node streaming.** Not exposed; the Node binding ships only `batch` for `OBV`. - **Node batch.** `obv.batch(close, volume)` returns `Array` 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 test `first_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::new` rejects non-finite OHLCV values before they reach OBV. - **Reset.** `reset()` zeroes the running total and clears the `has_emitted` / `prev_close` state. ## Examples ### Rust ```rust use wickra::{BatchExt, Candle, Indicator, Obv}; fn main() -> Result<(), Box> { 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 ```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 ```js 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. ## See also - [VWAP](../volume/Indicator-Vwap.md) — volume-weighted price benchmark; OBV and VWAP are the two canonical volume-aware indicators in the panel. - [MFI](../momentum-oscillators/Indicator-Mfi.md) — money-flow index, an oscillator blending typical price with volume. - [SMA](../moving-averages/Indicator-Sma.md) / [EMA](../moving-averages/Indicator-Ema.md) — the smoothers most commonly layered on top of OBV to define trade triggers.