Academic Article Series: Economics and Finance 101:

Technical Analysis Indicators: Customisable Relative Strength Indices (and Moving Averages & Volatilities)

Author:

Jonathan Legrand
Developer Advocate Developer Advocate

In this article, we build a data-frame to output an instrument's Daily (i) Close Prices, (ii) ln (logarithme naturel / natural logarithm) Return, (iii) Close Price's 10, 30 and 60 Day Moving Averages, (iv) Close Price's ln Return 10, 30 and 60 Day Moving Average, (v) Annualised Standard Deviation (i.e.: volatility) of the 10, 30 and 60 Day Rolling Window (Natural Log) Returns (based on CLOSE prices), and (vi) Relative Strength Index (RSI).

When it comes to the RSI data, a great article to read about its implementation can be found here (thank you Umer and Jason!). You may want to read into the installation of the TA-Lib library used to compute RSI data here. You may also want to read into how the way TA-lib calculates RSI; as per its GitHub documentation, it uses a Wilder Smoothing method to compute Moving Averages. Others may use different such methods (e.g.: Exponential Moving Average or Simple Moving Average). More information on Refinitiv Workspace's Chart App RSI can be found here and here. Previous RSI Python functions do not provide great amounts of customisation; in this article, we will create a Python function that does just that.

 

Webinar Video

This Webinar was broadcasted live on Twitch where I code live every Tuesday at 4pm GMT.

Using this Site

You may scale up or down the math figures at your convenience as per the below.

 

    	
            

start_date, end_date, bdpy = '2020-01-01', '2021-08-01', 360  # ' bdpy ' stands for 'business days per year'

maws = [10, 30, 60]  # ' maws ' stands for 'moving average windows'

rsiw = 14

rsi_method = "Wilder"  # "SMA", "EMA", "Wilder"

ric_list = ['KO.N', 'VOD.L']

    	
            

from datetime import datetime

from dateutil.relativedelta import relativedelta  # '2.8.1'

import numpy  # '1.18.2'

import statistics

import pandas  # '1.2.4'

pandas.set_option('display.max_columns', None) # This allows us to see all the collumns of our dataframe

    	
            

import eikon as ek

# The key is placed in a text file so that it may be used in this code without showing it itself:

eikon_key = open("eikon.txt", "r")

ek.set_app_key(str(eikon_key.read()))

# It is best to close the files we opened in order to make sure that we don't stop any other services/programs from accessing them if they need to:

eikon_key.close()

    	
            

df1, err = ek.get_data(

    instruments=ric_list,

    fields=['TR.PriceClose.date', 'TR.PriceOpen', 'TR.PriceClose', 'TR.VOlume'],  # could also have TR.RSISimple30D & TR.RSISimple14D

    parameters={'SDate': start_date, 'EDate': end_date})

    	
            

# Step 1: Create a list of days ranging throughout our period.

# The reason we look at every day is because some securities might trade on Sunday (e.g.: in middle-east)

delta = datetime.strptime(end_date, '%Y-%m-%d') - datetime.strptime(start_date, '%Y-%m-%d')

days = [datetime.strptime(start_date, '%Y-%m-%d') + relativedelta(days=i)

        for i in range(delta.days + 1)]

 

# Step 2: Create a pandas data-frame to populate with a loop

df2 = pandas.DataFrame(index=days)

 

# Step 3: Create a loop to populate our 'df' dataframe

# for m, i in enumerate(ric_list):

for i in ric_list:

 

    # get Close Prices: (You may use any way to collect such data, here we are recycling previous data)

    _df = pandas.DataFrame(

        data=[float(j) for j in df1[df1["Instrument"] == i]["Price Close"]],

        columns=[f"{i} Close"],

        index=[datetime.strptime(df1[df1["Instrument"] == i]["Date"][j],

                                 '%Y-%m-%dT%H:%M:%SZ')

               for j in list(df1[df1["Instrument"] == i]["Date"].index)])

 

    # get ln (logarithme naturel) return from Close Prices:

    _df[f"{i}'s ln Return"] = numpy.log(

        _df[f"{i} Close"] / _df[i + " Close"].shift(1))

 

    # get 1st difference in Close Prices:

    dCP = list((_df[f"{i} Close"] - _df[f"{i} Close"].shift(1)))

#     dCP.insert(0, numpy.nan)

    # list.reverse(dCP)

    _df[f"{i}'s 1d Close"] = dCP

 

    for j in maws:

        # get Close Price's 10, 30 and 60 Day Moving Averages

        _df[f"{i}'s Close {str(j)}D MA"] = _df[f"{i} Close"].dropna().rolling(j).mean()

 

        # get Close Price's ln Return 10, 30 and 60 Day Moving Average

        _df[f"{i}'s ln Return' {str(j)}D MA"] = _df[f"{i}'s ln Return"].dropna().rolling(j).mean()

