Momentum in R: Part 4 with Quantstrat

The past few posts on momentum with R focused on a relatively simple way to backtest momentum strategies. In part 4, I use the quantstrat framework to backtest a momentum strategy. Using quantstrat opens the door to several features and options as well as an order book to check the trades at the completion of the backtest.

I introduce a few new functions that are used to prep the data and compute the ranks. I won’t go through them in detail, these functions are available in my github repo in the rank-functions folder.

This first chunk of code just loads the necessary libraries, data, and applies the ave3ROC function to rank the assets based on averaging the 2, 4, and 6 month returns. Note that you will need to load the functions in Rank.R and monthly-fun.R.

library(quantstrat)
library(PerformanceAnalytics)

currency("USD")
symbols <- c("XLY", "XLP", "XLE", "AGG", "IVV")
stock(symbols, currency="USD")

# get data for the symbols
getSymbols(symbols, from="2005-01-01", to="2012-12-31")

# create an xts object of monthly adjusted close prices
symbols.close <- monthlyPrices(symbols)

# create an xts object of the symbol ranks
sym.rank <- applyRank(x=symbols.close, rankFun=ave3ROC, n=c(2, 4, 6))

Created by Pretty R at inside-R.org

The next chunk of code is a critical step in preparing the data to be used in quantstrat. With the ranks computed, the next step is to bind the ranks to the actual market data to be used with quantstrat. It is also important to change the column names to e.g. XLY.Rank because that will be used as the trade signal column when quantstrat is used.

# this is an important step in naming the columns, e.g. XLY.Rank
# the "Rank" column is used as the trade signal (similar to an indicator)
# in the qstratRank function
colnames(sym.rank) <- gsub(".Adjusted", ".Rank", colnames(sym.rank))

# ensure the order of order symbols is equal to the order of columns 
# in symbols.close
stopifnot(all.equal(gsub(".Adjusted", "", colnames(symbols.close)), symbols))

# bind the rank column to the appropriate symbol market data
# loop through symbols, convert the data to monthly and cbind the data
# to the rank
for(i in 1:length(symbols)) {
  x <- get(symbols[i])
  x <- to.monthly(x,indexAt='lastof',drop.time=TRUE)
  indexFormat(x) <- '%Y-%m-%d'
  colnames(x) <- gsub("x",symbols[i],colnames(x))
  x <- cbind(x, sym.rank[,i])
  assign(symbols[i],x)
}

Created by Pretty R at inside-R.org

Now the backtest can be run. The function qstratRank is just a convenience function that hides the quantstrat implementation for my Rank strategy.

For this first backtest, I am trading the top 2 assets with a position size of 1000 units.

# run the backtest
bt <- qstratRank(symbols=symbols, init.equity=100000, top.N=2,
                  max.size=1000, max.levels=1)

# chart of returns
charts.PerformanceSummary(bt$returns[,"total"], geometric=FALSE, 
                          wealth.index=TRUE, main="Total Performance")

Created by Pretty R at inside-R.org

Rplot1

Changing the argument to max.levels=2 gives the flexibility of “scaling” in a trade. In this example, say asset ABC is ranked 1 in the first month — I buy 500 units. In month 2, asset ABC is still ranked 1 — I buy another 500 units.

# run the backtest
bt <- qstratRank(symbols=symbols, init.equity=100000, top.N=2,
                  max.size=1000, max.levels=2)

# chart of returns
charts.PerformanceSummary(bt$returns[,"total"], geometric=FALSE, 
                          wealth.index=TRUE, main="Total Performance")

Created by Pretty R at inside-R.org

Rplot2

Full code available here: quantstrat-rank-backtest.R

Momentum in R: Part 3

In the previous post, I demonstrated simple backtests for trading a number of assets ranked based on their 3, 6, 9, or 12 (i.e lookback periods) month simple returns. While it was not an exhaustive backtest, the results showed that when trading the top 8 ranked assets, the ranking based 3, 6, 9, and 12 month returns resulted in similar performance.

If the results were similar for the different lookback periods, which lookback period should I choose for my strategy? My answer is to include multiple lookback periods in the ranking method.

This can be accomplished by taking the average of the 6, 9, and 12 month returns, or any other n-month returns. This gives us the benefit of diversifying across multiple lookback periods. If I believe that the lookback period of 9 month returns is better than that of the 6 and 12 month, I can use a weighted average to give the 9 month return a higher weight so that it has more influence on determining the rank. This can be implemented easily with what I am calling the WeightAve3ROC() function shown below.

WeightAve3ROC <- function(x, n = c(1,3,6), weights = c(1/3, 1/3, 1/3)){
	# Computes the weighted average rate of change based on a vector of periods
	# and a vector of weights
	#
	# args:
	#   x = xts object of simple returns
	#   n = vector of periods to use n = (period1, period2, period3)
	#   weights = a vector of weights for computing the weighted average
	#
	# Returns:
	#   xts object of weighted average asset rate of change

  if((sum(weights) != 1) || (length(n) != 3) || (length(weights) != 3)){
    stop("The sum of the weights must equal 1 and the length of n and weights must be 3")
  } else{
    roc1 <- ROC(x, n = n[1], type = "discrete")
    roc2 <- ROC(x, n = n[2], type = "discrete")
    roc3 <- ROC(x, n = n[3], type = "discrete")
    wave <- (roc1 * weights[1] + roc2 * weights[2] + roc3 * weights[3]) / sum(weights)
    return(wave)
  }
}

Created by Pretty R at inside-R.org

The function is pretty self explanatory, but feel free to ask if you have any questions.

Now to the test results. The graph below shows the results from using 6, 9, and 12 month returns as well as an average of 6, 9, and 12 month returns and weighted average of 6, 9, and 12 month returns.

  • Case 1: simple momentum test based on 6 month ROC to rank
  • Case 2: simple momentum test based on 9 month ROC to rank
  • Case 3: simple momentum test based on 12 month ROC to rank
  • Case 4: simple momentum test based on average of 6, 9, and 12 month ROC to rank
  • Case 5: simple momentum test based on weighted average of 6, 9, and 12 month ROC to rank. Weights are 1/6, 2/3, 1/6 for 6, 9, and 12 month returns.

rbresearch

Here is a table of the returns and maximum drawdowns for the test.

4 Assets
        6-Month    9-Month    12-Month     Ave      Weighted Ave
 CAGR   0.07576607 0.08270242 0.07040551 0.08278835  0.08466842
 Max DD 0.4219671  0.4045444  0.4304139  0.4211499   0.3930215

This test demonstrates how it may be possible to achieve better risk adjusted returns (higher CAGR and lower drawdowns in this case) by considering multiple lookback periods in the ranking method.

Full R code is below. I have included all the functions in the R script below to make it easy for you to reproduce the tests and try things out, but I would recommend putting the functions in a separate file and using source() to load the functions to keep the code cleaner.

# rank_test2.R

# script to run a simple backtest comparing different ranking methods
# for a momentum based trading system

