Introduction

Welcome to this tutorial on backtesting a trend-following or mean-reverting trading strategy using the S&P 500 energy sector stocks as your stock universe. Backtesting is a crucial step in the development and evaluation of trading strategies. It involves simulating the performance of a trading strategy on historical data to assess its potential profitability and risk.

In this tutorial, we will cover the steps to develop and backtest a daily trading strategy that buys and/or sells on the open, but allows positions to be carried over from one period to the next. We will use the S&P 500 energy sector stocks as our stock universe and a starting cash on hand of $100,000. We will allow long and short positions and implement either a two-trend or three-trend system, depending on the strategy chosen.

Our performance measures for the backtest will include all of the performance measures used in Homework 3. We will also discuss any optimization conducted and how we arrived at the backtest settings (model parameters) used. After completing the backtesting, we will rerun the strategy on another industrial sector of our choice and run the energy sector of stocks backtest on the period from January 2019 through December of 2021.

Learning Objectives

Before we dive into the problems we are going to address we want to outline the major learnings you can expect to be gaining at the end of this Tutorial. Here is the list of Learning Objectives for this tutorial:

  • Understand the basics of backtesting and its importance in evaluating trading strategies
  • Develop a trend-following or mean-reverting trading strategy for the S&P 500 energy sector stocks
  • Backtest your strategy using historical data
  • Interpret and evaluate the performance metrics of your strategy
  • Conduct optimization to improve the performance of your strategy
  • Analyze and compare the performance of the energy sector stocks strategy across different backtesting periods

Two-trend Strategy Overview

Summarized below is the overview of our trading strategy:

  • Implemented a two-trend strategy using 5-day and 10-day exponential moving averages
  • To reduce risk of false signals,we combined two-trend analysis with bollinger band analysis and relative strength index
  • The strategy enters into a long trade when fast exponential moving average crosses over slow exponential moving average and moving average signal crosses over upper band; exit when fast exponential moving average crosses below slow exponential moving average and get a down trend signal from bollinger band
  • For short trades, enter when fast exponential moving average crosses below slow exponential moving average and moving average crosses below lower band; exit when see an uptrend signal on bollinger bands and fast exponential moving average crosses above slow exponential moving average
  • Use Relative Strength Index to decide on trades to pick to satisfy max trades constraint, preferred trades in ascending order of Relative Strength Index

Preprocessing: Loading the Data

As done in previous assignments, we start off by setting the working directory and installing the required packages. We will also load the universe data from Rdata file. We have updated universe data file to include the sectors information for each symbol. Using this sectors information, we first filter out the stocks for Energy sector and then implement two trend strategy to maximize the portfolio stats.

Steps and Implementation

  • First we load the rstudioapi, which will be used to set the current path in working directory. We clear all the environment variables using rm function.
  • Then we load two R packages: quantmod and TTR. The quantmod package provides a suite of functions for financial data analysis, including downloading historical stock prices, charting, and technical analysis and the TTR package provides technical analysis functions for financial data.
  • Then we load the universe.rdata file containing the stocks OHLC and sectors data. Then we define the start and end dates for the analysis. Specifically, the from variable is set to December 17th, 2020 and the to variable is set to December 31st, 2022.
  • At this point we subset the universe data frame to only include stocks from the Energy sector that have a date within the specified range using the subset() function which is used to filter the universe data frame to only include rows where the Sector column is “Energy” and the date column falls within the specified from and to dates.

Parameter Initialization

We set the parameters that will be used later in the strategy. Specifically we set initial equity of 100,000 $ and set a limit on maximum number of trades allowed per day as 11. We also limit the amount of the equity traded in one transaction as 11,000$

Generating Indicators

Here we define the genIndicators function, which takes a stock symbol as input and generates technical indicators for that stock. We have developed a two trend strategy which takes into account the bollinger bands, fast and slow Exponential Moving Average (EMA), and Relative Strength Index (RSI) index to quantify the risk element. In this function, we initialize the all these parameters, that is generating the bollinger bands for the energy stocks, deriving the two moving averages and calculating the RSI index. We will use these three parameters later in the code to make decisions on indicators and signals generated.

