Developed by Joseph “Joe” Granville, On Balance Volume (OBV) is a compelling and straightforward cumulative indicator designed to measure buying and selling pressure by relating price changes to volume. It works on the principle that volume precedes price movement, meaning that significant changes in OBV can foreshadow major price shifts.
The core logic behind OBV calculation is as follows:
Current OBV = Previous OBV + Current Volume
Current OBV = Previous OBV - Current Volume
Current OBV = Previous OBV
The OBV line thus rises when volume on up days (days where the price closes higher) outpaces volume on down days (days where the price closes lower). Conversely, the OBV line falls when volume on down days is heavier. The starting point for OBV is arbitrary (often the first day’s volume or zero); it’s the direction of the OBV line that matters, not its absolute numerical value.
Usage: OBV is primarily used to confirm price trends and spot potential reversals through divergences. The underlying assumption is that OBV reflects the activity of “smart money” – institutional investors and other large market participants.
TA-Lib Function: The Technical Analysis Library (TA-Lib) offers a direct function for OBV calculation:
talib.OBV(close_prices, volume)
close_prices
: An array or series of closing
prices.volume
: An array or series of volume data corresponding
to the close_prices
. It’s important that the volume data is
of a numeric type (e.g., float).Code Example (Calculation & Plot with yfinance Data):
The Python code below demonstrates fetching financial data using
yfinance
, calculating the On Balance Volume, and then
plotting the asset’s price alongside its OBV in separate panels.
import yfinance as yf
import talib
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
# --- 1. Data Fetching using yfinance ---
= "MSFT" # Example: Microsoft Corp.
ticker_symbol = yf.download(ticker_symbol, start="2023-01-01", end="2024-05-01", auto_adjust=False, progress=False)
data
if data.empty or 'Volume' not in data.columns or data['Volume'].isnull().all():
print(f"No data or insufficient volume data found for {ticker_symbol}. Exiting.")
else:
# Complying with user preference for droplevel
if isinstance(data.columns, pd.MultiIndex):
= data.columns.droplevel(level=1)
data.columns
= data['Close'].dropna()
close_prices # Ensure volume corresponds to the close_prices index after dropna
= data['Volume'].reindex_like(close_prices).dropna()
volume_data # Ensure close_prices and volume_data have the same index after potential drops
= close_prices.index.intersection(volume_data.index)
common_index = close_prices.loc[common_index]
close_prices = volume_data.loc[common_index]
volume_data = common_index
date_index
# --- 2. OBV Calculation ---
# OBV requires Close prices and Volume.
# TA-Lib expects volume to be float or double. yfinance volume is typically int.
if len(close_prices) > 1 and len(volume_data) > 1: # Needs at least two data points
# Ensure volume is float for TA-Lib compatibility
= talib.OBV(close_prices, volume_data.astype(float))
obv_values = "OBV"
indicator_name print(f"\n--- {indicator_name} - On Balance Volume for {ticker_symbol} ---")
# TA-Lib's OBV might not start with NaN if first operation is valid.
# For printing, we just use the raw output.
if len(obv_values) >= 5:
print(f"Output {indicator_name} (last 5): {obv_values[-5:].round(0)}") # OBV values can be large integers
elif len(obv_values) > 0:
print(f"Output {indicator_name} (all): {obv_values.round(0)}")
else:
print(f"Output {indicator_name}: No OBV values calculated.")
# --- 3. Plotting ---
= plt.subplots(2, 1, figsize=(14, 10), sharex=True,
fig, axes ={'height_ratios': [3, 1]}) # Price chart 3x taller
gridspec_kw
# Plot Price
0].plot(date_index, close_prices, label='Close Price', color='blue')
axes[0].set_title(f'{ticker_symbol} Price and {indicator_name}')
axes[0].set_ylabel('Price')
axes[0].legend(loc='upper left')
axes[0].grid(True)
axes[
# Plot OBV
# For better visualization, OBV is often plotted without forcing y-axis to start at 0
# if values are consistently large positive or negative. Matplotlib handles this by default.
1].plot(date_index, obv_values, label=indicator_name, color='green')
axes[1].set_ylabel('On Balance Volume (OBV)')
axes[1].set_xlabel('Date')
axes[1].ticklabel_format(style='sci', axis='y', scilimits=(0,0)) # Use scientific notation for large OBV values
axes[1].legend(loc='upper left')
axes[1].grid(True)
axes[
# Adjust layout to prevent overlap
plt.tight_layout()
plt.show()
else:
print(f"\nSkipping OBV plot for {ticker_symbol}: Insufficient data or volume data missing after alignment.")
if not data.empty:
print(f"Data available for price from {data['Close'].dropna().index.min().date()} to {data['Close'].dropna().index.max().date()}.")
print(f"Data available for volume from {data['Volume'].dropna().index.min().date()} to {data['Volume'].dropna().index.max().date()}.")
Explanation of the Code:
yfinance
,
talib
, numpy
, matplotlib.pyplot
,
and pandas
.yf.download()
fetches historical data (including
‘Close’ and ‘Volume’) for the chosen ticker_symbol
. User
preferences auto_adjust=False
and droplevel
are applied.close_prices
and volume_data
are
extracted. Crucially, volume_data
is reindexed to match
close_prices
after dropna()
to ensure they
align perfectly. Any dates not common to both are dropped using
common_index
.if len(close_prices) > 1 and len(volume_data) > 1:
ensures there’s enough data.volume_data.astype(float)
explicitly converts the
volume series to float, as TA-Lib expects this type for its
calculations.talib.OBV(close_prices, volume_data.astype(float))
computes the OBV.plt.subplots(2, 1, ...)
creates a figure with two
vertically stacked subplots.axes[1].ticklabel_format(style='sci', axis='y', scilimits=(0,0))
is used to format the y-axis of the OBV plot with scientific notation if
the numbers are very large, improving readability.plt.tight_layout()
ensures the plot elements are
well-arranged.plt.show()
displays the chart.By tracking the cumulative volume flow, OBV provides a unique perspective on market dynamics, helping traders confirm trends and identify potential reversals that might not be immediately obvious from price action alone.