# remove objects from workspace
rm(list = ls())

# load required packages
library(FinancialInstrument)
library(TTR)
library(PerformanceAnalytics)

MonthlyAd <- function(x){
  # Converts daily data to monthly and returns only the monthly close 
  # Note: only used with Yahoo Finance data so far
  # Thanks to Joshua Ulrich for the Monthly Ad function
  # 
  # args:
  #   x = daily price data from Yahoo Finance
  #
  # Returns:
  #   xts object with the monthly adjusted close prices

  sym <- sub("\\..*$", "", names(x)[1])
  Ad(to.monthly(x, indexAt = 'lastof', drop.time = TRUE, name = sym))
}

CAGR <- function(x, m){
  # Function to compute the CAGR given simple returns
  #
  # args:
  #  x = xts of simple returns
  #  m = periods per year (i.e. monthly = 12, daily = 252)
  #
  # Returns the Compound Annual Growth Rate
  x <- na.omit(x)
  cagr <- apply(x, 2, function(x, m) prod(1 + x)^(1 / (length(x) / m)) - 1, m = m)
  return(cagr)
}

RankRB <- function(x){
  # Computes the rank of an xts object of ranking factors
  # ranking factors are the factors that are ranked (i.e. asset returns)
  #
  # args:
  #   x = xts object of ranking factors
  #
  # Returns:
  #   Returns an xts object with ranks
  #   (e.g. for ranking asset returns, the asset with the greatest return
  #    receives a  rank of 1)

  r <- as.xts(t(apply(-x, 1, rank, na.last = "keep")))
  return(r)
}

SimpleMomentumTest <- function(xts.ret, xts.rank, n = 1, ret.fill.na = 3){
  # returns a list containing a matrix of individual asset returns
  # and the comnbined returns
  # args:
  #  xts.ret = xts of one period returns
  #  xts.rank = xts of ranks
  #  n = number of top ranked assets to trade
  #  ret.fill.na = number of return periods to fill with NA
  #
  # Returns:
  #  returns an xts object of simple returns

  # trade the top n asset(s)
  # if the rank of last period is less than or equal to n,
  # then I would experience the return for this month.

  # lag the rank object by one period to avoid look ahead bias
  lag.rank <- lag(xts.rank, k = 1, na.pad = TRUE)
  n2 <- nrow(lag.rank[is.na(lag.rank[,1]) == TRUE])
  z <- max(n2, ret.fill.na)

  # for trading the top ranked asset, replace all ranks above n
  # with NA to set up for element wise multiplication to get
  # the realized returns
  lag.rank <- as.matrix(lag.rank)
  lag.rank[lag.rank > n] <- NA
  # set the element to 1 for assets ranked <= to rank
  lag.rank[lag.rank <= n] <- 1

  # element wise multiplication of the
  # 1 period return matrix and lagged rank matrix
  mat.ret <- as.matrix(xts.ret) * lag.rank

  # average the rows of the mat.ret to get the
  # return for that period
  vec.ret <- rowMeans(mat.ret, na.rm = TRUE)
  vec.ret[1:z] <- NA

  # convert to an xts object
  vec.ret <- xts(x = vec.ret, order.by = index(xts.ret))
  f <- list(mat = mat.ret, ret = vec.ret, rank = lag.rank)
  return(f)
}

WeightAve3ROC <- function(x, n = c(1,3,6), weights = c(1/3, 1/3, 1/3)){
  # Computes the weighted average rate of change based on a vector of periods
  # and a vector of weights
  #
  # args:
  #   x = xts object of simple returns
  #   n = vector of periods to use n = (period1, period2, period3)
  #   weights = a vector of weights for computing the weighted average
  #
  # Returns:
  #   xts object of weighted average asset rate of change

  if((sum(weights) != 1) || (length(n) != 3) || (length(weights) != 3)){
    stop("The sum of the weights must equal 1 and the length of n and weights must be 3")
  } else{
    roc1 <- ROC(x, n = n[1], type = "discrete")
    roc2 <- ROC(x, n = n[2], type = "discrete")
    roc3 <- ROC(x, n = n[3], type = "discrete")
    wave <- (roc1 * weights[1] + roc2 * weights[2] + roc3 * weights[3]) / sum(weights)
    return(wave)
  }
}

currency("USD")
symbols <- c("XLY", "XLP", "XLE", "XLF", "XLV", "XLI", "XLK", "XLB", "XLU", "EFA")
stock(symbols, currency = "USD", multiplier = 1)

# create new environment to store symbols
symEnv <- new.env()

# getSymbols and assign the symbols to the symEnv environment
getSymbols(symbols, from = '2002-09-01', to = '2012-10-20', env = symEnv)

# xts object of the monthly adjusted close prices
symbols.close <- do.call(merge, eapply(symEnv, MonthlyAd))

# monthly returns
monthly.returns <- ROC(x = symbols.close, n = 1, type = "discrete", na.pad = TRUE)

#############################################################################
# rate of change and rank based on a single period for 6, 9, and 12 months
#############################################################################

roc.six <- ROC(x = symbols.close , n = 6, type = "discrete")
rank.six <- RankRB(roc.six)

roc.nine <- ROC(x = symbols.close , n = 9, type = "discrete")
rank.nine <- RankRB(roc.nine)

roc.twelve <- ROC(x = symbols.close , n = 12, type = "discrete")
rank.twelve <- RankRB(roc.twelve)

#############################################################################
# rate of change and rank based on averaging 6, 9, and 12 month returns
#############################################################################
roc.ave <- WeightAve3ROC(x = symbols.close, n = c(6, 9, 12), 
                         weights = c(1/3, 1/3, 1/3))
rank.ave <- RankRB(roc.ave)

roc.weight.ave <- WeightAve3ROC(x = symbols.close, n = c(6, 9, 12), 
                                weights = c(1/6, 2/3, 1/6))
rank.weight.ave <- RankRB(roc.weight.ave)

#############################################################################
# run the backtest
#############################################################################

num.assets <- 4

# simple momentum test based on 6 month ROC to rank
case1 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.six,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on 9 month ROC to rank
case2 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.nine,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on 12 month ROC to rank
case3 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.twelve,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on average of 6, 9, and 12 month ROC to rank
case4 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.ave,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on weighted average of 6, 9, and 12 month ROC to rank
case5 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.weight.ave,
                            n = num.assets, ret.fill.na = 15)

returns <- cbind(case1$ret, case2$ret, case3$ret, case4$ret, case5$ret)
colnames(returns) <- c("6-Month", "9-Month", "12-Month", "Ave", "Weighted Ave")

charts.PerformanceSummary(R = returns, Rf = 0, geometric = TRUE, 
                          main = "Momentum Cumulative Return: Top 4 Assets")

table.Stats(returns)

cagr <- CAGR(returns, m = 12)
max.dd <- maxDrawdown(returns)
print(cagr)
print(max.dd)

print("End")

Created by Pretty R at inside-R.org

Momentum in R: Part 2

