In the dynamic world of finance, mastering trading requires a deep dive into advanced strategies. From approaches like sentiment analysis to RSI momentum, diverse tactics shape investors' paths. This article explores these strategies, revealing their mechanics, applications, and significance in modern markets. If you are new into this space, this guide offers insights for success.

Buy and Hold

Simply buying and holding each selected asset with an equal weighting that is inputted into the strategy. This is achieved by first defining a pandas dataframe full of ones and then dividing through by the number of tickers (or codes). Although this isn't much of an algorithm, it is useful for seeing how the skeleton code (Strategies class) works. Look at other algorithms for the application of this class to more complex strategies.

Code Snippet 1. Buy & Hold
          
'''
Install yfinance if not installed to run
'''
#pip install yfinance


import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yf
import matplotlib.pyplot as plt
import datetime


'''
Setting up class
'''
class Strategies():
    #A class that contains code that strategies later on will inherit from params:
    #codes = list of stock short codes


    #Every class has to have an init method and they define the class specific features.
    #In this case ticker list and two empty dataframes
    def __init__(self,codes):
    #this defines class spacific values
        #so ticker/code list
        self.codes = codes
        #creates 2 empty dataframes
        self.strat = pd.DataFrame()
        self.data = pd.DataFrame()



    def import_data(self, start_date, end_date):
    #this method downloads all data for each backtest from yahoo Finance.
    #and removes any nan values

        #start_date, end_date = string of dates for backtesting with format y-m-d
        #code is another name for ticker
        data = yf.download(self.codes, start_date, end_date)

        #if only one stock code is entered, data is reformatted so that is it the same format as if multiple stock were entered
        if len(self.codes) == 1:
            data.columns = [data.columns, self.codes*len(data.columns)]

        #returns data, and NAN values are removed
        return data.dropna()




    def backtest(self, start_date, end_date):
    #returns a list with elements of a time series from yfinance as well as an array of values between -1 and 1 that represent the strategy over the given period
    #1 represents a long position in one stock, 0 representing a neutral postiion and -1 representing a short position

        #sets up dataframes (defined in the init) to contain data in 1, and then all value weights from the strategy for each stock in the other dataframe
        self.data = self.import_data(start_date, end_date)
        #fills the dataframe with zeros
        self.strat = pd.DataFrame(data = np.zeros([len(self.data), len(self.codes)]), columns = self.codes, index = self.data.index)




    #evaluate method takes a backtest, and evaluates it so calcualte cumulative returns, sharpe and sortino ratios
    def evaluate(self, start_date, end_date, fig_strat=True, fig_other=False, percentage_risk_free_rate = 0.1, **kwargs):
    #returns a dataframe with columns including the daily returns of the portfolio, the cumulative returns, the sharpe ratio and all relevent plot of both the stock price of each stock
    #fig = boolean variable that can be used to produce figures
    #ris_free_rate = average rate of return from a save government issued bond used to calcualte sharpe ratio with
    #**kwargs = any specific keyword arguments that can be passed to the backtesting function to allow for comparison of the backtest for different possible parameters defined in the subclass

        #run the backtest function and define the stock price data to be once again self.data and signals self.strat
        self.strat = self.backtest(start_date, end_date, **kwargs)

        #convert the monthly risk free rate to the daily risk free rate for use when calculating sharpe and sortino ratios
        #e.g. (1+1/100)**(1/21)-1 = (1.01**(0.05)) - 1 =  0.00047 (look up EAR formula)
        #the value of 21 is due to there being 20 trading days in a month
        daily_rate = (1 + percentage_risk_free_rate/100)**(1/21) - 1

        #sets up  new DataFrame which will give the returns of the portfolio
        return_df = pd.DataFrame(columns=["daily returns", "cumulative returns"], index = self.data.index)
        #set return on day 0 to 0
        return_df["daily returns"][0] = 0

        #loops through remaining dates and calculates returns across the portfolio
        for i in range(1, len(self.data)):
            #for each stock, this is 100*value weighting from strategy the previous day*(closing price on current day X - closing price on day X-1)/(closing price on day X-1)
            #hence why if your value weighting is 1 (long position), and the stock goes up, your daily return is posiitve
            #if your weighting is -1 (short position), and the stock goes down, then your daily return is positive
            #this is then summed for the multiple stocks in the portfolio and the portfolio daily returns are given
            return_df["daily returns"][i] = sum(100*self.strat[c][i-1]*(self.data["Adj Close"][c][i]-self.data["Adj Close"][c][i-1])/self.data["Adj Close"][c][i-1] for c in self.codes)

        #calculates the cumulative return for each date
        #it does this by taking the daily return percentage, dividing my 100 and adding 1 to get it into a increase/decrease
        #e.g. daily return of 7% goes >>  (7/100)+1 = 1.07 This happens for all the values in the dataframe
        #then the cumprod returns the cumulative product of a dataframe (NOT SUM) e.g. 1.07*0.96*1.02 not 1.07+0.96+1.02
        #this results is then -1 and multiplied by 100... e.g. (1.12-1)*100 = 12%
        return_df["cumulative returns"] = (((return_df["daily returns"]/100)+1).cumprod()-1)*100
        return_df.dropna()


        #For each of the strategies we went through in the notebook they all require a calculations involving a certain
        #number of historic values. e.g previous percentage returns. As a result our strategies can't start straight away
        #as we need to wait until we have enough previous pieces of data for the code to work and produce signals.

        #calculates the length of time for which the strategy is inactive to begin with
        zero_count = 0
        #While True will run forever until a break command is run.
        while True:
            if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):
                #by using iloc[zero_count] when zero_count is 0 it looks at first row, in this is zero then zero_count = 1
                #and then it becomes iloc[1] which searches the second row in the strat dataframe
                #and so on....
                break
            zero_count += 1

            #python syntax allows for the simplification of
            #if not sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes) == 0:
            #to
            #if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):

            #^^^^ this basically says that if the rows equals zero, then carry on but if it doesnt equal zero then break
            #so if it is zero 5 times, it bypasses the break and adds to the zero_count, but then the first nonzero value
            #breaks the loop and the while becomes false, and the 5 is stored in zero_count



        #calculates the sharpe ratio, not including the first period of inactivity

        #Sharpe ratio is the sum of the differences in daily returns of the strategy and the risk-free rate
        #over a given period is divivded by the standard devisation of all daily returns.
        #( all return percentages summed/100 - (risk free rate * number of days) ) / standard devisation of all daily returns
        #e.g. ((1.02+0.95+1.06+1.03)/100 - 4*0.0005) / 0.03 = 1.29
        sharpe = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][zero_count:].std())
        print('Sharpe: ', sharpe)

        #Sortino ratio is similar, however we divide by the standard deviation of only the negative or downwards
        #returns (inferring that we only care about negative volatility)
        #This doesnt include the zero_count index in denominator, as there are no negative downturns anyway when there are 0 values, so these are filtered out
        sortino = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][(return_df["daily returns"] < 0)].std())
        print('Sortino: ', sortino)



        #plots figures if fig_strat = TRUE
        if fig_strat:
            #plot of strategy returns
            plt.figure()
            plt.title("Strategy Backtest from" + start_date + " to " + end_date)
            plt.plot(return_df["cumulative returns"])
            plt.show()

        #plots figure if fig_other = TRUE
        if fig_other:
            #plot of all inidividual stocks
            for c in self.codes:
                plt.figure()
                plt.title("Buy and Hold from" + start_date + " to " + end_date + " for " + str(c))
                plt.plot(((self.data["Adj Close"][c].pct_change()+1).cumprod()-1)*100)
                plt.show()
        print('Returns: ', return_df)
        return [return_df, sharpe, sortino]


