diff --git a/README.md b/README.md index d2f6e39..b356c59 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Supported statistics/indicators are: * PGO: Pretty Good Oscillator * PSL: Psychological Line * PVO: Percentage Volume Oscillator +* QQE: Quantitative Qualitative Estimation ## Installation @@ -1045,6 +1046,31 @@ The period of short, long EMA and signal line can be tuned with `set_dft_window('pvo', (short, long, signal))`. The default windows are 12 and 26 and 9. +#### [Quantitative Qualitative Estimation(QQE)](https://www.tradingview.com/script/0vn4HZ7O-Quantitative-Qualitative-Estimation-QQE/) + +The Qualitative Quantitative Estimation (QQE) indicator works like a smoother +version of the popular Relative Strength Index (RSI) indicator. QQE expands +on RSI by adding two volatility based trailing stop lines. These trailing +stop lines are composed of a fast and a slow moving Average True Range (ATR). +These ATR lines are smoothed making this indicator less susceptible to short +term volatility. + +Implementation reference: +https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/momentum/qqe.py + +Example: +* `df['qqe']` retrieves the QQE with RSI window 14, MA window 5. +* `df['qqel']` retrieves the QQE long +* `df['qqes']` retrieves the QQE short +* `df['qqe_10,4']` retrieves the QQE with RSI window 10, MA window 4 +* `df['qqel_10,4']` retrieves the QQE long with customized windows. + Initialized by retrieving `df['qqe_10,4']` +* `df['qqes_10,4']` retrieves the QQE short with customized windows + Initialized by retrieving `df['qqe_10,4']` + +The period of short, long EMA and signal line can be tuned with +`set_dft_window('qqe', (rsi, rsi_ma))`. The default windows are 14 and 5. + ## Issues We use [Github Issues](https://github.com/jealous/stockstats/issues) to track diff --git a/stockstats.py b/stockstats.py index 0d03100..7ac754c 100644 --- a/stockstats.py +++ b/stockstats.py @@ -26,6 +26,7 @@ from __future__ import unicode_literals +import functools import itertools import re from typing import Optional, Callable, Union @@ -75,6 +76,7 @@ class StockStatsError(Exception): 'ppo': (12, 26, 9), # short, long, signal 'pvo': (12, 26, 9), # short, long, signal 'psl': 12, + 'qqe': (14, 5), # rsi, rsi ema 'rsi': 14, 'rsv': 9, 'rvgi': 14, @@ -455,7 +457,8 @@ def _get_rsv(self, meta: _Meta): self[meta.name] = self._rsv(meta.int) def _rsi(self, window) -> pd.Series: - change = self._delta(self['close'], -1) + change = self.close.diff() + change.iloc[0] = 0 close_pm = (change + change.abs()) / 2 close_nm = (-change + change.abs()) / 2 p_ema = self.smma(close_pm, window) @@ -998,11 +1001,11 @@ def _get_roc(self, meta: _Meta): self[meta.name] = self.roc(self[meta.column], meta.int) @staticmethod - def ema(series, window, *, adjust=True): + def ema(series, window, *, adjust=True, min_periods=1): return series.ewm( ignore_na=False, span=window, - min_periods=1, + min_periods=min_periods, adjust=adjust).mean() @staticmethod @@ -1656,6 +1659,91 @@ def _psl(self, col_name: str, window: int) -> pd.Series: def _get_psl(self, meta: _Meta): self[meta.name] = self._psl(meta.column, meta.int) + def _get_qqe(self, meta: _Meta): + """ QQE (Quantitative Qualitative Estimation) + + https://www.tradingview.com/script/0vn4HZ7O-Quantitative-Qualitative-Estimation-QQE/ + + The Qualitative Quantitative Estimation (QQE) indicator works like a + smoother version of the popular Relative Strength Index (RSI) + indicator. QQE expands on RSI by adding two volatility based trailing + stop lines. These trailing stop lines are composed of a fast and a + slow moving Average True Range (ATR). These ATR lines are smoothed + making this indicator less susceptible to short term volatility. + + Implementation reference: + https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/momentum/qqe.py + + """ + rsi_window = meta.int0 + rsi_ma_window = meta.int1 + factor = 4.236 + wilder_window = rsi_window * 2 - 1 + ema = functools.partial(self.ema, adjust=False) + + rsi = self._rsi(rsi_window) + rsi.iloc[:rsi_window] = np.nan + rsi_ma = ema(rsi, rsi_ma_window) + tr = rsi_ma.diff().abs() + tr_ma = ema(tr, wilder_window) + tr_ma_ma = ema(tr_ma, wilder_window) * factor + + upper = list(rsi_ma + tr_ma_ma) + lower = list(rsi_ma - tr_ma_ma) + rsi_ma = list(rsi_ma) + + size = self.close.size + long = [0] * size + short = [0] * size + trend = [1] * size + qqe = [rsi_ma[0]] * size + qqe_long = [np.nan] * size + qqe_short = [np.nan] * size + + for i in range(1, size): + c_rsi, p_rsi = rsi_ma[i], rsi_ma[i - 1] + c_long, p_long = long[i - 1], long[i - 2] + c_short, p_short = short[i - 1], short[i - 2] + + # Long Line + if p_rsi > c_long and c_rsi > c_long: + long[i] = max(c_long, lower[i]) + else: + long[i] = lower[i] + + # Short Line + if p_rsi < c_short and c_rsi < c_short: + short[i] = min(c_short, upper[i]) + else: + short[i] = upper[i] + + # Trend & QQE Calculation + # Long: Current RSI_MA value Crosses the Prior Short Line Value + # Short: Current RSI_MA Crosses the Prior Long Line Value + rsi_ux_short = c_rsi > c_short and p_rsi < p_short + rsi_dx_short = c_rsi <= c_short and p_rsi >= p_short + rsi_ux_long = c_rsi > c_long and p_rsi < p_long + rsi_dx_long = c_rsi <= c_long and p_rsi >= p_long + if rsi_ux_short or rsi_dx_short: + trend[i] = 1 + qqe[i] = qqe_long[i] = long[i] + elif rsi_ux_long or rsi_dx_long: + trend[i] = -1 + qqe[i] = qqe_short[i] = short[i] + else: + trend[i] = trend[i - 1] + if trend[i] == 1: + qqe[i] = qqe_long[i] = long[i] + else: + qqe[i] = qqe_short[i] = short[i] + + self[meta.name] = self.to_series(qqe) + self[meta.name_ex('l')] = self.to_series(qqe_long) + self[meta.name_ex('s')] = self.to_series(qqe_short) + + def to_series(self, arr: list): + return pd.Series(arr, index=self.close.index).fillna(0) + @staticmethod def parse_column_name(name): m = re.match(r'(.*)_([\d\-+~,.]+)_(\w+)', name) @@ -1766,6 +1854,7 @@ def handler(self): ('macd', 'macds', 'macdh'): self._get_macd, ('pvo', 'pvos', 'pvoh'): self._get_pvo, ('ppo', 'ppos', 'ppoh'): self._get_ppo, + ('qqe', 'qqel', 'qqes'): self._get_qqe, ('cr', 'cr-ma1', 'cr-ma2', 'cr-ma3'): self._get_cr, ('tr',): self._get_tr, ('dx', 'adx', 'adxr'): self._get_dmi, diff --git a/test.py b/test.py index 11b5cf9..2884584 100644 --- a/test.py +++ b/test.py @@ -1086,3 +1086,20 @@ def test_pvo(self): assert_that(stock['pvo'].loc[20110331], near_to(3.4708)) assert_that(stock['pvos'].loc[20110331], near_to(-3.7464)) assert_that(stock['pvoh'].loc[20110331], near_to(7.2173)) + + def test_qqe(self): + stock = self.get_stock_90days() + _ = stock['qqe'] + _ = stock['qqe_14,5'] + _ = stock['qqe_10,4'] + + assert_that(stock.loc[20110125, 'qqe'], near_to(44.603)) + assert_that(stock.loc[20110125, 'qqel'], near_to(44.603)) + assert_that(stock.loc[20110125, 'qqes'], near_to(0)) + + assert_that(stock.loc[20110223, 'qqe'], near_to(53.26)) + assert_that(stock.loc[20110223, 'qqel'], near_to(0)) + assert_that(stock.loc[20110223, 'qqes'], near_to(53.26)) + + assert_that(stock.loc[20110125, 'qqe_14,5'], near_to(44.603)) + assert_that(stock.loc[20110125, 'qqe_10,4'], near_to(39.431))