Many of the sites I linked to in the previous post have articles or papers on momentum investing that investigate the typical ranking factors; 3, 6, 9, and 12 month returns. Most (not all) of the articles seek to find which is the “best” look-back period to rank the assets. Say that the outcome of the article is that the 6 month look-back has the highest returns. A trading a strategy that just uses a 6 month look-back period to rank the assets leaves me vulnerable to over-fitting based on the backtest results. The backtest tells us nothing more than which strategy performed the best in the past, it tells us nothing about the future… duh!

Whenever I review the results from backtests, I always ask myself a lot of “what if” questions. Here are 3 “what if” questions that I would ask for this backtest are:

  1. What if the strategy based on a 6 month look-back under performs and the 9 month or 3 month starts to over perform?
  2. What if the strategies based on 3, 6, and 9 month look-back periods have about the same return and risk profile, which strategy should I trade?
  3. What if the assets with high volatility are dominating the rankings and hence driving the returns?

The backtests shown are simple backtests meant to demonstrate the variability in returns based on look-back periods and number of assets traded.

The graphs below show the performance of a momentum strategy using 3, 6, 9, and 12 month returns and trading the Top 1, 4, and 8 ranked assets. You will notice that there is significant volatility and variability in returns only trading 1 asset. The variability between look-back periods is reduced, but there is still no one clear “best” look-back period. There are periods of under performance and over performance for all look back periods in the test.

rbresearch

rbresearch

rbresearch

 

Here is the R code used for the backtests and the plots. Leave a comment if you have any questions about the code below.

library(FinancialInstrument)
library(TTR)
library(PerformanceAnalytics)

RankRB <- function(x){
  # Computes the rank of an xts object of ranking factors
  # ranking factors are the factors that are ranked (i.e. asset returns)
  #
  # args:
  #   x = xts object of ranking factors
  #
  # Returns:
  #   Returns an xts object with ranks
  #   (e.g. for ranking asset returns, the asset with the greatest return
  #    receives a  rank of 1)

  r <- as.xts(t(apply(-x, 1, rank, na.last = "keep")))
  return(r)
}

MonthlyAd <- function(x){
  # Converts daily data to monthly and returns only the monthly close 
  # Note: only used with Yahoo Finance data so far
  # Thanks to Joshua Ulrich for the Monthly Ad function
  # 
  # args:
  #   x = daily price data from Yahoo Finance
  #
  # Returns:
  #   xts object with the monthly adjusted close prices

  sym <- sub("\\..*$", "", names(x)[1])
  Ad(to.monthly(x, indexAt = 'lastof', drop.time = TRUE, name = sym))
}

CAGR <- function(x, m){
  # Function to compute the CAGR given simple returns
  #
  # args:
  #  x = xts of simple returns
  #  m = periods per year (i.e. monthly = 12, daily = 252)
  #
  # Returns the Compound Annual Growth Rate
  x <- na.omit(x)
  cagr <- apply(x, 2, function(x, m) prod(1 + x)^(1 / (length(x) / m)) - 1, m = m)
  return(cagr)
}

SimpleMomentumTest <- function(xts.ret, xts.rank, n = 1, ret.fill.na = 3){
  # returns a list containing a matrix of individual asset returns
  # and the comnbined returns
  # args:
  #  xts.ret = xts of one period returns
  #  xts.rank = xts of ranks
  #  n = number of top ranked assets to trade
  #  ret.fill.na = number of return periods to fill with NA
  #
  # Returns:
  #  returns an xts object of simple returns

  # trade the top n asset(s)
  # if the rank of last period is less than or equal to n,
  # then I would experience the return for this month.

  # lag the rank object by one period to avoid look ahead bias
  lag.rank <- lag(xts.rank, k = 1, na.pad = TRUE)
  n2 <- nrow(lag.rank[is.na(lag.rank[,1]) == TRUE])
  z <- max(n2, ret.fill.na)

  # for trading the top ranked asset, replace all ranks above n
  # with NA to set up for element wise multiplication to get
  # the realized returns
  lag.rank <- as.matrix(lag.rank)
  lag.rank[lag.rank > n] <- NA
  # set the element to 1 for assets ranked <= to rank
  lag.rank[lag.rank <= n] <- 1

  # element wise multiplication of the
  # 1 period return matrix and lagged rank matrix
  mat.ret <- as.matrix(xts.ret) * lag.rank

  # average the rows of the mat.ret to get the
  # return for that period
  vec.ret <- rowMeans(mat.ret, na.rm = TRUE)
  vec.ret[1:z] <- NA

  # convert to an xts object
  vec.ret <- xts(x = vec.ret, order.by = index(xts.ret))
  f <- list(mat = mat.ret, ret = vec.ret, rank = lag.rank)
  return(f)
}

currency("USD")
symbols <- c("XLY", "XLP", "XLE", "XLF", "XLV", "XLI", "XLK", "XLB", "XLU", "EFA")#, "TLT", "IEF", "SHY")
stock(symbols, currency = "USD", multiplier = 1)

# create new environment to store symbols
symEnv <- new.env()

# getSymbols and assign the symbols to the symEnv environment
getSymbols(symbols, from = '2002-09-01', to = '2012-10-20', env = symEnv)

# xts object of the monthly adjusted close prices
symbols.close <- do.call(merge, eapply(symEnv, MonthlyAd))

# monthly returns
monthly.returns <- ROC(x = symbols.close, n = 1, type = "discrete", na.pad = TRUE)

#############################################################################
# rate of change and rank based on a single period for 3, 6, 9, and 12 months
#############################################################################

roc.three <- ROC(x = symbols.close , n = 3, type = "discrete")
rank.three <- RankRB(roc.three)

roc.six <- ROC(x = symbols.close , n = 6, type = "discrete")
rank.six <- RankRB(roc.six)

roc.nine <- ROC(x = symbols.close , n = 9, type = "discrete")
rank.nine <- RankRB(roc.nine)

roc.twelve <- ROC(x = symbols.close , n = 12, type = "discrete")
rank.twelve <- RankRB(roc.twelve)

num.assets <- 4

# simple momentum test based on 3 month ROC to rank
case1 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.three,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on 6 month ROC to rank
case2 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.six,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on 9 month ROC to rank
case3 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.nine,
                            n = num.assets, ret.fill.na = 15)

# simple momentum test based on 12 month ROC to rank
case4 <- SimpleMomentumTest(xts.ret = monthly.returns, xts.rank = rank.twelve,
                            n = num.assets, ret.fill.na = 15)

returns <- cbind(case1$ret, case2$ret, case3$ret, case4$ret)
colnames(returns) <- c("3-Month", "6-Month", "9-Month", "12-Month")

charts.PerformanceSummary(R = returns, Rf = 0, geometric = TRUE, 
                          main = "Momentum Cumulative Return: Top 4 Assets")

table.Stats(returns)

CAGR(returns, m = 12)

print("End")

Created by Pretty R at inside-R.org

Alternative to Monte Carlo Testing

