diff --git a/README.md b/README.md index b9407d7..c1b8d8d 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ _Pandas Technical Analysis_ (**Pandas TA**) is a free, Open Source, and easy to * [Candles](#candles-64) * [Cycles](#cycles-2) * [Momentum](#momentum-42) - * [Overlap](#overlap-36) + * [Overlap](#overlap-37) * [Performance](#performance-3) * [Statistics](#statistics-11) * [Transform](#transform-3) @@ -913,7 +913,7 @@ Back to [Contents](#contents)
-### **Overlap** (36) +### **Overlap** (37) * _Bill Williams Alligator_: **alligator** * _Arnaud Legoux Moving Average_: **alma** @@ -932,6 +932,8 @@ Back to [Contents](#contents) * _Jurik Moving Average_: **jma** * _Kaufman's Adaptive Moving Average_: **kama** * _Linear Regression_: **linreg** +* _Ehler's MESA Adapative Moving Average_: **mama** + * Includes: **fama** * _McGinley Dynamic_: **mcgd** * _Midpoint_: **midpoint** * _Midprice_: **midprice** @@ -1254,7 +1256,7 @@ TODO | **Status** | **Remaining TA Lib Indicators** | | - | - | | ☐ | Candlesticks | -| ☐ | Indicators: ```ht_dcperiod```, ```ht_dcphase```, ```ht_phasor```, ```ht_sine```, ```ht_trendline```, ```ht_trendmode```, ```mama``` | +| ☐ | Indicators: ```ht_dcperiod```, ```ht_dcphase```, ```ht_phasor```, ```ht_sine```, ```ht_trendline```, ```ht_trendmode``` | | ☐ | **Numpy**/**Numba**_-ify_ base indicators |
diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 4c1e8a7..9861e4f 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -364,13 +364,14 @@ class AnalysisIndicators(object): # Add prefix/suffix and append to the dataframe self._add_prefix_suffix(result=result, **kwargs) - if "append" in kwargs and kwargs["append"]: - # Default: Appends result to DataFrame - self._append(result=result, **kwargs) - if "append" in kwargs and kwargs["append"] is None: - # Issue 388 - No appending, just print to stdout - # No DatetimeIndex could break execution. - print(result) + if "append" in kwargs and isinstance(kwargs["append"], bool): + if not kwargs["append"]: + # Issue 388 - No appending, just print to stdout + # No DatetimeIndex could break execution. + print(result) + else: + # Default: Appends result to DataFrame + self._append(result=result, **kwargs) return result def _study_mode(self, *args: Args) -> Tuple: @@ -1232,6 +1233,11 @@ class AnalysisIndicators(object): result = linreg(close=close, length=length, offset=offset, adjust=adjust, **kwargs) return self._post_process(result, **kwargs) + def mama(self, fastlimit=None, slowlimit=None, prenan: Int = None, offset: Int = None, **kwargs: DictLike): + close = self._get_column(kwargs.pop("close", "close")) + result = mama(close=close, fastlimit=fastlimit, slowlimit=slowlimit, prenan=prenan, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + def mcgd(self, length=None, offset: Int = None, **kwargs: DictLike): close = self._get_column(kwargs.pop("close", "close")) result = mcgd(close=close, length=length, offset=offset, **kwargs) diff --git a/pandas_ta/maps.py b/pandas_ta/maps.py index cb2caf7..f04d49e 100644 --- a/pandas_ta/maps.py +++ b/pandas_ta/maps.py @@ -56,10 +56,10 @@ Category: Dict[str, ListStr] = { # Overlap "overlap": [ "alligator", "alma", "dema", "ema", "fwma", "hilo", "hl2", "hlc3", - "hma", "hwma", "ichimoku", "jma", "kama", "linreg", "mcgd", "midpoint", - "midprice", "ohlc4", "pwma", "rma", "sinwma", "sma", "smma", "ssf", - "ssf3", "supertrend", "swma", "t3", "tema", "trima", "vidya", "vwap", - "vwma", "wcp", "wma", "zlma" + "hma", "hwma", "ichimoku", "jma", "kama", "linreg", "mama", + "mcgd", "midpoint", "midprice", "ohlc4", "pwma", "rma", "sinwma", + "sma", "smma", "ssf", "ssf3", "supertrend", "swma", "t3", "tema", + "trima", "vidya", "vwap", "vwma", "wcp", "wma", "zlma" ], # Performance "performance": ["log_return", "percent_return"], diff --git a/pandas_ta/overlap/__init__.py b/pandas_ta/overlap/__init__.py index af83e17..2adc09f 100644 --- a/pandas_ta/overlap/__init__.py +++ b/pandas_ta/overlap/__init__.py @@ -13,6 +13,7 @@ from .ichimoku import ichimoku from .jma import jma from .kama import kama from .linreg import linreg +from .mama import mama from .mcgd import mcgd from .midpoint import midpoint from .midprice import midprice diff --git a/pandas_ta/overlap/mama.py b/pandas_ta/overlap/mama.py new file mode 100644 index 0000000..3e676cb --- /dev/null +++ b/pandas_ta/overlap/mama.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +from numpy import arctan, nan, zeros_like +from pandas import DataFrame, Series +from pandas_ta._typing import Array, DictLike, Int, IntFloat +from pandas_ta.maps import Imports +from pandas_ta.utils import v_offset, v_pos_default, v_series, v_talib + + +try: + from numba import njit +except ImportError: + def njit(_): return _ + + +@njit +def np_mama( + x: Array, fastlimit: IntFloat, slowlimit: IntFloat, prenan: Int +): + """Ehler's Mother of Adaptive Moving Averages + http://traders.com/documentation/feedbk_docs/2014/01/traderstips.html + """ + a1, a2 = 0.0962, 0.5769 + p_w, smp_w, smp_w_c = 0.2, 0.33, 0.67 # smp_w + smp_w_c = 1 + + sm = zeros_like(x) + dt, smp, q1, q2 = sm.copy(), sm.copy(), sm.copy(), sm.copy() + i1, i2, jI, jQ = sm.copy(), sm.copy(), sm.copy(), sm.copy() + re, im, alpha = sm.copy(), sm.copy(), sm.copy() + period, phase, mama, fama = sm.copy(), sm.copy(), sm.copy(), sm.copy() + + n = x.size + # Ehler's starts from 6, TV-LB starts at 3, TALib 32 + for i in range(3, n): + w_period = .075 * period[i - 1] + .54 + + # Smoother and Detrend the Smoother + sm[i] = 0.4 * x[i] + 0.3 * x[i - 1] + 0.2 * x[i - 2] + 0.1 * x[i - 3] + dt[i] = w_period * (a1 * sm[i] + a2 * sm[i - 2] - a2 * sm[i - 4] - a1 * sm[i - 6]) + + # Quadrature(Detrender) and In Phase Component + q1[i] = w_period * (a1 * dt[i] + a2 * dt[i - 2] - a2 * dt[i - 4] - a1 * dt[i - 6]) + i1[i] = dt[i - 3] + + # Phase advance I1 and Q1 by 90 degrees + jI[i] = w_period * (a1 * i1[i] + a2 * i1[i - 2] - a2 * i1[i - 4] - a1 * i1[i - 6]) + jQ[i] = w_period * (a1 * q1[i] + a2 * q1[i - 2] - a2 * q1[i - 4] - a1 * q1[i - 6]) + + # Phasor Addition for 3 Bar Averaging + i2[i] = i1[i] - jQ[i] + q2[i] = q1[i] + jI[i] + + # Smooth I and Q components + i2[i] = p_w * i2[i] + (1 - p_w) * i2[i - 1] + q2[i] = p_w * q2[i] + (1 - p_w) * q2[i - 1] + + # Homodyne Discriminator + re[i] = i2[i] * i2[i - 1] + q2[i] * q2[i - 1] + im[i] = i2[i] * q2[i - 1] + q2[i] * i2[i - 1] + + re[i] = p_w * re[i] + (1 - p_w) * re[i - 1] + im[i] = p_w * im[i] + (1 - p_w) * im[i - 1] + + if im[i] != 0.0 and re[i] != 0.0: + period[i] = 360 / arctan(im[i] / re[i]) + else: + period[i] = 0 + + if period[i] > 1.5 * period[i - 1]: + period[i] = 1.5 * period[i - 1] + if period[i] < 0.67 * period[i - 1]: + period[i] = 0.67 * period[i - 1] + if period[i] < 6: + period[i] = 6 + if period[i] > 50: + period[i] = 50 + + period[i] = p_w * period[i] + (1 - p_w) * period[i - 1] + smp[i] = smp_w * period[i] + smp_w_c * smp[i - 1] + + if i1[i] != 0.0: + phase[i] = arctan(q1[i] / i1[i]) + + dphase = phase[i - 1] - phase[i] + if dphase < 1: + dphase = 1 + + alpha[i] = fastlimit / dphase + if alpha[i] > fastlimit: + alpha[i] = fastlimit + if alpha[i] < slowlimit: + alpha[i] = slowlimit + + mama[i] = alpha[i] * x[i] + (1 - alpha[i]) * mama[i - 1] + fama[i] = 0.5 * alpha[i] * mama[i] + (1 - 0.5 * alpha[i]) * fama[i - 1] + + mama[:prenan], fama[:prenan] = nan, nan + return mama, fama + + +def mama( + close: Series, fastlimit: IntFloat = None, slowlimit: IntFloat = None, + prenan: Int = None, talib: bool = None, + offset: Int = None, **kwargs: DictLike +) -> Series: + """Ehler's MESA Adapative Moving Average (MAMA) + + Ehler's MESA Adapative Moving Average (MAMA) aka the Mother of All Moving + Averages attempts to adapt to the source's dynamic nature. The adapation + is based on the rate change of phase as measured by the Hilbert + Transform Discriminator. The advantage of this method of adaptation is + that it features a fast attack average and a slow decay average so that + the composite average rapidly adjusts to price changes and holds + the average value until the next change occurs. This indicator also + includes FAMA. + + Sources: + Ehler's Mother of Adaptive Moving Averages: + http://traders.com/documentation/feedbk_docs/2014/01/traderstips.html + https://www.tradingview.com/script/foQxLbU3-Ehlers-MESA-Adaptive-Moving-Average-LazyBear/ + + Args: + close (pd.Series): Series of 'close's + fastlimit (float): Fast limit. Default: 0.5 + slowlimit (float): Slow limit. Default: 0.05 + prenan (int): Prenans to apply. TV-LB 3, Ehler's 6, TALib 32 + Default: 3 + talib (bool): If TA Lib is installed and talib is True, Returns + the TA Lib version. Default: True + 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: MAMA and FAMA columns. + """ + # Validate + close = v_series(close) + + if close is None: + return + + fastlimit = v_pos_default(fastlimit, 0.5) + slowlimit = v_pos_default(slowlimit, 0.05) + prenan = v_pos_default(prenan, 3) + mode_tal = v_talib(talib) + offset = v_offset(offset) + + # Calculate + np_close = close.values + if Imports["talib"] and mode_tal: + from talib import MAMA + mama, fama = MAMA(np_close, fastlimit, slowlimit) + else: + mama, fama = np_mama(np_close, fastlimit, slowlimit, prenan) + + # Name and Category + _props = f"_{fastlimit}_{slowlimit}" + df = DataFrame({ + f"MAMA{_props}": mama, + f"FAMA{_props}": fama, + }, index=close.index) + + df.name = f"MAMA{_props}" + df.category = "overlap" + + # Offset + if offset != 0: + df = df.shift(offset) + + # Fill + if "fillna" in kwargs: + df.fillna(kwargs["fillna"], inplace=True) + + if "fill_method" in kwargs: + df.fillna(method=kwargs["fill_method"], inplace=True) + + return df \ No newline at end of file diff --git a/tests/test_ext_indicator_overlap_ext.py b/tests/test_ext_indicator_overlap_ext.py index 70f7ac1..d9c5461 100644 --- a/tests/test_ext_indicator_overlap_ext.py +++ b/tests/test_ext_indicator_overlap_ext.py @@ -88,6 +88,11 @@ class TestOverlapExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], "LR_14") + def test_mama_ext(self): + self.data.ta.mama(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(list(self.data.columns[-2:]), ["MAMA_0.5_0.05", "FAMA_0.5_0.05"]) + def test_mcgd_ext(self): self.data.ta.mcgd(append=True) self.assertIsInstance(self.data, DataFrame) diff --git a/tests/test_indicator_overlap.py b/tests/test_indicator_overlap.py index d33ec29..9c0968f 100644 --- a/tests/test_indicator_overlap.py +++ b/tests/test_indicator_overlap.py @@ -271,6 +271,26 @@ class TestOverlap(TestCase): self.assertIsInstance(result, Series) self.assertEqual(result.name, "FWMA_15") + def test_mama(self): + """Overlap: MAMA/FAMA""" + result = pandas_ta.mama(self.close, talib=False) + self.assertIsInstance(result, DataFrame) + self.assertEqual(result.name, "MAMA_0.5_0.05") + + try: + expected = tal.MAMA(self.close) + pdt.assert_series_equal(result, expected, check_names=False) + except AssertionError: + try: + corr = pandas_ta.utils.df_error_analysis(result, expected) + self.assertGreater(corr, CORRELATION_THRESHOLD) + except Exception as ex: + error_analysis(result, CORRELATION, ex) + + result = pandas_ta.mama(self.close) + self.assertIsInstance(result, DataFrame) + self.assertEqual(result.name, "MAMA_0.5_0.05") + def test_mcgd(self): """Overlap: MCGD""" result = pandas_ta.mcgd(self.close) diff --git a/tests/test_utils.py b/tests/test_utils.py index aab59df..d1ee359 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -42,19 +42,19 @@ class TestUtilities(TestCase): del self.utils def test__add_prefix_suffix(self): - result = self.data.ta.hl2(append=False, prefix="pre") + result = self.data.ta.hl2(prefix="pre") self.assertEqual(result.name, "pre_HL2") - result = self.data.ta.hl2(append=False, suffix="suf") + result = self.data.ta.hl2(suffix="suf") self.assertEqual(result.name, "HL2_suf") - result = self.data.ta.hl2(append=False, prefix="pre", suffix="suf") + result = self.data.ta.hl2(prefix="pre", suffix="suf") self.assertEqual(result.name, "pre_HL2_suf") - result = self.data.ta.hl2(append=False, prefix=1, suffix=2) + result = self.data.ta.hl2(prefix=1, suffix=2) self.assertEqual(result.name, "1_HL2_2") - result = self.data.ta.macd(append=False, prefix="pre", suffix="suf") + result = self.data.ta.macd(prefix="pre", suffix="suf") for col in result.columns: self.assertTrue(col.startswith("pre_") and col.endswith("_suf"))