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")
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"
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:
Date
field from the row namesNA
)Year
and Month
columns from Date
Year
and Month
Date
, StochRSI Signal, and Symbol
columns of the data frameThe 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
}
}
}
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:
NA
values$5.00
$1,000
100,000
50,000
on trigger dateThe 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.
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.
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