When we backtest a strategy on a portfolio, it is a simple analysis of a single period in time. There are ways to “stress test” a strategy such as monte carlo, random portfolios, or shuffling the returns in a random order. I could never really wrap my head around monte carlo and shuffling the returns seemed to be a better approach because the actual returns of the backtest are used, but it misses one important thing… the impact of consecutive periods of returns. If we are backtesting a strategy and we want to minimize max drawdown, consecutive down periods have a significant impact on max drawdown. If, for example, the max drawdown occured due to 4 consecutive months during 2008, we wan’t to keep those 4 months together when shuffling returns.

In my opinion, a better way to shuffle returns is to shuffle “blocks” of returns. This is nothing new, the TradingBlox software does monte carlo analysis this way. I had a look at the boot package and tseries package for their boot functions, but it was not giving me what I wanted. I wanted to visual a number of equity curves with blocks of returns randomly shuffled.

To accomplish this in R, I wrote two functions.  The shuffle_returns function takes an xts object of returns, the number of samples to run (i.e. how many equity curves to generate), and a number for how many periods of returns makes up a ‘block’ as arguments.The ran_gen function function is a function within the shuffle_returns function that is used to generate random blocks of returns.

shuffle_returns returns an xts object with the random blocks of returns so we can do further analysis such as max drawdown, plotting, or pretty much anything in the PerformanceAnalytics package that takes an xts object as an argument.

This is not a perfect implementation of this idea, so if anybody knows of a better way I’d be glad to hear from you.

The example below uses sample data from edhec and generates 100 equity curves with blocks of 5 consecutive period of returns.

R code:

require(PerformanceAnalytics)

#Function that grabs a random number and then repeats that number r times
ran_gen <- function(x, r){
  #x is an xts object of asset returns
  #r is for how many consecutive returns make up a 'block'
  vec <- c()
  total_length <- length(x)
  n <- total_length/r
  for(i in 1:n){
    vec <- append(vec,c(rep(sample(1:(n*100),1), r)))
  }
  diff <- as.integer(total_length - length(vec))
  vec <- append(vec, c(rep(sample(1:(n*100),1), len = diff)))
  return(vec)
}

shuffle_returns <- function(x, n, r){
  #x is an xts object of asset returns
  #n is the number of samples to run
  #r is for how many consecutive returns make up a 'block' and is passed to ran_gen

  mat <- matrix(data = x, nrow = length(x))
  for(i in 1:n){
    temp_random <- ran_gen(x = x, r = r)
    temp_mat <- as.matrix(cbind(x, temp_random))
    temp_mat <- temp_mat[order(temp_mat[,2]),]
    temp_ret_mat <- matrix(data = temp_mat[,1])
    mat <- cbind(mat, temp_ret_mat)
  }
  final_xts <- xts(mat, index(x))
  return(final_xts)
}

#get sample data
data(edhec)
a <- edhec[,1]
a <- head(a, -1)

start <- Sys.time()
yy <- shuffle_returns(a, 100, 5)
chart.CumReturns(yy[,1:NCOL(yy)], wealth.index = TRUE, ylab = "Equity", main ="Generated Equity Curve of Shuffled Returns")
end <- Sys.time()
print(end-start)

Created by Pretty R at inside-R.org

Simple Moving Average Strategy with a Volatility Filter: Follow-Up Part 3

In part 2, we saw that adding a volatility filter to a single instrument test did little to improve performance or risk adjusted returns. How will the volatility filter impact a multiple instrument portfolio?

In part 3 of the follow up, I will evaluate the impact of the volatility filter on a multiple instrument test.

The tests will use nine of the Select Sector SPDR ETFs listed below.

XLY – Consumer Discretionary Select Sector SPDR
XLP – Consumer Staples Select Sector SPDR
XLE – Energy Select Sector SPDR
XLF – Financial Select Sector SPDR
XLV – Health Care Select Sector SPDR
XLI – Industrial Select Sector SPDR
XLK – Technology Select Sector SPDR
XLB – Materials Select Sector SPDR
XLU – Utilities Select Sector SPDR

Test #1 – without volatility filter

Start Date*: 2001-01-01

Test#2 – with volatility filter

Start Date*: 2000-01-01

*Note the difference in start dates. The volatility filter requires an extra 52 periodsto process the RBrev1 indicator so the test dates are offset by 52 weeks (one year).

Both tests will risk 1% of account equity and the stop size is 1 standard deviation.

Test #1 is a simple moving average strategy without a volatility filter on a portfolio of the nine sector ETFs mentioned previously. This will be the baseline for comparison of the strategy with the volatility filter.

Test #1 Buy and Exit Rules

  • Buy Rule: Go long if close crosses above the 52 period SMA
  • Exit Rule: Exit if close crosses below the 52 period SMA
Test #1 Performance Statistics
Test CAGR (%) MaxDD (%) MAR
Test#1 7.976377 -14.92415 0.534461

rbresearch

Test #2 will be a simple moving average strategy with a volatility filter on the same 9 ETFs. The volatility filter is the same measure used in Follow-Up Part 2. The volatility filter is simply the 52 period standard deviation of close prices.

Test #2 Buy and Exit Rules

The new volatility filter will be the 52 period standard deviation of close prices. Now, the buy rule can be interpreted as follows:

  • Buy Rule: Go long if close is greater than the 52 period SMA and the 52 period standard deviation of close prices is less than its median over the last 52 periods.
  • Exit Rule: Exit if long and close is less than the 52 period SMA

Test#2 Performance Statistics

Test CAGR (%) MaxDD (%) MAR
Test#2 7.6694587 -14.6590123 0.523191

rbresearch

Both strategies perform fairly well. I would give a slight edge to Test#1, the strategy without a volatility filter. The strategy without a volatility filter has a slightly higher maximum drawdown (MaxDD), but also a higher CAGR.

Test CAGR (%) MaxDD (%) MAR
Test#1 7.976377 -14.92415 0.534461
Test#2 7.6694587 -14.65901 0.523191

Below I will include the R code for the test#2, shoot me an email if you want the code for test#1.

#Weekly Timing Strategy with Volatility Filter
require(PerformanceAnalytics)
require(quantstrat)

suppressWarnings(rm("order_book.TimingWeekly",pos=.strategy))
suppressWarnings(rm("account.TimingWeekly","portfolio.TimingWeekly",pos=.blotter))
suppressWarnings(rm("account.st","portfolio.st","symbols","stratBBands","initDate","initEq",'start_t','end_t'))

##### Begin Functions #####