#         # You may also want to look into TA-lib's Simple Moving Average function:

#         _df[i + "'s ln Return' " + str(j) + " Day Moving Average"] = ta.SMA(_df[i + " Close"], j)

 

        # get Annualised Standard Deviation (i.e.: volatility) of the 10, 30 and 60 Day Rolling Window (Natural Log) Returns (based on CLOSE prices)

        _df[f"{i}'s Ann. S.D. of {str(j)}D Roll. of ln Returns"] = _df[f"{i}'s ln Return"].dropna().rolling(j).std()*(bdpy**0.5)

 

 

    # RSI: Simple Moving Average

    if rsi_method == "SMA" or rsi_method == "Cutler":

        RSIw = []

        for k in range(len(dCP) - rsiw):

            up_moves, down_moves = [], []

            for j in range(k, k + rsiw):

                up_moves.append(max(dCP[j + 1], 0))  # ' + 1 ' because we're looking at 1st difference data, the 1st element of which is always nan.

                down_moves.append(abs(min(dCP[j + 1], 0)))   # ' + 1 ' because we're looking at 1st difference data, the 1st element of which is always nan.

            AvgU = statistics.mean(up_moves)

            AvgD = statistics.mean(down_moves)

            if AvgD == 0:

                RSIw.append(100)

            else:

                RSw = AvgU/AvgD

                RSIw.append(100-(100/(1+RSw)))

    # RSI: Exponential Moving Average

    if rsi_method == "EMA" or rsi_method == "MEMA" or rsi_method == "Wilder":

        if rsi_method == "MEMA" or rsi_method == "Wilder":

            a = 1 / rsiw

        elif rsi_method == "EMA":

            a = 2 / (rsiw + 1)

        RSIw, up_moves, down_moves = [], [], []

        for k in dCP[1:]:  # ' [1:] ' because we're looking at 1st difference data, the 1st element of which is always nan.

            up_moves.append(max(k, 0))

            down_moves.append(abs(min(k, 0)))

        AvgU = [statistics.mean(up_moves[0:rsiw])]

        AvgD = [statistics.mean(down_moves[0:rsiw])]

        if AvgD[0] == 0:

            RSIw.append(100)

        else:

            RSw = AvgU[0] / AvgD[0]

            RSIw.append(100 - (100 / (1 + RSw)))

        for k in range(rsiw, len(up_moves)):

            AvgU.append(a * up_moves[k] + (1 - a) * AvgU[k - rsiw])

            AvgD.append(a * down_moves[k] + (1 - a) * AvgD[k - rsiw])

            RSw = AvgU[k - rsiw] / AvgD[k - rsiw]

            RSIw.append(100 - (100 / (1 + RSw)))

    RSIw.append(numpy.nan)  # This is needed to inser the nan from the 1st difference that was already nan

    for k in range(len(_df.index) - len(RSIw)):

        RSIw.insert(0, numpy.nan)

    _df[f"{i}'s RSI{rsiw}"] = RSIw

 

    # Finally: merge all.

    df2 = pandas.merge(

        df2, _df, how="outer", left_index=True, right_index=True)

display response

    	
            

import plotly

import plotly.express

from plotly.graph_objs import *

from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot

init_notebook_mode(connected=True)

import cufflinks

cufflinks.go_offline()

    	
            

df2["KO.N's RSI14"].dropna().iplot(title="KO.N's Wilder RSI14",

                                   colors="#001EFF", theme="solar")

Define a Python Function

    	
            

