Skip to content

Commit

Permalink
[GH-173] Support QQE (#174)
Browse files Browse the repository at this point in the history
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.

https://www.tradingview.com/script/0vn4HZ7O-Quantitative-Qualitative-Estimation-QQE/

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.
  • Loading branch information
jealous authored Jul 10, 2023
1 parent 493570b commit 37ae9c1
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 3 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Supported statistics/indicators are:
* PGO: Pretty Good Oscillator
* PSL: Psychological Line
* PVO: Percentage Volume Oscillator
* QQE: Quantitative Qualitative Estimation

## Installation

Expand Down Expand Up @@ -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
Expand Down
95 changes: 92 additions & 3 deletions stockstats.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from __future__ import unicode_literals

import functools
import itertools
import re
from typing import Optional, Callable, Union
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

0 comments on commit 37ae9c1

Please sign in to comment.