#Custom Order Sizing Function to trade percent of equity based on a stopsize
osPCTEQ <- function(timestamp, orderqty, portfolio, symbol, ruletype, ...){
  tempPortfolio <- getPortfolio(portfolio.st)
  dummy <- updatePortf(Portfolio=portfolio.st, Dates=paste('::',as.Date(timestamp),sep=''))
  trading.pl <- sum(getPortfolio(portfolio.st)$summary$Realized.PL) #change to ..$summary$Net.Trading.PL for Total Equity Position Sizing
  assign(paste("portfolio.",portfolio.st,sep=""),tempPortfolio,pos=.blotter)
  total.equity <- initEq+trading.pl
  DollarRisk <- total.equity * trade.percent
  ClosePrice <- as.numeric(Cl(mktdata[timestamp,]))
  mavg <- as.numeric(mktdata$SMA[timestamp,])
  sign1 <- ifelse(ClosePrice > mavg, 1, -1)
  sign1[is.na(sign1)] <- 1
  Posn = getPosQty(Portfolio = portfolio.st, Symbol = symbol, Date = timestamp)
  StopSize <- as.numeric(mktdata$SDEV[timestamp,]*StopMult) #Stop = SDAVG * StopMult !Must have SDAVG or other indictor to determine stop size
  #orderqty <- round(DollarRisk/StopSize, digits=0)
  orderqty <- ifelse(Posn == 0, sign1*round(DollarRisk/StopSize), 0) # number contracts traded is equal to DollarRisk/StopSize
  return(orderqty)
}

#Function that calculates the n period standard deviation of close prices.
#This is used in place of ATR so that I can use only close prices.
SDEV <- function(x, n){
  sdev <- runSD(x, n, sample = FALSE)
  colnames(sdev) <- "SDEV"
  reclass(sdev,x)
}

#Custom indicator function 
RBrev1 <- function(x,n){
  x <- x
  sd <- runSD(x, n, sample= FALSE)
  med <- runMedian(sd,n)
  mavg <- SMA(x,n)
  signal <- ifelse(sd < med & x > mavg,1,0)
  colnames(signal) <- "RB"
  #ret <- cbind(x,roc,sd,med,mavg,signal) #Only use for further analysis of indicator
  #colnames(ret) <- c("close","roc","sd","med","mavg","RB") #Only use for further analysis of indicator
  reclass(signal,x)
}

##### End Functions #####

#Symbols to be used in test
#XLY - Consumer Discretionary Select Sector SPDR
#XLP - Consumer Staples Select Sector SPDR
#XLE - Energy Select Sector SPDR
#XLF - Financial Select Sector SPDR
#XLV - Health Care Select Sector SPDR
#XLI - Industrial Select Sector SPDR
#XLK - Technology Select Sector SPDR
#XLB - Materials Select Sector SPDR
#XLU - Utilities Select Sector SPDR

#Symbol list to pass to the getSymbols function
symbols = c("XLY", "XLP", "XLE", "XLF", "XLV", "XLI", "XLK", "XLB", "XLU")

#Load ETFs from yahoo
currency("USD")
stock(symbols, currency="USD",multiplier=1)
getSymbols(symbols, src='yahoo', index.class=c("POSIXt","POSIXct"), from='2000-01-01')

#Data is downloaded as daily data
#Convert to weekly
for(symbol in symbols) {
  x<-get(symbol)
  x<-to.weekly(x,indexAt='lastof',drop.time=TRUE)
  indexFormat(x)<-'%Y-%m-%d'
  colnames(x)<-gsub("x",symbol,colnames(x))
  assign(symbol,x)
}

#Use the adjusted close prices
#this for loop sets the "Close" column equal to the "Adjusted Close" column
#because the trades are executed based on the "Close" column
for(symbol in symbols) {
  x<-get(symbol)
  x[,4] <- x[,6]
  assign(symbol,x)
}

initDate='1900-01-01'
initEq <- 100000

trade.percent <- 0.01 #percent risk used in sizing function
StopMult = 1 #stop size used in sizing function

#Name the portfolio and account
portfolio.st = 'TimingWeekly'
account.st = 'TimingWeekly'

#Initialization
initPortf(portfolio.st, symbols=symbols, initPosQty=0, initDate=initDate, currency="USD")
initAcct(account.st,portfolios=portfolio.st, initDate=initDate, initEq=initEq)
initOrders(portfolio=portfolio.st,initDate=initDate)

#Name the strategy
strat <- strategy('TimingWeekly')

#Add indicators
#The first indicator is the 52 period SMA
#The second indicator is the SDEV indicator used for stop and position sizing
strat <- add.indicator(strategy = strat, name = "SMA", arguments = list(x = quote(Cl(mktdata)), n=52), label="SMA")
strat <- add.indicator(strategy = strat, name = "RBrev1", arguments = list(x = quote(Cl(mktdata)), n=52), label="RB")
strat <- add.indicator(strategy = strat, name = "SDEV", arguments = list(x = quote(Cl(mktdata)), n=52), label="SDEV")

#Add signals
#The buy signal is when the RB indicator crosses from 0 to 1
#The exit signal is when the close crosses below the SMA
strat <- add.signal(strategy = strat, name="sigThreshold", arguments = list(threshold=1, column="RB",relationship="gte", cross=TRUE),label="RB.gte.1")
strat <- add.signal(strategy = strat, name="sigCrossover", arguments = list(columns=c("Close","SMA"),relationship="lt"),label="Cl.lt.SMA")

#Add rules
strat <- add.rule(strategy = strat, name='ruleSignal', arguments = list(sigcol="RB.gte.1", sigval=TRUE, orderqty=1000, ordertype='market', orderside='long', osFUN = 'osPCTEQ', pricemethod='market', replace=FALSE), type='enter', path.dep=TRUE)
strat <- add.rule(strategy = strat, name='ruleSignal', arguments = list(sigcol="Cl.lt.SMA", sigval=TRUE, orderqty='all', ordertype='market', orderside='long', pricemethod='market',TxnFees=0), type='exit', path.dep=TRUE)

# Process the indicators and generate trades
start_t<-Sys.time()
out<-try(applyStrategy(strategy = strat, portfolios = portfolio.st))
end_t<-Sys.time()
print("Strategy Loop:")
print(end_t-start_t)

start_t<-Sys.time()
updatePortf(Portfolio=portfolio.st,Dates=paste('::',as.Date(Sys.time()),sep=''))
end_t<-Sys.time()
print("updatePortf execution time:")
print(end_t-start_t)

#chart.Posn(Portfolio=portfolio.st,Symbol=symbols)

#Update Account
updateAcct(account.st)

#Update Ending Equity
updateEndEq(account.st)

#ending equity
getEndEq(account.st, Sys.Date()) + initEq

tstats <- tradeStats(Portfolio=portfolio.st, Symbol=symbols)

#View order book to confirm trades
#getOrderBook(portfolio.st)

#Trade Statistics for CAGR, Max DD, and MAR
#calculate total equity curve performance Statistics
ec <- tail(cumsum(getPortfolio(portfolio.st)$summary$Net.Trading.PL),-1)
ec$initEq <- initEq
ec$totalEq <- ec$Net.Trading.PL + ec$initEq
ec$maxDD <- ec$totalEq/cummax(ec$totalEq)-1
ec$logret <- ROC(ec$totalEq, n=1, type="continuous")
ec$logret[is.na(ec$logret)] <- 0