def Refinitiv_RSI(start_date='2020-01-01', end_date='2021-08-01', bdpy=360,  # ' bdpy ' stands for 'business days per year'

                  maws=[10, 30, 60],  # ' maws ' stands for 'moving average windows'

                  rsiw=14, rsi_method="Wilder",  # "SMA", "EMA", "Wilder"

                  ric_list=['KO.N', 'VOD.L']):

    """Refinitiv_RSI(start_date='2020-01-01', end_date='2021-08-01', bdpy=360, maws=[10, 30, 60], rsiw=14, rsi_method="Wilder", ric_list=['KO.N', 'VOD.L']) Version 1.0

    This Python function returns the close prices of instruments asked for and

    calculates their (i) Natural Log Returns, (ii) 1st Difference,

    (iii) Moving Averages, (iv)Natural Log Returns' Moving Averages,

    (v) Annual Standard Deviations of a Rolling window of Natural Log Returns,

    and (vi) Relative Strength Indices

 

    Dependencies

    ----------

 

    Python library 'eikon' version 1.1.8

    Python library 'numpy' version 1.18.2

    Python library 'statistics'

    Python library 'pandas' version 1.2.4

 

    As well as the:

 

    Python library 'datetime' from 'datetime'. Imported via following line:

    >>> from datetime import datetime

 

    Python sub-library 'dateutil.relativedelta' version 2.8.1 from 'relativedelta'. Imported via following line:

    >>> from dateutil.relativedelta import relativedelta

 

    Parameters

    ----------

 

    start_date: str

        The starting date in the format '%Y-%m-%d'.

        Default: start_date='2020-01-01'.

 

    end_date: str

        The end date in the format '%Y-%m-%d'.

        Default: start_date='2021-08-01'.

 

    bdpy: int

        ' bdpy ' stands for 'business days per year'

        Default: bdpy=360.

 

    maws: list

        List of integers of the moving average windows asked for.

        Default: maws=[10, 30, 60].

 

    rsiw: int

        Relative Strength Index window

        Default: rsiw=14.

 

    rsi_method: str

        To choose from "SMA" for 'Simple Moving Average, "EMA" for 'Exponential Moving Average', or "Wilder".

        Default: rsi_method="Wilder".

 

    ric_list: list

        List of strings of RICs of instruments for which data is requested.

        Default: ric_list=['KO.N', 'VOD.L'].

 

 

    Returns

    -------

 

    Pandas data-frame

    """

 

    df1, err = ek.get_data(

        instruments=ric_list,

        fields=['TR.PriceClose.date', 'TR.PriceOpen', 'TR.PriceClose', 'TR.VOlume'],  # could also have TR.RSISimple30D & TR.RSISimple14D

        parameters={'SDate': start_date, 'EDate': end_date})

 

    # Step 1: Create a list of days ranging throughout our period.

    # The reason we look at every day is because some securities might trade on Sunday (e.g.: in middle-east)

    delta = datetime.strptime(end_date, '%Y-%m-%d') - datetime.strptime(start_date, '%Y-%m-%d')

    days = [datetime.strptime(start_date, '%Y-%m-%d') + relativedelta(days=i)

            for i in range(delta.days + 1)]

 

    # Step 2: Create a pandas data-frame to populate with a loop

    df2 = pandas.DataFrame(index=days)

 

    # Step 3: Create a loop to populate our 'df' dataframe

    # for m, i in enumerate(ric_list):

    for i in ric_list:

 

        # get Close Prices: (You may use any way to collect such data, here we are recycling previous data)

        _df = pandas.DataFrame(

            data=[float(j) for j in df1[df1["Instrument"] == i]["Price Close"]],

            columns=[f"{i} Close"],

            index=[datetime.strptime(df1[df1["Instrument"] == i]["Date"][j],

                                     '%Y-%m-%dT%H:%M:%SZ')

                   for j in list(df1[df1["Instrument"] == i]["Date"].index)])

 

        # get ln (logarithme naturel) return from Close Prices:

        _df[f"{i}'s ln Return"] = numpy.log(

            _df[f"{i} Close"] / _df[i + " Close"].shift(1))

 

        # get 1st difference in Close Prices:

        dCP = list((_df[f"{i} Close"] - _df[f"{i} Close"].shift(1)))

        # list.reverse(dCP)

        _df[f"{i}'s 1d Close"] = dCP

 

        for j in maws:

            # get Close Price's 10, 30 and 60 Day Moving Averages

            _df[f"{i}'s Close {str(j)}D MA"] = _df[f"{i} Close"].dropna().rolling(j).mean()

 

            # get Close Price's ln Return 10, 30 and 60 Day Moving Average

            _df[f"{i}'s ln Return' {str(j)}D MA"] = _df[f"{i}'s ln Return"].dropna().rolling(j).mean()

#             # You may also want to look into TA-lib's Simple Moving Average function:

