volume indicators added

This commit is contained in:
Kevin Johnson
2019-02-26 08:54:12 -08:00
parent bb10952fb7
commit 71b9cbbf44
2 changed files with 817 additions and 0 deletions
+2
View File
@@ -6,4 +6,6 @@ pandas_ta/__init__.py
pandas_ta/core.py
pandas_ta/overlap.py
pandas_ta/performance.py
pandas_ta/statistics.py
pandas_ta/utils.py
pandas_ta/volatility.py
+815
View File
@@ -0,0 +1,815 @@
# -*- coding: utf-8 -*-
"""
.. module:: volume
:synopsis: Volume Indicators.
.. moduleauthor:: Dario Lopez Padial (Bukosabino)
"""
import numpy as np
import pandas as pd
from .utils import get_drift, get_offset, signed_series, verify_series
from .momentum import roc
from .overlap import hl2, hlc3, ema
def ad(high, low, close, volume, open_=None, offset=None, **kwargs):
"""Indicator: Accumulation/Distribution (AD)"""
# Validate Arguments
high = verify_series(high)
low = verify_series(low)
close = verify_series(close)
volume = verify_series(volume)
offset = get_offset(offset)
# Calculate Result
if open_ is not None:
open_ = verify_series(open_)
ad = close - open_ # AD with Open
else:
ad = 2 * close - high - low # AD with High, Low, Close
hl_range = high - low
ad *= volume / hl_range
ad = ad.cumsum()
# Offset
if offset != 0:
ad = ad.shift(offset)
# Handle fills
if 'fillna' in kwargs:
ad.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
ad.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
ad.name = f"AD"
ad.category = 'volume'
return ad
def adosc(high, low, close, volume, open_=None, fast=None, slow=None, offset=None, **kwargs):
"""Indicator: Accumulation/Distribution Oscillator"""
# Validate Arguments
high = verify_series(high)
low = verify_series(low)
close = verify_series(close)
volume = verify_series(volume)
fast = int(fast) if fast and fast > 0 else 12
slow = int(slow) if slow and slow > 0 else 26
if slow < fast:
fast, slow = slow, fast
offset = get_offset(offset)
# Calculate Result
ad_ = ad(high=high, low=low, close=close, volume=volume, open_=open_)
fast_ad = ema(close=ad_, length=fast)
slow_ad = ema(close=ad_, length=slow)
adosc = fast_ad - slow_ad
# Offset
if offset != 0:
adosc = adosc.shift(offset)
# Handle fills
if 'fillna' in kwargs:
adosc.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
adosc.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
adosc.name = f"ADOSC_{fast}_{slow}"
adosc.category = 'volume'
return adosc
def cmf(high, low, close, volume, open_=None, length=None, offset=None, **kwargs):
"""Indicator: Chaikin Money Flow (CMF)"""
# Validate Arguments
high = verify_series(high)
low = verify_series(low)
close = verify_series(close)
volume = verify_series(volume)
length = int(length) if length and length > 0 else 20
min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length
offset = get_offset(offset)
# Calculate Result
if open_ is not None:
open_ = verify_series(open_)
ad = close - open_ # AD with Open
else:
ad = 2 * close - high - low # AD with High, Low, Close
hl_range = high - low
ad *= volume / hl_range
cmf = ad.rolling(length, min_periods=min_periods).sum() / volume.rolling(length, min_periods=min_periods).sum()
# Offset
if offset != 0:
cmf = cmf.shift(offset)
# Handle fills
if 'fillna' in kwargs:
cmf.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
cmf.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
cmf.name = f"CMF_{length}"
cmf.category = 'volume'
return cmf
def efi(close, volume, length=None, drift=None, mamode=None, offset=None, **kwargs):
"""Indicator: Elder's Force Index (EFI)"""
# Validate arguments
close = verify_series(close)
volume = verify_series(volume)
length = int(length) if length and length > 0 else 13
min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length
drift = get_drift(drift)
mamode = mamode.lower() if mamode else None
offset = get_offset(offset)
# Calculate Result
pv_diff = close.diff(drift) * volume
if mamode == 'sma':
efi = pv_diff.rolling(length, min_periods=min_periods).mean()
else:
efi = pv_diff.ewm(span=length, min_periods=min_periods).mean()
# Offset
if offset != 0:
efi = efi.shift(offset)
# Handle fills
if 'fillna' in kwargs:
efi.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
efi.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
efi.name = f"EFI_{length}"
efi.category = 'volume'
return efi
def eom(high, low, close, volume, length=None, divisor=None, drift=None, offset=None, **kwargs):
"""Indicator: Ease of Movement (EOM)"""
# Validate arguments
high = verify_series(high)
low = verify_series(low)
close = verify_series(close)
volume = verify_series(volume)
length = int(length) if length and length > 0 else 14
min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length
divisor = divisor if divisor and divisor > 0 else 100000000
drift = get_drift(drift)
offset = get_offset(offset)
# Calculate Result
hl_range = high - low
distance = hl2(high=high, low=low) - hl2(high=high.shift(drift), low=low.shift(drift))
box_ratio = (volume / divisor) / hl_range
eom = distance / box_ratio
eom = eom.rolling(length, min_periods=min_periods).mean()
# Offset
if offset != 0:
eom = eom.shift(offset)
# Handle fills
if 'fillna' in kwargs:
eom.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
eom.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
eom.name = f"EOM_{length}_{divisor}"
eom.category = 'volume'
return eom
def mfi(high, low, close, volume, length=None, drift=None, offset=None, **kwargs):
"""Indicator: Money Flow Index (MFI)"""
# Validate arguments
high = verify_series(high)
low = verify_series(low)
close = verify_series(close)
volume = verify_series(volume)
length = int(length) if length and length > 0 else 14
drift = get_drift(drift)
offset = get_offset(offset)
# Calculate Result
typical_price = hlc3(high=high, low=low, close=close)
raw_money_flow = typical_price * volume
tdf = pd.DataFrame({'diff': 0, 'rmf': raw_money_flow, '+mf': 0, '-mf': 0})
tdf.loc[(typical_price.diff(drift) > 0), 'diff'] = 1
tdf.loc[tdf['diff'] == 1, '+mf'] = raw_money_flow
tdf.loc[(typical_price.diff(drift) < 0), 'diff'] = -1
tdf.loc[tdf['diff'] == -1, '-mf'] = raw_money_flow
psum = tdf['+mf'].rolling(length).sum()
nsum = tdf['-mf'].rolling(length).sum()
tdf['mr'] = psum / nsum
mfi = 100 * psum / (psum + nsum)
tdf['mfi'] = mfi
# Offset
if offset != 0:
mfi = mfi.shift(offset)
# Handle fills
if 'fillna' in kwargs:
mfi.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
mfi.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
mfi.name = f"MFI_{length}"
mfi.category = 'momentum'
return mfi
def nvi(close, volume, length=None, initial=None, offset=None, **kwargs):
"""Indicator: Negative Volume Index (NVI)"""
# Validate arguments
close = verify_series(close)
volume = verify_series(volume)
length = int(length) if length and length > 0 else 1
min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else length
initial = int(initial) if initial and initial > 0 else 1000
offset = get_offset(offset)
# Calculate Result
roc_ = roc(close=close)
signed_volume = signed_series(volume, initial=1)
nvi = signed_volume[signed_volume < 0].abs() * roc_
nvi.fillna(0, inplace=True)
nvi.iloc[0]= initial
nvi = nvi.cumsum()
# Offset
if offset != 0:
nvi = nvi.shift(offset)
# Handle fills
if 'fillna' in kwargs:
nvi.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
nvi.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
nvi.name = f"NVI_{length}"
nvi.category = 'volume'
return nvi
def obv(close, volume, offset=None, **kwargs):
"""Indicator: On Balance Volume (OBV)"""
# Validate arguments
close = verify_series(close)
volume = verify_series(volume)
offset = get_offset(offset)
# Calculate Result
signed_volume = signed_series(close, initial=1) * volume
obv = signed_volume.cumsum()
# Offset
if offset != 0:
obv = obv.shift(offset)
# Handle fills
if 'fillna' in kwargs:
obv.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
obv.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
obv.name = f"OBV"
obv.category = 'volume'
return obv
def pvol(close, volume, signed=True, offset=None, **kwargs):
"""Indicator: Price-Volume (PVOL)"""
# Validate arguments
close = verify_series(close)
volume = verify_series(volume)
offset = get_offset(offset)
# Calculate Result
if signed:
pvol = signed_series(close, 1) * close * volume
else:
pvol = close * volume
# Offset
if offset != 0:
pvol = pvol.shift(offset)
# Handle fills
if 'fillna' in kwargs:
pvol.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
pvol.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
pvol.name = f"PVOL"
pvol.category = 'volume'
return pvol
def pvt(close, volume, drift=None, offset=None, **kwargs):
"""Indicator: Price-Volume Trend (PVT)"""
# Validate arguments
close = verify_series(close)
volume = verify_series(volume)
drift = get_drift(drift)
offset = get_offset(offset)
# Calculate Result
pv = roc(close=close, length=drift) * volume
pvt = pv.cumsum()
# Offset
if offset != 0:
pvt = pvt.shift(offset)
# Handle fills
if 'fillna' in kwargs:
pvt.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
pvt.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
pvt.name = f"PVT"
pvt.category = 'volume'
return pvt
def vp(close, volume, width=None, **kwargs):
"""Indicator: Volume Profile (VP)"""
# Validate arguments
close = verify_series(close)
volume = verify_series(volume)
width = int(width) if width and width > 0 else 10
# Setup
signed_volume = signed_series(volume, initial=1)
pos_volume = signed_volume[signed_volume > 0] * volume
neg_volume = signed_volume[signed_volume < 0] * -volume
vp = pd.concat([close, pos_volume, neg_volume], axis=1)
close_col = f"{vp.columns[0]}"
high_price_col = f"high_{close_col}"
low_price_col = f"low_{close_col}"
mean_price_col = f"mean_{close_col}"
mid_price_col = f"mid_{close_col}"
volume_col = f"{vp.columns[1]}"
pos_volume_col = f"pos_{volume_col}"
neg_volume_col = f"neg_{volume_col}"
total_volume_col = f"total_{volume_col}"
vp.columns = [close_col, pos_volume_col, neg_volume_col]
# sort_close: Sort by close before splitting into ranges. Default: False
# If False, it sorts by date index or chronological versus by price
if kwargs.pop('sort_close', False):
vp.sort_values(by=[close_col], inplace=True)
# Calculate Result
vp_ranges = np.array_split(vp, width)
result = ({
low_price_col: r[close_col].min(),
mean_price_col: r[close_col].mean(),
high_price_col: r[close_col].max(),
pos_volume_col: r[pos_volume_col].sum(),
neg_volume_col: r[neg_volume_col].sum(),
} for r in vp_ranges)
vpdf = pd.DataFrame(result)
vpdf[total_volume_col] = vpdf[pos_volume_col] + vpdf[neg_volume_col]
# Handle fills
if 'fillna' in kwargs:
vpdf.fillna(kwargs['fillna'], inplace=True)
if 'fill_method' in kwargs:
vpdf.fillna(method=kwargs['fill_method'], inplace=True)
# Name and Categorize it
vpdf.name = f"VP"
vpdf.category = 'volume'
return vpdf
# Volume Documentation
ad.__doc__ = \
"""Accumulation/Distribution (AD)
Accumulation/Distribution indicator utilizes the relative position
of the close to it's High-Low range with volume. Then it is cumulated.
Sources:
https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/accumulationdistribution-ad/
Calculation:
CUM = Cumulative Sum
if 'open':
AD = close - open
else:
AD = 2 * close - high - low
hl_range = high - low
AD = AD * volume / hl_range
AD = CUM(AD)
Args:
high (pd.Series): Series of 'high's
low (pd.Series): Series of 'low's
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
open (pd.Series): Series of 'open's
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
adosc.__doc__ = \
"""Accumulation/Distribution Oscillator or Chaikin Oscillator
Accumulation/Distribution Oscillator indicator utilizes
Accumulation/Distribution and treats it similarily to MACD
or APO.
Sources:
https://www.investopedia.com/articles/active-trading/031914/understanding-chaikin-oscillator.asp
Calculation:
Default Inputs:
fast=12, slow=26
AD = Accum/Dist
ad = AD(high, low, close, open)
fast_ad = EMA(ad, fast)
slow_ad = EMA(ad, slow)
ADOSC = fast_ad - slow_ad
Args:
high (pd.Series): Series of 'high's
low (pd.Series): Series of 'low's
close (pd.Series): Series of 'close's
open (pd.Series): Series of 'open's
volume (pd.Series): Series of 'volume's
fast (int): The short period. Default: 12
slow (int): The long period. Default: 26
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
cmf.__doc__ = \
"""Chaikin Money Flow (CMF)
Chailin Money Flow measures the amount of money flow volume over a specific
period in conjunction with Accumulation/Distribution.
Sources:
https://www.tradingview.com/wiki/Chaikin_Money_Flow_(CMF)
https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:chaikin_money_flow_cmf
Calculation:
Default Inputs:
length=20
if 'open':
ad = close - open
else:
ad = 2 * close - high - low
hl_range = high - low
ad = ad * volume / hl_range
CMF = SUM(ad, length) / SUM(volume, length)
Args:
high (pd.Series): Series of 'high's
low (pd.Series): Series of 'low's
close (pd.Series): Series of 'close's
open (pd.Series): Series of 'open's
volume (pd.Series): Series of 'volume's
length (int): The short period. Default: 20
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
efi.__doc__ = \
"""Elder's Force Index (EFI)
Elder's Force Index measures the power behind a price movement using price
and volume as well as potential reversals and price corrections.
Sources:
https://www.tradingview.com/wiki/Elder%27s_Force_Index_(EFI)
https://www.motivewave.com/studies/elders_force_index.htm
Calculation:
Default Inputs:
length=20, drift=1, mamode=None
EMA = Exponential Moving Average
SMA = Simple Moving Average
pv_diff = close.diff(drift) * volume
if mamode == 'sma':
EFI = SMA(pv_diff, length)
else:
EFI = EMA(pv_diff, length)
Args:
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
length (int): The short period. Default: 13
drift (int): The diff period. Default: 1
mamode (str): Two options: None or 'sma'. Default: None
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
eom.__doc__ = \
"""Ease of Movement (EOM)
Ease of Movement is a volume based oscillator that is designed to measure the
relationship between price and volume flucuating across a zero line.
Sources:
https://www.tradingview.com/wiki/Ease_of_Movement_(EOM)
https://www.motivewave.com/studies/ease_of_movement.htm
https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ease_of_movement_emv
Calculation:
Default Inputs:
length=14, divisor=100000000, drift=1
SMA = Simple Moving Average
hl_range = high - low
distance = 0.5 * (high - high.shift(drift) + low - low.shift(drift))
box_ratio = (volume / divisor) / hl_range
eom = distance / box_ratio
EOM = SMA(eom, length)
Args:
high (pd.Series): Series of 'high's
low (pd.Series): Series of 'low's
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
length (int): The short period. Default: 14
drift (int): The diff period. Default: 1
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
mfi.__doc__ = \
"""Money Flow Index (MFI)
Money Flow Index is an oscillator indicator that is used to measure buying and
selling pressure by utilizing both price and volume.
Sources:
https://www.tradingview.com/wiki/Money_Flow_(MFI)
Calculation:
Default Inputs:
length=14, drift=1
tp = typical_price = hlc3 = (high + low + close) / 3
rmf = raw_money_flow = tp * volume
pmf = pos_money_flow = SUM(rmf, length) if tp.diff(drift) > 0 else 0
nmf = neg_money_flow = SUM(rmf, length) if tp.diff(drift) < 0 else 0
MFR = money_flow_ratio = pmf / nmf
MFI = money_flow_index = 100 * pmf / (pmf + nmf)
Args:
high (pd.Series): Series of 'high's
low (pd.Series): Series of 'low's
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
length (int): The sum period. Default: 14
drift (int): The difference period. Default: 1
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
nvi.__doc__ = \
"""Negative Volume Index (NVI)
The Negative Volume Index is a cumulative indicator that uses volume change in
an attempt to identify where smart money is active.
Sources:
https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:negative_volume_inde
https://www.motivewave.com/studies/negative_volume_index.htm
Calculation:
Default Inputs:
length=20, initial=1000
ROC = Rate of Change
roc = ROC(close, length)
signed_volume = signed_series(volume, initial=1)
nvi = signed_volume[signed_volume < 0].abs() * roc_
nvi.fillna(0, inplace=True)
nvi.iloc[0]= initial
nvi = nvi.cumsum()
Args:
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
length (int): The short period. Default: 13
initial (int): The short period. Default: 1000
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
obv.__doc__ = \
"""On Balance Volume (OBV)
On Balance Volume is a cumulative indicator to measure buying and selling
pressure.
Sources:
https://www.tradingview.com/wiki/On_Balance_Volume_(OBV)
https://www.tradingtechnologies.com/help/x-study/technical-indicator-definitions/on-balance-volume-obv/
https://www.motivewave.com/studies/on_balance_volume.htm
Calculation:
signed_volume = signed_series(close, initial=1) * volume
obv = signed_volume.cumsum()
Args:
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
pvol.__doc__ = \
"""Price-Volume (PVOL)
Returns a series of the product of price and volume.
Calculation:
if signed:
pvol = signed_series(close, 1) * close * volume
else:
pvol = close * volume
Args:
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
signed (bool): Keeps the sign of the difference in 'close's. Default: True
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
pvt.__doc__ = \
"""Price-Volume Trend (PVT)
The Price-Volume Trend utilizes the Rate of Change with volume to
and it's cumulative values to determine money flow.
Sources:
https://www.tradingview.com/wiki/Price_Volume_Trend_(PVT)
Calculation:
Default Inputs:
drift=1
ROC = Rate of Change
pv = ROC(close, drift) * volume
PVT = pv.cumsum()
Args:
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
drift (int): The diff period. Default: 1
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: New feature generated.
"""
vp.__doc__ = \
"""Volume Profile (VP)
Calculates the Volume Profile by slicing price into ranges. Note: Value Area is not calculated.
Sources:
https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:volume_by_price
https://www.tradingview.com/wiki/Volume_Profile
http://www.ranchodinero.com/volume-tpo-essentials/
https://www.tradingtechnologies.com/blog/2013/05/15/volume-at-price/
Calculation:
Default Inputs:
width=10
vp = pd.concat([close, pos_volume, neg_volume], axis=1)
vp_ranges = np.array_split(vp, width)
result = ({high_close, low_close, mean_close, neg_volume, pos_volume} foreach range in vp_ranges)
vpdf = pd.DataFrame(result)
vpdf['total_volume'] = vpdf['pos_volume'] + vpdf['neg_volume']
Args:
close (pd.Series): Series of 'close's
volume (pd.Series): Series of 'volume's
width (int): How many ranges to distrubute price into. Default: 10
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
sort_close (value, optional): Whether to sort by close before splitting into ranges. Default: False
Returns:
pd.DataFrame: New feature generated.
"""