WI <- exp(cumsum(ec$logret)) #growth of $1
#write.zoo(nofilterWI, file = "E:\\nofiltertest.csv", sep=",")

period.count <- NROW(ec)-104 #Use 104 because there is a 104 week lag for the 52 week SD and 52 week median of SD
year.count <- period.count/52
maxDD <- min(ec$maxDD)*100
totret <- as.numeric(last(ec$totalEq))/as.numeric(first(ec$totalEq))
CAGR <- (totret^(1/year.count)-1)*100
MAR <- CAGR/abs(maxDD)

Perf.Stats <- c(CAGR, maxDD, MAR)
names(Perf.Stats) <- c("CAGR", "maxDD", "MAR")
Perf.Stats

#transactions <- getTxns(Portfolio = portfolio.st, Symbol = symbols)
#write.zoo(transactions, file = "E:\\nofiltertxn.csv")

charts.PerformanceSummary(ec$logret, wealth.index = TRUE, ylog = TRUE, colorset = "steelblue2", main = "Strategy with Volatility Filter")

Created by Pretty R at inside-R.org


Simple Moving Average Strategy with a Volatility Filter: Follow-Up Part 2

In the Follow-Up Part 1, I explored some of the functions in the quantstrat package that allowed us to drill down trade by trade to explain the difference in performance of the two strategies. By doing this, I found that my choice of a volatility measure may not have been the best choice. Although the volatility filter kept me out of trades during periods of higher volatility, it also had a negative impact on position sizing and overall return.

The volatility measure presented in the original post was the 52 period standard deviation of the 1 period change of close prices. I made a custom indicator to incorporate the volatility filter into the buy rule. Here is the original RB function:

#Custom indicator function 
RB <- function(x,n){
  x <- x
  roc <- ROC(x, n=1, type="discrete")
  sd <- runSD(roc,n, sample= FALSE)
  med <- runMedian(sd,n)
  mavg <- SMA(x,n)
  signal <- ifelse(sd < med & x > mavg,1,0)
  colnames(signal) <- "RB"
  reclass(signal,x)
  }

Created by Pretty R at inside-R.org

The new volatility filter will be the 52 period standard deviation of close prices. Now, the buy rule can be interpreted as follows:

  • Buy Rule: Go long if close is greater than the 52 period SMA and the 52 period standard deviation of close prices is less than its median over the last N periods.
  • Exit Rule: Exit if long and close is less than the N period SMA

A slight change to the RB function will do the trick, I will call it RBrev1 (that is my creative side coming out ;))

#Custom indicator function 
RBrev1 <- function(x,n){
  x <- x
  sd <- runSD(x, n, sample= FALSE)
  med <- runMedian(sd,n)
  mavg <- SMA(x,n)
  signal <- ifelse(sd < med & x > mavg,1,0)
  colnames(signal) <- "RB"
  #ret <- cbind(x,roc,sd,med,mavg,signal) #Only use for further analysis of indicator
  #colnames(ret) <- c("close","roc","sd","med","mavg","RB") #Only use for further analysis of indicator
  reclass(signal,x)
  }

Created by Pretty R at inside-R.org

I will test the strategy on the adjusted close of the S&P500 using weekly prices from 1/1/1990 to 1/1/2000 just as in the previous post.

And the winner is… both! There is no difference in performance on this single instrument in this specific window of time I used for the test.

rbresearch

Always do your own testing to decide whether or not a filter of any kind will add value to your system. This single instrument test in the series of posts showed that choosing the “wrong” volatility filter can hinder performance and another choice of volatility filter doesn’t have much impact, if any, at all.

How do you think the volatility filter will affect a multiple instrument test?

require(PerformanceAnalytics)
require(quantstrat)

suppressWarnings(rm("order_book.RBtest",pos=.strategy))
suppressWarnings(rm("account.RBtest","portfolio.RBtest",pos=.blotter))
suppressWarnings(rm("account.st","portfolio.st","symbols","stratBBands","initDate","initEq",'start_t','end_t'))

sym.st = "GSPC"
currency("USD")
stock(sym.st, currency="USD",multiplier=1)
getSymbols("^GSPC", src='yahoo', index.class=c("POSIXt","POSIXct"), from='1990-01-01', to='2012-04-17')
GSPC <- to.weekly(GSPC,indexAt='lastof',drop.time=TRUE)

#Custom Order Sizing Function to trade percent of equity based on a stopsize
osPCTEQ <- function(timestamp, orderqty, portfolio, symbol, ruletype, ...){
  tempPortfolio <- getPortfolio(portfolio.st)
  dummy <- updatePortf(Portfolio=portfolio.st, Dates=paste('::',as.Date(timestamp),sep=''))
  trading.pl <- sum(getPortfolio(portfolio.st)$summary$Realized.PL) #change to ..$summary$Net.Trading.PL for Total Equity Position Sizing
  assign(paste("portfolio.",portfolio.st,sep=""),tempPortfolio,pos=.blotter)
  total.equity <- initEq+trading.pl
  DollarRisk <- total.equity * trade.percent
  ClosePrice <- as.numeric(Cl(mktdata[timestamp,]))
  mavg <- as.numeric(mktdata$SMA52[timestamp,])
  sign1 <- ifelse(ClosePrice > mavg, 1, -1)
  sign1[is.na(sign1)] <- 1
  Posn = getPosQty(Portfolio = portfolio.st, Symbol = sym.st, Date = timestamp)
  StopSize <- as.numeric(mktdata$SDEV[timestamp,]*StopMult) #Stop = SDAVG * StopMult !Must have SDAVG or other indictor to determine stop size
  orderqty <- ifelse(Posn == 0, sign1*round(DollarRisk/StopSize), 0) # number contracts traded is equal to DollarRisk/StopSize
  return(orderqty)
}

#Function that calculates the n period standard deviation of close prices.
#This is used in place of ATR so that I can use only close prices.
SDEV <- function(x, n){
  sdev <- runSD(x, n, sample = FALSE)
  colnames(sdev) <- "SDEV"
  reclass(sdev,x)
}

#Custom indicator function 
RBrev1 <- function(x,n){
  x <- x
  sd <- runSD(x, n, sample= FALSE)
  med <- runMedian(sd,n)
  mavg <- SMA(x,n)
  signal <- ifelse(sd < med & x > mavg,1,0)
  colnames(signal) <- "RB"
  #ret <- cbind(x,roc,sd,med,mavg,signal) #Only use for further analysis of indicator
  #colnames(ret) <- c("close","roc","sd","med","mavg","RB") #Only use for further analysis of indicator
  reclass(signal,x)
  }

initDate='1900-01-01'
initEq <- 100000

trade.percent <- .05 #percent risk used in sizing function
StopMult = 1 #stop size used in sizing function

#Name the portfolio and account
portfolio.st='RBtest'
account.st='RBtest'

