Background
On-balance volume is a technical analysis indicator intended to relate price and volume, and it is based on a cumulative total volume.
OBV is generally used to confirm price moves. The idea is that volume is higher on days where the price move is in the dominant direction, for example in a strong uptrend there is more volume on up days than down days.
On-balance volume is calculated by using the following formula:
Python Implementation
import pandas as pd
import sqlite3
# load S&P500 data from stored database
sp500_db = sqlite3.connect(database="sp500_data.sqlite")
df = pd.read_sql_query(sql="SELECT * FROM SP500",
con=sp500_db,
parse_dates={"Date"})
df.head()
level_0 | index | Date | Ticker | Adj Close | Close | High | Low | Open | Volume | ... | rsi | atr | macd | macd signal | bb_low | bb_mid | bb_high | sma | ema | ad | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 2014-04-29 | A | 35.049534 | 38.125893 | 38.304722 | 37.174534 | 38.118740 | 4688612.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 3.204858e+06 |
1 | 1 | 1 | 2014-04-29 | AAL | 33.476742 | 35.509998 | 35.650002 | 34.970001 | 35.200001 | 8994200.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 5.290623e+06 |
2 | 2 | 2 | 2014-04-29 | AAPL | 18.633333 | 21.154642 | 21.285000 | 21.053928 | 21.205000 | 337377600.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -4.328196e+07 |
3 | 3 | 3 | 2014-04-29 | ABBV | 34.034985 | 51.369999 | 51.529999 | 50.759998 | 50.939999 | 5601300.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 3.273491e+06 |
4 | 4 | 4 | 2014-04-29 | ABT | 31.831518 | 38.540001 | 38.720001 | 38.259998 | 38.369999 | 4415600.0 | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 9.599290e+05 |
5 rows × 21 columns
# remove irrelevant columns
df = df.drop('index', axis=1)
#df = df.drop('level_0', axis=1)
df.head()
level_0 | Date | Ticker | Adj Close | Close | High | Low | Open | Volume | garmin_klass_vol | rsi | atr | macd | macd signal | bb_low | bb_mid | bb_high | sma | ema | ad | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 2014-04-29 | A | 35.049534 | 38.125893 | 38.304722 | 37.174534 | 38.118740 | 4688612.0 | -0.002274 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 3.204858e+06 |
1 | 1 | 2014-04-29 | AAL | 33.476742 | 35.509998 | 35.650002 | 34.970001 | 35.200001 | 8994200.0 | -0.000788 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 5.290623e+06 |
2 | 2 | 2014-04-29 | AAPL | 18.633333 | 21.154642 | 21.285000 | 21.053928 | 21.205000 | 337377600.0 | -0.006397 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | -4.328196e+07 |
3 | 3 | 2014-04-29 | ABBV | 34.034985 | 51.369999 | 51.529999 | 50.759998 | 50.939999 | 5601300.0 | -0.062705 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 3.273491e+06 |
4 | 4 | 2014-04-29 | ABT | 31.831518 | 38.540001 | 38.720001 | 38.259998 | 38.369999 | 4415600.0 | -0.013411 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 9.599290e+05 |
import pandas_ta
df = df.set_index(['Date','Ticker'])
# compute OBV
def compute_obv(stock_data):
obv = pandas_ta.obv(close=stock_data['Close'],
volume=stock_data['Volume'],
length=14)
return obv
df['obv'] = df.groupby(level=1, group_keys=False).apply(compute_obv)
df.tail()
level_0 | Adj Close | Close | High | Low | Open | Volume | garmin_klass_vol | rsi | atr | macd | macd signal | bb_low | bb_mid | bb_high | sma | ema | ad | obv | ||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Date | Ticker | |||||||||||||||||||
2024-04-25 | XYL | 1232347 | 130.610001 | 130.610001 | 131.199997 | 128.100006 | 129.619995 | 963600.0 | 0.000264 | 61.482745 | 0.665306 | 0.355146 | 0.255276 | 4.845591 | 4.863502 | 4.881413 | 128.482000 | 128.582695 | 1.381747e+08 | 42359600.0 |
YUM | 1232348 | 141.559998 | 141.559998 | 142.169998 | 140.389999 | 141.979996 | 1693100.0 | 0.000076 | 65.668163 | 0.322566 | 0.550830 | 0.285784 | 4.913482 | 4.937968 | 4.962454 | 138.497000 | 138.658563 | 8.902989e+07 | 253995207.0 | |
ZBH | 1232349 | 119.750000 | 119.750000 | 121.349998 | 118.769997 | 120.709999 | 1078800.0 | 0.000206 | 35.636078 | -0.350196 | -0.889200 | -0.646480 | 4.772846 | 4.835888 | 4.898931 | 125.012999 | 123.565569 | 7.870077e+07 | -62096220.0 | |
ZBRA | 1232350 | 292.529999 | 292.529999 | 293.290009 | 271.630005 | 274.359985 | 674700.0 | 0.001355 | 56.172272 | 0.500501 | -0.391439 | -0.242456 | 5.588730 | 5.666387 | 5.744044 | 288.205502 | 284.262362 | 5.349014e+07 | 37474700.0 | |
ZTS | 1232351 | 153.360001 | 153.360001 | 153.589996 | 150.039993 | 150.970001 | 4567200.0 | 0.000178 | 39.809619 | 1.374957 | -3.202379 | -3.584353 | 4.967408 | 5.065065 | 5.162723 | 157.579352 | 157.003521 | 2.636284e+08 | 255609600.0 |
# update the database
df = df.reset_index()
df.to_sql(name="SP500",
con=sp500_db,
if_exists="replace",
index=True)
sp500_db.close()
import matplotlib.pyplot as plt
from datetime import datetime
# select AAPL
aapl = df[df['Ticker'] == 'AAPL'].set_index('Date')
# only select the data from 2022-01-01
aapl_new = aapl[aapl.index > datetime(2022,1,1)]
# set the theme of the chart
plt.style.use('fivethirtyeight')
plt.rcParams['figure.figsize'] = (20,16)
# create two charts on the same figure
ax1 = plt.subplot2grid((10,1),(0,0), rowspan=4, colspan=1)
ax2 = plt.subplot2grid((10,1),(5,0), rowspan=4, colspan=1)
# plot the closing price on the first chart
ax1.plot(aapl_new['Adj Close'])
ax1.set_title('AAPL Adjust Close Price')
# plot the OBV on the second chart
ax2.plot(aapl_new['obv'], color='orange', linewidth=1)
ax2.set_title('AAPL On-balance Volume')
Text(0.5, 1.0, 'AAPL On-balance Volume')
Reference
On-Balance Volume (OBV): Definition, Formula, and Uses As Indicator by Adam Hayes on Investopedia.