'''
Class specifically for Buy and Hold Strat
'''
#this inherits all of the methods from the Strategies class
#this strategy as the name suggest buys and holds an equal amount of each security
class StrategyBuyAndHold(Strategies):

    def backtest(self, start_date, end_date):

        Strategies.backtest(self, start_date, end_date)
        #creates a normalised set of weightings representing a buy and hold strat with each column summing to one

        #this fills the dataframe with data = ones, but divides by number of stocks to ensure equal distribution
        ##e.g. if you have gc=f,aapl,ride (3stocks), then the dataframe is filled with 1/3
        #then the columns are the ticker codes
        return pd.DataFrame(data = (np.ones([len(self.data), len(self.codes)])/len(self.codes)), columns = self.codes)




'''
Example of a backtest for this Strategy
'''
testbh = StrategyBuyAndHold((["^FTSE","^GSPC","AAPL","GC=F","ZC=F"]))
#the code(ticker) is GC=F, AAPL, RIDE then the start_date and end_date are below, and the fig_other is set to TRUE
testbh.evaluate("2020-07-24","2022-07-24", fig_other = True)
#this basically creates an instance os the StrategyBuyAndHold and inputs the 3 stocks into it.
#it also automatically runs the __init__ so creates the empty dataframes
#then, on the second line the dates are inputted into the evaluate method of the instance.
#this means that when the evaluate method runs, this runs the backtest method which in turn runs the import_data method
          
        

Time Series Momentum

This again uses the Strategies class as the basis for this strategy as seen in my Buy and Hold algorithm. In this strategy, a Time Series Momentum Strategy is Created. This takes a positions of every asset in the given basket. If an asset have a negative historic return, then a short position is taken and if it has a positive historic return, then a long position is taken. The lookback period for which returns are calculated is the previous t days. We also only adjust the portfolio weights every q days. Note the first date we can calculate weights will be on date t so we aim to adjust the portfolio weights every q days after this. Then aim to normalise weights by ensuring sum of the absolute values of all weights on any given date is 1.

Code Snippet 2. Time Series Momentum
          
'''
Install yfinance if not installed to run
'''
#pip install yfinance


import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yf
import matplotlib.pyplot as plt
import datetime


'''
Setting up class
'''
class Strategies():
    #A class that contains code that strategies later on will inherit from params:
    #codes = list of stock short codes


    #Every class has to have an init method and they define the class specific features.
    #In this case ticker list and two empty dataframes
    def __init__(self,codes):
    #this defines class spacific values
        #so ticker/code list
        self.codes = codes
        #creates 2 empty dataframes
        self.strat = pd.DataFrame()
        self.data = pd.DataFrame()



    def import_data(self, start_date, end_date):
    #this method downloads all data for each backtest from yahoo Finance.
    #and removes any nan values

        #start_date, end_date = string of dates for backtesting with format y-m-d
        #code is another name for ticker
        data = yf.download(self.codes, start_date, end_date)

        #if only one stock code is entered, data is reformatted so that is it the same format as if multiple stock were entered
        if len(self.codes) == 1:
            data.columns = [data.columns, self.codes*len(data.columns)]

        #returns data, and NAN values are removed
        return data.dropna()




    def backtest(self, start_date, end_date):
    #returns a list with elements of a time series from yfinance as well as an array of values between -1 and 1 that represent the strategy over the given period
    #1 represents a long position in one stock, 0 representing a neutral postiion and -1 representing a short position

        #sets up dataframes (defined in the init) to contain data in 1, and then all value weights from the strategy for each stock in the other dataframe
        self.data = self.import_data(start_date, end_date)
        #fills the dataframe with zeros
        self.strat = pd.DataFrame(data = np.zeros([len(self.data), len(self.codes)]), columns = self.codes, index = self.data.index)




    #evaluate method takes a backtest, and evaluates it so calcualte cumulative returns, sharpe and sortino ratios
    def evaluate(self, start_date, end_date, fig_strat=True, fig_other=False, percentage_risk_free_rate = 0.1, **kwargs):
    #returns a dataframe with columns including the daily returns of the portfolio, the cumulative returns, the sharpe ratio and all relevent plot of both the stock price of each stock
    #fig = boolean variable that can be used to produce figures
    #ris_free_rate = average rate of return from a save government issued bond used to calcualte sharpe ratio with
    #**kwargs = any specific keyword arguments that can be passed to the backtesting function to allow for comparison of the backtest for different possible parameters defined in the subclass

        #run the backtest function and define the stock price data to be once again self.data and signals self.strat
        self.strat = self.backtest(start_date, end_date, **kwargs)

        #convert the monthly risk free rate to the daily risk free rate for use when calculating sharpe and sortino ratios
        #e.g. (1+1/100)**(1/21)-1 = (1.01**(0.05)) - 1 =  0.00047 (look up EAR formula)
        #the value of 21 is due to there being 20 trading days in a month
        daily_rate = (1 + percentage_risk_free_rate/100)**(1/21) - 1

        #sets up  new DataFrame which will give the returns of the portfolio
        return_df = pd.DataFrame(columns=["daily returns", "cumulative returns"], index = self.data.index)
        #set return on day 0 to 0
        return_df["daily returns"][0] = 0

        #loops through remaining dates and calculates returns across the portfolio
        for i in range(1, len(self.data)):
            #for each stock, this is 100*value weighting from strategy the previous day*(closing price on current day X - closing price on day X-1)/(closing price on day X-1)
            #hence why if your value weighting is 1 (long position), and the stock goes up, your daily return is posiitve
            #if your weighting is -1 (short position), and the stock goes down, then your daily return is positive
            #this is then summed for the multiple stocks in the portfolio and the portfolio daily returns are given
            return_df["daily returns"][i] = sum(100*self.strat[c][i-1]*(self.data["Adj Close"][c][i]-self.data["Adj Close"][c][i-1])/self.data["Adj Close"][c][i-1] for c in self.codes)

        #calculates the cumulative return for each date
        #it does this by taking the daily return percentage, dividing my 100 and adding 1 to get it into a increase/decrease
        #e.g. daily return of 7% goes >>  (7/100)+1 = 1.07 This happens for all the values in the dataframe
        #then the cumprod returns the cumulative product of a dataframe (NOT SUM) e.g. 1.07*0.96*1.02 not 1.07+0.96+1.02
        #this results is then -1 and multiplied by 100... e.g. (1.12-1)*100 = 12%
        return_df["cumulative returns"] = (((return_df["daily returns"]/100)+1).cumprod()-1)*100
        return_df.dropna()


        #For each of the strategies we went through in the notebook they all require a calculations involving a certain
        #number of historic values. e.g previous percentage returns. As a result our strategies can't start straight away
        #as we need to wait until we have enough previous pieces of data for the code to work and produce signals.

        #calculates the length of time for which the strategy is inactive to begin with
        zero_count = 0
        #While True will run forever until a break command is run.
        while True:
            if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):
                #by using iloc[zero_count] when zero_count is 0 it looks at first row, in this is zero then zero_count = 1
                #and then it becomes iloc[1] which searches the second row in the strat dataframe
                #and so on....
                break
            zero_count += 1

            #python syntax allows for the simplification of
            #if not sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes) == 0:
            #to
            #if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):

            #^^^^ this basically says that if the rows equals zero, then carry on but if it doesnt equal zero then break
            #so if it is zero 5 times, it bypasses the break and adds to the zero_count, but then the first nonzero value
            #breaks the loop and the while becomes false, and the 5 is stored in zero_count



        #calculates the sharpe ratio, not including the first period of inactivity

        #Sharpe ratio is the sum of the differences in daily returns of the strategy and the risk-free rate
        #over a given period is divivded by the standard devisation of all daily returns.
        #( all return percentages summed/100 - (risk free rate * number of days) ) / standard devisation of all daily returns
        #e.g. ((1.02+0.95+1.06+1.03)/100 - 4*0.0005) / 0.03 = 1.29
        sharpe = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][zero_count:].std())
        print('Sharpe: ', sharpe)

        #Sortino ratio is similar, however we divide by the standard deviation of only the negative or downwards
        #returns (inferring that we only care about negative volatility)
        #This doesnt include the zero_count index in denominator, as there are no negative downturns anyway when there are 0 values, so these are filtered out
        sortino = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][(return_df["daily returns"] < 0)].std())
        print('Sortino: ', sortino)



        #plots figures if fig_strat = TRUE
        if fig_strat:
            #plot of strategy returns
            plt.figure()
            plt.title("Strategy Backtest from" + start_date + " to " + end_date)
            plt.plot(return_df["cumulative returns"])
            plt.show()

        #plots figure if fig_other = TRUE
        if fig_other:
            #plot of all inidividual stocks
            for c in self.codes:
                plt.figure()
                plt.title("Buy and Hold from" + start_date + " to " + end_date + " for " + str(c))
                plt.plot(((self.data["Adj Close"][c].pct_change()+1).cumprod()-1)*100)
                plt.show()
        print('Returns: ', return_df)
        return [return_df, sharpe, sortino]