Steps and Implementation

  1. We first subset the universe data to obtain only the data for the input stock symbol. We then create an xts object from the stock data and create a copy of it as temp.xts

2. Then we use BBands function from the TTR package to calculate Bollinger Bands for the stock, using a 5-day simple moving average and a standard deviation of 2. If there is an error or warning during this calculation, the resulting bb object is set to NULL. Then we set the parameters such as down, moving average and up in the xts object to the corresponding indicators from bollinger bands.

3. After bollinger bands we calculate the 5-day and 10-day exponential moving averages (EMAs) for the stock using the EMA function from the TTR package. If there is an error or warning during these calculations, the resulting ema5 and ema10 objects are set to NULL.

4. Finally, we calculate the 14-day relative strength index (RSI) for the stock using the RSI function from the TTR package. We create a data frame from the xts object and add the stock symbol as a column. The resulting data frame is returned as the output of the function.

Generating Signals

We use the function genSignals to generate trading signals for a given stock symbol using technical indicators such as Bollinger Bands and exponential moving averages. The function takes one argument sym, which is the stock symbol for which the signals are to be generated. The function performs the following steps:

  1. Here first we subset the indicators data frame to extract the rows corresponding to the given stock symbol and then convert the subsetted data frame into an xts object with the stock prices and the technical indicators as columns.

2. Calculates the Bollinger Bands signals as follows

  1. stock.xts$cross.upper indicates a buy signal when the closing price is above the upper Bollinger Band.
  2. stock.xts$cross.lower indicates a sell signal when the closing price is below the lower Bollinger Band.
  3. stock.xts$cross.trendup indicates a bullish signal when the closing price is above the moving average.
  4. stock.xts$cross.trenddn indicates a bearish signal when the closing price is below the moving average.

Calculates the EMA signals as follows

  1. stock.xts$cross.upper_ema indicates a buy signal when the closing price is above both the 5-day and 10-day EMA.
  2. stock.xts$cross.trenddn_ema indicates a sell signal when the closing price is below the 5-day EMA.
  3. stock.xts$cross.lower_ema indicates a sell signal when the closing price is below both the 5-day and 10-day EMA.
  4. stock.xts$cross.trendup_ema indicates a buy signal when the closing price is above the 5-day EMA.
  1. Finally we prepare the output data in the desired format, with columns for date, symbol, and the various trading signals generated by the function. We first convert the stock.xts object generated in the previous step to a data frame called stock. We then extract the row names of stock (which correspond to the dates) and convert them to the date format using the as.Date function. The date column is then added to the stock data frame using the cbind function. The first column of stock is then renamed as “symbol” using the names function.

Close Positions

The close positions function has been modified to apply rules based on the trading strategy proposed in the beginning of the tutorial. In this case, we are closing positions which are open if it satisfies the following conditions for short and long positions respectively -

Short positions -

  • The trend of the signal is increasing
  • The signal crosses the moving average line and has an increasing trend

This ensures that we recognise that the stock has an upwards trend which is not desirable for short positions. The modified code is given below -

Long positions -

  • The trend of the signal is downwards
  • The signal crosses the moving average line and has a downward trend

This ensures that we recognise that the stock has a downward trend and that is going to lose money for a long position. The modified part of the code is given below -

The last line of code above combines all rows for the closed long and short positions for the given day after which the below piece of code computes the closed cash, sell and buy prices, profit and close cash.

Open Positions

The open positions function has been modified to apply rules based on the trading strategy proposed in the beginning of the tutorial. In this case, we are entering positions if it satisfies the following conditions for short and long positions respectively -

Short positions -

  • The signal crosses the lower trend line
  • The signal has a downward trend, is lower than the slow moving average line and the faster moving average trend is lower than the slower moving average trend line

These conditions ensure that the trend of the signal is downwards and hence it is wise to short the position.

