From bc1faa8f5d51e521db5d10d21be3feee6e0e4bb6 Mon Sep 17 00:00:00 2001 From: Ross Garbutt Date: Sun, 17 Mar 2024 14:32:23 +1100 Subject: [PATCH] PVI update to match Investopedia definition --- pandas_ta/core.py | 4 +- pandas_ta/volume/pvi.py | 74 ++++++++++++++++++++++------------ tests/test_indicator_volume.py | 4 +- 3 files changed, 52 insertions(+), 30 deletions(-) diff --git a/pandas_ta/core.py b/pandas_ta/core.py index c3f90f24..b2be5634 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -1806,10 +1806,10 @@ def obv(self, offset=None, **kwargs: DictLike): result = obv(close=close, volume=volume, offset=offset, **kwargs) return self._post_process(result, **kwargs) - def pvi(self, length=None, initial=None, signed=True, offset=None, **kwargs: DictLike): + def pvi(self, length=None, initial=None, mamode=None, offset=None, **kwargs: DictLike): close = self._get_column(kwargs.pop("close", "close")) volume = self._get_column(kwargs.pop("volume", "volume")) - result = pvi(close=close, volume=volume, length=length, initial=initial, signed=signed, offset=offset, **kwargs) + result = pvi(close=close, volume=volume, length=length, initial=initial, mamode=mamode, offset=offset, **kwargs) return self._post_process(result, **kwargs) def pvo(self, fast=None, slow=None, signal=None, scalar=None, offset=None, **kwargs: DictLike): diff --git a/pandas_ta/volume/pvi.py b/pandas_ta/volume/pvi.py index bf0ec3b2..05ab5e42 100644 --- a/pandas_ta/volume/pvi.py +++ b/pandas_ta/volume/pvi.py @@ -1,14 +1,16 @@ # -*- coding: utf-8 -*- +import pandas as pd +import numpy as np from pandas import Series from pandas_ta._typing import DictLike, Int +from pandas_ta.ma import ma from pandas_ta.momentum import roc -from pandas_ta.utils import signed_series, v_offset, v_pos_default, v_series - +from pandas_ta.utils import signed_series, v_offset, v_mamode, v_pos_default, v_series def pvi( close: Series, volume: Series, length: Int = None, initial: Int = None, - offset: Int = None, **kwargs: DictLike -) -> Series: + mamode: str = None, offset: Int = None, **kwargs: DictLike +) -> pd.DataFrame: """Positive Volume Index (PVI) The Positive Volume Index is a cumulative indicator that uses volume @@ -21,8 +23,9 @@ def pvi( 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 + length (int): The short period. Default: 255 + initial (int): The short period. Default: 100 + mamode (str): See ``help(ta.ma)``. Default: 'ema' offset (int): How many periods to offset the result. Default: 0 Kwargs: @@ -30,39 +33,58 @@ def pvi( fill_method (value, optional): Type of fill method Returns: - pd.Series: New feature generated. + pd.DataFrame: New DataFrame with ['close', 'volume', 'PVI_1', 'PVIs_'] """ # Validate - length = v_pos_default(length, 1) + mamode = v_mamode(mamode, "ema") + length = v_pos_default(length, 255) close = v_series(close, length + 1) volume = v_series(volume, length + 1) + initial = v_pos_default(initial, 100) + offset = v_offset(offset) if close is None or volume is None: return - initial = v_pos_default(initial, 1000) - offset = v_offset(offset) + # Create a dataframe from the close and volume series + # retain index of the close series + df = close.to_frame('close') + df['volume'] = volume + + df['PVI_1'] = pd.Series(dtype=float) + df.iloc[0, df.columns.get_loc('PVI_1')] = initial + + # Get numpy arrays of the data + close_prices = df['close'].values + volumes = df['volume'].values + pvis = np.empty(len(df)) + + # Set the first value from from initial + pvis[0] = df.iloc[0]['PVI_1'] # Calculate - signed_volume = signed_series(volume, 1) - _roc = roc(close=close, length=length) - pvi = _roc * signed_volume[signed_volume > 0].abs() - pvi.fillna(0, inplace=True) - pvi.iloc[0] = initial - pvi = pvi.cumsum() - - # Offset + for i in range(1, len(df)): + if volumes[i] > volumes[i-1]: + # PVI = Yesterday’s PVI + [[(Close – Yesterday’s Close) / Yesterday’s Close] * Yesterday’s PVI + pvis[i] = pvis[i-1] + (((close_prices[i] - close_prices[i-1]) / close_prices[i-1]) * pvis[i-1]) + else: + # PVI = Yesterday’s PVI + pvis[i] = pvis[i-1] + + # Update the df + df['PVI_1'] = pvis + df.name = "PVI_1" + if offset != 0: - pvi = pvi.shift(offset) + df['PVI_1'] = df['PVI_1'].shift(offset) + + sig_series = ma(mamode, df['PVI_1'], length=length) + df[f'PVIs_{length}'] = sig_series # Fill if "fillna" in kwargs: - pvi.fillna(kwargs["fillna"], inplace=True) + df.fillna(kwargs["fillna"], inplace=True) if "fill_method" in kwargs: - pvi.fillna(method=kwargs["fill_method"], inplace=True) - - # Name and Category - pvi.name = f"PVI_{length}" - pvi.category = "volume" + df.fillna(method=kwargs["fill_method"], inplace=True) - return pvi + return df diff --git a/tests/test_indicator_volume.py b/tests/test_indicator_volume.py index 98290ee3..cf0a7c89 100644 --- a/tests/test_indicator_volume.py +++ b/tests/test_indicator_volume.py @@ -138,7 +138,7 @@ def test_obv(df): def test_pvi(df): result = ta.pvi(df.close, df.volume) - assert isinstance(result, Series) + assert isinstance(result, DataFrame) assert result.name == "PVI_1" @@ -253,7 +253,7 @@ def test_ext_nvi(df): def test_ext_pvi(df): df.ta.pvi(append=True) - assert df.columns[-1] == "PVI_1" + assert list(df.columns) == ["close", "volume", "PVI_1", "PVIs_255"] def test_ext_pvol(df):