'''
Class specifically for Time Series Momentum Strat
'''
class StrategyTimeSeriesMomentum(Strategies):


    #parameters include start_Date and end_date
    #t = lookback period
    #q = time length between portfolio adjustments
    def backtest(self, start_date, end_date, q=14, t=50):


        #imports all code from the parent class so we dont have to rewrite them
        Strategies.backtest(self, start_date, end_date)

        #then loop through each time step to calculate the signals
        #start at time t and loop through all remaining time vals (you need the time period t to start the momentum strategy)
        for i in range(t, len(self.data)):

            #every q days, we adjust our portfolio
            #when using the modulo (%) operator, the 'if' statement is TRUE if there is a remainder
            #so say q=5, t=10 and we are on day 30, then i-t = 20 and this is divisable by 5 to give 0 remainder, so continue
            #If it doesnt given an integer, then go to the else statement as we arent adjusting the portfolio on this day
            if not (i-t) % q:

                #a Series is another pandas object which is one column of data along with an index
                #In this case the index is the codes/tickers
                #The data is the returns over the last t days

                #this goes through each code/ticker, and does 100*(the close price of the previous day - close price of the day t days ago) / close price of the day t days ago
                signals = pd.Series(data = (100*(self.data["Adj Close"][c][i-1]-self.data["Adj Close"][c][i-t])/self.data["Adj Close"][c][i-t] for c in self.codes),index = self.codes)
                #this Series inludes the percentrage returns, not the actual returns in pounds!

                #np.sign literally calculates the sign, and if its positive then it put +1, and if negative puts -1
                self.strat.iloc[i] = np.sign(signals)
                #the iloc located the ith row, and makes it equal to the sign of signals

                #the row sum is the sum of the row so if there are 6 stocks, then row_sum is 6 regardless of sign because of abs()
                row_sum = sum((abs(self.strat[c][i]) for c in self.codes))
                if row_sum:
                    self.strat.iloc[i] /= row_sum
            #if it is not one of these q days
            else:
                #set the days weighting equal to the previous days weighting
                self.strat.iloc[i] = self.strat.iloc[i-1]
        return self.strat




'''
Example of a backtest for this strat
'''
testTSM = StrategyTimeSeriesMomentum(["HG=F","GC=F","ZC=F", "SI=F", "PA=F","RB=F"])
testTSM.evaluate("2020-07-24","2022-07-24", t=50)
#in summary, this strategy gives equal weighting to all stocks in the portfolio, but longs some and shorts some, depending
#on values in the strat dataframe. These values are then fed back to the evaluate method where they are combined with the
#data dataframe and the returns and graphs are produced
          
        

Relative Momentum

This again, like the Time Momentum and Buy and Hold algorithms uses the Strategies class as a basis. Here we will implement a backtest for the relative momentum strategy. This is a similar strategy to the time series momentum, but instead of going short and long on all stocks, we only go long the the best 'p' performing stocks, and short the worst 'p' performing stocks. So the lookback period is still the previous t days and we still only adjust the portfolio every q days. As usual, across a row in the strat dataframe, the ABSOLUTE values must all add up to 1. Therefore, the weights for the worst stocks are -1/(2p) and for the best p stocks 1/(2p) which if you take the ABSOLUTE values, then they will add to 1.

Code Snippet 3. Relative Momentum
          
'''
Install yfinance if not installed to run
'''
#pip install yfinance


import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yf
import matplotlib.pyplot as plt
import datetime