Long positions -

  • The signal crosses the upper trend line
  • The signal has a upward trend, is greater than the slow moving average line and the faster moving average trend is greater the slower moving average trend line

These conditions ensure that the trend of the signal is upwards and hence it is wise to long the position.

The last line in the above screenshot shows that we are then combining long and short positions to arrive at all open positions.

Further, in order to ensure that we do not cross the maximum number of trades on a given day, we sort all of the positions based on the RSI values and pick trade positions with lower RSI positions up to the maximum number of trades. This is achieved using the range field that was created in the generate indicators section.

We then move forward to split out trade amount equally between all available stocks and invest the amount accordingly to both long and short positions. The function returns a list of a dataframe opened which contains details of the opened positions and the cash value as cashout.

Apply Rules

The apply rules function will check if there are any open positions and close them by running the close position function which applies all of the rules that have been defined. Similarly, the function then checks whether the signal is already open and if not, it will then apply rules and open positions accordingly using the Open positions function.

CALCULATE PORTFOLIO STATISTICS

This is a function called “portfolioStats” which takes three arguments:

  • trades: a data frame containing information about trades (date, type, closedate, etc.)
  • pvalue: a numeric vector representing the portfolio value at each point in time
  • tdays: a vector of dates representing the time period

Here we perform several calculations and generates a plot to provide information about the portfolio’s performance:

  • Calculates the number of days traded, the percentage of days traded, and the total number of trades
  • Calculates the daily percentage return and separates the trades into short and long positions
  • Calculates the cumulative return, maximum return, and maximum value at each point in time using a for loop
  • Calculates the drawdown and drawdown percentage, as well as the maximum drawdown and its percentage
  • Calculates the average holding period, mean return, and Sharpe ratio
  • Generates a plot of the portfolio’s cumulative return, maximum return, and daily return over time
  • Returns a list of performance metrics including total trades, long and short trades, cumulative return, mean return, Sharpe ratio, maximum drawdown, maximum drawdown percentage, length of the longest drawdown streak, and average holding period

Details:

  1. The code calculates some basic statistics related to the trading history, such as the number of unique trading days, total trading days, percentage of days traded, total number of trades made, and the number of short and long trades. The code achieves this by first counting the number of unique trading days in the trades data frame and the total number of trading days in the tdays vector. It then calculates the percentage of days traded by dividing the number of unique trading days by the total number of trading days. Finally, it determines the total number of trades made and the number of short and long trades by subsetting the trades data frame based on the trade type. We also calculate the cumulative return of the portfolio over time and the maximum return achieved up to that point. It does this by initializing two vectors, cumreturn and maxreturn, with length equal to totaldays, and setting all their values to 1. The code then calculates the cumulative return up to each day in the tdays vector using the prod function and saves the result in the corresponding position of the cumreturn vector. It also updates the maxreturn vector with the maximum cumulative return achieved up to that point. Finally, it calculates the percentage drawdown, i.e., the percentage decline in the portfolio’s value from its peak, by subtracting maxreturn from cumreturn and dividing the result by maxreturn

2. This section calculates the maximum drawdown and drawdown duration of the portfolio using a for loop. It initializes two variables, streak and maxstreak, to 0. The for loop iterates over all the days and increments the streak variable if the portfolio return is negative. If the portfolio return is positive or zero, the streak is reset to 0. The maximum streak is updated if the current streak is greater than the previous maximum. Finally, the function plots the cumulative return of the portfolio, maximum return, and portfolio return using the plot and lines functions.

3. This section of code computes several performance metrics for a portfolio based on the input of trading data. It calculates the cumulative return of the portfolio, the mean return, the Sharpe ratio, the maximum drawdown, and the length of the longest drawdown streak. The results are stored in a list and returned for further Analysis.

Run Strategy

Now that we have built the basic framework for our trading strategy, it is now time to run the strategy and test the output. The final output is the performance of the trading strategy in terms of the calculated statistics.

Implementation

Data Preperation

