Equity Backtesting

Simple Stochastic RSI Backtest



Project Description

The Relative Strength Index (RSI) is a momentum indicator that compares the magnitude of recent gains and losses over a specified time period to measure the speed and change of price movements of a security. It is primarily used in an attempt to identify overbought or oversold conditions in the trading of an asset.

The Stochastic RSI is an oscillator that measures the level of the RSI relative to its high/low range over a defined period of time. Instead of using the Stochastic’s formula on the normal OHLC values, the formula is applied to a stock’s RSI values.

The StochRSI was developed by Chande and Kroll to increase the sensitivity of the RSI and generate more overbought/oversold conditions. The reasoning was that a trader may see RSI levels oscillating between the thresholds they’ve defined, say 70 for overbought and 30 for oversold, and never see a trigger period indicating a buy/sell condition.

The important thing to understand is that the StochRSI is an indicator of an indicator. This means that it is two steps removed from the actual stock price. The price of the stock has undergone two changes to become StochRSI. The first step is to convert prices to RSI. The second step is to convert the RSI to the Stochastic Oscillator.

The default time period for a short-term indicator would be 14 days. The StochRSI has a range between 0 and 100 with 20 being considered the threshold for oversold and 80 being the threshold for overbought. The smoothing time period for a short-term indicator would be 3 days.


This project will be a simple test for oversold stocks with a threshold of 20 and a comparison time period of 14 days. A 3 month return on the stock will be calculated along with an optimal return which identifies the highest return possible within the 3 month period.

The source data will be the backtest environment created in the previous project, Building the Backtest Environment. Recall that the environment contains historical pricing data from 2007-2016. We also have a symbols table that contains all of the stocks that were successfully loaded in the environment.

As before, we will be using the quantmod package to retrieve data from the backtest environment and perform the RSI generation.


Libraries Required

library(quantmod)   # Quantitative financial strategies
library(dplyr)      # Data manipulation
library(lubridate)  # Date and time processing
library(knitr)      # Dynamic report generation

Working directory

setwd("U:/Equity Backtesting")

Prepare the Environment

We will require the two files previously created in order to perform the StochRSI backtest. To access the environment we created we simply need to load the environment file. The symbols table is in the form of an .rds.

Load Backtest Environment

load("Environments/bt_env_2007_2016.Rdata")

Read Backtest Symbols Table

bt_env_symbols <- readRDS("Symbols/bt_env_symbols.rds")

We can check to make sure that the data is loaded correctly and contains expected values. This time we can look for symbols that start with “NV”.

Verify Backtest Environment

ls(bt_env_2007_2016, pattern = "^NV")
##  [1] "NVAX" "NVCN" "NVCR" "NVDA" "NVDQ" "NVEC" "NVEE" "NVET" "NVFY" "NVGN"
## [11] "NVGS" "NVIV" "NVLN" "NVLS" "NVMI" "NVO"  "NVR"  "NVRO" "NVS"  "NVTA"
## [21] "NVTR" "NVUS"

Verify Data in Backtest Environment

kable(head(bt_env_2007_2016$NVDA, 10))
NVDA.Open NVDA.High NVDA.Low NVDA.Close NVDA.Volume NVDA.Adjusted
37.07 37.52 34.79 36.08 28870500 24.05333
35.95 36.08 35.03 35.91 19932400 23.94000
35.06 35.20 33.42 33.66 31083600 22.44000
33.78 34.56 33.20 33.91 16431700 22.60667
33.96 34.19 33.21 33.25 19104100 22.16667
32.90 35.20 32.40 34.89 27718600 23.26000
34.89 35.16 34.19 34.76 23112600 23.17333
34.24 35.37 34.08 35.23 17454700 23.48667
35.50 35.52 34.89 35.29 17796600 23.52667
34.80 35.01 34.42 34.55 17698300 23.03333

Verify Backtest Symbols