'''
Setting up class
'''
class Strategies():
    #A class that contains code that strategies later on will inherit from params:
    #codes = list of stock short codes


    #Every class has to have an init method and they define the class specific features.
    #In this case ticker list and two empty dataframes
    def __init__(self,codes):
    #this defines class spacific values
        #so ticker/code list
        self.codes = codes
        #creates 2 empty dataframes
        self.strat = pd.DataFrame()
        self.data = pd.DataFrame()



    def import_data(self, start_date, end_date):
    #this method downloads all data for each backtest from yahoo Finance.
    #and removes any nan values

        #start_date, end_date = string of dates for backtesting with format y-m-d
        #code is another name for ticker
        data = yf.download(self.codes, start_date, end_date)

        #if only one stock code is entered, data is reformatted so that is it the same format as if multiple stock were entered
        if len(self.codes) == 1:
            data.columns = [data.columns, self.codes*len(data.columns)]

        #returns data, and NAN values are removed
        return data.dropna()




    def backtest(self, start_date, end_date):
    #returns a list with elements of a time series from yfinance as well as an array of values between -1 and 1 that represent the strategy over the given period
    #1 represents a long position in one stock, 0 representing a neutral postiion and -1 representing a short position

        #sets up dataframes (defined in the init) to contain data in 1, and then all value weights from the strategy for each stock in the other dataframe
        self.data = self.import_data(start_date, end_date)
        #fills the dataframe with zeros
        self.strat = pd.DataFrame(data = np.zeros([len(self.data), len(self.codes)]), columns = self.codes, index = self.data.index)




    #evaluate method takes a backtest, and evaluates it so calcualte cumulative returns, sharpe and sortino ratios
    def evaluate(self, start_date, end_date, fig_strat=True, fig_other=False, percentage_risk_free_rate = 0.1, **kwargs):
    #returns a dataframe with columns including the daily returns of the portfolio, the cumulative returns, the sharpe ratio and all relevent plot of both the stock price of each stock
    #fig = boolean variable that can be used to produce figures
    #ris_free_rate = average rate of return from a save government issued bond used to calcualte sharpe ratio with
    #**kwargs = any specific keyword arguments that can be passed to the backtesting function to allow for comparison of the backtest for different possible parameters defined in the subclass

        #run the backtest function and define the stock price data to be once again self.data and signals self.strat
        self.strat = self.backtest(start_date, end_date, **kwargs)

        #convert the monthly risk free rate to the daily risk free rate for use when calculating sharpe and sortino ratios
        #e.g. (1+1/100)**(1/21)-1 = (1.01**(0.05)) - 1 =  0.00047 (look up EAR formula)
        #the value of 21 is due to there being 20 trading days in a month
        daily_rate = (1 + percentage_risk_free_rate/100)**(1/21) - 1

        #sets up  new DataFrame which will give the returns of the portfolio
        return_df = pd.DataFrame(columns=["daily returns", "cumulative returns"], index = self.data.index)
        #set return on day 0 to 0
        return_df["daily returns"][0] = 0

        #loops through remaining dates and calculates returns across the portfolio
        for i in range(1, len(self.data)):
            #for each stock, this is 100*value weighting from strategy the previous day*(closing price on current day X - closing price on day X-1)/(closing price on day X-1)
            #hence why if your value weighting is 1 (long position), and the stock goes up, your daily return is posiitve
            #if your weighting is -1 (short position), and the stock goes down, then your daily return is positive
            #this is then summed for the multiple stocks in the portfolio and the portfolio daily returns are given
            return_df["daily returns"][i] = sum(100*self.strat[c][i-1]*(self.data["Adj Close"][c][i]-self.data["Adj Close"][c][i-1])/self.data["Adj Close"][c][i-1] for c in self.codes)

        #calculates the cumulative return for each date
        #it does this by taking the daily return percentage, dividing my 100 and adding 1 to get it into a increase/decrease
        #e.g. daily return of 7% goes >>  (7/100)+1 = 1.07 This happens for all the values in the dataframe
        #then the cumprod returns the cumulative product of a dataframe (NOT SUM) e.g. 1.07*0.96*1.02 not 1.07+0.96+1.02
        #this results is then -1 and multiplied by 100... e.g. (1.12-1)*100 = 12%
        return_df["cumulative returns"] = (((return_df["daily returns"]/100)+1).cumprod()-1)*100
        return_df.dropna()


        #For each of the strategies we went through in the notebook they all require a calculations involving a certain
        #number of historic values. e.g previous percentage returns. As a result our strategies can't start straight away
        #as we need to wait until we have enough previous pieces of data for the code to work and produce signals.

        #calculates the length of time for which the strategy is inactive to begin with
        zero_count = 0
        #While True will run forever until a break command is run.
        while True:
            if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):
                #by using iloc[zero_count] when zero_count is 0 it looks at first row, in this is zero then zero_count = 1
                #and then it becomes iloc[1] which searches the second row in the strat dataframe
                #and so on....
                break
            zero_count += 1

            #python syntax allows for the simplification of
            #if not sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes) == 0:
            #to
            #if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):

            #^^^^ this basically says that if the rows equals zero, then carry on but if it doesnt equal zero then break
            #so if it is zero 5 times, it bypasses the break and adds to the zero_count, but then the first nonzero value
            #breaks the loop and the while becomes false, and the 5 is stored in zero_count



        #calculates the sharpe ratio, not including the first period of inactivity

        #Sharpe ratio is the sum of the differences in daily returns of the strategy and the risk-free rate
        #over a given period is divivded by the standard devisation of all daily returns.
        #( all return percentages summed/100 - (risk free rate * number of days) ) / standard devisation of all daily returns
        #e.g. ((1.02+0.95+1.06+1.03)/100 - 4*0.0005) / 0.03 = 1.29
        sharpe = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][zero_count:].std())
        print('Sharpe: ', sharpe)

        #Sortino ratio is similar, however we divide by the standard deviation of only the negative or downwards
        #returns (inferring that we only care about negative volatility)
        #This doesnt include the zero_count index in denominator, as there are no negative downturns anyway when there are 0 values, so these are filtered out
        sortino = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][(return_df["daily returns"] < 0)].std())
        print('Sortino: ', sortino)



        #plots figures if fig_strat = TRUE
        if fig_strat:
            #plot of strategy returns
            plt.figure()
            plt.title("Strategy Backtest from" + start_date + " to " + end_date)
            plt.plot(return_df["cumulative returns"])
            plt.show()

        #plots figure if fig_other = TRUE
        if fig_other:
            #plot of all inidividual stocks
            for c in self.codes:
                plt.figure()
                plt.title("Buy and Hold from" + start_date + " to " + end_date + " for " + str(c))
                plt.plot(((self.data["Adj Close"][c].pct_change()+1).cumprod()-1)*100)
                plt.show()
        print('Returns: ', return_df)
        return [return_df, sharpe, sortino]


'''
Class specifically for Time Series Momentum Strat
'''
class StrategyRelativeMomentum(Strategies):

    #p = half the number of stock a position is taken in
    #t = lookback period lenth for calculating momentum of each stock
    #q = time length between portfolio adjustments
    def backtest(self, start_date, end_date, p=2, q=15, t=50):

        #import all code from the parent class
        Strategies.backtest(self, start_date, end_date)

        #loop through each time step to calculate the signals
        for i in range(t, len(self.data)):
            if (i-t) % q == 0:

                #this is similar to the time series momentum Pandas Series,
                #for each time step define a variable signals that is a pd Series with
                #index of stock codes, data equal to the percentage returns over the previous
                #t time steps.
                #however here, we also use sort_values to sort into descending order
                signals = pd.Series(data = (100*(self.data["Adj Close"][c][i-1]-self.data["Adj Close"][c][i-t])/self.data["Adj Close"][c][i-t] for c in self.codes), index = self.codes).sort_values()


                #The following code also normalises the values so they sum to 1:

                #select the p codes with a long position (looking at the end of the sorted Series)
                for c in signals[-p:].keys():
                    self.strat[c][i] = 1/(2*p)

                #select p codes with short position by looking at start of Series
                for c in signals[:p].keys():
                    self.strat[c][i] = -1/(2*p)

            else:
                self.strat.iloc[i] = self.strat.iloc[i-1]
        return self.strat




