diff --git a/README.md b/README.md index 8fe7b07..31c06d1 100644 --- a/README.md +++ b/README.md @@ -667,6 +667,7 @@ df = df.ta.cdl_pattern(name=["doji", "inside"]) ### **Cycles** (1) * _Even Better Sinewave_: **ebsw** +* _Reflex_ (companion of trendflex): **reflex**
@@ -820,6 +821,7 @@ Use parameter: cumulative=**True** for cumulative results. * _Parabolic Stop and Reverse_: **psar** * _Q Stick_: **qstick** * _Short Run_: **short_run** +* _Trendflex_ (companion of reflex): **trendflex** * _Trend Signals_: **tsignals** * _TTM Trend_: **ttm_trend** * _Vertical Horizontal Filter_: **vhf** diff --git a/df_file b/df_file new file mode 100644 index 0000000..d534068 Binary files /dev/null and b/df_file differ diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index 3fdb0c2..dd43365 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -43,7 +43,7 @@ Category = { "cdl_pattern", "cdl_z", "ha" ], # Cycles - "cycles": ["ebsw"], + "cycles": ["ebsw", "reflex"], # Momentum "momentum": [ "ao", "apo", "bias", "bop", "brar", "cci", "cfo", "cg", "cmo", @@ -70,7 +70,7 @@ Category = { # Trend "trend": [ "adx", "amat", "aroon", "chop", "cksp", "decay", "decreasing", "dpo", - "increasing", "long_run", "psar", "qstick", "short_run", "tsignals", + "increasing", "long_run", "psar", "qstick", "short_run", "trendflex", "tsignals", "ttm_trend", "vhf", "vortex", "xsignals" ], # Volatility diff --git a/pandas_ta/core.py b/pandas_ta/core.py index a3db7df..9392865 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -900,6 +900,11 @@ class AnalysisIndicators(BasePandasObject): close = self._get_column(kwargs.pop("close", "close")) result = ebsw(close=close, length=length, bars=bars, offset=offset, **kwargs) return self._post_process(result, **kwargs) + + def reflex(self, close=None, length=None, smooth_bars=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = reflex(close=close, length=length, smooth_bars=bars, offset=offset, **kwargs) + return self._post_process(result, **kwargs) # Momentum def ao(self, fast=None, slow=None, offset=None, **kwargs): @@ -1499,7 +1504,12 @@ class AnalysisIndicators(BasePandasObject): close = self._get_column(kwargs.pop("close", "close")) result = supertrend(high=high, low=low, close=close, period=period, multiplier=multiplier, mamode=mamode, drift=drift, offset=offset, **kwargs) return self._post_process(result, **kwargs) - + + def trendflex(self, close=None, length=None, smooth_bars=None, offset=None, **kwargs): + close = self._get_column(kwargs.pop("close", "close")) + result = trendflex(close=close, length=length, smooth_bars=bars, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + def tsignals(self, trend=None, asbool=None, trend_reset=None, trend_offset=None, offset=None, **kwargs): if trend is None: return self._df diff --git a/pandas_ta/cycles/__init__.py b/pandas_ta/cycles/__init__.py index 65bf123..9289e7f 100644 --- a/pandas_ta/cycles/__init__.py +++ b/pandas_ta/cycles/__init__.py @@ -1,2 +1,3 @@ # -*- coding: utf-8 -*- from .ebsw import ebsw +from .reflex import reflex diff --git a/pandas_ta/cycles/ebsw.py b/pandas_ta/cycles/ebsw.py index 6fc008b..438e84e 100644 --- a/pandas_ta/cycles/ebsw.py +++ b/pandas_ta/cycles/ebsw.py @@ -5,58 +5,94 @@ from numpy import nan as npNaN from numpy import pi as npPi from numpy import sin as npSin from numpy import sqrt as npSqrt +from numpy import zeros as npZeros +from numpy import roll as npRoll +from numpy import mean as npMean from pandas import Series from pandas_ta.utils import get_offset, verify_series -def ebsw(close, length=None, bars=None, offset=None, **kwargs): +def ebsw(close, length=None, bars=None, offset=None, initial_version=False, **kwargs): """Indicator: Even Better SineWave (EBSW)""" # Validate arguments - length = int(length) if length and length > 38 else 40 + length = int(length) if length and length > 10 else 40 bars = int(bars) if bars and bars > 0 else 10 close = verify_series(close, length) + initial_version = bool(initial_version) # allow initial version to be used (more responsive/caution!) offset = get_offset(offset) if close is None: return - # variables - alpha1 = HP = 0 # alpha and HighPass - a1 = b1 = c1 = c2 = c3 = 0 - Filt = Pwr = Wave = 0 + if initial_version: # not the default version that is active + # variables + alpha1 = HP = 0 # alpha and HighPass + a1 = b1 = c1 = c2 = c3 = 0 + Filt = Pwr = Wave = 0 - lastClose = lastHP = 0 - FilterHist = [0, 0] # Filter history + lastClose = lastHP = 0 + FilterHist = [0, 0] # Filter history - # Calculate Result - m = close.size - result = [npNaN for _ in range(0, length - 1)] + [0] - for i in range(length, m): - # HighPass filter cyclic components whose periods are shorter than Duration input - alpha1 = (1 - npSin(360 / length)) / npCos(360 / length) - HP = 0.5 * (1 + alpha1) * (close[i] - lastClose) + alpha1 * lastHP + # Calculate Result + m = close.size + result = [npNaN for _ in range(0, length - 1)] + [0] + for i in range(length, m): + # HighPass filter cyclic components whose periods are shorter than Duration input + alpha1 = (1 - npSin(360 / length)) / npCos(360 / length) + HP = 0.5 * (1 + alpha1) * (close[i] - lastClose) + alpha1 * lastHP - # Smooth with a Super Smoother Filter from equation 3-3 - a1 = npExp(-npSqrt(2) * npPi / bars) - b1 = 2 * a1 * npCos(npSqrt(2) * 180 / bars) - c2 = b1 - c3 = -1 * a1 * a1 + # Smooth with a Super Smoother Filter from equation 3-3 + a1 = npExp(-npSqrt(2) * npPi / bars) + b1 = 2 * a1 * npCos(npSqrt(2) * 180 / bars) + c2 = b1 + c3 = -1 * a1 * a1 + c1 = 1 - c2 - c3 + Filt = c1 * (HP + lastHP) / 2 + c2 * FilterHist[1] + c3 * FilterHist[0] + # Filt = float("{:.8f}".format(float(Filt))) # to fix for small scientific notations, the big ones fail + + # 3 Bar average of Wave amplitude and power + Wave = (Filt + FilterHist[1] + FilterHist[0]) / 3 + Pwr = (Filt * Filt + FilterHist[1] * FilterHist[1] + FilterHist[0] * FilterHist[0]) / 3 + + # Normalize the Average Wave to Square Root of the Average Power + Wave = Wave / npSqrt(Pwr) + + # update storage, result + FilterHist.append(Filt) # append new Filt value + FilterHist.pop(0) # remove first element of list (left) -> updating/trim + lastHP = HP + lastClose = close[i] + result.append(Wave) + else: # this version is the default version + # Instance Variables + lastHP = lastClose = 0 + filtHist = npZeros(3) + result = [npNaN] * (length - 1) + [0] + + # Calculate constants + angle = 2 * npPi / length + alpha1 = (1 - npSin(angle)) / npCos(angle) + ang = 2 ** .5 * npPi / bars + a1 = npExp(-ang) + c2 = 2 * a1 * npCos(ang) + c3 = -a1 ** 2 c1 = 1 - c2 - c3 - Filt = c1 * (HP + lastHP) / 2 + c2 * FilterHist[1] + c3 * FilterHist[0] - # Filt = float("{:.8f}".format(float(Filt))) # to fix for small scientific notations, the big ones fail - # 3 Bar average of Wave amplitude and power - Wave = (Filt + FilterHist[1] + FilterHist[0]) / 3 - Pwr = (Filt * Filt + FilterHist[1] * FilterHist[1] + FilterHist[0] * FilterHist[0]) / 3 + for i in range(length, close.size): + HP = 0.5 * (1 + alpha1) * (close[i] - lastClose) + alpha1 * lastHP - # Normalize the Average Wave to Square Root of the Average Power - Wave = Wave / npSqrt(Pwr) + # Rotate filters to overwrite oldest value + filtHist = npRoll(filtHist, -1) + filtHist[-1] = c1 * (HP + lastHP) / 2 + c2 * filtHist[1] + c3 * filtHist[0] - # update storage, result - FilterHist.append(Filt) # append new Filt value - FilterHist.pop(0) # remove first element of list (left) -> updating/trim - lastHP = HP - lastClose = close[i] - result.append(Wave) + # Wave calculation + wave = npMean(filtHist) + rms = npSqrt(npMean(filtHist ** 2)) + wave = wave / rms + + # Update past values + lastHP = HP + lastClose = close[i] + result.append(wave) ebsw = Series(result, index=close.index) @@ -78,22 +114,30 @@ def ebsw(close, length=None, bars=None, offset=None, **kwargs): ebsw.__doc__ = \ -"""Even Better SineWave (EBSW) *beta* +"""Even Better SineWave (EBSW) This indicator measures market cycles and uses a low pass filter to remove noise. Its output is bound signal between -1 and 1 and the maximum length of a detected trend is limited by its length input. Written by rengel8 for Pandas TA based on a publication at 'prorealcode.com' and -a book by J.F.Ehlers. +a book by J.F.Ehlers. According to the suggestion by Squigglez2* and major differences between +the initial version's output close to the implementation from Ehler's, the default version is now +more closely related to the code from pro-realcode. -* This implementation seems to be logically limited. It would make sense to -implement exactly the version from prorealcode and compare the behaviour. +Remark: +The default version is now more cycle oriented and tends to be less whipsaw-prune. Thus the older version +might offer earlier signals at medium and stronger reversals. +A test against the version at TradingView showed very close results with the advantage to be one bar/candle +faster, than the corresponding reference value. This might be pre-roll related and was not further investigated. + + +* https://github.com/twopirllc/pandas-ta/issues/350 Sources: - https://www.prorealcode.com/prorealtime-indicators/even-better-sinewave/ - J.F.Ehlers 'Cycle Analytics for Traders', 2014 + - https://www.prorealcode.com/prorealtime-indicators/even-better-sinewave/ + - J.F.Ehlers 'Cycle Analytics for Traders', 2014 Calculation: refer to 'sources' or implementation diff --git a/pandas_ta/cycles/reflex.py b/pandas_ta/cycles/reflex.py new file mode 100644 index 0000000..8d9b81a --- /dev/null +++ b/pandas_ta/cycles/reflex.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +from numpy import NaN as npNaN +from numpy import cos as npCos +from numpy import exp as npExp +from numpy import full as npFull +from numpy import sqrt as npSqrt +from pandas import DataFrame, Series, concat +from pandas_ta.overlap import rma +from pandas_ta.utils import get_drift, get_offset, verify_series, signals + + +def reflex(close, length=None, smooth_bars=None, offset=None, **kwargs): + """Indicator: Reflex""" + # Validate arguments + close = verify_series(close, length) + length = int(length) if length and length > 0 else 20 + smooth_bars = int(smooth_bars) if smooth_bars and smooth_bars > 0 else 20 + offset = get_offset(offset) + + # Precalculations + a1 = npExp(-1.414 * 3.14159 / smooth_bars) + b1 = 2 * a1 * npCos(1.414 * 180 / smooth_bars) + c2 = b1 + c3 = -a1 * a1 + c1 = 1 - c2 - c3 + Filt = npFull(close.size, 0) + MS = npFull(close.size, 0) + # Reflex = list(Filt) + Reflex = npFull(close.size, npNaN) + + # Calculation + for i in range(1, close.size): + # Gently smooth the data in a SuperSmoother + Filt[i] = c1 * (close[i] + close[i - 1]) / 2 + c2 * Filt[i - 1] + c3 * Filt[i - 2] + + # Length is assumed cycle period + Slope = (Filt[i - length] - Filt[i]) / length + + # Sum the differences + Sum = 0 + for count in range(1, length): + Sum = Sum + (Filt[i] + count * Slope) - Filt[i - count] + Sum = Sum / length + + # Normalize in terms of Standard Deviations + MS[i] = .04 * Sum * Sum + .96 * MS[i - 1] + if MS[i] != 0: + Reflex[i] = Sum / npSqrt(MS[i]) + else: + Reflex[i] = Sum / 0.00001 + + result = Series(Reflex, index=close.index) + + # Neutralize pre-roll phase + result.iloc[0:length] = npNaN + + # Offset + if offset != 0: + result = result.shift(offset) + + # Handle fills + if "fillna" in kwargs: + result.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + result.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + result.name = f"REFLEX_{length}_{smooth_bars}" + result.category = "cycles" + + return result + + +reflex.__doc__ = \ +"""Reflex (reflex) + +John F. Ehlers introduced two indicators within the article "Reflex: A New Zero-Lag Indicator” +in February 2020, TASC magazine. One of which is the Reflex, a lag reduced cycle indicator. +Both indicators (Reflex/Trendflex) are oscillators and complement each other with the focus for +cycle and trend. + +Written for Pandas TA by rengel8 (2021-08-11) based on the implementation on prorealcode (refer to source). +Beyond the mentioned source, this implementation has a separate control parameter for the internal +applied SuperSmoother. + +Sources: + https://www.prorealcode.com/prorealtime-indicators/reflex-and-trendflex-indicators-john-f-ehlers/ + +Calculation: + Refer to provided source or the code above. + +Args: + close (pd.Series): Series of 'close's + length (int): It's period. Default: 20 + smooth_bars (int): Period of internal SuperSmoother (default: asmooth_bars = length). 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. +""" diff --git a/pandas_ta/trend/__init__.py b/pandas_ta/trend/__init__.py index 4544a01..ce0b86d 100644 --- a/pandas_ta/trend/__init__.py +++ b/pandas_ta/trend/__init__.py @@ -12,6 +12,7 @@ from .long_run import long_run from .psar import psar from .qstick import qstick from .short_run import short_run +from .trendflex import trendflex from .tsignals import tsignals from .ttm_trend import ttm_trend from .vhf import vhf diff --git a/pandas_ta/trend/trendflex.py b/pandas_ta/trend/trendflex.py new file mode 100644 index 0000000..356137d --- /dev/null +++ b/pandas_ta/trend/trendflex.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +from numpy import NaN as npNaN +from numpy import cos as npCos +from numpy import exp as npExp +from numpy import full as npFull +from numpy import sqrt as npSqrt +from pandas import DataFrame, Series, concat +from pandas_ta.overlap import rma +from pandas_ta.utils import get_drift, get_offset, verify_series, signals + + +def trendflex(close, length=None, smooth_bars=None, offset=None, **kwargs): + """Indicator: Reflex""" + # Validate arguments + close = verify_series(close, length) + length = int(length) if length and length > 0 else 20 + smooth_bars = int(smooth_bars) if smooth_bars and smooth_bars > 0 else 20 + offset = get_offset(offset) + + # Precalculations + a1 = npExp(-1.414 * 3.14159 / smooth_bars) + b1 = 2 * a1 * npCos(1.414 * 180 / smooth_bars) + c2 = b1 + c3 = -a1 * a1 + c1 = 1 - c2 - c3 + Filt = npFull(close.size, 0) + MS = npFull(close.size, 0) + Trendflex = list(Filt) + + # Calculation + for i in range(1, close.size): + # Gently smooth the data in a SuperSmoother + Filt[i] = c1 * (close[i] + close[i - 1]) / 2 + c2 * Filt[i - 1] + c3 * Filt[i - 2] + + # Sum the differences + Sum = 0 + for count in range(1, length): + Sum = Sum + Filt[i] - Filt[i - count] + Sum = Sum / length + + # Normalize in terms of Standard Deviations + MS[i] = .04 * Sum * Sum + .96 * MS[i - 1] + if MS[i] != 0: + Trendflex[i] = Sum / npSqrt(MS[i]) + else: + Trendflex[i] = Sum / 0.00001 + + result = Series(Trendflex, index=close.index) + + # Neutralize pre-roll phase + result.iloc[0:length] = npNaN + + # Offset + if offset != 0: + result = result.shift(offset) + + # Handle fills + if "fillna" in kwargs: + result.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + result.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + result.name = f"TRENDFLEX_{length}_{smooth_bars}" + result.category = "trend" + + return result + +trendflex.__doc__ = \ +"""Trendflex (trendflex) + +John F. Ehlers introduced two indicators within the article "Reflex: A New Zero-Lag Indicator” +in February 2020, TASC magazine. One of which is the Trendflex, a lag reduced trend indicator. +Both indicators (Reflex/Trendflex) are oscillators and complement each other with the focus for +cycle and trend. + +Written for Pandas TA by rengel8 (2021-08-11) based on the implementation on prorealcode (refer to source). +Beyond the mentioned source, this implementation has a separate control parameter for the internal +applied SuperSmoother. + +Sources: + https://www.prorealcode.com/prorealtime-indicators/reflex-and-trendflex-indicators-john-f-ehlers/ + +Calculation: + Refer to provided source or the code above. + +Args: + close (pd.Series): Series of 'close's + length (int): It's period. Default: 20 + smooth_bars (int): Period of internal SuperSmoother (default: asmooth_bars = length). 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. +"""