kable(head(bt_env_symbols, 10))
Symbol Sector Industry Name Exchange
AAAP Health Care Major Pharmaceuticals Advanced Accelerator Applications S.A. NASDAQ
AAL Transportation Air Freight/Delivery Services American Airlines Group, Inc. NASDAQ
AAME Finance Life Insurance Atlantic American Corporation NASDAQ
AAOI Technology Semiconductors Applied Optoelectronics, Inc. NASDAQ
AAON Capital Goods Industrial Machinery/Components AAON, Inc. NASDAQ
AAPC Consumer Services Services-Misc. Amusement & Recreation Atlantic Alliance Partnership Corp. NASDAQ
AAPL Technology Computer Manufacturing Apple Inc. NASDAQ
AAWW Transportation Transportation Services Atlas Air Worldwide Holdings NASDAQ
ABAC Consumer Non-Durables Farming/Seeds/Milling Aoxin Tianli Group, Inc. NASDAQ
ABAX Capital Goods Industrial Machinery/Components ABAXIS, Inc. NASDAQ
## [1] "Number of Backtest Symbols: 4667"

Process for StochRSI

Since this is a backtest project, we want to define the criteria for the data we are going to be retrieving. As was previously mentioned, the StochRSI is a two step process.

The first step is to calculate the RSI for a stock using the RSI() function in quantmod.

The second step takes the results of the RSI as input for the stoch() function. This function normally uses a data table with “Open”, “High”, “Low”, and “Close” values. However, it can also take a data table with a single array of values, in this case the RSI.

There are 3 statistics generated by quantmod for the StochRSI.

The next item to define is the value of the StochRSI that we are interested in. Since a StochRSI value of 20 is considered to be the signal for oversold conditions, we will be looking for StochRSI values that are equal to or less than 20 in each of the 3 statistics returned. We will then compare returns across the statistics and at different levels of the StochRSI to see if there are differences in the ratio of postive to negative returns and the average returns realized.

Reminder, the developer of the Stochastic Oscillator indicated that the FastD was the only signal he would recommend using to trigger a buy/sell. The SlowD is a more conservative signal than the FastD and would also be considered a trigger signal.


The first step in the processing is to retrieve the desired symbol data from the backtest environment. This is accomplished using the following:

get(symbol_name, source_environment)

The second step is to approximate, or substitute, any NA values in the time series. This is needed when a time series has a missing set of values for a given date. If the data is not adjusted, the RSI call will fail and we lose that stock as a potential test object.

Next, we invoke the RSI command and specify which field to perform the test on. The suggested field to use is adjusted closing price.

We do need an error check for the RSI call for the situations in which there isn’t enough data to perform the calculation. So we will use the function try() to validate the call and test whether the call inherits an error.

If there is no error, the results of the RSI call are used as input to the Stochastic Oscillator. Another error check is required to handle calls that fail.

If the call is successful the results will be placed in a data frame and then processed. The processing consists of the following for each of the 3 signals:

The data is grouped and filtered to keep only one occurrence of the StochRSI threshold in order to test the StochRSI as a trigger for initiating a buy condition for the stock for a given month. The data frame created is then joined to a master data frame which will contain all occurrences of the StochRSI threshold being met for the first time in a given month for all of the stocks.


Create Symbol Array

bt_symbols <- bt_env_symbols$Symbol

StochRSI Gather Loop

j <- 1