'''
Example of a backtest for this strat
'''
testRM = StrategyRelativeMomentum(["HG=F","GC=F","ZC=F", "SI=F", "PA=F","RB=F"])
testRM.evaluate("2020-07-25","2022-07-25", t=50)
          
        

Interested in learning more? Feel free to reach out...

Sentiment Based

A project to scrape headlines from news sites, use NLP to score them and suggest whether certain stocks are likely to increase in value or not. Retail trading performed by 'amateur' investors is becoming more and more common. It has more than doubled between 2019 and 2020, and with more 'amateurs' trading and potentially making decisions based on news they see, sentiment analysis will become more important, with global banks already using these strategies. Below is the code to help make these decisions on whether stocks should be brought or sold. Data is extracted using FinViz and then Pythons Natural Language Toolkit helps to analyse the headline data.

Code Snippet 4. Sentiment Based
            
#Before running ensure nltk is installed

import os
#used to visualise the graphs
import matplotlib.pyplot as plt
#BeautifulSoup used to parse data from website
from bs4 import BeautifulSoup
#Pandas library to store data in DataFrame objects
import pandas as pd
#Used to perform sentiment analysis on the news headlines
import nltk
nltk.downloader.download('vader_lexicon')
from nltk.sentiment.vader import SentimentIntensityAnalyzer
#requests library to get the data
from urllib.request import urlopen, Request


def sentiment(codes):
    '''
    Extract and Store the Data
    '''
    #to parse a webside, the stock ticker is added to this URL
    web_url = 'https://finviz.com/quote.ashx?t='
    #create an empty dictionary
    news_tables = {}
    #these are the stocks we are analysing
    tickers = codes
    #make iterations, extracting news data for one of the stocks per iteration
    for tick in tickers:
        # add ticker onto URL
        url = web_url + tick
        #request the data
        req = Request(url=url,headers={"User-Agent": "Chrome"})
        response = urlopen(req)
        #parse the html
        html = BeautifulSoup(response,"html.parser")
        #we are looking for headings, found in the HTML of the webpage in a table under the id of 'news-table'
        news_table = html.find(id='news-table')
        #add it into the dictionary with the key being the ticker
        news_tables[tick] = news_table


    '''
    Code to parse date, time and headlines into a Python List
    '''
    #these will go into the empty new_list
    news_list = []
    #loop over the news
    for file_name, news_table in news_tables.items():
        #iterate over all the  tages in news_table containing the headline
        for i in news_table.findAll('tr'):
            #.get_text() function extracts text placed within the  tag, but only text within the  tag
            text = i.a.get_text()
            #.split() function splits text placed in  tag into a list
            date_scrape = i.td.text.split()
            #if the length of this split data = 1 , time will be loaded as the only element
            if len(date_scrape) == 1:
                time = date_scrape[0]
            #otherwise date will be loaded as the first element and time as the second
            else:
                date = date_scrape[0]
                time = date_scrape[1]
            tick = file_name.split('_')[0]
            news_list.append([tick, date, time, text])

    '''
    Sentiment Analysis Section
    '''
    vader = SentimentIntensityAnalyzer()
    columns = ['ticker', 'date', 'time', 'headline']
    news_df = pd.DataFrame(news_list, columns=columns)
    scores = news_df['headline'].apply(vader.polarity_scores).tolist()
    scores_df = pd.DataFrame(scores)
    news_df = news_df.join(scores_df, rsuffix='_right')
    news_df['date'] = pd.to_datetime(news_df.date).dt.date
    #this is an optional piece of 5 line code that removes totally neutral news
    for index, row in news_df.iterrows():
        if int(row.neu) == 1:
            news_df = news_df.drop(index)
        else:
            pass
    #.head() function returns rows of the dataframe
    print(news_df.head(n=len(news_df)))
    #the compound columns gives us the sentiment scores
    #1 is positive, -1 is negative

    '''
    Visualise the Sentiment Scores
    '''
    plt.rcParams['figure.figsize'] = [10, 6]
    mean_scores = news_df.groupby(['ticker','date']).mean()
    #.unstack() function helps to unstack the ticker column
    mean_scores = mean_scores.unstack()
    #.transpose() obtains the cross-section of compound in the columns axis
    mean_scores = mean_scores.xs('compound', axis="columns").transpose()
    #.plot() and set the kind of graph to 'bar'
    mean_scores.plot(kind = 'bar')
    plt.grid()
    plt.show()


'''
Test of the code with 4 stocks
'''
testsentiment = sentiment(['GOOG', 'AMZN', 'TSLA', 'AAPL'])
            
          

Bollinger Based

A systematic trading strategy using Bollinger Bands to create the buy/sell signals. After inputting one stock, the algorithm calculates a moving average, the standard deviations and the upper and lower bounds of the Bollinger Bands. It then produces buy/sell signals if the stocks close price on a given day is below/above the bands. The signals are also visualised on plots and returns are calculated from the inputted time frame of the backtest.

Code Snippet 5. Bollinger Based
            
'''
If not installed, install yfinance
'''
#pip install yfinance


import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yf
import matplotlib.pyplot as plt
import datetime


'''
Strategies class which is also used in my other strategy algorithms
'''

