OBV in Python — Coding It from Scratch (pandas)

A vectorized On-Balance Volume and a divergence check

OBV computed in Python
OBV computed in Python

OBV is the most beginner-friendly indicator in this whole series: a running total with an up/down rule, as the explainer covered. In pandas it’s a genuine one-liner once you spot the trick.

What you’ll need

  • Python 3.9+, pandas/numpy
  • close and volume series.

The vectorized version

The rule “add volume on up closes, subtract on down closes, ignore flat ones” is exactly sign(price change) × volume, accumulated:

import numpy as np
import pandas as pd


def obv(close: pd.Series, volume: pd.Series) -> pd.Series:
    direction = np.sign(close.diff()).fillna(0)   # +1 up, -1 down, 0 flat/first bar
    return (direction * volume).cumsum().rename("obv")

np.sign(close.diff()) gives +1/−1/0 per bar, multiplying by volume applies the sign, and cumsum() accumulates it into the running total from the chart above. No loop needed.

Reading it in code: divergence

Since the OBV level is meaningless, you compare its slope to price’s. A crude but illustrative divergence check:

obv_line = obv(df["close"], df["volume"])
win = 20
price_up = df["close"] > df["close"].shift(win)
obv_down = obv_line < obv_line.shift(win)
df["bearish_divergence"] = price_up & obv_down   # price up, volume flow down

That flags spots where price rose over the window but OBV fell — a classic distribution warning to test in AlgoGen.

Gotchas

  • The first bar. close.diff() is NaN on bar 0; fillna(0) keeps it out of the sum so OBV starts flat.
  • Exactly-flat closes. np.sign(0) is 0, so unchanged closes correctly leave OBV untouched — matching Granville’s rule.
  • Absolute value is noise. Only compare OBV’s direction to price. Never threshold on the raw OBV number; it depends on your arbitrary start point.
  • Starting value. We start at 0. Some platforms seed with the first bar’s volume; it shifts the whole line by a constant and changes nothing that matters.

An OBV signal line

Since the raw level is arbitrary, a common practical move is to smooth OBV and trade the crossover — turning a directionless total into discrete signals:

obv_line = obv(df["close"], df["volume"])
obv_ma = obv_line.rolling(20).mean()
df["obv_bull"] = (obv_line > obv_ma) & (obv_line.shift(1) <= obv_ma.shift(1))

obv_bull fires when OBV crosses above its own 20-period average — a cleaner, more testable event than eyeballing the slope, and a good companion to the divergence check above.

The lazy one-liner

import pandas_ta as ta
df["obv"] = ta.obv(df["close"], df["volume"])

Same indicator elsewhere: MQL5, Pine Script, EasyLanguage, NinjaScript.


This post is educational, not financial advice. Indicators describe the past; they don’t predict the future. Backtest anything before you risk real money on it.

Sources and further reading

  1. New Key to Stock Market Profits (Google Books)
  2. Windowing operations (pandas documentation)

Historical research from the Algogen archive. Not investment advice.

Blog

Have a strategy idea?

Describe it in plain English, preview where your rules fire, then decide whether the evidence is worth a backtest.

Keep me postedHow it works