for(i in 1:length(bt_symbols)) {
        
        sym_sum  <- get(bt_symbols[i], envir = bt_env_2007_2016)
        
        sym_sum  = na.approx(sym_sum)
        
        get_try  <- try(RSI(Ad(sym_sum), 14))
        
        if(inherits(get_try, "try-error")) {
                
        } 
        
        else {
         
              rsi_sum <- RSI(Ad(sym_sum), 14)
              
              get_try <- try(stoch(rsi_sum, nFastK=14, nFastD=3, nSlowD=3))
              
              if(inherits(get_try, "try-error")) {
                
              } 
        
              else {
              
                   str_sum <- data.frame(stoch(rsi_sum, nFastK=14, nFastD=3, nSlowD=3))
              
                   str_fastK  <- str_sum %>%
                                 select(fastK) %>%
                                 mutate(Date = rownames(.)) %>%
                                 transform(fastK = round(fastK * 100, 1)) %>%
                                 filter(complete.cases(.),
                                        fastK <= 20) %>%
                                 mutate(Symbol = bt_symbols[i],
                                        Year   = year(Date),
                                        Mnth   = month(Date)) %>%
                                 group_by(Year, Mnth) %>%
                                 slice(1L) %>%
                                 ungroup() %>%
                                 select(Symbol, Date, fastK)
  
                   if(j == 1) { str_fastK_frame <- str_fastK } else
                              { str_fastK_frame <- bind_rows(str_fastK_frame, str_fastK) }
              
              
                   str_fastD  <- str_sum %>%
                                 select(fastD) %>%
                                 mutate(Date = rownames(.)) %>%
                                 transform(fastD = round(fastD * 100, 1)) %>%
                                 filter(complete.cases(.),
                                        fastD <= 20) %>%
                                 mutate(Symbol = bt_symbols[i],
                                        Year   = year(Date),
                                        Mnth   = month(Date)) %>%
                                 group_by(Year, Mnth) %>%
                                 slice(1L) %>%
                                 ungroup() %>%
                                 select(Symbol, Date, fastD)
  
                   if(j == 1) { str_fastD_frame <- str_fastD } else
                              { str_fastD_frame <- bind_rows(str_fastD_frame, str_fastD) }
              
              
                   str_slowD  <- str_sum %>%
                                 select(slowD) %>%
                                 mutate(Date = rownames(.)) %>%
                                 transform(slowD = round(slowD * 100, 1)) %>%
                                 filter(complete.cases(.),
                                        slowD <= 20) %>%
                                 mutate(Symbol = bt_symbols[i],
                                        Year   = year(Date),
                                        Mnth   = month(Date)) %>%
                                 group_by(Year, Mnth) %>%
                                 slice(1L) %>%
                                 ungroup() %>%
                                 select(Symbol, Date, slowD)
  
                   if(j == 1) { str_slowD_frame <- str_slowD } else
                              { str_slowD_frame <- bind_rows(str_slowD_frame, str_slowD) }
              
              
                   j <- j + 1
                   
                   }
              
         }
}

Process for Returns

The goal for the backtest is to determine if a StochRSI trigger, indicating a buy signal, is a potential predictor for positive returns based on the expectation of a bounceback in stock price after an oversold condition. For this example, we will look at a 3 month return from the date of the trigger.

Using the backtest environment again, we will retrieve the adjusted closing prices beginning with the date of the trigger and ending 3 months after. If the trigger date is such that the calculated end data is greater than 12/30/2016, the last trading day of 2016 and the last date in our backtest environment, the recorded returns will be NA.

During the 3 month testing window, it can be expected that the stock price will have volatility and that the starting and ending prices for the window may not reflect the low and high stock prices observed. To determine the potential for returns, we will also record the highest stock price during the window. It is possible that a 3 month return may show a negative value but there is an opportunity for a positive return within the window.

There are 3 unique data frames that need to be tested, one for each of the 3 signals previously defined. In order to create an efficient testing environment, I have created multiple functions that will be called using the desired data frames. These functions will be described as they appear below.


The signal data frames will be updated with the following:


The first function, Prepare_Frame(), will take the signal data frame to be processed as input and add/initialize the needed return fields.

Prepare Frame Function

Prepare_Frame <- function(temp_frame) {
        
        temp_frame <- temp_frame %>%
                      mutate(Trigger_Cl = 0,
                             Period_Cl  = 0,
                             Return_3mt = 0,
                             Period_Hi  = 0,
                             Return_Hi  = 0,
                             Volume_Min = 0,
                             Volume_Avg = 0)
        
        return(temp_frame)
}

The following calls Prepare_Frame() and places the results in the target signal data frame.

Add Fields to StochRSI Frame

str_fastK_frame <- Prepare_Frame(str_fastK_frame)
str_fastD_frame <- Prepare_Frame(str_fastD_frame)
str_slowD_frame <- Prepare_Frame(str_slowD_frame)

Generating the returns requires a processing loop to retrieve the symbol and date of the StochRSI trigger. A target end date is generated for the 3 month window. Each month, on average, contains 4.33 weeks. So a 3 month testing window will consist of 13 weeks.

If the calculated end date falls outside the range of the backtest environment, we simply set a value of NA to the desired fields. If the date range is within the backtest environment range, we can process the record and retrieve/calculate the statistics. We will be using the adjusted close field for all of the data points.

