From bc52d67b9963b0adcf015fbf7040c20ed7e15ea6 Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Tue, 2 Jun 2020 15:32:53 -0700 Subject: [PATCH] BUG dpo calc fix ENH pvo added MAINT minor --- README.md | 6 +- pandas_ta/core.py | 13 +++- pandas_ta/momentum/__init__.py | 1 + pandas_ta/momentum/apo.py | 4 +- pandas_ta/momentum/macd.py | 19 +++--- pandas_ta/momentum/ppo.py | 33 +++++----- pandas_ta/momentum/pvo.py | 91 ++++++++++++++++++++++++++++ pandas_ta/trend/dpo.py | 24 +++++--- tests/test_indicator_momentum.py | 7 ++- tests/test_indicator_momentum_ext.py | 9 ++- tests/test_indicator_trend.py | 2 +- tests/test_indicator_trend_ext.py | 2 +- 12 files changed, 165 insertions(+), 46 deletions(-) create mode 100644 pandas_ta/momentum/pvo.py diff --git a/README.md b/README.md index f3d94c5..054b733 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # __Technical Analysis Library in Python 3.7__ ![Example Chart](/images/TA_Chart.png) -__Pandas Technical Analysis__ (Pandas TA) is an easy to use library that is built upon Python's Pandas library with more than 100 Indicators. These indicators are comminly used for financial time series datasets with columns or labels similar to: datetime, open, high, low, close, volume, et al. Many commonly used indicators are included, such as: _Simple Moving Average_ (*SMA*) _Moving Average Convergence Divergence_ (*MACD*), _Hull Exponential Moving Average_ (*HMA*), _Bollinger Bands_ (*BBANDS*), _On-Balance Volume_ (*OBV*), _Aroon & Aroon Oscillator_ (*AROON*) and more. +__Pandas Technical Analysis__ (Pandas TA) is an easy to use library that is built upon Python's Pandas library with more than 100 Indicators. These indicators are commonly used for financial time series datasets with columns or labels similar to: datetime, open, high, low, close, volume, et al. Many commonly used indicators are included, such as: _Simple Moving Average_ (*SMA*) _Moving Average Convergence Divergence_ (*MACD*), _Hull Exponential Moving Average_ (*HMA*), _Bollinger Bands_ (*BBANDS*), _On-Balance Volume_ (*OBV*), _Aroon & Aroon Oscillator_ (*AROON*) and more. This version contains both the orignal code branch as well as a newly refactored branch with the option to use [Pandas DataFrame Extension](https://pandas.pydata.org/pandas-docs/stable/extending.html) mode. All the indicators return a named Series or a DataFrame in uppercase underscore parameter format. For example, MACD(fast=12, slow=26, signal=9) will return a DataFrame with columns: ['MACD_12_26_9', 'MACDH_12_26_9', 'MACDS_12_26_9']. @@ -38,6 +38,7 @@ All the indicators return a named Series or a DataFrame in uppercase underscore Parabolic Stop and Reverse (psar) Price Distance (pdist) Psycholigical Line (psl) + Price Volume Oscillator (pvo) Supertrend (supertrend) Weighted Closing Price (wcp) ### __Added utilities:__ @@ -188,7 +189,7 @@ df.ta.adjusted = None * _Heikin-Ashi_: **ha** -## _Momentum_ (25) +## _Momentum_ (26) * _Awesome Oscillator_: **ao** * _Absolute Price Oscillator_: **apo** @@ -206,6 +207,7 @@ df.ta.adjusted = None * _Momentum_: **mom** * _Percentage Price Oscillator_: **ppo** * _Psychological Line_: **psl** +* _Percentage Volume Oscillator_: **pvo** * _Rate of Change_: **roc** * _Relative Strength Index_: **rsi** * _Relative Vigor Index_: **rvi** diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 189dc40..b81ac0e 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -15,7 +15,7 @@ from pandas_ta.volatility import * from pandas_ta.volume import * from pandas_ta.utils import * -version = ".".join(("0", "1", "65b")) +version = ".".join(("0", "1", "67b")) def finalize(method): @wraps(method) @@ -491,10 +491,10 @@ class AnalysisIndicators(BasePandasObject): return result @finalize - def ppo(self, close=None, fast=None, slow=None, percentage=True, offset=None, **kwargs): + def ppo(self, close=None, fast=None, slow=None, scalar=None, offset=None, **kwargs): close = self._get_column(close, 'close') - result = ppo(close=close, fast=fast, slow=slow, percentage=percentage, offset=offset, **kwargs) + result = ppo(close=close, fast=fast, slow=slow, scalar=scalar, offset=offset, **kwargs) return result @finalize @@ -506,6 +506,13 @@ class AnalysisIndicators(BasePandasObject): result = psl(close=close, open_=open_, length=length, scalar=scalar, drift=drift, offset=offset, **kwargs) return result + @finalize + def pvo(self, volume=None, fast=None, slow=None, signal=None, scalar=None, offset=None, **kwargs): + volume = self._get_column(volume, 'volume') + + result = pvo(volume=volume, fast=fast, slow=slow, signal=signal, scalar=scalar, offset=offset, **kwargs) + return result + @finalize def roc(self, close=None, length=None, offset=None, **kwargs): close = self._get_column(close, 'close') diff --git a/pandas_ta/momentum/__init__.py b/pandas_ta/momentum/__init__.py index 9032346..b104c9a 100644 --- a/pandas_ta/momentum/__init__.py +++ b/pandas_ta/momentum/__init__.py @@ -15,6 +15,7 @@ from .macd import macd from .mom import mom from .ppo import ppo from .psl import psl +from .pvo import pvo from .roc import roc from .rsi import rsi from .rvi import rvi diff --git a/pandas_ta/momentum/apo.py b/pandas_ta/momentum/apo.py index 52a7b56..a0d1a5c 100644 --- a/pandas_ta/momentum/apo.py +++ b/pandas_ta/momentum/apo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from ..overlap.sma import sma -from ..utils import get_offset, verify_series +from pandas_ta.overlap import sma +from pandas_ta.utils import get_offset, verify_series def apo(close, fast=None, slow=None, offset=None, **kwargs): """Indicator: Absolute Price Oscillator (APO)""" diff --git a/pandas_ta/momentum/macd.py b/pandas_ta/momentum/macd.py index 8704a84..547f5c5 100644 --- a/pandas_ta/momentum/macd.py +++ b/pandas_ta/momentum/macd.py @@ -40,22 +40,23 @@ def macd(close, fast=None, slow=None, signal=None, offset=None, **kwargs): signalma.fillna(method=kwargs['fill_method'], inplace=True) # Name and Categorize it - macd.name = f"MACD_{fast}_{slow}_{signal}" - histogram.name = f"MACDH_{fast}_{slow}_{signal}" - signalma.name = f"MACDS_{fast}_{slow}_{signal}" - macd.category = histogram.category = signalma.category = 'momentum' + _props = f"_{fast}_{slow}_{signal}" + macd.name = f"MACD{_props}" + histogram.name = f"MACDh{_props}" + signalma.name = f"MACDs{_props}" + macd.category = histogram.category = signalma.category = "momentum" # Prepare DataFrame to return data = {macd.name: macd, histogram.name: histogram, signalma.name: signalma} - macddf = DataFrame(data) - macddf.name = f"MACD_{fast}_{slow}_{signal}" - macddf.category = 'momentum' + df = DataFrame(data) + df.name = f"MACD{_props}" + df.category = macd.category signal_indicators = kwargs.pop('signal_indicators', False) if signal_indicators: signalsdf = concat( [ - macddf, + df, signals( indicator=histogram, xa=kwargs.pop('xa', 0), @@ -84,7 +85,7 @@ def macd(close, fast=None, slow=None, signal=None, offset=None, **kwargs): return signalsdf else: - return macddf + return df diff --git a/pandas_ta/momentum/ppo.py b/pandas_ta/momentum/ppo.py index c943bc1..bdcceac 100644 --- a/pandas_ta/momentum/ppo.py +++ b/pandas_ta/momentum/ppo.py @@ -1,33 +1,35 @@ # -*- coding: utf-8 -*- from pandas import DataFrame -from ..overlap.ema import ema -from ..utils import get_offset, verify_series +from pandas_ta.overlap import ema, sma +from pandas_ta.utils import get_offset, verify_series -def ppo(close, fast=None, slow=None, signal=None, offset=None, **kwargs): +def ppo(close, fast=None, slow=None, signal=None, scalar=None, offset=None, **kwargs): """Indicator: Percentage Price Oscillator (PPO)""" # Validate Arguments close = verify_series(close) fast = int(fast) if fast and fast > 0 else 12 slow = int(slow) if slow and slow > 0 else 26 signal = int(signal) if signal and signal > 0 else 9 + scalar = float(scalar) if scalar else 100 if slow < fast: fast, slow = slow, fast min_periods = int(kwargs['min_periods']) if 'min_periods' in kwargs and kwargs['min_periods'] is not None else fast offset = get_offset(offset) # Calculate Result - fastma = close.rolling(fast, min_periods=min_periods).mean() - slowma = close.rolling(slow, min_periods=min_periods).mean() + fastma = sma(close, length=fast) + slowma = sma(close, length=slow) + ppo = scalar * (fastma - slowma) + ppo /= slowma - ppo = 100 * (fastma - slowma) / slowma - signalma = ema(close=ppo, length=signal, **kwargs) + signalma = ema(ppo, length=signal) histogram = ppo - signalma # Offset if offset != 0: ppo = ppo.shift(offset) - signalma = signalma.shift(offset) histogram = histogram.shift(offset) + signalma = signalma.shift(offset) # Handle fills if 'fillna' in kwargs: @@ -42,17 +44,17 @@ def ppo(close, fast=None, slow=None, signal=None, offset=None, **kwargs): # Name and Categorize it _props = f"_{fast}_{slow}_{signal}" ppo.name = f"PPO{_props}" - histogram.name = f"PPOH{_props}" - signalma.name = f"PPOS{_props}" - ppo.category = histogram.category = signalma.category = 'momentum' + histogram.name = f"PPOh{_props}" + signalma.name = f"PPOs{_props}" + ppo.category = histogram.category = signalma.category = "momentum" # Prepare DataFrame to return data = {ppo.name: ppo, histogram.name: histogram, signalma.name: signalma} - ppodf = DataFrame(data) - ppodf.name = f"PPO{_props}" - ppodf.category = 'momentum' + df = DataFrame(data) + df.name = f"PPO{_props}" + df.category = ppo.category - return ppodf + return df @@ -80,6 +82,7 @@ Args: fast(int): The short period. Default: 12 slow(int): The long period. Default: 26 signal(int): The signal period. Default: 9 + scalar (float): How much to magnify. Default: 100 offset(int): How many periods to offset the result. Default: 0 Kwargs: diff --git a/pandas_ta/momentum/pvo.py b/pandas_ta/momentum/pvo.py new file mode 100644 index 0000000..a08f7c0 --- /dev/null +++ b/pandas_ta/momentum/pvo.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from pandas import DataFrame +from pandas_ta.overlap import ema +from pandas_ta.utils import get_offset, verify_series + +def pvo(volume, fast=None, slow=None, signal=None, scalar=None, offset=None, **kwargs): + """Indicator: Percentage Volume Oscillator (PVO)""" + # Validate Arguments + volume = verify_series(volume) + fast = int(fast) if fast and fast > 0 else 12 + slow = int(slow) if slow and slow > 0 else 26 + signal = int(signal) if signal and signal > 0 else 9 + scalar = float(scalar) if scalar else 100 + if slow < fast: + fast, slow = slow, fast + offset = get_offset(offset) + + # Calculate Result + fastma = ema(volume, length=fast) + slowma = ema(volume, length=slow) + pvo = scalar * (fastma - slowma) + pvo /= slowma + + signalma = ema(pvo, length=signal) + histogram = pvo - signalma + + # Offset + if offset != 0: + pvo = pvo.shift(offset) + histogram = histogram.shift(offset) + signalma = signalma.shift(offset) + + # Handle fills + if 'fillna' in kwargs: + pvo.fillna(kwargs['fillna'], inplace=True) + histogram.fillna(kwargs['fillna'], inplace=True) + signalma.fillna(kwargs['fillna'], inplace=True) + if 'fill_method' in kwargs: + pvo.fillna(method=kwargs['fill_method'], inplace=True) + histogram.fillna(method=kwargs['fill_method'], inplace=True) + signalma.fillna(method=kwargs['fill_method'], inplace=True) + + # Name and Categorize it + _props = f"_{fast}_{slow}_{signal}" + pvo.name = f"PVO{_props}" + histogram.name = f"PVOh{_props}" + signalma.name = f"PVOs{_props}" + pvo.category = histogram.category = signalma.category = "momentum" + + # + data = {pvo.name: pvo, histogram.name: histogram, signalma.name: signalma} + df = DataFrame(data) + df.name = pvo.name + df.category = pvo.category + + return df + + + +pvo.__doc__ = \ +"""Percentage Volume Oscillator (PVO) + +Percentage Volume Oscillator is a Momentum Oscillator for Volume. + +Sources: + https://www.fmlabs.com/reference/default.htm?url=PVO.htm + +Calculation: + Default Inputs: + fast=12, slow=26, signal=9 + EMA = Exponential Moving Average + + PVO = (EMA(volume, fast) - EMA(volume, slow)) / EMA(volume, slow) + Signal = EMA(PVO, signal) + Histogram = PVO - Signal + +Args: + volume (pd.Series): Series of 'volume's + fast (int): The short period. Default: 12 + slow (int): The long period. Default: 26 + signal (int): The signal period. Default: 9 + scalar (float): How much to magnify. Default: 100 + 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.DataFrame: pvo, histogram, signal columns. +""" diff --git a/pandas_ta/trend/dpo.py b/pandas_ta/trend/dpo.py index e5cb002..b5c69fb 100644 --- a/pandas_ta/trend/dpo.py +++ b/pandas_ta/trend/dpo.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- -from ..utils import get_offset, verify_series +from pandas_ta.overlap import sma +from pandas_ta.utils import get_offset, verify_series def dpo(close, length=None, centered=True, offset=None, **kwargs): """Indicator: Detrend Price Oscillator (DPO)""" # Validate Arguments close = verify_series(close) - 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 + length = int(length) if length and length > 0 else 20 offset = get_offset(offset) # Calculate Result - drift = int(0.5 * length) + 1 # int((0.5 * length) + 1) - dpo = close.shift(drift) - close.rolling(length, min_periods=min_periods).mean() + t = int(0.5 * length) + 1 + ma = sma(close, length) + + dpo = close - ma.shift(t) if centered: - dpo = dpo.shift(-drift) + dpo = (close.shift(t) - ma).shift(-t) # Offset if offset != 0: @@ -40,17 +42,19 @@ Is an indicator designed to remove trend from price and make it easier to identify cycles. Sources: + https://www.tradingview.com/scripts/detrendedpriceoscillator/ + https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/dpo http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:detrended_price_osci Calculation: Default Inputs: - length=1, centered=True + length=20, centered=True SMA = Simple Moving Average - drift = int(0.5 * length) + 1 + t = int(0.5 * length) + 1 - DPO = close.shift(drift) - SMA(close, length) + DPO = close.shift(t) - SMA(close, length) if centered: - DPO = DPO.shift(-drift) + DPO = DPO.shift(-t) Args: close (pd.Series): Series of 'close's diff --git a/tests/test_indicator_momentum.py b/tests/test_indicator_momentum.py index 04dc026..0861343 100644 --- a/tests/test_indicator_momentum.py +++ b/tests/test_indicator_momentum.py @@ -166,7 +166,7 @@ class TestMomentum(TestCase): try: expected = tal.MACD(self.close) - expecteddf = DataFrame({'MACD_12_26_9': expected[0], 'MACDH_12_26_9': expected[2], 'MACDS_12_26_9': expected[1]}) + expecteddf = DataFrame({'MACD_12_26_9': expected[0], 'MACDh_12_26_9': expected[2], 'MACDs_12_26_9': expected[1]}) pdt.assert_frame_equal(result, expecteddf) except AssertionError as ae: try: @@ -222,6 +222,11 @@ class TestMomentum(TestCase): self.assertIsInstance(result, Series) self.assertEqual(result.name, 'PSL_12') + def test_pvo(self): + result = pandas_ta.pvo(self.volume) + self.assertIsInstance(result, DataFrame) + self.assertEqual(result.name, 'PVO_12_26_9') + def test_roc(self): result = pandas_ta.roc(self.close) self.assertIsInstance(result, Series) diff --git a/tests/test_indicator_momentum_ext.py b/tests/test_indicator_momentum_ext.py index 637fb08..68e729f 100644 --- a/tests/test_indicator_momentum_ext.py +++ b/tests/test_indicator_momentum_ext.py @@ -86,7 +86,7 @@ class TestMomentumExtension(TestCase): def test_macd_ext(self): self.data.ta.macd(append=True) self.assertIsInstance(self.data, DataFrame) - self.assertEqual(list(self.data.columns[-3:]), ['MACD_12_26_9', 'MACDH_12_26_9', 'MACDS_12_26_9']) + self.assertEqual(list(self.data.columns[-3:]), ['MACD_12_26_9', 'MACDh_12_26_9', 'MACDs_12_26_9']) def test_mom_ext(self): self.data.ta.mom(append=True) @@ -96,13 +96,18 @@ class TestMomentumExtension(TestCase): def test_ppo_ext(self): self.data.ta.ppo(append=True) self.assertIsInstance(self.data, DataFrame) - self.assertEqual(list(self.data.columns[-3:]), ['PPO_12_26_9', 'PPOH_12_26_9', 'PPOS_12_26_9']) + self.assertEqual(list(self.data.columns[-3:]), ['PPO_12_26_9', 'PPOh_12_26_9', 'PPOs_12_26_9']) def test_psl_ext(self): self.data.ta.psl(append=True) self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], 'PSL_12') + def test_pvo_ext(self): + self.data.ta.pvo(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(list(self.data.columns[-3:]), ['PVO_12_26_9', 'PVOh_12_26_9', 'PVOs_12_26_9']) + def test_roc_ext(self): self.data.ta.roc(append=True) self.assertIsInstance(self.data, DataFrame) diff --git a/tests/test_indicator_trend.py b/tests/test_indicator_trend.py index 269a184..90d04ff 100644 --- a/tests/test_indicator_trend.py +++ b/tests/test_indicator_trend.py @@ -107,7 +107,7 @@ class TestTrend(TestCase): def test_dpo(self): result = pandas_ta.dpo(self.close) self.assertIsInstance(result, Series) - self.assertEqual(result.name, 'DPO_1') + self.assertEqual(result.name, 'DPO_20') def test_increasing(self): result = pandas_ta.increasing(self.close) diff --git a/tests/test_indicator_trend_ext.py b/tests/test_indicator_trend_ext.py index 88afc81..5f06337 100644 --- a/tests/test_indicator_trend_ext.py +++ b/tests/test_indicator_trend_ext.py @@ -56,7 +56,7 @@ class TestTrendExtension(TestCase): def test_dpo_ext(self): self.data.ta.dpo(append=True) self.assertIsInstance(self.data, DataFrame) - self.assertEqual(self.data.columns[-1], 'DPO_1') + self.assertEqual(self.data.columns[-1], 'DPO_20') def test_increasing_ext(self): self.data.ta.increasing(append=True)