#Initialization
initPortf(portfolio.st, symbols=sym.st, initPosQty=0, initDate=initDate, currency="USD")
initAcct(account.st,portfolios=portfolio.st, initDate=initDate, initEq=initEq)
initOrders(portfolio=portfolio.st,initDate=initDate)

#Name the strategy
stratRB <- strategy('RBtest')

#Add indicators
#The first indicator is the 52 period SMA
#The second indicator is the RB indicator. The RB indicator returns a value of 1 when close > SMA & volatility < runMedian(volatility, n = 52)
stratRB <- add.indicator(strategy = stratRB, name = "SMA", arguments = list(x = quote(Cl(mktdata)), n=52), label="SMA52")
stratRB <- add.indicator(strategy = stratRB, name = "RBrev1", arguments = list(x = quote(Cl(mktdata)), n=52), label="RB")
stratRB <- add.indicator(strategy = stratRB, name = "SDEV", arguments = list(x = quote(Cl(mktdata)), n=52), label="SDEV")

#Add signals
#The buy signal is when the RB indicator crosses from 0 to 1
#The exit signal is when the close crosses below the SMA
stratRB <- add.signal(strategy = stratRB, name="sigThreshold", arguments = list(threshold=1, column="RB",relationship="gte", cross=TRUE),label="RB.gte.1")
stratRB <- add.signal(strategy = stratRB, name="sigCrossover", arguments = list(columns=c("Close","SMA52"),relationship="lt"),label="Cl.lt.SMA")

#Add rules
stratRB <- add.rule(strategy = stratRB, name='ruleSignal', arguments = list(sigcol="RB.gte.1", sigval=TRUE, orderqty=1000, ordertype='market', orderside='long', osFUN = 'osPCTEQ', pricemethod='market', replace=FALSE), type='enter', path.dep=TRUE)
stratRB <- add.rule(strategy = stratRB, name='ruleSignal', arguments = list(sigcol="Cl.lt.SMA", sigval=TRUE, orderqty='all', ordertype='market', orderside='long', pricemethod='market',TxnFees=0), type='exit', path.dep=TRUE)

# Process the indicators and generate trades
start_t<-Sys.time()
out<-try(applyStrategy(strategy=stratRB , portfolios=portfolio.st))
end_t<-Sys.time()
print("Strategy Loop:")
print(end_t-start_t)

start_t<-Sys.time()
updatePortf(Portfolio=portfolio.st,Dates=paste('::',as.Date(Sys.time()),sep=''))
end_t<-Sys.time()
print("updatePortf execution time:")
print(end_t-start_t)

chart.Posn(Portfolio=portfolio.st,Symbol=sym.st)

#Update Account
updateAcct(account.st)

#Update Ending Equity
updateEndEq(account.st)

#ending equity
getEndEq(account.st, Sys.Date()) + initEq

tstats <- tradeStats(Portfolio=portfolio.st, Symbol=sym.st)

#View order book to confirm trades
getOrderBook(portfolio.st)

#Trade Statistics for CAGR, Max DD, and MAR
#calculate total equity curve performance Statistics
ec <- tail(cumsum(getPortfolio(portfolio.st)$summary$Net.Trading.PL),-1)
ec$initEq <- initEq
ec$totalEq <- ec$Net.Trading.PL + ec$initEq
ec$maxDD <- ec$totalEq/cummax(ec$totalEq)-1
ec$logret <- ROC(ec$totalEq, n=1, type="continuous")
ec$logret[is.na(ec$logret)] <- 0

RBrev1WI <- exp(cumsum(ec$logret)) #growth of $1
#write.zoo(RBrev1WI, file = "E:\\volfiltertest.csv", sep=",")

period.count <- NROW(ec)-104 #Use 104 because there is a 104 week lag for the 52 week SD and 52 week median of SD
year.count <- period.count/52
maxDD <- min(ec$maxDD)*100
totret <- as.numeric(last(ec$totalEq))/as.numeric(first(ec$totalEq))
CAGR <- (totret^(1/year.count)-1)*100
MAR <- CAGR/abs(maxDD)

Perf.Stats <- c(CAGR, maxDD, MAR)
names(Perf.Stats) <- c("CAGR", "maxDD", "MAR")
Perf.Stats

transactions <- getTxns(Portfolio = portfolio.st, Symbol = sym.st)
#write.zoo(transactions, file = "E:\\filtertxn.csv")

charts.PerformanceSummary(ec$logret, wealth.index = TRUE, ylog = TRUE, colorset = "steelblue2", main = "SMA with Volatility Filter System Performance")

Created by Pretty R at inside-R.org

Simple Moving Average Strategy with a Volatility Filter

I would describe my trading approach as systematic long term trend following. A trend following strategy can be difficult mentally to trade after experiencing multiple consecutive losses when a trade reverses due to a volatility spike or the trend reverses. Volatility tends to increase when prices fall. This is not good for a long only trend following strategy, especially when initially entering trades.

Can adding a volatility filter to a simple system improve performance?

SMA System with Volatility Filter Rules

  • Buy Rule: Go long if close is greater than the N period SMA and a volatility measure is less than its median over the last N periods.
  • Exit Rule: Exit if long and close is less than the N period SMA

SMA System without Volatility Filter Rules

  • Buy Rule: Go long if close is greater than the N period SMA
  • Exit Rule: Exit if close is less than the N period SMA

For this test, my volatility measure is the 52 period standard deviation of the 1 period change of close prices and I will use a 52 period SMA.

I will test the strategy on the total return series of the S&P500 using weekly prices from 1/1/1990 to 4/17/2012.

yuck… the equity curves look pretty good up until 1999, then not so good after that.

rbresearch

rbresearch

CAGR maxDD MAR # Trades Ending Equity Percent Winning Trades
SMA with Volatility Filter 4.369174 -22.3993 0.195059 34 $239,104.70 58.82
SMA System 7.442673 -22.2756 0.334119 57 $464,198.80 53.57

This test shows that adding a volatility filter to our entries can actually hinder performance. Keep in mind this is ny no means an exhaustive test on a single instrument. I also chose the 52 period SMA and SDEV somewhat arbitrarily because it represents a year.

Reading through trading forums, it is clear to see that people are in search of the “holy grail” trading system. Some people claim to have found the “holy grail” system, but that system is usually combination of 10+ indicators and rules that say “use indicator A, B, and C when the market is doing X or use indicators D, E, and F when the market is doing Y.” Beware of these “filters” and always test yourself.

Stay tuned for future posts that will look at adding a similar filter on a multiple instrument test.

What have you found with adding entry filters to trading systems?

require(PerformanceAnalytics)
require(quantstrat)

sym.st = "GSPC"
currency("USD")
stock(sym.st, currency="USD",multiplier=1)
getSymbols("^GSPC", src='yahoo', index.class=c("POSIXt","POSIXct"), from='1990-01-01')
GSPC <- to.weekly(GSPC,indexAt='lastof',drop.time=TRUE)