The second function, Generate_Returns(), will take the signal data frame to be processed, perform the data retrieval, perform the required statistical processing, and update the data frame accordingly.

Generate Returns Function

Generate_Returns <- function(temp_frame) {
        
        for(i in 1:nrow(temp_frame)) {
                
        Symbol   <- temp_frame$Symbol[i]
        
        Beg_Date <- as.Date(temp_frame$Date[i])
        End_Date <- Beg_Date + dweeks(13)
        
        if(End_Date > "2016-12-31") {
                
                temp_frame$Trigger_Cl[i]     <- NA
                temp_frame$Period_Cl[i]      <- NA
                temp_frame$Return_3mt[i]     <- NA
                temp_frame$Period_Hi[i]      <- NA
                temp_frame$Return_Hi[i]      <- NA
                temp_frame$Volume_Min[i]     <- NA
                temp_frame$Volume_Avg[i]     <- NA
                
        } 
        else {
                
                sym_sum    <- get(Symbol, envir = bt_env_2007_2016)
                
                date_range <- paste(as.character(Beg_Date),"/",as.character(End_Date), sep = "")
                
                sym_sum    <- data.frame(sym_sum[date_range])
                
                colnames(sym_sum) <- c("Open", "High", "Low", "Close", "Volume", "AdjClose")
                
                temp_frame$Trigger_Cl[i] <- as.numeric(sym_sum$AdjClose[1])
                temp_frame$Period_Cl[i]  <- as.numeric(sym_sum$AdjClose[nrow(sym_sum)])
                
                temp_frame$Return_3mt[i] <- round((temp_frame$Period_Cl[i]  - temp_frame$Trigger_Cl[i])
                                                 / temp_frame$Trigger_Cl[i], 3)
                
                temp_frame$Period_Hi[i]  <- max(sym_sum$AdjClose)
                
                temp_frame$Return_Hi[i]  <- round((temp_frame$Period_Hi[i]  - temp_frame$Trigger_Cl[i])
                                                 / temp_frame$Trigger_Cl[i], 3)
                
                temp_frame$Volume_Min[i] <- min(sym_sum$Volume)
                temp_frame$Volume_Avg[i] <- round(mean(sym_sum$Volume),0)

                }
        }
        
        return(temp_frame)
        
}

The following calls Generate_Returns() and places the results in the target signal data frame.

Note: There are many thousands of rows that need to be processed so the completion of this section will take some time to finish depending on the machine performing the work (CPU, Memory, etc…).

Generate Returns

str_fastK_frame <- Generate_Returns(str_fastK_frame)
str_fastD_frame <- Generate_Returns(str_fastD_frame)
str_slowD_frame <- Generate_Returns(str_slowD_frame)

Once the data has been retrieved and the desired information stored in the data frame we can filter to remove records that do not meet our acceptance criteria. We will define the rejection criteria as the following:

The purpose of the acceptance criteria is to eliminate irregularly traded stocks which might otherwise skew the results.


The third function, Clean_Frame(), will take the signal data frame to be processed and perform the cleaning and filtering described above.

Clean Frame Function

Clean_Frame <- function(temp_frame) {
        
        temp_final <- temp_frame %>%
                      filter(complete.cases(.),
                      Trigger_Cl >= 5,
                      Trigger_Cl < 1000,
                      Volume_Min > 50000,
                      Volume_Avg >= 100000)
        
        return(temp_final)
}

The following calls Clean_Frame() with the target data frame and loads the result into the final version of the data frame which will be used for return analysis.

Clean Frames

