diff --git a/README.md b/README.md index fb8a0d3..a73e1fc 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ Use parameter: cumulative=**True** for cumulative results. |:--------:| | ![Example ATR](/images/SPY_ATR.png) | -## _Volume_ (11) +## _Volume_ (12) * _Accumulation/Distribution Index_: **ad** * _Accumulation/Distribution Oscillator_: **adosc** @@ -198,6 +198,7 @@ Use parameter: cumulative=**True** for cumulative results. * _Money Flow Index_: **mfi** * _Negative Volume Index_: **nvi** * _On-Balance Volume_: **obv** +* _Positive Volume Index_: **pvi** * _Price-Volume_: **pvol** * _Price Volume Trend_: **pvt** * _Volume Profile_: **vp** diff --git a/pandas_ta/core.py b/pandas_ta/core.py index effba1d..f99c3c0 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -748,6 +748,13 @@ class AnalysisIndicators(BasePandasObject): self._append(result, **kwargs) return result + def pvi(self, close=None, volume=None, length=None, initial=None, signed=True, offset=None, **kwargs): + close = self._get_column(close, 'close') + volume = self._get_column(volume, 'volume') + result = pvi(close=close, volume=volume, length=length, initial=initial, signed=signed, offset=offset, **kwargs) + self._append(result, **kwargs) + return result + def pvol(self, close=None, volume=None, signed=True, offset=None, **kwargs): close = self._get_column(close, 'close') volume = self._get_column(volume, 'volume') diff --git a/pandas_ta/volume.py b/pandas_ta/volume.py index ed3bbf7..e75cd1d 100644 --- a/pandas_ta/volume.py +++ b/pandas_ta/volume.py @@ -249,7 +249,7 @@ def nvi(close, volume, length=None, initial=None, offset=None, **kwargs): offset = get_offset(offset) # Calculate Result - roc_ = roc(close=close) + roc_ = roc(close=close, length=length) signed_volume = signed_series(volume, initial=1) nvi = signed_volume[signed_volume < 0].abs() * roc_ nvi.fillna(0, inplace=True) @@ -301,6 +301,41 @@ def obv(close, volume, offset=None, **kwargs): return obv +def pvi(close, volume, length=None, initial=None, offset=None, **kwargs): + """Indicator: Positive Volume Index (PVI)""" + # 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, length=length) + signed_volume = signed_series(volume, initial=1) + pvi = signed_volume[signed_volume > 0].abs() * roc_ + pvi.fillna(0, inplace=True) + pvi.iloc[0]= initial + pvi = pvi.cumsum() + + # Offset + if offset != 0: + pvi = pvi.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + pvi.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + pvi.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + pvi.name = f"PVI_{length}" + pvi.category = 'volume' + + return pvi + + def pvol(close, volume, signed=True, offset=None, **kwargs): """Indicator: Price-Volume (PVOL)""" # Validate arguments @@ -662,7 +697,7 @@ Sources: Calculation: Default Inputs: - length=20, initial=1000 + length=1, initial=1000 ROC = Rate of Change roc = ROC(close, length) @@ -717,6 +752,43 @@ Returns: """ +pvi.__doc__ = \ +"""Positive Volume Index (PVI) + +The Positive Volume Index is a cumulative indicator that uses volume change in +an attempt to identify where smart money is active. Used in conjunction with NVI. + +Sources: + https://www.investopedia.com/terms/p/pvi.asp + +Calculation: + Default Inputs: + length=1, initial=1000 + ROC = Rate of Change + + roc = ROC(close, length) + signed_volume = signed_series(volume, initial=1) + pvi = signed_volume[signed_volume > 0].abs() * roc_ + pvi.fillna(0, inplace=True) + pvi.iloc[0]= initial + pvi = pvi.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. +""" + + pvol.__doc__ = \ """Price-Volume (PVOL) diff --git a/setup.py b/setup.py index 958e186..b9770cb 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ long_description = "An easy to use Python 3 Pandas Extension of Technical Analys setup( name = "pandas_ta", packages = ["pandas_ta"], - version = "0.1.5a", + version = "0.1.6a", description=long_description, long_description=long_description, author = "Kevin Johnson", diff --git a/tests/test_indicator_volume.py b/tests/test_indicator_volume.py index 6cc16b6..c3012c6 100644 --- a/tests/test_indicator_volume.py +++ b/tests/test_indicator_volume.py @@ -116,6 +116,11 @@ class TestVolume(TestCase): except Exception as ex: error_analysis(result, CORRELATION, ex) + def test_pvi(self): + result = self.volume.pvi(self.close, self.volume_) + self.assertIsInstance(result, Series) + self.assertEqual(result.name, 'PVI_1') + def test_pvol(self): result = self.volume.pvol(self.close, self.volume_) self.assertIsInstance(result, Series) diff --git a/tests/test_indicator_volume_ext.py b/tests/test_indicator_volume_ext.py index abcf63d..2823bfc 100644 --- a/tests/test_indicator_volume_ext.py +++ b/tests/test_indicator_volume_ext.py @@ -63,6 +63,11 @@ class TestVolumeExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], 'OBV') + def test_pvi_ext(self): + self.data.ta.pvi(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], 'PVI_1') + def test_pvol_ext(self): self.data.ta.pvol(append=True) self.assertIsInstance(self.data, DataFrame)