Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-173] Support QQE #174

Merged
merged 1 commit into from
Jul 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))