class Strategies():
    #A class that contains code that strategies later on will inherit from params:
    #codes = list of stock short codes



    #Classes usually has to have an init method and they define the class specific features.
    #In this case ticker list and two empty dataframes
    def __init__(self,codes):
    #this defines class spacific values
        #so ticker/code list
        self.codes = codes
        #creates 2 empty dataframes
        self.strat = pd.DataFrame()
        self.data = pd.DataFrame()


    def import_data(self, start_date, end_date):
    #this method downloads all data for each backtest from yahoo Finance.
    #and removes any nan values

        #start_date, end_date = string of dates for backtesting with format y-m-d
        #code is another name for ticker
        data = yf.download(self.codes, start_date, end_date)
        #these are additional columns for the Bollinger Strat
        data['SMA'] = 0
        data['STD'] = 0
        data['Upper'] = 0
        data['Lower'] = 0
        #if only one stock code is entered, data is reformatted so that is it the same format as if multiple stock were entered
        if len(self.codes) == 1:
            data.columns = [data.columns, self.codes*len(data.columns)]

        #returns data, and NAN values are removed
        return data.dropna()


    def backtest(self, start_date, end_date):
    #returns a list with elements of a time series from yfinance as well as an array of values between -1 and 1 that represent the strategy over the given period
    #1 represents a long position in one stock, 0 representing a neutral postiion and -1 representing a short position

        #sets up dataframes (defined in the init) to contain data in 1, and then all value weights from the strategy for each stock in the other dataframe
        self.data = self.import_data(start_date, end_date)
        #fills the dataframe with zeros
        self.strat = pd.DataFrame(data = np.zeros([len(self.data), len(self.codes)]), columns = self.codes, index = self.data.index)


    #evaluate method takes a backtest, and evaluates it so calcualte cumulative returns, sharpe and sortino ratios
    def evaluate(self, start_date, end_date, fig_strat=True, fig_other=False, percentage_risk_free_rate = 0.1, **kwargs):
    #returns a dataframe with columns including the daily returns of the portfolio, the cumulative returns, the sharpe ratio and all relevent plot of both the stock price of each stock
    #fig = boolean variable that can be used to produce figures
    #risk_free_rate = average rate of return from a save government issued bond used to calcualte sharpe ratio with
    #**kwargs = any specific keyword arguments that can be passed to the backtesting function to allow for comparison of the backtest for different possible parameters defined in the subclass

        #run the backtest function and define the stock price data to be once again self.data and signals self.strat
        self.strat = self.backtest(start_date, end_date, **kwargs)

        #convert the monthly risk free rate to the daily risk free rate for use when calculating sharpe and sortino ratios
        #e.g. (1+1/100)**(1/21)-1 = (1.01**(0.05)) - 1 =  0.00047 (look up EAR formula)
        #the value of 21 is due to there being 20 trading days in a month
        daily_rate = (1 + percentage_risk_free_rate/100)**(1/21) - 1

        #sets up  new DataFrame which will give the returns of the portfolio
        return_df = pd.DataFrame(columns=["daily returns", "cumulative returns"], index = self.data.index)
        #set return on day 0 to 0
        return_df["daily returns"][0] = 0

        #loops through remaining dates and calculates returns across the portfolio
        for i in range(1, len(self.data)):
            #for each stock, this is 100*value weighting from strategy the previous day*(closing price on current day X - closing price on day X-1)/(closing price on day X-1)
            #hence why if your value weighting is 1 (long position), and the stock goes up, your daily return is posiitve
            #if your weighting is -1 (short position), and the stock goes down, then your daily return is positive
            #this is then summed for the multiple stocks in the portfolio and the portfolio daily returns are given
            return_df["daily returns"][i] = sum(100*self.strat[c][i-1]*(self.data["Adj Close"][c][i]-self.data["Adj Close"][c][i-1])/self.data["Adj Close"][c][i-1] for c in self.codes)

        #calculates the cumulative return for each date
        #it does this by taking the daily return percentage, dividing my 100 and adding 1 to get it into a increase/decrease
        #e.g. daily return of 7% goes >>  (7/100)+1 = 1.07 This happens for all the values in the dataframe
        #then the cumprod returns the cumulative product of a dataframe (NOT SUM) e.g. 1.07*0.96*1.02 not 1.07+0.96+1.02
        #this results is then -1 and multiplied by 100... e.g. (1.12-1)*100 = 12%
        return_df["cumulative returns"] = (((return_df["daily returns"]/100)+1).cumprod()-1)*100
        return_df.dropna()


        #For each of the strategies we went through in the notebook they all require a calculations involving a certain
        #number of historic values. e.g previous percentage returns. As a result our strategies can't start straight away
        #as we need to wait until we have enough previous pieces of data for the code to work and produce signals.

        #calculates the length of time for which the strategy is inactive to begin with
        zero_count = 0
        #While True will run forever until a break command is run.
        while True:
            if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):
                #by using iloc[zero_count] when zero_count is 0 it looks at first row, in this is zero then zero_count = 1
                #and then it becomes iloc[1] which searches the second row in the strat dataframe
                #and so on....
                break
            zero_count += 1

            #python syntax allows for the simplification of
            #if not sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes) == 0:
            #to
            #if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):

            # ^^^ this says that if the rows equals zero, then carry on but if it doesnt equal zero then break
            #so if it is zero 5 times, it bypasses the break and adds to the zero_count, but then the first nonzero value
            #breaks the loop and the while becomes false, and the 5 is stored in zero_count


        #calculates the sharpe ratio, not including the first period of inactivity

        #Sharpe ratio is the sum of the differences in daily returns of the strategy and the risk-free rate
        #over a given period is divivded by the standard devisation of all daily returns.
        #(all return percentages summed/100 - (risk free rate * number of days) ) / standard devisation of all daily returns
        #e.g. ((1.02+0.95+1.06+1.03)/100 - 4*0.0005) / 0.03 = 1.29
        sharpe = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][zero_count:].std())
        print('Sharpe: ',sharpe)

        #Sortino ratio is similar, however we divide by the standard deviation of only the negative or downwards
        #returns (inferring that we only care about negative volatility)
        #This doesnt include the zero_count index in denominator, as there are no negative downturns anyway when there are 0 values, so these are filtered out
        sortino = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][(return_df["daily returns"] < 0)].std())
        print('Sortino: ',sortino)

        #plots figures if fig_strat = TRUE
        if fig_strat:
            #plot of strategy returns
            plt.figure()
            plt.title("Strategy Backtest from" + start_date + " to " + end_date)
            plt.plot(return_df["cumulative returns"])
            plt.show()

        #plots figure if fig_other = TRUE
        if fig_other:
            #plot of all inidividual stocks
            for c in self.codes:
                plt.figure()
                plt.title("Buy and Hold from" + start_date + " to " + end_date + " for " + str(c))
                plt.plot(((self.data["Adj Close"][c].pct_change()+1).cumprod()-1)*100)
                plt.show()
        print('Returns DataFrame ',return_df)
        return [return_df, sharpe, sortino]



'''
Specific Strategy for Bollinger
'''

