From 6aeec8caa0dedb1d65acfbe0e1f81772e1242b50 Mon Sep 17 00:00:00 2001 From: Richard Luong Date: Wed, 30 Sep 2020 14:23:02 -0700 Subject: [PATCH] added Elders Thermometer 90%correlation --- pandas_ta/__init__.py | 2 +- pandas_ta/core.py | 7 ++ pandas_ta/volatility/__init__.py | 1 + pandas_ta/volatility/thermo.py | 126 +++++++++++++++++++++++++ tests/test_ext_indicator_volatility.py | 5 + tests/test_indicator_volatility.py | 5 + 6 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 pandas_ta/volatility/thermo.py diff --git a/pandas_ta/__init__.py b/pandas_ta/__init__.py index 03f06b7..a68f1b8 100644 --- a/pandas_ta/__init__.py +++ b/pandas_ta/__init__.py @@ -51,7 +51,7 @@ Category = { "trend": ["adx", "amat", "aroon", "chop", "cksp", "decay", "decreasing", "dpo", "increasing", "long_run", "psar", "qstick", "short_run", "ttm_trend", "vortex"], # Volatility - "volatility": ["aberration", "accbands", "atr", "bbands", "donchian", "kc", "massi", "natr", "pdist", "rvi", "true_range", "ui"], + "volatility": ["aberration", "accbands", "atr", "bbands", "donchian", "kc", "massi", "natr", "pdist", "rvi", "thermo", "true_range", "ui"], # Volume, "vp" or "Volume Profile" is unique "volume": ["ad", "adosc", "aobv", "cmf", "efi", "eom", "mfi", "nvi", "obv", "pvi", "pvol", "pvt"], diff --git a/pandas_ta/core.py b/pandas_ta/core.py index 8d3a381..5fe580b 100644 --- a/pandas_ta/core.py +++ b/pandas_ta/core.py @@ -1366,6 +1366,13 @@ class AnalysisIndicators(BasePandasObject): result = rvi(high=high, low=low, close=close, length=length, scalar=scalar, refined=refined, thirds=thirds, mamode=mamode, drift=drift, offset=offset, **kwargs) return self._post_process(result, **kwargs) + def thermo(self, long=None, short= None, length=None, mamode=None, drift=None, offset=None, **kwargs): + high = self._get_column(kwargs.pop("high", "high")) + low = self._get_column(kwargs.pop("low", "low")) + + result = thermo(high=high, low=low, long=long, short=short, length=length, mamode=mamode, drift=drift, offset=offset, **kwargs) + return self._post_process(result, **kwargs) + def true_range(self, drift=None, offset=None, **kwargs): high = self._get_column(kwargs.pop("high", "high")) low = self._get_column(kwargs.pop("low", "low")) diff --git a/pandas_ta/volatility/__init__.py b/pandas_ta/volatility/__init__.py index fa2abda..486c8e1 100644 --- a/pandas_ta/volatility/__init__.py +++ b/pandas_ta/volatility/__init__.py @@ -9,5 +9,6 @@ from .massi import massi from .pdist import pdist from .natr import natr from .rvi import rvi +from .thermo import thermo from .true_range import true_range from .ui import ui \ No newline at end of file diff --git a/pandas_ta/volatility/thermo.py b/pandas_ta/volatility/thermo.py new file mode 100644 index 0000000..5dd715b --- /dev/null +++ b/pandas_ta/volatility/thermo.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +import numpy as np +from pandas import DataFrame, Series +from pandas_ta.overlap import ema +from pandas_ta.utils import get_offset, verify_series, get_drift + +def thermo(high, low, long=None, short=None, length=None, mamode=None, drift=None, offset=None, **kwargs): + """Indicator: Elders Thermometer (THERMO)""" + # Validate arguments + high = verify_series(high) + low = verify_series(low) + drift = get_drift(drift) + offset = get_offset(offset) + + length = int(length) if length and length > 0 else 20 + long = float(long) if long and long > 0 else 2 + short = float(short) if short and short > 0 else 0.5 + mamode = mamode.lower() if mamode else "ema" + + asint = kwargs.pop("asint", True) + lazybear = kwargs.pop("lazybear", False) + + + # Calculate Result + thermoL = (low.shift(drift) - low).abs() + thermoH = (high - high.shift(drift)).abs() + if lazybear: + thermo = (high < high.shift(drift)) & (low > low.shift(drift)) + if thermo.any(): + thermo = thermoL + thermo = thermo.where(thermoH < thermoL, thermoH) + thermo.index = high.index + + thermoma = ema(thermo,length) + + else: + thermo = thermoL + thermo = thermo.where(thermoH < thermoL, thermoH) + thermo.index = high.index + + thermoma = ema(thermo, length) + + # Create signals + thermo_long = thermo < (thermoma * long) # Returns T/F + thermo_short = thermo > (thermoma * short) # Returns T/F + + # Binary output, useful for signals + if asint: + thermo_long = thermo_long.astype(int) + thermo_short = thermo_short.astype(int) + + # Offset + if offset != 0: + thermo = thermo.shift(offset) + thermoma = thermoma.shift(offset) + + # Handle fills + if "fillna" in kwargs: + thermo.fillna(kwargs["fillna"], inplace=True) + thermoma.fillna(kwargs["fillna"], inplace=True) + if "fill_method" in kwargs: + thermo.fillna(method=kwargs["fill_method"], inplace=True) + thermoma.fillna(method=kwargs["fill_method"], inplace=True) + + # Name and Categorize it + _props = f"_{length}_{long}_{short}" + thermo.name = f"THERMO{_props}" + thermoma.name = f"THERMOma{_props}" + thermo_long.name = f"THERMOl{_props}" + thermo_short.name = f"THERMOs{_props}" + + thermo.category = thermo_long.category = thermo_short.category = thermoma.category = "volatility" + + # Prepare Dataframe to return + data = {thermo.name: thermo, thermoma.name: thermoma, thermo_long.name: thermo_long, thermo_short.name: thermo_short} + df = DataFrame(data) + df.name = f"THERMO_{length}" + df.category = thermo.category + + return df + +thermo.__doc__ = \ +"""Elders Thermometer (THERMO) + +Elder's Thermometer measures price volatility. + +Sources: + https://www.motivewave.com/studies/elders_thermometer.htm + https://www.tradingview.com/script/HqvTuEMW-Elder-s-Market-Thermometer-LazyBear/ + +Calculation: + Default Inputs: + length=20, drift=1, mamode=EMA, long=2 short=0.5 + + EMA = Exponential Moving Average + + thermoL = (low.shift(drift) - low).abs() + thermoH = (high - high.shift(drift)).abs() + thermo = np.where(thermoH > thermoL, thermoH, thermoL) + + thermoma = ema(thermo, length) + + thermo_long = thermo < (thermoma * long) # Returns T/F + thermo_short = thermo > (thermoma * short) # Returns T/F + + Binary output + thermo_long = thermo_long.astype(int) + thermo_short = thermo_short.astype(int) + +Args: + high (pd.Series): Series of 'high's + low (pd.Series): Series of 'low's + long(int): The buy factor + short(float): The sell factor + length (int): The period. Default: 20 + drift (int): The diff period. Default: 1 + mamode (str): Two options: None or "sma". Default: ema + 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: thermo, thermoma, thermo_long, thermo_short columns. +""" \ No newline at end of file diff --git a/tests/test_ext_indicator_volatility.py b/tests/test_ext_indicator_volatility.py index ad09b10..00efd89 100644 --- a/tests/test_ext_indicator_volatility.py +++ b/tests/test_ext_indicator_volatility.py @@ -79,6 +79,11 @@ class TestVolatilityExtension(TestCase): self.assertIsInstance(self.data, DataFrame) self.assertEqual(self.data.columns[-1], "RVIt_14") + def test_thermo_ext(self): + self.data.ta.thermo(append=True) + self.assertIsInstance(self.data, DataFrame) + self.assertEqual(list(self.data.columns[-4:]), ["THERMO_20_2_0.5", "THERMOma_20_2_0.5", "THERMOl_20_2_0.5", "THERMOs_20_2_0.5"]) + def test_true_range_ext(self): self.data.ta.true_range(append=True) self.assertIsInstance(self.data, DataFrame) diff --git a/tests/test_indicator_volatility.py b/tests/test_indicator_volatility.py index 29f20f2..b8357f7 100644 --- a/tests/test_indicator_volatility.py +++ b/tests/test_indicator_volatility.py @@ -144,6 +144,11 @@ class TestVolatility(TestCase): self.assertIsInstance(result, Series) self.assertEqual(result.name, "RVIt_14") + def test_thermo(self): + result = pandas_ta.thermo(self.high, self.low) + self.assertIsInstance(result, DataFrame) + self.assertEqual(result.name, "THERMO_20_2_0.5") + def test_true_range(self): result = pandas_ta.true_range(self.high, self.low, self.close) self.assertIsInstance(result, Series)