#Custom Order Sizing Function to trade percent of equity based on a stopsize
osPCTEQ <- function(timestamp, orderqty, portfolio, symbol, ruletype, ...){
  tempPortfolio <- getPortfolio(portfolio.st)
  dummy <- updatePortf(Portfolio=portfolio.st, Dates=paste('::',as.Date(timestamp),sep=''))
  trading.pl <- sum(getPortfolio(portfolio.st)$summary$Realized.PL) #change to ..$summary$Net.Trading.PL for Total Equity Position Sizing
  assign(paste("portfolio.",portfolio.st,sep=""),tempPortfolio,pos=.blotter)
  total.equity <- initEq+trading.pl
  DollarRisk <- total.equity * trade.percent
  ClosePrice <- as.numeric(Cl(mktdata[timestamp,]))
  mavg <- as.numeric(mktdata$SMA52[timestamp,])
  sign1 <- ifelse(ClosePrice > mavg, 1, -1)
  sign1[is.na(sign1)] <- 1
  Posn = getPosQty(Portfolio = portfolio.st, Symbol = sym.st, Date = timestamp)
  StopSize <- as.numeric(mktdata$SDEV[timestamp,]*StopMult) #Stop = SDAVG * StopMult !Must have SDAVG or other indictor to determine stop size
  orderqty <- ifelse(Posn == 0, sign1*round(DollarRisk/StopSize), 0) # number contracts traded is equal to DollarRisk/StopSize
  return(orderqty)
}

#Function that calculates the n period standard deviation of close prices.
#This is used in place of ATR so that I can use only close prices.
SDEV <- function(x, n){
  sdev <- runSD(x, n, sample = FALSE)
  colnames(sdev) <- "SDEV"
  reclass(sdev,x)
}

#Custom indicator function 
RB <- function(x,n){
  x <- x
  roc <- ROC(x, n=1, type="discrete")
  sd <- runSD(roc,n, sample= FALSE)
  sd[is.na(sd)] <- 0
  med <- runMedian(sd,n)
  med[is.na(med)] <- 0
  mavg <- SMA(x,n)
  signal <- ifelse(sd < med & x > mavg,1,0)
  colnames(signal) <- "RB"
  #ret <- cbind(x,roc,sd,med,mavg,signal)
  #colnames(ret) <- c("close","roc","sd","med","mavg","lowvol")
  reclass(signal,x)
  }

initDate='1900-01-01'
initEq <- 100000

trade.percent <- .05 #percent risk used in sizing function
StopMult = 1 #stop size used in sizing function

#Name the portfolio and account
portfolio.st='RBtest'
account.st='RBtest'

#Initialization
initPortf(portfolio.st, symbols=sym.st, initPosQty=0, initDate=initDate, currency="USD")
initAcct(account.st,portfolios=portfolio.st, initDate=initDate, initEq=initEq)
initOrders(portfolio=portfolio.st,initDate=initDate)

#Name the strategy
stratRB <- strategy('RBtest')

#Add indicators
#The first indicator is the 52 period SMA
#The second indicator is the RB indicator. The RB indicator returns a value of 1 when close > SMA & volatility < runMedian(volatility, n = 52)
stratRB <- add.indicator(strategy = stratRB, name = "SMA", arguments = list(x = quote(Cl(mktdata)), n=52), label="SMA52")
stratRB <- add.indicator(strategy = stratRB, name = "RB", arguments = list(x = quote(Cl(mktdata)), n=52), label="RB")
stratRB <- add.indicator(strategy = stratRB, name = "SDEV", arguments = list(x = quote(Cl(mktdata)), n=52), label="SDEV")

#Add signals
#The buy signal is when the RB indicator crosses from 0 to 1
#The exit signal is when the close crosses below the SMA
stratRB <- add.signal(strategy = stratRB, name="sigThreshold", arguments = list(threshold=1, column="RB",relationship="gte", cross=TRUE),label="RB.gte.1")
stratRB <- add.signal(strategy = stratRB, name="sigCrossover", arguments = list(columns=c("Close","SMA52"),relationship="lt"),label="Cl.lt.SMA")

#Add rules
stratRB <- add.rule(strategy = stratRB, name='ruleSignal', arguments = list(sigcol="RB.gte.1", sigval=TRUE, orderqty=1000, ordertype='market', orderside='long', osFUN = 'osPCTEQ', pricemethod='market', replace=FALSE), type='enter', path.dep=TRUE)
stratRB <- add.rule(strategy = stratRB, name='ruleSignal', arguments = list(sigcol="Cl.lt.SMA", sigval=TRUE, orderqty='all', ordertype='market', orderside='long', pricemethod='market',TxnFees=0), type='exit', path.dep=TRUE)

# Process the indicators and generate trades
start_t<-Sys.time()
out<-try(applyStrategy(strategy=stratRB , portfolios=portfolio.st))
end_t<-Sys.time()
print("Strategy Loop:")
print(end_t-start_t)

start_t<-Sys.time()
updatePortf(Portfolio=portfolio.st,Dates=paste('::',as.Date(Sys.time()),sep=''))
end_t<-Sys.time()
print("updatePortf execution time:")
print(end_t-start_t)

chart.Posn(Portfolio=portfolio.st,Symbol=sym.st)

#Update Account
updateAcct(account.st)

#Update Ending Equity
updateEndEq(account.st)

#ending equity
getEndEq(account.st, Sys.Date()) + initEq

tstats <- tradeStats(Portfolio=portfolio.st, Symbol=sym.st)

#View order book to confirm trades
#getOrderBook(portfolio.st)

#Trade Statistics for CAGR, Max DD, and MAR
#calculate total equity curve performance Statistics
ec <- tail(cumsum(getPortfolio(portfolio.st)$summary$Net.Trading.PL),-1)
ec$initEq <- initEq
ec$totalEq <- ec$Net.Trading.PL + ec$initEq
ec$maxDD <- ec$totalEq/cummax(ec$totalEq)-1
ec$logret <- ROC(ec$totalEq, n=1, type="continuous")
ec$logret[is.na(ec$logret)] <- 0

Strat.Wealth.Index <- exp(cumsum(ec$logret)) #growth of $1

period.count <- NROW(ec)-104 #Use 104 because there is a 104 week lag for the 52 week SD and 52 week median of SD
year.count <- period.count/52
maxDD <- min(ec$maxDD)*100
totret <- as.numeric(last(ec$totalEq))/as.numeric(first(ec$totalEq))
CAGR <- (totret^(1/year.count)-1)*100
MAR <- CAGR/abs(maxDD)

Perf.Stats <- c(CAGR, maxDD, MAR)
names(Perf.Stats) <- c("CAGR", "maxDD", "MAR")
Perf.Stats
#write.zoo(mktdata, file = "E:\\a.csv")

charts.PerformanceSummary(ec$logret, wealth.index = TRUE, colorset = "steelblue2", main = "SMA with Volatility Filter System Performance")

Created by Pretty R at inside-R.org

Disclaimer: Past results do not guarantee future returns. Information on this website is for informational purposes only and does not offer advice to buy or sell any securities.