2026-05-22 16:18:04 +02:00
|
|
|
# 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 |
|
|
|
|
|
|---------------------|--------------------------------------------------------------|
|
2026-05-22 21:21:56 +02:00
|
|
|
| Family | Volume |
|
2026-05-22 16:18:04 +02:00
|
|
|
| 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<f64>;
|
|
|
|
|
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<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 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<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
|
|
|
|
|
|
|
|
|
|
```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
|
|
|
|
|
|
2026-05-22 21:21:56 +02:00
|
|
|
- [VWAP](../volume/Indicator-Vwap.md) — volume-weighted price benchmark; OBV and
|
2026-05-22 16:18:04 +02:00
|
|
|
VWAP are the two canonical volume-aware indicators in the panel.
|
2026-05-22 21:21:56 +02:00
|
|
|
- [MFI](../momentum-oscillators/Indicator-Mfi.md) — money-flow index, an oscillator blending
|
2026-05-22 16:18:04 +02:00
|
|
|
typical price with volume.
|
2026-05-22 21:21:56 +02:00
|
|
|
- [SMA](../moving-averages/Indicator-Sma.md) / [EMA](../moving-averages/Indicator-Ema.md) — the smoothers
|
2026-05-22 16:18:04 +02:00
|
|
|
most commonly layered on top of OBV to define trade triggers.
|