Relative Strength Moving Averages With Hann Windowing (RSIH)
References
█ OVERVIEW
In his article in TASC's January 2022 edition Traders' Tips, "(Yet Another) Improved RSI," John Ehlers explains how he enhances the RSI by taking advantage of Hann windowing. The RSIH indicator provides a smoother calculation than the classic RSI and has a zero mean. The inherent smoothing in the computation removes the need for supplemental filtering. The best length to use for the RSIH is described to be one that is on the order of the dominant cycle period in the data.
█ CONCEPTS
By employing a Hann windowed finite impulse response filter (FIR), John Ehlers has enhanced the "Relative Strength Indicator" (RSI) to provide an improved oscillator with exceptional smoothness.
█ CALCULATIONS
The method of calculations using "closes up" and "closes down" from Welles Wilder's RSI described in his 1978 book is still inherent to Ehlers enhanced formula. However, a finite impulse response (FIR) Hann windowing technique is employed following the closes up/down calculations instead of the original Wilder infinite impulse response averaging filter. The resulting oscillator waveform is confined between +/-1.0 with a 0.0 centerline regardless of chart interval, as opposed to Wilder's original formulation, which was confined between 0 and 100 with a centerline of 50. On any given trading timeframe, the value of Ehlers' enhanced RSI found above the centerline typically represents an overvalued region, while undervalued regions are typically found below the centerline.
Load basic packages
import pandas as pd
import numpy as np
import os
import gc
import copy
from pathlib import Path
from datetime import datetime, timedelta, time, date
#this package is to download equity price data from yahoo finance
#the source code of this package can be found here: https://github.com/ranaroussi/yfinance/blob/main
import yfinance as yf
pd.options.display.max_rows = 100
pd.options.display.max_columns = 100
import warnings
warnings.filterwarnings("ignore")
import pytorch_lightning as pl
random_seed=1234
pl.seed_everything(random_seed)
Global seed set to 1234
1234
#S&P 500 (^GSPC), Dow Jones Industrial Average (^DJI), NASDAQ Composite (^IXIC)
#Russell 2000 (^RUT), Crude Oil Nov 21 (CL=F), Gold Dec 21 (GC=F)
#Treasury Yield 10 Years (^TNX)
#benchmark_tickers = ['^GSPC', '^DJI', '^IXIC', '^RUT', 'CL=F', 'GC=F', '^TNX']
benchmark_tickers = ['^GSPC']
tickers = benchmark_tickers + ['GSK', 'BST', 'PFE']
#https://github.com/ranaroussi/yfinance/blob/main/yfinance/base.py
# def history(self, period="1mo", interval="1d",
# start=None, end=None, prepost=False, actions=True,
# auto_adjust=True, back_adjust=False,
# proxy=None, rounding=False, tz=None, timeout=None, **kwargs):
dfs = {}
for ticker in tickers:
cur_data = yf.Ticker(ticker)
hist = cur_data.history(period="max", start='2000-01-01')
print(datetime.now(), ticker, hist.shape, hist.index.min(), hist.index.max())
dfs[ticker] = hist
2022-09-04 15:36:33.419826 ^GSPC (5706, 7) 1999-12-31 00:00:00 2022-09-02 00:00:00
2022-09-04 15:36:33.792559 GSK (5706, 7) 1999-12-31 00:00:00 2022-09-02 00:00:00
2022-09-04 15:36:33.966659 BST (1976, 7) 2014-10-29 00:00:00 2022-09-02 00:00:00
2022-09-04 15:36:34.294939 PFE (5706, 7) 1999-12-31 00:00:00 2022-09-02 00:00:00
ticker = '^GSPC'
dfs[ticker].tail(5)
Open | High | Low | Close | Volume | Dividends | Stock Splits | |
---|---|---|---|---|---|---|---|
Date | |||||||
2022-08-29 | 4034.580078 | 4062.989990 | 4017.419922 | 4030.610107 | 2963020000 | 0 | 0 |
2022-08-30 | 4041.250000 | 4044.979980 | 3965.209961 | 3986.159912 | 3190580000 | 0 | 0 |
2022-08-31 | 4000.669922 | 4015.370117 | 3954.530029 | 3955.000000 | 3797860000 | 0 | 0 |
2022-09-01 | 3936.729980 | 3970.229980 | 3903.649902 | 3966.850098 | 3754570000 | 0 | 0 |
2022-09-02 | 3994.659912 | 4018.429932 | 3906.209961 | 3924.260010 | 4134920000 | 0 | 0 |
Define (Yet Another) Improved RSI Enhanced With Hann Windowing calculation function
import math
def cal_rsih(ohlc: pd.DataFrame,
period: int = 14,
column: str = "close") -> pd.Series:
"""
source: https://traders.com/Documentation/FEEDbk_docs/2022/01/TradersTips.html
// TASC JAN 2022, RSIH - RSI with Hann Windowing, John F. Ehlers
inputs:
RSILength(14);
// Accumulate "Closes Up" and "Closes Down"
CU = 0;
CD = 0;
for count = 1 to RSILength begin
if Close[count - 1] - Close[count] > 0 then
CU = CU + (1 - Cosine(360*count / (RSILength + 1)))
*(Close[count - 1] - Close[count]);
if Close[count] - Close[count - 1] > 0 then
CD = CD + (1 - Cosine(360*count / (RSILength + 1)))
*(Close[count] - Close[count - 1]);
end;
if CU + CD <> 0 then
MyRSI = (CU - CD) / (CU + CD);
"""
def _hann(c, rsi_len):
cu = 0
cd = 0
for i in range(1, rsi_len):
j = i + 1
delta = c[i] - c[i-1] #e.g. i=1, delta = c[0] - c[-1] => current close minus previous close.
if delta>0:
cu = cu + (1 - math.cos(360*j/(rsi_len + 1)))*delta
else:
cd = cd - (1 - math.cos(360*j/(rsi_len + 1)))*delta
re = 0
if (cu + cd) != 0:
re = (cu - cd)/(cu + cd)
return re
c = ohlc[column]
rsi_ = c.rolling(window=period, min_periods=period).apply(lambda x: _hann(x, period))
return pd.Series(rsi_,index=ohlc.index, name=f"RSIH")
import math
def cal_rsih(ohlc: pd.DataFrame,
period: int = 14,
column: str = "close") -> pd.Series:
"""
source: https://traders.com/Documentation/FEEDbk_docs/2022/01/TradersTips.html
lengthInput = input.int(14, "Length:", minval = 2)
rsih(length) =>
var float PIx2 = 2 * math.pi
// Accumulate "Closes Up" and "Closes Down"
cu = 0.0
cd = 0.0
for count = 1 to length
change = close[count] - close[count - 1]
absChange = math.abs(change)
cosPart = math.cos(PIx2 * count / (length + 1))
if change < 0
cu := cu + (1 - cosPart) * absChange
else if change > 0
cd := cd + (1 - cosPart) * absChange
result = nz((cu - cd) / (cu + cd))
"""
def _hann(_data, _len):
pi_ = 2*math.pi
#Accumulate "Closes Up" and "Closes Down"
cu = 0.0
cd = 0.0
for i in range(1, _len):
delta = _data[i] - _data[i-1]
delta_abs = np.abs(delta)
cos_ = math.cos(pi_*i/(_len + 1))
if delta>0:
cu = cu + (1 - cos_)*delta_abs
else:
cd = cd + (1 - cos_)*delta_abs
re = 0
if (cu + cd) != 0:
re = (cu - cd)/(cu + cd)
return re
c = ohlc[column]
rsi_ = c.rolling(window=period, min_periods=period).apply(lambda x: _hann(x, period))
return pd.Series(rsi_,index=ohlc.index, name=f"RSIH")
def _hann(_data, _len):
out_ = np.zeros(_len)
for i in range(_len):
out_[i] = _data[i]*(1-math.cos(2*math.pi*(i+1)/(_len + 1)))
Calculate (Yet Another) Improved RSI Enhanced With Hann Windowing
the 2 functions in above cells render very different results. use the 2nd function
df = dfs[ticker][['Open', 'High', 'Low', 'Close', 'Volume']]
df = df.round(2)
cal_rsih
<function __main__.cal_rsih(ohlc: pandas.core.frame.DataFrame, period: int = 14, column: str = 'close') -> pandas.core.series.Series>
df_ta = cal_rsih(df, period=14, column="Close")
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )
del df_ta
gc.collect()
164
from core.finta import TA
df_ta = TA.EMA(df, period = 14, column="close")
df_ta.name='EMA'
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )
del df_ta
gc.collect()
21
display(df.head(5))
display(df.tail(5))
Open | High | Low | Close | Volume | RSIH | EMA | |
---|---|---|---|---|---|---|---|
Date | |||||||
1999-12-31 | 1464.47 | 1472.42 | 1458.19 | 1469.25 | 374050000 | NaN | 1469.250000 |
2000-01-03 | 1469.25 | 1478.00 | 1438.36 | 1455.22 | 931800000 | NaN | 1461.733929 |
2000-01-04 | 1455.22 | 1455.22 | 1397.43 | 1399.42 | 1009000000 | NaN | 1437.929796 |
2000-01-05 | 1399.42 | 1413.27 | 1377.68 | 1402.11 | 1085500000 | NaN | 1426.971510 |
2000-01-06 | 1402.11 | 1411.90 | 1392.10 | 1403.45 | 1092300000 | NaN | 1420.834784 |
Open | High | Low | Close | Volume | RSIH | EMA | |
---|---|---|---|---|---|---|---|
Date | |||||||
2022-08-29 | 4034.58 | 4062.99 | 4017.42 | 4030.61 | 2963020000 | -0.520148 | 4147.551970 |
2022-08-30 | 4041.25 | 4044.98 | 3965.21 | 3986.16 | 3190580000 | -0.573418 | 4126.033040 |
2022-08-31 | 4000.67 | 4015.37 | 3954.53 | 3955.00 | 3797860000 | -0.595874 | 4103.228635 |
2022-09-01 | 3936.73 | 3970.23 | 3903.65 | 3966.85 | 3754570000 | -0.593899 | 4085.044817 |
2022-09-02 | 3994.66 | 4018.43 | 3906.21 | 3924.26 | 4134920000 | -0.602275 | 4063.606841 |
df[['RSIH']].hist(bins=50)
array([[<AxesSubplot:title={'center':'RSIH'}>]], dtype=object)
#https://github.com/matplotlib/mplfinance
#this package help visualize financial data
import mplfinance as mpf
import matplotlib.colors as mcolors
# all_colors = list(mcolors.CSS4_COLORS.keys())#"CSS Colors"
# all_colors = list(mcolors.TABLEAU_COLORS.keys()) # "Tableau Palette",
# all_colors = list(mcolors.BASE_COLORS.keys()) #"Base Colors",
all_colors = ['dodgerblue', 'firebrick','limegreen','skyblue','lightgreen', 'navy','yellow','plum', 'yellowgreen']
#https://github.com/matplotlib/mplfinance/issues/181#issuecomment-667252575
#list of colors: https://matplotlib.org/stable/gallery/color/named_colors.html
#https://github.com/matplotlib/mplfinance/blob/master/examples/styles.ipynb
def make_3panels2(main_data, add_data, mid_panel=None, chart_type='candle', names=None, figratio=(14,9)):
style = mpf.make_mpf_style(base_mpf_style='yahoo', #charles
base_mpl_style = 'seaborn-whitegrid',
# marketcolors=mpf.make_marketcolors(up="r", down="#0000CC",inherit=True),
gridcolor="whitesmoke",
gridstyle="--", #or None, or - for solid
gridaxis="both",
edgecolor = 'whitesmoke',
facecolor = 'white', #background color within the graph edge
figcolor = 'white', #background color outside of the graph edge
y_on_right = False,
rc = {'legend.fontsize': 'small',#or number
#'figure.figsize': (14, 9),
'axes.labelsize': 'small',
'axes.titlesize':'small',
'xtick.labelsize':'small',#'x-small', 'small','medium','large'
'ytick.labelsize':'small'
},
)
if (chart_type is None) or (chart_type not in ['ohlc', 'line', 'candle', 'hollow_and_filled']):
chart_type = 'candle'
len_dict = {'candle':2, 'ohlc':3, 'line':1, 'hollow_and_filled':2}
kwargs = dict(type=chart_type, figratio=figratio, volume=True, volume_panel=2,
panel_ratios=(4,2, 2), tight_layout=True, style=style, returnfig=True)
if names is None:
names = {'main_title': '', 'sub_tile': ''}
added_plots = {
# 'S': mpf.make_addplot(add_data['S'], panel=0, color='blue', type='scatter', marker=r'${S}$' , markersize=100, secondary_y=False),
# 'B': mpf.make_addplot(add_data['B'], panel=0, color='blue', type='scatter', marker=r'${B}$' , markersize=100, secondary_y=False),
'EMA': mpf.make_addplot(add_data['EMA'], panel=0, color='dodgerblue', width=1 ,secondary_y=False),
}
if mid_panel is not None:
i = 0
for name_, data_ in mid_panel.iteritems():
added_plots[name_] = mpf.make_addplot(data_, panel=1, width=1,color=all_colors[i])
i = i + 1
# fb_bbands2_ = dict(y1=-0.5*np.ones(mid_panel.shape[0]),
# y2=0.5*np.ones(mid_panel.shape[0]),color="lightskyblue",alpha=0.1,interpolate=True)
# fb_bbands2_['panel'] = 1
# fb_bbands.append(fb_bbands2_)
fb_bbands = []
fb_span_up = dict(y1=np.zeros(mid_panel.shape[0]),y2=mid_panel['RSIH'].values,where=mid_panel['RSIH']<0,color="#FF008055",alpha=0.2, panel=1, interpolate=True)
fb_span_dn = dict(y1=np.zeros(mid_panel.shape[0]),y2=mid_panel['RSIH'].values,where=mid_panel['RSIH']>0,color="palegreen",alpha=0.2, panel=1, interpolate=True)
fb_bbands= [fb_span_up, fb_span_dn]
fig, axes = mpf.plot(main_data, **kwargs,
addplot=list(added_plots.values()),
fill_between = fb_bbands,
)
# add a new suptitle
fig.suptitle(names['main_title'], y=1.05, fontsize=12, x=0.1285)
# axes[0].legend([None]*4)
# handles = axes[0].get_legend().legendHandles
# axes[0].legend(handles=handles[2:],labels=['RS_EMA', 'EMA'])
# axes[0].set_title(names['sub_tile'], fontsize=10, style='italic', loc='left')
# axes[0].set_ylabel(names['y_tiles'][0])
# axes[2].set_ylabel(names['y_tiles'][1])
return fig, axes
start = -250
end = -160#df.shape[0]
names = {'main_title': f'{ticker}',
'sub_tile': 'RSI with Hann'}
aa_, bb_ = make_3panels2(df.iloc[start:end][['Open', 'High', 'Low', 'Close', 'Volume']],
df.iloc[start:end][['EMA']],
df.iloc[start:end][['RSIH']],
chart_type='hollow_and_filled',names = names)