From 2116108b18c67a4cb968aa1c31dbc19088a10280 Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Wed, 16 Sep 2020 09:39:10 -0700 Subject: [PATCH] ENH cfo indicator added MAINT linreg refactor --- README.md | 14 +++++----- pandas_ta/__init__.py | 2 +- pandas_ta/core.py | 7 ++++- pandas_ta/momentum/cfo.py | 26 ++++++++++--------- pandas_ta/overlap/linreg.py | 38 ++++++++++++++-------------- tests/test_ext_indicator_momentum.py | 5 ++++ tests/test_indicator_momentum.py | 5 ++++ 7 files changed, 58 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 3351ae3..93bc888 100644 --- a/README.md +++ b/README.md @@ -67,14 +67,15 @@ Thank you for your contribution! * _Linear Decay_ (**linear_decay**): Renamed to _Decay_ (**decay**) and with the option for Exponential decay using ```mode="exp"```. See: ```help(ta.decay)``` ## __New Indicators__ -* _Squeeze_ (**squeeze**). A Momentum indicator. Both John Carter's TTM **and** Lazybear's TradingView versions are implemented. The default is John Carter's, or ```lazybear=False```. Set ```lazybear=True``` to enable Lazybear's. -* _TTM Trend_ (**ttm_trend**). A trend indicator inspired from John Carter's book "Mastering the Trade". +* _Chande Forecast Oscillator_ (**cfo**) It calculates the percentage difference between the actual price and the Time Series Forecast (the endpoint of a linear regression line). +* _Gann High-Low Activator_ (**hilo**) The Gann High Low Activator Indicator was created by Robert Krausz in a 1998. +* _Inside Bar_ (**cdl_inside**) An Inside Bar is a bar contained within it's previous bar's high and low See: ```help(ta.cdl_inside)``` * _SMI Ergodic_ (**smi**) Developed by William Blau, the SMI Ergodic Indicator is the same as the True Strength Index (TSI) except the SMI includes a signal line and oscillator. -* _Gann High-Low Activator_ (**hilo**) The Gann High Low Activator Indicator was created by Robert Krausz in a 1998 +* _Squeeze_ (**squeeze**). A Momentum indicator. Both John Carter's TTM **and** Lazybear's TradingView versions are implemented. The default is John Carter's, or ```lazybear=False```. Set ```lazybear=True``` to enable Lazybear's. +* _Stochastic RSI_ (**stochrsi**) "Stochastic RSI and Dynamic Momentum Index" was created by Tushar Chande and Stanley Kroll. In line with Trading View's calculation. See: ```help(ta.stochrsi)``` +* _TTM Trend_ (**ttm_trend**). A trend indicator inspired from John Carter's book "Mastering the Trade". issue of Stocks & Commodities Magazine. It is a moving average based trend indicator consisting of two different simple moving averages. -* _Stochastic RSI_ (**stochrsi**) "Stochastic RSI and Dynamic Momentum Index" was created by Tushar Chande and Stanley Kroll. In line with Trading View's calculation. See: ```help(ta.stochrsi)``` -* _Inside Bar_ (**cdl_inside**) An Inside Bar is a bar contained within it's previous bar's high and low See: ```help(ta.cdl_inside)``` ## __Updated Indicators__ * _Average True Range_ (**atr**): Added option to return **atr** as a percentage. See: ```help(ta.atr)``` @@ -365,7 +366,7 @@ print(bothhl2.name) # "pre_HL2_post" * _Inside Bar_: **cdl_inside** * _Heikin-Ashi_: **ha** -## _Momentum_ (33) +## _Momentum_ (34) * _Awesome Oscillator_: **ao** * _Absolute Price Oscillator_: **apo** @@ -373,6 +374,7 @@ print(bothhl2.name) # "pre_HL2_post" * _Balance of Power_: **bop** * _BRAR_: **brar** * _Commodity Channel Index_: **cci** +* _Chande Forecast Oscillator_: **cfo** * _Center of Gravity_: **cg** * _Chande Momentum Oscillator_: **cmo** * _Coppock Curve_: **coppock** diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index 61c7092..dada2a5 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -25,7 +25,7 @@ Category = { "candles": ["cdl_doji", "cdl_inside", "ha"], # Momentum - "momentum": ["ao", "apo", "bias", "bop", "brar", "cci", "cg", "cmo", "coppock", "er", "eri", "fisher", "inertia", "kdj", "kst", "macd", "mom", "pgo", "ppo", "psl", "pvo", "roc", "rsi", "rvgi", "slope", "smi", "squeeze", "stoch", "stochrsi", "trix", "tsi", "uo", "willr"], + "momentum": ["ao", "apo", "bias", "bop", "brar", "cci", "cfo", "cg", "cmo", "coppock", "er", "eri", "fisher", "inertia", "kdj", "kst", "macd", "mom", "pgo", "ppo", "psl", "pvo", "roc", "rsi", "rvgi", "slope", "smi", "squeeze", "stoch", "stochrsi", "trix", "tsi", "uo", "willr"], # Overlap "overlap": ["dema", "ema", "fwma", "hilo", "hl2", "hlc3", "hma", "ichimoku", "kama", "linreg", "midpoint", "midprice", "ohlc4", "pwma", "rma", "sinwma", "sma", "supertrend", "swma", "t3", "tema", "trima", "vwap", "vwma", "wcp", "wma", "zlma"], diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 05858f4..24780e2 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -23,7 +23,7 @@ from pandas_ta.volatility import * from pandas_ta.volume import * from pandas_ta.utils import * -version = ".".join(("0", "2", "09b")) +version = ".".join(("0", "2", "10b")) # Strategy DataClass @@ -684,6 +684,11 @@ class AnalysisIndicators(BasePandasObject): result = cci(high=high, low=low, close=close, length=length, c=c, offset=offset, **kwargs) return self._post_process(result, **kwargs) + def cfo(self, length=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = cfo(close=close, length=length, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + def cg(self, length=None, offset=None, **kwargs): close = self._get_column(kwargs.pop("close", "close")) result = cg(close=close, length=length, offset=offset, **kwargs) diff --git a/pandas_ta/momentum/cfo.py b/pandas_ta/momentum/cfo.py index 1d4663f..8a379f3 100644 --- a/pandas_ta/momentum/cfo.py +++ b/pandas_ta/momentum/cfo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from ..utils import get_drift, get_offset, verify_series -from ..overlap.linreg import linreg +from pandas_ta.overlap import linreg +from pandas_ta.utils import get_drift, get_offset, verify_series def cfo(close, length=None, scalar=None, drift=None, offset=None, **kwargs): """Indicator: Chande Forcast Oscillator (CFO)""" @@ -12,8 +12,9 @@ def cfo(close, length=None, scalar=None, drift=None, offset=None, **kwargs): offset = get_offset(offset) #Finding linear regression of Series - linreg_series = linreg(close,length=length) - cfo = ((close-linreg_series)/close *100) + cfo = scalar * (close - linreg(close, length=length, tsf=True)) + cfo /= close + # Offset if offset != 0: cfo = cfo.shift(offset) @@ -28,13 +29,13 @@ def cfo(close, length=None, scalar=None, drift=None, offset=None, **kwargs): cfo.name = f"CFO_{length}" cfo.category = "momentum" - return cmo + return cfo cfo.__doc__ = \ """Chande Forcast Oscillator (CFO) -The Forecast Oscillator calculates the percentage difference between the actual price -and the Time Series Forecast (the endpoint of a linear regression line). +The Forecast Oscillator calculates the percentage difference between the actual +price and the Time Series Forecast (the endpoint of a linear regression line). Sources: https://www.fmlabs.com/reference/default.htm?url=ForecastOscillator.htm @@ -42,15 +43,16 @@ Sources: Calculation: Default Inputs: length=9, drift=1, scalar=100 + LINREG = Linear Regression - # Same Calculation as RSI except for this step - CFO = ( ( CLOSE- LINERREG ) / CLOSE * 100 ) + CFO = scalar * (close - LINERREG(length, tdf=True)) / close Args: close (pd.Series): Series of 'close's - scalar (float): How much to magnify. Default: 100 - drift (int): The short period. Default: 1 - offset (int): How many periods to offset the result. Default: 0 + length (int): The period. Default: 9 + scalar (float): How much to magnify. Default: 100 + drift (int): The short period. Default: 1 + offset (int): How many periods to offset the result. Default: 0 Kwargs: fillna (value, optional): pd.DataFrame.fillna(value) diff --git a/pandas_ta/overlap/linreg.py b/pandas_ta/overlap/linreg.py index 3529a27..0fc1f96 100644 --- a/pandas_ta/overlap/linreg.py +++ b/pandas_ta/overlap/linreg.py @@ -1,20 +1,20 @@ # -*- coding: utf-8 -*- import math -from ..utils import get_offset, verify_series +from pandas_ta.utils import get_offset, verify_series def linreg(close, length=None, offset=None, **kwargs): """Indicator: Linear Regression""" # Validate arguments close = verify_series(close) 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 + min_periods = int(kwargs["min_periods"]) if "min_periods" in kwargs and kwargs["min_periods"] is not None else length offset = get_offset(offset) - angle = kwargs.pop('angle', False) - intercept = kwargs.pop('intercept', False) - degrees = kwargs.pop('degrees', False) - r = kwargs.pop('r', False) - slope = kwargs.pop('slope', False) - tsf = kwargs.pop('tsf', False) + angle = kwargs.pop("angle", False) + intercept = kwargs.pop("intercept", False) + degrees = kwargs.pop("degrees", False) + r = kwargs.pop("r", False) + slope = kwargs.pop("slope", False) + tsf = kwargs.pop("tsf", False) # Calculate Result x = range(1, length + 1) # [1, 2, ..., n] from 1 to n keeps Sum(xy) low @@ -54,10 +54,10 @@ def linreg(close, length=None, offset=None, **kwargs): linreg = linreg.shift(offset) # Handle fills - if 'fillna' in kwargs: - linreg.fillna(kwargs['fillna'], inplace=True) - if 'fill_method' in kwargs: - linreg.fillna(method=kwargs['fill_method'], inplace=True) + if "fillna" in kwargs: + linreg.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + linreg.fillna(method=kwargs["fill_method"], inplace=True) # Name and Categorize it linreg.name = f"LR" @@ -66,7 +66,7 @@ def linreg(close, length=None, offset=None, **kwargs): if angle: linreg.name += "a" if r: linreg.name += "r" linreg.name += f"_{length}" - linreg.category = 'overlap' + linreg.category = "overlap" return linreg @@ -104,12 +104,12 @@ Args: offset (int): How many periods to offset the result. Default: 0 Kwargs: - angle (bool, optional): Default: False. If True, returns the angle of the slope in radians - degrees (bool, optional): Default: False. If True, returns the angle of the slope in degrees - intercept (bool, optional): Default: False. If True, returns the angle of the slope in radians - r (bool, optional): Default: False. If True, returns it's correlation 'r' - slope (bool, optional): Default: False. If True, returns the slope - tsf (bool, optional): Default: False. If True, returns the Time Series Forecast value. + angle (bool, optional): Default: False. If True, returns the angle of the slope in radians + degrees (bool, optional): Default: False. If True, returns the angle of the slope in degrees + intercept (bool, optional): Default: False. If True, returns the angle of the slope in radians + r (bool, optional): Default: False. If True, returns it's correlation 'r' + slope (bool, optional): Default: False. If True, returns the slope + tsf (bool, optional): Default: False. If True, returns the Time Series Forecast value. fillna (value, optional): pd.DataFrame.fillna(value) fill_method (value, optional): Type of fill method diff --git a/tests/test_ext_indicator_momentum.py b/tests/test_ext_indicator_momentum.py index 8477590..cb463a4 100644 --- a/tests/test_ext_indicator_momentum.py +++ b/tests/test_ext_indicator_momentum.py @@ -49,6 +49,11 @@ class TestMomentumExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], "CCI_14_0.015") + def test_cfo_ext(self): + self.data.ta.cfo(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(self.data.columns[-1], "CFO_9") + def test_cg_ext(self): self.data.ta.cg(append=True) self.assertIsInstance(self.data, DataFrame) diff --git a/tests/test_indicator_momentum.py b/tests/test_indicator_momentum.py index 0efc501..a914360 100644 --- a/tests/test_indicator_momentum.py +++ b/tests/test_indicator_momentum.py @@ -119,6 +119,11 @@ class TestMomentum(TestCase): except Exception as ex: error_analysis(result, CORRELATION, ex) + def test_cfo(self): + result = pandas_ta.cfo(self.close) + self.assertIsInstance(result, Series) + self.assertEqual(result.name, "CFO_9") + def test_cg(self): result = pandas_ta.cg(self.close) self.assertIsInstance(result, Series)