str_fastK_final <- Clean_Frame(str_fastK_frame)
str_fastD_final <- Clean_Frame(str_fastD_frame)
str_slowD_final <- Clean_Frame(str_slowD_frame)
Symbol Date fastK Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAL 2007-02-20 0.0 56.38 34.77 -0.383 56.38 0.000 1102500 2628806
AAL 2007-03-01 7.9 52.08 35.65 -0.315 52.08 0.000 1036200 2682342
AAL 2007-04-23 5.4 43.21 34.50 -0.202 43.21 0.000 1036200 3178547
AAL 2007-05-01 2.6 37.06 31.01 -0.163 37.06 0.000 1036200 3085808
AAL 2007-06-07 0.0 30.20 31.14 0.031 36.00 0.192 796400 2685427
Symbol Date fastD Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAL 2007-02-21 11.4 55.73 34.95 -0.373 55.73 0.000 1102500 2651518
AAL 2007-03-01 5.9 52.08 35.65 -0.315 52.08 0.000 1036200 2682342
AAL 2007-04-25 1.8 42.65 34.84 -0.183 42.65 0.000 1036200 3136583
AAL 2007-05-01 0.9 37.06 31.01 -0.163 37.06 0.000 1036200 3085808
AAL 2007-06-08 19.3 30.89 29.81 -0.035 36.00 0.165 796400 2622077
Symbol Date slowD Trigger_Cl Period_Cl Return_3mt Period_Hi Return_Hi Volume_Min Volume_Avg
AAL 2007-02-22 13.1 54.62 35.13 -0.357 54.62 0.000 1125900 2672794
AAL 2007-03-01 3.1 52.08 35.65 -0.315 52.08 0.000 1036200 2682342
AAL 2007-04-26 8.7 39.33 34.57 -0.121 39.33 0.000 1036200 3183817
AAL 2007-05-01 0.3 37.06 31.01 -0.163 37.06 0.000 1036200 3085808
AAL 2007-06-11 19.9 29.66 29.51 -0.005 36.00 0.214 796400 2572064
## [1] "Total Remaining Records - FastK: 188539"
## [1] "Total Remaining Records - FastD: 172106"
## [1] "Total Remaining Records - SlowD: 160570"

As we would expect, the number of records remaining in the signal data frames decreases as we move from FastK to FastD and to SlowD.


Review Results

The first thing we will review is the overall 3 month totals based on the adjusted closing prices from the beginning and end of the test window.

The fourth function, Three_Month_Stats(), will generate the summary statistics for each of the final signal data frames and print the results.

3 Month Return Function

Three_Month_Stats <- function(temp_frame) {
        
        pos_ret_3mt <- nrow(temp_frame %>%
                            filter(Return_3mt > 0))

        neg_ret_3mt <- nrow(temp_frame %>%
                            filter(Return_3mt <= 0))

        pos_pct_3mt <- round(pos_ret_3mt / nrow(temp_frame), 3)

        avg_ret_3mt <- round(mean(temp_frame$Return_3mt), 3)

        avg_pos_ret <- round(mean(temp_frame$Return_3mt[temp_frame$Return_3mt > 0]), 3)
        avg_neg_ret <- round(mean(temp_frame$Return_3mt[temp_frame$Return_3mt <= 0]), 3)
        
        
        print(paste("  Total Positive Returns:", pos_ret_3mt))
        print(paste("  Total Negative Returns:", neg_ret_3mt))
        print(paste("Percent Positive Returns:", pos_pct_3mt))
        print(paste("          Average Return:", avg_ret_3mt))
        print(paste(" Average Positive Return:", avg_pos_ret))
        print(paste(" Average Negative Return:", avg_neg_ret))

}

FastK 3 Month Returns

Three_Month_Stats(str_fastK_final)
## [1] "  Total Positive Returns: 103333"
## [1] "  Total Negative Returns: 85206"
## [1] "Percent Positive Returns: 0.548"
## [1] "          Average Return: 0.02"
## [1] " Average Positive Return: 0.152"
## [1] " Average Negative Return: -0.141"

The FastK signal resulted in an average return of 2.0% with 54.8% of observations resulting in a positive return at the end of the testing window.

FastD 3 Month Returns

Three_Month_Stats(str_fastD_final)
## [1] "  Total Positive Returns: 94710"
## [1] "  Total Negative Returns: 77396"
## [1] "Percent Positive Returns: 0.55"
## [1] "          Average Return: 0.021"
## [1] " Average Positive Return: 0.154"
## [1] " Average Negative Return: -0.141"

The FastD signal improved on the results by generating an average return of 2.1% and positive return percentage of 55.0%.

SlowD 3 Month Returns