This includes generating indicators for a list of stocks and generating signals required to be able to apply different conditions to run the trading strategy.

  • We first create indicator as an empty dataframe that will be used to store the technical indicators generated for each stock.
  • Using for loop we iterate over each stock symbol in symbols and generates technical indicators using the genIndicators function explained above.
  • If stock is empty, temp is assigned to it. Otherwise, temp is appended to stock using the rbind() function.
  • Similarly, we generate signals by using a for loop and iterating through each symbol present in the stock universe.

Trading Simulation

This involves looping through each trading day and applying the trading rules to determine which stocks to buy or sell. We first compute the results for each day by applying rules using the apply rules function and then use that to determine current cash. This current cash is then carried forward to the next day to be able to apply rules using this money. The value pvalue monitors the daily portfolio value which can also be used to check the portfolio value and can be used as a performance metric.

Portfolio Evaluation

This includes calling the portfolioStats function to calculate portfolio statistics such as total returns, Sharpe ratio, maximum drawdown, etc. based on the trade information and daily returns stores the results of the evaluation in the performance dataframe.

Conclusion

Our strategy of combining two trend system with bollinger bands and using RSI to mitigate risky trades helps to reduce the trades on false signals and hence we a significant improvement in the performance of our trade shown below:

Here’s what each metric means and whether it indicates good performance or not:

  • $totaltrades: This is the total number of trades taken by the strategy. It doesn’t provide much information about performance on its own.
  • $longtrades and $shorttrades: These are the number of long and short trades taken by the strategy, respectively. Depending on the strategy, one of these numbers may be higher than the other. Again, this doesn’t provide much information about performance on its own.
  • $cumreturn: This is the cumulative return of the strategy over the period being analyzed. A value of 1.0 would mean that the strategy didn’t make or lose any money, while a value higher than 1.0 indicates that the strategy made a profit. In this case, the value is 1.894225, which means that the strategy made a profit.
  • $meanreturn: This is the average return per trade. A value higher than 1.0 means that the strategy is profitable on average. In this case, the value is 1.002436, which indicates that the strategy is profitable.
  • $sharpe: This is the Sharpe ratio, which measures the risk-adjusted performance of a strategy. A value higher than 1.0 means that the strategy is generating returns that are higher than its risk. In this case, the value is 2.103954, which is a good Sharpe ratio.
  • $maxdraw: This is the largest drawdown experienced by the strategy. A drawdown is the amount of money lost from a peak to a trough in the strategy’s equity curve. A large drawdown indicates that the strategy is risky. In this case, the value is -16311.26, which is quite large.
  • $maxdrawpct: This is the percentage of the largest drawdown relative to the starting equity. A value higher than -20% is generally considered to be bad. In this case, the value is -9.757428, which is a relatively small drawdown compared to the starting equity.
  • $drawlength: This is the length of the longest drawdown period in terms of the number of trades taken. The longer the drawdown period, the more difficult it is for the strategy to recover. In this case, the drawdown period lasted for 31 trades.
  • $meanhold: This is the average holding period of the trades taken by the strategy. Depending on the strategy, a shorter or longer holding period may be preferable. In this case, the average holding period is 7.830739.

As we can infer from the definitions, the performance of the strategy seems good based on the Sharpe ratio, average return per trade, and cumulative return. However, the large drawdown and relatively long drawdown period suggest that the strategy may be risky and could benefit from risk management techniques.

We also applied our strategy on the “Utilities” sector to check the applicability of our strategy in a different environment.

Here again we can see we perform decently in terms of sharpe ratio and our strategy was able to make 49.25% over the course of our trading period.

We also observed the improvement combining the two strategies gave us over just using the two trend system. Attached below is the performance metric for strategy just using the ema signals:

We can clearly see how bad the performance is compared to our strategy of combining multiple signals to perform a trade and using RSI for risk ordering.

--

--

Kushagra Jain

I am a Masters in Computer Science student at Texas A&M University. Currently I am trying to get into the domain of Algorithmic Trading and HFTs.