class StrategyBollinger(Strategies):
    #this is the Bollinger Strategy Class

    #parameters include start_Date and end_date
    #t = used for the Simple Moving Average in this strat
    def backtest(self, start_date, end_date, t=20):


        #imports all code from the parent class so we dont have to rewrite them
        Strategies.backtest(self, start_date, end_date)


        #calculate the Simple Moving Average (SMA)
        self.data['SMA'] = self.data['Close'].rolling(window=t).mean()
        #get stnadard deviation
        self.data['STD'] = self.data['Close'].rolling(window=t).std()
        #calculate the upper Bollinger band
        self.data['Upper'] = self.data['SMA'] + (self.data['STD'] * 2)
        self.data['Lower'] = self.data['SMA'] - (self.data['STD'] * 2)

        #create a list of columns to keep
        column_list = ['Close', 'SMA', 'Upper', 'Lower']

        #signal_df is a seperate dataframe to display the buy and sell signals on the plot
        signal_df = self.strat.copy()


        for i in range(t, len(self.data)):
            for c in self.codes:
                close = self.data['Close'][c][i]
                upper = self.data['Upper'][c][i]
                lower = self.data['Lower'][c][i]

                if (close > upper): #then you should sell
                    self.strat.iloc[i] = -1
                    signal_df.iloc[i] = -1

                elif (close < lower): # then you should buy
                    self.strat.iloc[i] = 1
                    signal_df.iloc[i] = 1

                else:
                    #notice signal_df not here as that df is only used to show the signal, not used in the 'evaluate' method
                    #this line says that if none of the bollinger bands are exceeded, then this day should be same as previous
                    self.strat.iloc[i] = self.strat.iloc[i-1]


        #pd.set_option('display.max_rows', 10)

        buy_signal = pd.DataFrame(data = np.zeros([len(self.strat), len(self.codes)]), columns = self.codes, index = self.strat.index)
        sell_signal = pd.DataFrame(data = np.zeros([len(self.strat), len(self.codes)]), columns = self.codes, index = self.strat.index)


        #this section gets dates from the signal_df , and adds the dates of buy and sell signals to the buy_signal and sell_signal dataframe respectively
        for i, row in signal_df.iloc[t:].iterrows():
            for c in self.codes:
                if int(row) == 1:
                    sell_signal.loc[i] = 0
                    buy_signal.loc[i] = self.data['Close'][c][i]
                elif int(row) == -1:
                    buy_signal.loc[i] = 0
                    sell_signal.loc[i] = self.data['Close'][c][i]



        #Plot all of the data
        #get the figure and figure size
        fig = plt.figure(figsize=(12,6))
        #add subplot
        ax = fig.add_subplot(1,1,1)
        #get the index values of the dataframe
        x_axis = self.data.index
        #plot and shade the area between upper and and lower band grey
        ax.fill_between(x_axis,self.data['Upper'][self.codes[0]], self.data['Lower'][self.codes[0]], color = 'grey', label='Bollinger Band')
        #Plot closing price and moving average
        ax.plot(x_axis, self.data['Close'], color = 'gold', lw=1, label='Close Price', alpha = 0.5)
        ax.plot(x_axis, self.data['SMA'], color = 'blue', lw=1, label='Simple Moving Average', alpha = 0.5)
        #adds in the green and red  arrows corresponding to the buy and sell signals
        ax.scatter(x_axis, buy_signal, color = 'green', lw = 1, label='Buy', marker='^', alpha = 1)
        ax.scatter(x_axis, sell_signal, color = 'red', lw = 1, label='Sell', marker='v', alpha = 1)

        #set title and show image
        ax.set_title('Bollinger Band for Stock(s)')
        ax.set_xlabel('Date')
        ax.set_ylabel('USD Price ($)')
        plt.xticks(rotation = 45)
        ax.legend()
        plt.show()


        return (self.strat)


'''
Example of the strategy using Apple stock
'''
#only input a single stock
testTSM = StrategyBollinger(["AAPL"])
testTSM.evaluate("2021-07-27","2022-07-27", t=30)
print(testTSM.data)
            
          

RSI Momentum

Relative Strength Index measures the speed and magnitude of a security's recent price changes to evaluate overvalued or undervalued conditions. The value of the RSI ranges from 0 to 1. If the value of the RSI is close to 0, then this indicates that the asset is underpriced. If it is close to 1 then it is overpriced. The boundary values are 0.3 and 0.7 for low and high respectively. The algorithm will:

  • Calculate RSI at each date
  • If RSI is less than 0.3 enter long position
  • If RSI greater than 0.7 then enter long position
  • If RSI is in between then keep the same position as before

Code Snippet 6. RSI Momentum
            
'''
Install yfinance if not installed to run
'''
#pip install yfinance


import numpy as np
import pandas as pd
import pandas_datareader as pdr
import yfinance as yf
import matplotlib.pyplot as plt
import datetime


'''
Setting up class
'''
class Strategies():
    #A class that contains code that strategies later on will inherit from params:
    #codes = list of stock short codes


    #Every class has to have an init method and they define the class specific features.
    #In this case ticker list and two empty dataframes
    def __init__(self,codes):
    #this defines class spacific values
        #so ticker/code list
        self.codes = codes
        #creates 2 empty dataframes
        self.strat = pd.DataFrame()
        self.data = pd.DataFrame()



    def import_data(self, start_date, end_date):
    #this method downloads all data for each backtest from yahoo Finance.
    #and removes any nan values

        #start_date, end_date = string of dates for backtesting with format y-m-d
        #code is another name for ticker
        data = yf.download(self.codes, start_date, end_date)

        #if only one stock code is entered, data is reformatted so that is it the same format as if multiple stock were entered
        if len(self.codes) == 1:
            data.columns = [data.columns, self.codes*len(data.columns)]

        #returns data, and NAN values are removed
        return data.dropna()




    def backtest(self, start_date, end_date):
    #returns a list with elements of a time series from yfinance as well as an array of values between -1 and 1 that represent the strategy over the given period
    #1 represents a long position in one stock, 0 representing a neutral postiion and -1 representing a short position

        #sets up dataframes (defined in the init) to contain data in 1, and then all value weights from the strategy for each stock in the other dataframe
        self.data = self.import_data(start_date, end_date)
        #fills the dataframe with zeros
        self.strat = pd.DataFrame(data = np.zeros([len(self.data), len(self.codes)]), columns = self.codes, index = self.data.index)




    #evaluate method takes a backtest, and evaluates it so calcualte cumulative returns, sharpe and sortino ratios
    def evaluate(self, start_date, end_date, fig_strat=True, fig_other=False, percentage_risk_free_rate = 0.1, **kwargs):
    #returns a dataframe with columns including the daily returns of the portfolio, the cumulative returns, the sharpe ratio and all relevent plot of both the stock price of each stock
    #fig = boolean variable that can be used to produce figures
    #ris_free_rate = average rate of return from a save government issued bond used to calcualte sharpe ratio with
    #**kwargs = any specific keyword arguments that can be passed to the backtesting function to allow for comparison of the backtest for different possible parameters defined in the subclass

        #run the backtest function and define the stock price data to be once again self.data and signals self.strat
        self.strat = self.backtest(start_date, end_date, **kwargs)

        #convert the monthly risk free rate to the daily risk free rate for use when calculating sharpe and sortino ratios
        #e.g. (1+1/100)**(1/21)-1 = (1.01**(0.05)) - 1 =  0.00047 (look up EAR formula)
        #the value of 21 is due to there being 20 trading days in a month
        daily_rate = (1 + percentage_risk_free_rate/100)**(1/21) - 1

        #sets up  new DataFrame which will give the returns of the portfolio
        return_df = pd.DataFrame(columns=["daily returns", "cumulative returns"], index = self.data.index)
        #set return on day 0 to 0
        return_df["daily returns"][0] = 0

        #loops through remaining dates and calculates returns across the portfolio
        for i in range(1, len(self.data)):
            #for each stock, this is 100*value weighting from strategy the previous day*(closing price on current day X - closing price on day X-1)/(closing price on day X-1)
            #hence why if your value weighting is 1 (long position), and the stock goes up, your daily return is posiitve
            #if your weighting is -1 (short position), and the stock goes down, then your daily return is positive
            #this is then summed for the multiple stocks in the portfolio and the portfolio daily returns are given
            return_df["daily returns"][i] = sum(100*self.strat[c][i-1]*(self.data["Adj Close"][c][i]-self.data["Adj Close"][c][i-1])/self.data["Adj Close"][c][i-1] for c in self.codes)

        #calculates the cumulative return for each date
        #it does this by taking the daily return percentage, dividing my 100 and adding 1 to get it into a increase/decrease
        #e.g. daily return of 7% goes >>  (7/100)+1 = 1.07 This happens for all the values in the dataframe
        #then the cumprod returns the cumulative product of a dataframe (NOT SUM) e.g. 1.07*0.96*1.02 not 1.07+0.96+1.02
        #this results is then -1 and multiplied by 100... e.g. (1.12-1)*100 = 12%
        return_df["cumulative returns"] = (((return_df["daily returns"]/100)+1).cumprod()-1)*100
        return_df.dropna()


        #For each of the strategies we went through in the notebook they all require a calculations involving a certain
        #number of historic values. e.g previous percentage returns. As a result our strategies can't start straight away
        #as we need to wait until we have enough previous pieces of data for the code to work and produce signals.

        #calculates the length of time for which the strategy is inactive to begin with
        zero_count = 0
        #While True will run forever until a break command is run.
        while True:
            if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):
                #by using iloc[zero_count] when zero_count is 0 it looks at first row, in this is zero then zero_count = 1
                #and then it becomes iloc[1] which searches the second row in the strat dataframe
                #and so on....
                break
            zero_count += 1

            #python syntax allows for the simplification of
            #if not sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes) == 0:
            #to
            #if sum(abs(self.strat[c].iloc[zero_count]) for c in self.codes):

            #^^^^ this basically says that if the rows equals zero, then carry on but if it doesnt equal zero then break
            #so if it is zero 5 times, it bypasses the break and adds to the zero_count, but then the first nonzero value
            #breaks the loop and the while becomes false, and the 5 is stored in zero_count



        #calculates the sharpe ratio, not including the first period of inactivity

        #Sharpe ratio is the sum of the differences in daily returns of the strategy and the risk-free rate
        #over a given period is divivded by the standard devisation of all daily returns.
        #( all return percentages summed/100 - (risk free rate * number of days) ) / standard devisation of all daily returns
        #e.g. ((1.02+0.95+1.06+1.03)/100 - 4*0.0005) / 0.03 = 1.29
        sharpe = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][zero_count:].std())
        print('Sharpe: ', sharpe)

        #Sortino ratio is similar, however we divide by the standard deviation of only the negative or downwards
        #returns (inferring that we only care about negative volatility)
        #This doesnt include the zero_count index in denominator, as there are no negative downturns anyway when there are 0 values, so these are filtered out
        sortino = ((return_df["daily returns"][zero_count:].sum()/100 - len(return_df[zero_count:]) * daily_rate) / return_df["daily returns"][(return_df["daily returns"] < 0)].std())
        print('Sortino: ', sortino)



        #plots figures if fig_strat = TRUE
        if fig_strat:
            #plot of strategy returns
            plt.figure()
            plt.title("Strategy Backtest from" + start_date + " to " + end_date)
            plt.plot(return_df["cumulative returns"])
            plt.show()

        #plots figure if fig_other = TRUE
        if fig_other:
            #plot of all inidividual stocks
            for c in self.codes:
                plt.figure()
                plt.title("Buy and Hold from" + start_date + " to " + end_date + " for " + str(c))
                plt.plot(((self.data["Adj Close"][c].pct_change()+1).cumprod()-1)*100)
                plt.show()
        print('Returns: ', return_df)
        return [return_df, sharpe, sortino]


