From a1df65746c8e3ca79c63ed5d77e1dd6bcfcb3f28 Mon Sep 17 00:00:00 2001 From: Kevin Johnson Date: Tue, 1 Sep 2020 12:43:29 -0700 Subject: [PATCH] ENH #92 + smi indicator MAINT refactoring --- .gitignore | 6 +- README.md | 4 +- pandas_ta/__init__.py | 2 +- pandas_ta/candles/cdl_doji.py | 16 ++--- pandas_ta/core.py | 13 +++- pandas_ta/momentum/__init__.py | 1 + pandas_ta/momentum/macd.py | 46 ++++++------- pandas_ta/momentum/smi.py | 98 ++++++++++++++++++++++++++++ pandas_ta/momentum/tsi.py | 34 +++++----- pandas_ta/utils.py | 2 +- setup.py | 2 +- tests/test_indicator_momentum.py | 12 ++++ tests/test_indicator_momentum_ext.py | 9 +++ 13 files changed, 188 insertions(+), 57 deletions(-) create mode 100644 pandas_ta/momentum/smi.py diff --git a/.gitignore b/.gitignore index af07da9..f91f1bb 100644 --- a/.gitignore +++ b/.gitignore @@ -120,9 +120,9 @@ data/datas.csv data/GLD_D_tv.csv data/SPY_5min.csv data/SPY_1min.csv -data/SPY_D_adxfish.csv -data/SPY_D_lbsz.csv -data/similang-ch.csv +data/SPY_D_TV1.csv +data/SPY_D_TV2.csv +data/SPY_D_TV3.csv data/TV_5min.csv data/tulip.csv diff --git a/README.md b/README.md index 25d859a..eaec674 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ and _Weighted Moving Average_. ## __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". +* _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. ## __Updated Indicators__ * _Fisher Transform_ (**fisher**): Added Fisher's default **ema** signal line. To change the length of the signal line, use the argument: ```signal=5```. Default: 5 @@ -316,7 +317,7 @@ print(bothhl2.name) # "pre_HL2_post" * _Doji_: **cdl_doji** * _Heikin-Ashi_: **ha** -## _Momentum_ (31) +## _Momentum_ (32) * _Awesome Oscillator_: **ao** * _Absolute Price Oscillator_: **apo** @@ -343,6 +344,7 @@ print(bothhl2.name) # "pre_HL2_post" * _Relative Strength Index_: **rsi** * _Relative Vigor Index_: **rvgi** * _Slope_: **slope** +* _SMI Ergodic_ **smi** * _Squeeze_: **squeeze** * Default is John Carter's. Enable Lazybear's by ```lazybear=True``` * _Stochastic Oscillator_: **stoch** diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index b3f0da4..433a031 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -25,7 +25,7 @@ Category = { "candles": ["cdl_doji", "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", "squeeze", "stoch", "trix", "tsi", "uo", "willr"], + "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", "trix", "tsi", "uo", "willr"], # Overlap "overlap": ["dema", "ema", "fwma", "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/candles/cdl_doji.py b/pandas_ta/candles/cdl_doji.py index 8f724ed..109d1b9 100644 --- a/pandas_ta/candles/cdl_doji.py +++ b/pandas_ta/candles/cdl_doji.py @@ -14,7 +14,7 @@ def cdl_doji(open_, high, low, close, length=None, factor=None, scalar=None, asi factor = float(factor) if is_percent(factor) else 10 scalar = float(scalar) if scalar else 100 offset = get_offset(offset) - naive = kwargs.pop('naive', False) + naive = kwargs.pop("naive", False) # Calculate Result body = real_body(open_, close).abs() @@ -32,10 +32,10 @@ def cdl_doji(open_, high, low, close, length=None, factor=None, scalar=None, asi doji = doji.shift(offset) # Handle fills - if 'fillna' in kwargs: - doji.fillna(kwargs['fillna'], inplace=True) - if 'fill_method' in kwargs: - doji.fillna(method=kwargs['fill_method'], inplace=True) + if "fillna" in kwargs: + doji.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + doji.fillna(method=kwargs["fill_method"], inplace=True) # Name and Categorize it doji.name = f"CDL_DOJI_{length}_{0.01 * factor}" @@ -69,9 +69,9 @@ Args: high (pd.Series): Series of 'high's low (pd.Series): Series of 'low's close (pd.Series): Series of 'close's - length (int): The period. Default: 10 - factor (float): Doji value. Default: 100 - scalar (float): How much to magnify. Default: 100 + length (int): The period. Default: 10 + factor (float): Doji value. Default: 100 + scalar (float): How much to magnify. Default: 100 asint (bool): Keep results numerical instead of boolean. Default: True Kwargs: diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 156ea6e..06a2a3d 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", "1", "97b")) +version = ".".join(("0", "1", "98b")) def mp_worker(args): @@ -820,6 +820,13 @@ class AnalysisIndicators(BasePandasObject): result = slope(close=close, length=length, offset=offset, **kwargs) return result + @finalize + def smi(self, close=None, fast=None, slow=None, signal=None, scalar=None, offset=None, **kwargs): + close = self._get_column(close, "close") + + result = smi(close=close, fast=fast, slow=slow, signal=signal, scalar=scalar, offset=offset, **kwargs) + return result + @finalize def squeeze(self, high=None, low=None, close=None, bb_length=None, bb_std=None, kc_length=None, kc_scalar=None, mom_length=None, mom_smooth=None, use_tr=None, offset=None, **kwargs): high = self._get_column(high, "high") @@ -846,10 +853,10 @@ class AnalysisIndicators(BasePandasObject): return result @finalize - def tsi(self, close=None, fast=None, slow=None, drift=None, offset=None, **kwargs): + def tsi(self, close=None, fast=None, slow=None, scalar=None, drift=None, offset=None, **kwargs): close = self._get_column(close, "close") - result = tsi(close=close, fast=fast, slow=slow, drift=drift, offset=offset, **kwargs) + result = tsi(close=close, fast=fast, slow=slow, scalar=scalar, drift=drift, offset=offset, **kwargs) return result @finalize diff --git a/pandas_ta/momentum/__init__.py b/pandas_ta/momentum/__init__.py index c531a84..0321474 100644 --- a/pandas_ta/momentum/__init__.py +++ b/pandas_ta/momentum/__init__.py @@ -24,6 +24,7 @@ from .roc import roc from .rsi import rsi from .rvgi import rvgi from .slope import slope +from .smi import smi from .squeeze import squeeze from .stoch import stoch from .trix import trix diff --git a/pandas_ta/momentum/macd.py b/pandas_ta/momentum/macd.py index 55362ee..8fe7aa1 100644 --- a/pandas_ta/momentum/macd.py +++ b/pandas_ta/momentum/macd.py @@ -29,14 +29,14 @@ def macd(close, fast=None, slow=None, signal=None, offset=None, **kwargs): signalma = signalma.shift(offset) # Handle fills - if 'fillna' in kwargs: - macd.fillna(kwargs['fillna'], inplace=True) - histogram.fillna(kwargs['fillna'], inplace=True) - signalma.fillna(kwargs['fillna'], inplace=True) - if 'fill_method' in kwargs: - macd.fillna(method=kwargs['fill_method'], inplace=True) - histogram.fillna(method=kwargs['fill_method'], inplace=True) - signalma.fillna(method=kwargs['fill_method'], inplace=True) + if "fillna" in kwargs: + macd.fillna(kwargs["fillna"], inplace=True) + histogram.fillna(kwargs["fillna"], inplace=True) + signalma.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + macd.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}" @@ -51,31 +51,31 @@ def macd(close, fast=None, slow=None, signal=None, offset=None, **kwargs): df.name = f"MACD{_props}" df.category = macd.category - signal_indicators = kwargs.pop('signal_indicators', False) + signal_indicators = kwargs.pop("signal_indicators", False) if signal_indicators: signalsdf = concat( [ df, signals( indicator=histogram, - xa=kwargs.pop('xa', 0), - xb=kwargs.pop('xb', None), - xserie=kwargs.pop('xserie', None), - xserie_a=kwargs.pop('xserie_a', None), - xserie_b=kwargs.pop('xserie_b', None), - cross_values=kwargs.pop('cross_values', True), - cross_series=kwargs.pop('cross_series', True), + xa=kwargs.pop("xa", 0), + xb=kwargs.pop("xb", None), + xserie=kwargs.pop("xserie", None), + xserie_a=kwargs.pop("xserie_a", None), + xserie_b=kwargs.pop("xserie_b", None), + cross_values=kwargs.pop("cross_values", True), + cross_series=kwargs.pop("cross_series", True), offset=offset, ), signals( indicator=macd, - xa=kwargs.pop('xa', 0), - xb=kwargs.pop('xb', None), - xserie=kwargs.pop('xserie', None), - xserie_a=kwargs.pop('xserie_a', None), - xserie_b=kwargs.pop('xserie_b', None), - cross_values=kwargs.pop('cross_values', False), - cross_series=kwargs.pop('cross_series', True), + xa=kwargs.pop("xa", 0), + xb=kwargs.pop("xb", None), + xserie=kwargs.pop("xserie", None), + xserie_a=kwargs.pop("xserie_a", None), + xserie_b=kwargs.pop("xserie_b", None), + cross_values=kwargs.pop("cross_values", False), + cross_series=kwargs.pop("cross_series", True), offset=offset, ), ], diff --git a/pandas_ta/momentum/smi.py b/pandas_ta/momentum/smi.py new file mode 100644 index 0000000..8ba7ace --- /dev/null +++ b/pandas_ta/momentum/smi.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +from pandas import DataFrame, concat +from .tsi import tsi +from pandas_ta.overlap import ema +from pandas_ta.utils import get_offset, verify_series, signals + +def smi(close, fast=None, slow=None, signal=None, scalar=None, offset=None, **kwargs): + """Indicator: SMI Ergodic Indicator (SMIIO)""" + # Validate arguments + close = verify_series(close) + fast = int(fast) if fast and fast > 0 else 5 + slow = int(slow) if slow and slow > 0 else 20 + signal = int(signal) if signal and signal > 0 else 5 + if slow < fast: + fast, slow = slow, fast + scalar = float(scalar) if scalar else 1 + offset = get_offset(offset) + + # Calculate Result + smi = tsi(close, fast=fast, slow=slow, scalar=scalar) + signalma = ema(smi, signal) + osc = smi - signalma + + # Offset + if offset != 0: + smi = smi.shift(offset) + signalma = signalma.shift(offset) + osc = osc.shift(offset) + + # Handle fills + if "fillna" in kwargs: + smi.fillna(kwargs["fillna"], inplace=True) + signalma.fillna(kwargs["fillna"], inplace=True) + osc.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + smi.fillna(method=kwargs["fill_method"], inplace=True) + signalma.fillna(method=kwargs["fill_method"], inplace=True) + osc.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + _scalar = f"_{scalar}" if scalar != 1 else "" + _props = f"_{fast}_{slow}_{signal}{_scalar}" + smi.name = f"SMI{_props}" + signalma.name = f"SMIs{_props}" + osc.name = f"SMIo{_props}" + smi.category = signalma.category = osc.category = "momentum" + + # Prepare DataFrame to return + data = {smi.name: smi, signalma.name: signalma, osc.name: osc} + df = DataFrame(data) + df.name = f"SMI{_props}" + df.category = smi.category + + return df + + + +smi.__doc__ = \ +"""SMI Ergodic Indicator (SMI) + +The SMI Ergodic Indicator is the same as the True Strength Index (TSI) developed +by William Blau, except the SMI includes a signal line. The SMI uses double +moving averages of price minus previous price over 2 time frames. The signal +line, which is an EMA of the SMI, is plotted to help trigger trading signals. +The trend is bullish when crossing above zero and bearish when crossing below +zero. This implementation includes both the SMI Ergodic Indicator and SMI +Ergodic Oscillator. + +Sources: + https://www.motivewave.com/studies/smi_ergodic_indicator.htm + https://www.tradingview.com/script/Xh5Q0une-SMI-Ergodic-Oscillator/ + https://www.tradingview.com/script/cwrgy4fw-SMIIO/ + +Calculation: + Default Inputs: + fast=5, slow=20, signal=5 + TSI = True Strength Index + EMA = Exponential Moving Average + + ERG = TSI(close, fast, slow) + Signal = EMA(ERG, signal) + OSC = ERG - Signal + +Args: + close (pd.Series): Series of 'close's + fast (int): The short period. Default: 5 + slow (int): The long period. Default: 20 + signal (int): The signal period. Default: 5 + scalar (float): How much to magnify. Default: 1 + 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: smi, signal, oscillator columns. +""" \ No newline at end of file diff --git a/pandas_ta/momentum/tsi.py b/pandas_ta/momentum/tsi.py index 604828d..fa95bb0 100644 --- a/pandas_ta/momentum/tsi.py +++ b/pandas_ta/momentum/tsi.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- -from ..overlap.ema import ema -from ..utils import get_drift, get_offset, verify_series +from pandas_ta.overlap import ema +from pandas_ta.utils import get_drift, get_offset, verify_series -def tsi(close, fast=None, slow=None, drift=None, offset=None, **kwargs): +def tsi(close, fast=None, slow=None, scalar=None, drift=None, offset=None, **kwargs): """Indicator: True Strength Index (TSI)""" # Validate Arguments close = verify_series(close) fast = int(fast) if fast and fast > 0 else 13 slow = int(slow) if slow and slow > 0 else 25 - if slow < fast: - fast, slow = slow, fast + # if slow < fast: + # fast, slow = slow, fast + scalar = float(scalar) if scalar else 100 drift = get_drift(drift) offset = get_offset(offset) @@ -22,21 +23,21 @@ def tsi(close, fast=None, slow=None, drift=None, offset=None, **kwargs): abs_slow_ema = ema(close=abs_diff, length=slow, **kwargs) abs_fast_slow_ema = ema(close=abs_slow_ema, length=fast, **kwargs) - tsi = 100 * fast_slow_ema / abs_fast_slow_ema + tsi = scalar * fast_slow_ema / abs_fast_slow_ema # Offset if offset != 0: tsi = tsi.shift(offset) # Handle fills - if 'fillna' in kwargs: - tsi.fillna(kwargs['fillna'], inplace=True) - if 'fill_method' in kwargs: - tsi.fillna(method=kwargs['fill_method'], inplace=True) + if "fillna" in kwargs: + tsi.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + tsi.fillna(method=kwargs["fill_method"], inplace=True) # Name and Categorize it tsi.name = f"TSI_{fast}_{slow}" - tsi.category = 'momentum' + tsi.category = "momentum" return tsi @@ -54,7 +55,7 @@ Sources: Calculation: Default Inputs: - fast=13, slow=25, drift=1 + fast=13, slow=25, scalar=100, drift=1 EMA = Exponential Moving Average diff = close.diff(drift) @@ -64,13 +65,14 @@ Calculation: abs_diff_slow_ema = absolute_diff_ema = EMA(ABS(diff), slow) abema = abs_diff_fast_slow_ema = EMA(abs_diff_slow_ema, fast) - TSI = 100 * fast_slow_ema / abema + TSI = scalar * fast_slow_ema / abema Args: close (pd.Series): Series of 'close's - fast (int): The short period. Default: 13 - slow (int): The long period. Default: 25 - drift (int): The difference period. Default: 1 + fast (int): The short period. Default: 13 + slow (int): The long period. Default: 25 + scalar (float): How much to magnify. Default: 100 + drift (int): The difference period. Default: 1 offset (int): How many periods to offset the result. Default: 0 Kwargs: diff --git a/pandas_ta/utils.py b/pandas_ta/utils.py index 18dfe76..c2739b6 100644 --- a/pandas_ta/utils.py +++ b/pandas_ta/utils.py @@ -10,7 +10,7 @@ from functools import reduce from operator import mul from sys import float_info as sflt -TRADING_DAYS_PER_YEAR = 251 +TRADING_DAYS_PER_YEAR = 252 # Keep even TRADING_HOURS_PER_DAY = 6.5 MINUTES_PER_HOUR = 60 diff --git a/setup.py b/setup.py index 61fc4aa..0b250ae 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from distutils.core import setup from pandas_ta.core import version -long_description = "An easy to use Python 3 Pandas Extension with 100+ Technical Analysis Indicators. Can be called from a Pandas DataFrame or standalone like TA-Lib." +long_description = "An easy to use Python 3 Pandas Extension with 115+ Technical Analysis Indicators. Can be called from a Pandas DataFrame or standalone like TA-Lib. Correlation tested with TA-Lib." setup( name ="pandas_ta", diff --git a/tests/test_indicator_momentum.py b/tests/test_indicator_momentum.py index 38cd1b6..0222866 100644 --- a/tests/test_indicator_momentum.py +++ b/tests/test_indicator_momentum.py @@ -306,6 +306,18 @@ class TestMomentum(TestCase): self.assertIsInstance(result, Series) self.assertEqual(result.name, "ANGLEd_1") + def test_smi(self): + result = pandas_ta.smi(self.close) + self.assertIsInstance(result, DataFrame) + self.assertEqual(result.name, "SMI_5_20_5") + self.assertEqual(len(result.columns), 3) + + def test_smi_scalar(self): + result = pandas_ta.smi(self.close, scalar=10) + self.assertIsInstance(result, DataFrame) + self.assertEqual(result.name, "SMI_5_20_5_10.0") + self.assertEqual(len(result.columns), 3) + def test_squeeze(self): result = pandas_ta.squeeze(self.high, self.low, self.close) self.assertIsInstance(result, DataFrame) diff --git a/tests/test_indicator_momentum_ext.py b/tests/test_indicator_momentum_ext.py index 20e37ea..7439337 100644 --- a/tests/test_indicator_momentum_ext.py +++ b/tests/test_indicator_momentum_ext.py @@ -166,6 +166,15 @@ class TestMomentumExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], "ANGLEd_1") + def test_smi_ext(self): + self.data.ta.smi(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(list(self.data.columns[-3:]), ["SMI_5_20_5", "SMIs_5_20_5", "SMIo_5_20_5"]) + + self.data.ta.smi(scalar=10, append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(list(self.data.columns[-3:]), ["SMI_5_20_5_10.0", "SMIs_5_20_5_10.0", "SMIo_5_20_5_10.0"]) + def test_squeeze_ext(self): self.data.ta.squeeze(append=True) self.assertIsInstance(self.data, DataFrame)