Three_Month_Stats(str_slowD_final)
## [1] "  Total Positive Returns: 88586"
## [1] "  Total Negative Returns: 71984"
## [1] "Percent Positive Returns: 0.552"
## [1] "          Average Return: 0.022"
## [1] " Average Positive Return: 0.155"
## [1] " Average Negative Return: -0.141"

The more conservative SlowD sees an average return of 2.2% and a positive return percentage of 55.2%.

Even though the statistics improve across the 3 signals, there isn’t a significant difference in the results indicating that one of the signals is better than the others.


Now we can look at whether there was a potential for higher returns by looking at the period high stock price observed within the testing period instead of using the testing window closing price.

The fifth function, Three_Month_Period(), will generate the summary statistics using the high stock price in the testing period to determine returns.

3 Month Period Returns

Three_Month_Period <- function(temp_frame) {
        
        pos_ret_per <- nrow(temp_frame %>%
                            filter(Return_Hi > 0))

        neg_ret_per <- nrow(temp_frame %>%
                            filter(Return_Hi <= 0))

        pos_pct_per <- round(pos_ret_per / nrow(temp_frame), 3)

        avg_ret_per <- round(mean(temp_frame$Return_Hi[temp_frame$Return_Hi > 0]), 3) 
        
        
        print(paste("  Total Positive Returns:", pos_ret_per))
        print(paste("  Total Negative Returns:", neg_ret_per))
        print(paste("Percent Positive Returns:", pos_pct_per))
        print(paste(" Average Positive Return:", avg_ret_per))

}

FastK 3 Month Period Returns

Three_Month_Period(str_fastK_final)
## [1] "  Total Positive Returns: 175586"
## [1] "  Total Negative Returns: 12953"
## [1] "Percent Positive Returns: 0.931"
## [1] " Average Positive Return: 0.152"

The FastK signal yielded a potential average return for positive stocks of 15.2%. This is not an improvement on the same statistic observed when using the ending price to calculate returns. There is an improvement in the percentage of positive returns, 93.1%, however.

FastD 3 Month Period Returns

Three_Month_Period(str_fastD_final)
## [1] "  Total Positive Returns: 160570"
## [1] "  Total Negative Returns: 11536"
## [1] "Percent Positive Returns: 0.933"
## [1] " Average Positive Return: 0.154"

The FastD signal yielded the same potential average return for positive stocks of 15.4% as before. There is an improvement in the percentage of positive returns, 93.3%.

SlowD 3 Month Period Returns

Three_Month_Period(str_slowD_final)
## [1] "  Total Positive Returns: 150264"
## [1] "  Total Negative Returns: 10306"
## [1] "Percent Positive Returns: 0.936"
## [1] " Average Positive Return: 0.155"

The FastD signal yielded the same potential average return for positive stocks of 15.5%. There is an improvement in the percentage of positive returns, 93.6%.

The potential positive return within the testing period is the same for the period return for those stocks with a positive return. The upside is that over 93% of all stocks had the potential for a positive return across all 3 signals.


Of course, using the average return potentially creates a false expectation of returns because large outliers can skew the statistical result. So we can look at the positive returns broken out by percentages of occurence. This helps identify a potential threshold for returns within the 3 month window that could be used as an exit point for the stock. That is, if we employ a strategy that sought a 5% return, or bounceback, from the entry point of the stock, triggered by the StochRSI, what would be the percentage of investments that would yield the desired return?

FastK Quantiles for Positive Period Returns

quantile(str_fastK_final$Return_Hi[str_fastK_final$Return_Hi > 0], prob = seq(0, 1, length = 11), type = 5)
##    0%   10%   20%   30%   40%   50%   60%   70%   80%   90%  100% 
## 0.001 0.022 0.042 0.062 0.084 0.109 0.138 0.175 0.228 0.325 7.083

Around 75% of all observations had a 5% return or better. Slightly more than 50% had a return of 10% or better.

FastD Quantiles for Positive Returns

quantile(str_fastD_final$Return_Hi[str_fastD_final$Return_Hi > 0], prob = seq(0, 1, length = 11), type = 5)
##    0%   10%   20%   30%   40%   50%   60%   70%   80%   90%  100% 
## 0.001 0.022 0.042 0.063 0.085 0.110 0.139 0.176 0.231 0.328 5.176

Around 75% of all observations had a 5% return or better. Around 55% had a return of 10% or better.

