Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Low volatility and minimum variance strategies have been getting a lot of attention lately due to their outperformance in recent years. Let’s take a look at how we can incorporate this low volatility effect into a monthly rotational strategy with a basket of ETFs.
Performance Summary from Low Volatility Test in quantstrat
Starting Equity: 100,000
Ending Equity: 114,330
CAGR: 1.099%
maxDD: -38.325%
MAR: 0.0287
Not the greatest performance stats in the world. There are some things we can do to improve this strategy. I will save that for later. The purpose of this post was an exercise using quantstrat to implement a low volatility ranking system.
We can see from the chart that the low volatility strategy does what it is supposed to do… the drawdown is reduced compared to a buy and hold strategy on SPY. This is by no means a conclusive test. Ideally, the test would cover 20, 40, 60+ years of data to show the “longer” term performance of both strategies.
Here is a step by step approach to implement the strategy in R
The first step is fire up R and require the quantstrat package.
require(quantstrat)
This test will use nine of the Select Sector SPDR ETFs.
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 monthly for(symbol in symbols) { x<-get(symbol) x<-to.monthly(x,indexAt='lastof',drop.time=TRUE) indexFormat(x)<-'%Y-%m-%d' colnames(x)<-gsub("x",symbol,colnames(x)) assign(symbol,x) }
Here is what the data for XLB looks like after it is downloaded
> tail(XLB) XLB.Open XLB.High XLB.Low XLB.Close XLB.Volume XLB.Adjusted 2011-11-30 33.10 35.73 31.41 34.52 290486300 34.15 2011-12-31 34.34 35.01 31.86 33.50 233453200 33.37 2012-01-31 34.24 37.73 34.23 37.18 171601400 37.04 2012-02-29 37.48 37.97 36.40 36.97 179524000 36.83 2012-03-31 37.19 37.65 35.80 36.97 201651000 36.97 2012-04-30 36.92 37.63 35.10 35.59 85846600 35.59
The measure of volatility that I will use is a rolling 12 period standard deviation of the 1 period ROC. The 1 period ROC is taken on the Adjusted Close prices. My approach for the ranking system is to first apply the standard deviation to the market data and then assign a rank of 1, 2, …9 for the instruments. There may be a more elegant way to do this in R, so if you have an alternative way to implement this I am all ears.
#Calcuate the ranking factors for each symbol and bind to its symbol #This loops through the list of symbols and adds a "RANK" column for(symbol in symbols) { x <- get(symbol) x1 <- ROC(Ad(x), n=1, type="continuous", na.pad=TRUE) colnames(x1) <- "ROC" colnames(x1) <- paste("x",colnames(x1), sep =".") #x2 is the 12 period standard deviation of the 1 month return x2 <- runSD(x1, n=12) colnames(x2) <- "RANK" colnames(x2) <- paste("x",colnames(x2), sep =".") x <- cbind(x,x2) colnames(x)<-gsub("x",symbol,colnames(x)) assign(symbol,x) }
Now the XLB data has an extra column of the 12 period SD of the 1 period ROC named “RANK”
> tail(XLB) XLB.Open XLB.High XLB.Low XLB.Close XLB.Volume XLB.Adjusted XLB.RANK 2011-11-30 33.10 35.73 31.41 34.52 290486300 34.15 0.08300814 2011-12-31 34.34 35.01 31.86 33.50 233453200 33.37 0.07752127 2012-01-31 34.24 37.73 34.23 37.18 171601400 37.04 0.08425784 2012-02-29 37.48 37.97 36.40 36.97 179524000 36.83 0.08381949 2012-03-31 37.19 37.65 35.80 36.97 201651000 36.97 0.08360368 2012-04-30 36.92 37.63 35.10 35.59 85846600 35.59 0.08367737
#Bind each symbols's "RANK" column into a single xts object rank.factors <- cbind(XLB$XLB.RANK, XLE$XLE.RANK, XLF$XLF.RANK, XLI$XLI.RANK, XLK$XLK.RANK, XLP$XLP.RANK, XLU$XLU.RANK, XLV$XLV.RANK, XLY$XLY.RANK)
Here is what our rank.factors object looks like.
> tail(rank.factors) XLB.RANK XLE.RANK XLF.RANK XLI.RANK XLK.RANK XLP.RANK XLU.RANK XLV.RANK XLY.RANK 2011-11-30 0.08300814 0.08837101 0.07381782 0.06492454 0.04169398 0.02930909 0.01532320 0.03559538 0.04946373 2011-12-31 0.07752127 0.08522966 0.06612174 0.06136258 0.03898518 0.02811202 0.01555798 0.03451478 0.04843218 2012-01-31 0.08425784 0.08291821 0.07063470 0.06389852 0.04171582 0.02806211 0.02160217 0.03502983 0.05052721 2012-02-29 0.08381949 0.08192191 0.07192495 0.06410781 0.04552402 0.02863641 0.02164171 0.03451369 0.04946965 2012-03-31 0.08360368 0.08223880 0.07536219 0.06385518 0.04589758 0.02914123 0.02158078 0.03581751 0.05032237 2012-04-30 0.08367737 0.08291464 0.07608845 0.06423188 0.04573648 0.02728300 0.02114430 0.03341575 0.05064814
Now we need to apply a “RANK” of 1 through 9 (because there are 9 symbols).
#ranked in order such that the symbol with the lowest volatility is given a rank of 1 r <- as.xts(t(apply(rank.factors, 1, rank)))
Here is what the r object looks like with each symbol being ranked by volatility > tail(r) XLB.RANK XLE.RANK XLF.RANK XLI.RANK XLK.RANK XLP.RANK XLU.RANK XLV.RANK XLY.RANK 2011-11-30 8 9 7 6 4 2 1 3 5 2011-12-31 8 9 7 6 4 2 1 3 5 2012-01-31 9 8 7 6 4 2 1 3 5 2012-02-29 9 8 7 6 4 2 1 3 5 2012-03-31 9 8 7 6 4 2 1 3 5 2012-04-30 9 8 7 6 4 2 1 3 5
#Set the symbol's market data back to its original structure so we don't have 2 columns named "RANK" for (symbol in symbols){ x <- get(symbol) x <- x[,1:6] assign(symbol,x) }
#Bind the symbol's rank to the symbol's market data XLB <- cbind(XLB,r$XLB.RANK) XLE <- cbind(XLE,r$XLE.RANK) XLF <- cbind(XLF,r$XLF.RANK) XLI <- cbind(XLI,r$XLI.RANK) XLK <- cbind(XLK,r$XLK.RANK) XLP <- cbind(XLP,r$XLP.RANK) XLU <- cbind(XLU,r$XLU.RANK) XLV <- cbind(XLV,r$XLV.RANK) XLY <- cbind(XLY,r$XLY.RANK)
Now we can see that each symbol has an extra “RANK” column
> tail(XLB) XLB.Open XLB.High XLB.Low XLB.Close XLB.Volume XLB.Adjusted XLB.RANK 2011-11-30 33.10 35.73 31.41 34.52 290486300 34.15 8 2011-12-31 34.34 35.01 31.86 33.50 233453200 33.37 8 2012-01-31 34.24 37.73 34.23 37.18 171601400 37.04 9 2012-02-29 37.48 37.97 36.40 36.97 179524000 36.83 9 2012-03-31 37.19 37.65 35.80 36.97 201651000 36.97 9 2012-04-30 36.92 37.63 35.10 36.56 99089100 36.56 9
Now that the market data is “prepared”, we can easily implement the strategy using quantstrat. Note that the signal is when the “RANK” column is less than 3. This means that the strategy buys the 3 instruments with the lowest volatility. See end of post for quantstrat code.
#Market data is prepared with each symbols rank based on the factors chosen #Now use quantstrat to execute the strategy #Set Initial Values initDate='1900-01-01' #initDate must be before the first date in the market data initEq=100000 #initial equity #Name the portfolio portfolio.st='RSRANK' #Name the account account.st='RSRANK' #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) #Initialize strategy object stratRSRANK <- strategy(portfolio.st) # There are two signals: # The first is when Rank is less than or equal to N (i.e. trades the #1 ranked symbol if N=1) stratRSRANK <- add.signal(strategy = stratRSRANK, name="sigThreshold",arguments = list(threshold=3, column="RANK",relationship="lte", cross=TRUE),label="Rank.lte.N") # The second is when Rank is greater than N stratRSRANK <- add.signal(strategy = stratRSRANK, name="sigThreshold",arguments = list(threshold=3, column="RANK",relationship="gt",cross=TRUE),label="Rank.gt.N") # There is one rule: # The first is to buy when the Rank crosses above the threshold stratRSRANK <- add.rule(strategy = stratRSRANK, name='ruleSignal', arguments = list(sigcol="Rank.lte.N", sigval=TRUE, orderqty=1000, ordertype='market', orderside='long', pricemethod='market', replace=FALSE), type='enter', path.dep=TRUE) #Exit when the symbol Rank falls below the threshold stratRSRANK <- add.rule(strategy = stratRSRANK, name='ruleSignal', arguments = list(sigcol="Rank.gt.N", sigval=TRUE, orderqty='all', ordertype='market', orderside='long', pricemethod='market', replace=FALSE), type='exit', path.dep=TRUE) #Apply the strategy to the portfolio start_t<-Sys.time() out<-try(applyStrategy(strategy=stratRSRANK , portfolios=portfolio.st)) end_t<-Sys.time() print(end_t-start_t) #Update Portfolio start_t<-Sys.time() updatePortf(Portfolio=portfolio.st,Dates=paste('::',as.Date(Sys.time()),sep='')) end_t<-Sys.time() print("trade blotter portfolio update:") print(end_t-start_t) #Update Account updateAcct(account.st) #Update Ending Equity updateEndEq(account.st) #get ending equity getEndEq(account.st, Sys.Date()) + initEq #View order book to confirm trades getOrderBook(portfolio.st) tstats <- tradeStats(Portfolio=portfolio.st, Symbol=symbols) chart.Posn(Portfolio=portfolio.st,Symbol="XLF") #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 write.zoo(Strat.Wealth.Index, file = "E:\\a.csv") period.count <- NROW(ec) year.count <- period.count/12 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") #tstats Perf.Stats #Benchmark against a buy and hold strategy with SPY require(PerformanceAnalytics) getSymbols("SPY", src='yahoo', index.class=c("POSIXt","POSIXct"), from='2001-01-01') SPY <- to.monthly(SPY,indexAt='lastof',drop.time=TRUE) SPY.ret <- Return.calculate(Ad(SPY), method="compound") SPY.ret[is.na(SPY.ret)] <- 0 SPY.wi <- exp(cumsum(SPY.ret)) write.zoo(SPY.wi, file = "E:\\a1.csv")
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.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.