'''
Class specifically for RSI Strat
'''
class StrategyRSI(Strategies):
    def backtest(self, start_date, end_date, t=5, q=5, weighting=False):

        #still had to import previous code from parent backtest function
        Strategies.backtest(self, start_date, end_date)

        #initisalise the RSI dataframe, and this calculates the RSI values for each stock at each time instance
        RSI = pd.DataFrame(data = np.zeros([len(self.data), len(self.codes)]), columns = self.codes, index = self.data.index)


        #then we loop through the time index
        for i in range(t, len(self.data)):

            #pct_change computes the percentage change from i-t to i
            data_pct_change = self.data["Adj Close"][i-t:i].pct_change()

            #basically calculating the mean of the positive returns over that period
            #we use a Boolean index for this with: data_pct_change[c] >= 0
            #it says 'is the percantage change for some given code data_pct_change[c] greater than 0. If it is then include it in the mean
            numerator = pd.Series(data = (data_pct_change[c][(data_pct_change[c] >= 0 )].mean() for c in self.codes), index = self.codes)

            #mean of the negative returns
            denominator = pd.Series(data = (data_pct_change[c][(data_pct_change[c] <= 0 )].mean() for c in self.codes), index = self.codes)

            #an issue is say we look a period of 10 days and all returns are positive, then denominator has no values to calculate a mean
            #so python will say its not a number. And same if all negative returns

            #so we treat these two cases here
            for c in self.codes:

                #if no positive returns in past t days, set RSI to 0 as we think its underbrought
                if numerator.isnull()[c]:
                    RSI[c][i] = 0

                #if no negative returns in past t days, set RSI to 1 as we think its overbrought
                elif denominator.isnull()[c]:
                    RSI[c][i] = 1

                #otherwise, we can calulate the RSI
                else:
                    RSI[c][i] = 1 - 1/(1-numerator[c]/denominator[c])



                #if it is not being weighted
                if not weighting:
                    #if the RSI value is less that 0.3 for that stock on that day, then go long
                    if RSI[c][i] < 0.3:
                        self.strat[c][i] = 1

                    #and continue going long on that stock until it becomes larger than 0.7
                    #we then exit the trade and enter a merket neutral position, not shorting
                    elif RSI[c][i] > 0.7:
                        self.strat[c][i] = 0

                    #if it doesnt go into any of these boundaries, then keep the same as before
                    else:
                        self.strat[c][i] = self.strat[c][i-1]

                #if weighted
                else:
                    if (i-t) % q:
                        self.strat.iloc[i] = (0.8 - RSI.iloc[i])
                    else:
                        self.strat.iloc[i] = self.strat.iloc[i-1]

            #again we have to normalise across the rows as we did in previous strategies
            #unlike the first two strategies, we dont know how many assets we are going to take a position in so we cant just divide by the number of assets
            #e.g. if we have 5 assets and long positions in all of them, then we have to divide by 5,
            #but if we have invested in only 2 stocks, then we have to divide by 2 instead of the assets avaliable
            row_sum = sum(abs(self.strat.iloc[i]))
            if row_sum:
                self.strat.iloc[i] /= row_sum
            else:
                self.strat.iloc[i] = self.strat.iloc[i-1]


        return self.strat



'''
Example of a backtest for this strat
'''
testRSI = StrategyRSI(["^FTSE","^GSPC","AAPL","GC=F","ZC=F","HG=F","SIEGY","SIE.DE"])
testRSI.evaluate("2020-07-25","2022-07-25", t=5, weighting=False)
#basic RSI without the weighting function
            
          
Download All Scripts

Related Articles