SlowD Quantiles for Positive Returns

quantile(str_slowD_final$Return_Hi[str_slowD_final$Return_Hi > 0], prob = seq(0, 1, length = 11), type = 5)
##    0%   10%   20%   30%   40%   50%   60%   70%   80%   90%  100% 
## 0.001 0.022 0.043 0.064 0.086 0.111 0.140 0.177 0.232 0.331 4.846

Around 75% of all observations had a 5% return or better. Around 55% had a return of 10% or better.

Overall, there is very little difference between the three signals when looking at percentage of positive returns.


Finally, let’s look at the average positive returns by StochRSI using two levels:

FastK StochRSI Breakdown

round(mean(str_fastK_final$Return_Hi[str_fastK_final$Return_Hi > 0 &
                                     str_fastK_final$fastK > 10]), 3)
## [1] 0.153
round(mean(str_fastK_final$Return_Hi[str_fastK_final$Return_Hi > 0 &
                                     str_fastK_final$fastK <= 10]), 3)
## [1] 0.152

FastD StochRSI Breakdown

round(mean(str_fastD_final$Return_Hi[str_fastD_final$Return_Hi > 0 &
                                     str_fastD_final$fastD > 10]), 3)
## [1] 0.153
round(mean(str_fastD_final$Return_Hi[str_fastD_final$Return_Hi > 0 &
                                     str_fastD_final$fastD <= 10]), 3)
## [1] 0.155

SlowD StochRSI Breakdown

round(mean(str_slowD_final$Return_Hi[str_slowD_final$Return_Hi > 0 &
                                     str_slowD_final$slowD > 10]), 3)
## [1] 0.154
round(mean(str_slowD_final$Return_Hi[str_slowD_final$Return_Hi > 0 &
                                     str_slowD_final$slowD <= 10]), 3)
## [1] 0.159

The optimal return, 15.9%, is found using the SlowD signal and a StochRSI trigger of 10 or less.


Conclusion

The StochRSI delivers on its goal to provide a greater number of signals for oversold conditions. By applying smoothing of the FastK signal, the success rate and potential rates increase as the risk of false signals, by the FastK alone, decrease. The StochRSI has the ability, like the SMI we saw in a previous project, to act as both a trigger for oversold conditions and a momentum indicator for identifying an appropriate level to trigger a buy.

As is the case with most of the signals for evaluating securities, there are many adjustments one can make to find the appropriate signal generator based on the desired type of strategy. The period can be altered to lengthen the signal evaluation, the smoothing period can be altered to provide a faster or slower sensitivity to the FastK, and the StochRSI trigger value can be adjusted to evaluate different types of conditions and their related returns.

As always, further analysis can be done to look at specific companies, sectors, or industries to determine whether any of those categories have tendencies towards better or worse results when using the StochRSI.




sessionInfo()
## R version 3.4.0 (2017-04-21)
## Platform: x86_64-w64-mingw32/x64 (64-bit)
## Running under: Windows 10 x64 (build 15063)
## 
## Matrix products: default
## 
## locale:
## [1] LC_COLLATE=English_United States.1252 
## [2] LC_CTYPE=English_United States.1252   
## [3] LC_MONETARY=English_United States.1252
## [4] LC_NUMERIC=C                          
## [5] LC_TIME=English_United States.1252    
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] knitr_1.16      lubridate_1.6.0 dplyr_0.5.0     quantmod_0.4-9 
## [5] TTR_0.23-1      xts_0.9-7       zoo_1.8-0      
## 
## loaded via a namespace (and not attached):
##  [1] Rcpp_0.12.11     magrittr_1.5     lattice_0.20-35  R6_2.2.1        
##  [5] rlang_0.1.1      stringr_1.2.0    highr_0.6        tools_3.4.0     
##  [9] grid_3.4.0       DBI_0.6-1        htmltools_0.3.6  lazyeval_0.2.0  
## [13] yaml_2.1.14      rprojroot_1.2    digest_0.6.12    assertthat_0.2.0
## [17] tibble_1.3.3     evaluate_0.10    rmarkdown_1.5    stringi_1.1.5   
## [21] compiler_3.4.0   backports_1.1.0