#             _df[i + "'s ln Return' " + str(j) + " Day Moving Average"] = ta.SMA(_df[i + " Close"], j)

 

            # get Annualised Standard Deviation (i.e.: volatility) of the 10, 30 and 60 Day Rolling Window (Natural Log) Returns (based on CLOSE prices)

            _df[f"{i}'s Ann. S.D. of {str(j)}D Roll. of ln Returns"] = _df[f"{i}'s ln Return"].dropna().rolling(j).std()*(bdpy**0.5)

 

 

        # RSI: Simple Moving Average

        if rsi_method == "SMA" or rsi_method == "Cutler":

            RSIw = []

            for k in range(len(dCP) - rsiw):

                up_moves, down_moves = [], []

                for j in range(k, k + rsiw):

                    up_moves.append(max(dCP[j + 1], 0))  # ' + 1 ' because we're looking at 1st difference data, the 1st element of which is always nan.

                    down_moves.append(abs(min(dCP[j + 1], 0)))   # ' + 1 ' because we're looking at 1st difference data, the 1st element of which is always nan.

                AvgU = statistics.mean(up_moves)

                AvgD = statistics.mean(down_moves)

                if AvgD == 0:

                    RSIw.append(100)

                else:

                    RSw = AvgU/AvgD

                    RSIw.append(100-(100/(1+RSw)))

        # RSI: Exponential Moving Average

        if rsi_method == "EMA" or rsi_method == "MEMA" or rsi_method == "Wilder":

            if rsi_method == "MEMA" or rsi_method == "Wilder":

                a = 1 / rsiw

            elif rsi_method == "EMA":

                a = 2 / (rsiw + 1)

            RSIw, up_moves, down_moves = [], [], []

            for k in dCP[1:]:  # ' [1:] ' because we're looking at 1st difference data, the 1st element of which is always nan.

                up_moves.append(max(k, 0))

                down_moves.append(abs(min(k, 0)))

            AvgU = [statistics.mean(up_moves[0:rsiw])]

            AvgD = [statistics.mean(down_moves[0:rsiw])]

            if AvgD[0] == 0:

                RSIw.append(100)

            else:

                RSw = AvgU[0] / AvgD[0]

                RSIw.append(100 - (100 / (1 + RSw)))

            for k in range(rsiw, len(up_moves)):

                AvgU.append(a * up_moves[k] + (1 - a) * AvgU[k - rsiw])

                AvgD.append(a * down_moves[k] + (1 - a) * AvgD[k - rsiw])

                RSw = AvgU[k - rsiw] / AvgD[k - rsiw]

                RSIw.append(100 - (100 / (1 + RSw)))

        RSIw.append(numpy.nan)  # This is needed to inser the nan from the 1st difference that was already nan

        for k in range(len(_df.index) - len(RSIw)):

            RSIw.insert(0, numpy.nan)

        _df[f"{i}'s RSI{rsiw}"] = RSIw

 

        # Finally: merge all.

        return pandas.merge(df2, _df, how="outer", left_index=True, right_index=True)

    	
            df = Refinitiv_RSI(ric_list=['MSFT.O'])
        
        
    
    	
            df.dropna()
        
        
    
  MSFT.O Close MSFT.O's ln Return MSFT.O's 1d Close MSFT.O's Close 10D MA MSFT.O's ln Return' 10D MA MSFT.O's Ann. S.D. of 10D Roll. of ln Returns MSFT.O's Close 30D MA MSFT.O's ln Return' 30D MA MSFT.O's Ann. S.D. of 30D Roll. of ln Returns MSFT.O's Close 60D MA MSFT.O's ln Return' 60D MA MSFT.O's Ann. S.D. of 60D Roll. of ln Returns MSFT.O's RSI14
30/03/2020 160.23 0.067977 10.53 146.431 0.016823 0.996665 158.844 -0.004855 1.170754 164.625167 -0.000041 0.847859 52.673834
31/03/2020 157.71 -0.015852 -2.52 147.545 0.007325 0.919086 157.86 -0.005719 1.170091 164.61 -0.000096 0.848196 51.337963
01/04/2020 152.11 -0.036154 -5.6 148.716 0.008011 0.904883 156.687667 -0.006933 1.174572 164.494667 -0.000741 0.852745 48.400379
02/04/2020 155.26 0.020497 3.15 149.971 0.008429 0.906763 155.715667 -0.005737 1.177938 164.456 -0.000247 0.854052 50.129028
03/04/2020 153.83 -0.009253 -1.43 151.619 0.011332 0.862599 154.890333 -0.004975 1.174237 164.351667 -0.000665 0.853384 49.321227
... ... ... ... ... ... ... ... ... ... ... ... ... ...
23/07/2021 289.67 0.012261 3.53 281.613 0.004134 0.178585 272.2775 0.003958 0.161648 260.488917 0.002153 0.206477 75.427556
26/07/2021 289.05 -0.002143 -0.62 282.786 0.004143 0.178457 273.316167 0.003802 0.162964 261.097917 0.002252 0.205188 73.90081
27/07/2021 286.54 -0.008722 -2.51 283.342 0.001959 0.182599 274.2045 0.003254 0.167933 261.670583 0.002129 0.20677 67.908078
28/07/2021 286.22 -0.001117 -0.32 283.713 0.001305 0.181845 275.133167 0.003414 0.165493 262.24325 0.002131 0.206756 67.16034
29/07/2021 286.5 0.000978 0.28 284.26 0.001928 0.176626 276.103833 0.003573 0.163726 262.888417 0.002419 0.20163 67.497578
    	
            

df["MSFT.O's RSI14"].dropna().iplot(title="MSFT.O's Wilder RSI14",

                                    colors="#001EFF", theme="solar")

  • Login
  • Please Login
Contact Us MyRefinitiv