Adding a Risk-Free Rate To Your Analyses

[This article was first published on QuantStrat TradeR » R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

First off, before beginning this post, I’d like to make my readers aware of the release of a book that I contributed almost an entire chapter for.

Quantitative Trading With R is a primer on quantitative trading in R written by Harry Georgakopoulos, one of Chicago’s better quants. I contributed almost the entire chapter on quantstrat. If you’ve been able to follow and understand the code I write on this blog, then said chapter will mostly be review and a basic nuts and bolts reference. But for those of my readers who gloss over the code and wait for the punchline, I highly recommend it. In addition, there are chapters on high frequency trading, options, spreads, and other things that I do not believe are available in any other book that actually teaches readers the details of implementation. Now, onto the post.

As part of my continuation of Elastic Asset Allocation, I wanted to cover how to implement a measure of a risk-free rate in your analyses. In this post, I’ll analyze two slight variations of EAA from the last post.

Essentially, the idea that rather than look at absolute return, we should look at *excess* return over some sort of risk-free rate, such as the 13-week treasury bill.

Luckily, Yahoo actually *has* a way of getting the returns of the risk-free asset, namely, IRX. But first, let’s get the similarities to the last post out of the way.

require(quantmod)
require(PerformanceAnalytics)

symbols <- c("VTSMX", "FDIVX", "VEIEX", "VBMFX", "VFISX", "VGSIX", "QRAAX")

getSymbols(symbols, from="1990-01-01")
prices <- list()
for(i in 1:length(symbols)) {
  prices[[i]] <- Ad(get(symbols[i]))  
}
prices <- do.call(cbind, prices)
colnames(prices) <- gsub("\.[A-z]*", "", colnames(prices))
ep <- endpoints(prices, "months")
prices <- prices[ep,]
prices <- prices["1997-03::"]

Okay, everything fine so far, same as before. Now here’s the new innovation, brought to my attention by TrendXplorer. It turns out that the IRX index is actually the annualized yield for the short-term (three month) treasuries. So by adding 1, raising it to the 252nd root, and taking the cumulative product, we can actually get the “price” of the risk-free rate, and from that, compute daily returns (this is most likely redundant, but I want all my returns computed the same way).

getSymbols("^IRX", from="1990-01-01")
dailyYield <- (1+(Cl(IRX)/100))^(1/252) - 1
threeMoPrice <- cumprod(1+dailyYield)
threeMoPrice <- threeMoPrice["1997-03::"]
threeMoPrice <- threeMoPrice[endpoints(threeMoPrice, "months"),]

So how does this fit into EAA? Well, simply, I added a new argument called monthlyRiskFree, which will let a user pass in the monthly price series of the risk-free asset, in this case the derived IRX price series. That information is then used to compute a risk-free return, which is subtracted from the returns of all assets, and rather than taking the absolute return of the assets in the universe, instead the algorithm computes the return in excess of the risk-free asset.

Here’s the modified function:

EAA <- function(monthlyPrices, wR=1, wV=0, wC=.5, wS=2, errorJitter=1e-6, 
                cashAsset=NULL, bestN=1+ceiling(sqrt(ncol(monthlyPrices))),
                enableCrashProtection = TRUE, returnWeights=FALSE, monthlyRiskFree=NULL) {
  returns <- Return.calculate(monthlyPrices)
  returns <- returns[-1,] #return calculation uses one observation
  if(!is.null(monthlyRiskFree)) {
    returnsRF <- Return.calculate(monthlyRiskFree)
    returnsRF <- returnsRF[-1,]
  }
  
  if(is.null(cashAsset)) {
    returns$zeroes <- 0
    cashAsset <- "zeroes"
    warning("No cash security specified. Recommended to use one of: quandClean('CHRIS/CME_US'), SHY, or VFISX. 
            Using vector of zeroes instead.")
  }
  
  cashCol <- grep(cashAsset, colnames(returns))
  
  weights <- list()
  for(i in 1:(nrow(returns)-11)) {
    returnsData <- returns[i:(i+11),] #each chunk will be 12 months of returns data
    #per-month mean of cumulative returns of 1, 3, 6, and 12 month periods
    periodReturn <- ((returnsData[12,] + Return.cumulative(returnsData[10:12,]) + 
                      Return.cumulative(returnsData[7:12,]) + Return.cumulative(returnsData)))/22
    
    if(!is.null(monthlyRiskFree)) {
      rfData <- returnsRF[i:(i+11),]
      rfReturn <- ((rfData[12,] + Return.cumulative(rfData[10:12,]) + 
                    Return.cumulative(rfData[7:12,]) + Return.cumulative(rfData)))/22
      periodReturn <- periodReturn - as.numeric(rfReturn)
    }
    
    vols <- StdDev.annualized(returnsData) 
    mktIndex <- xts(rowMeans(returnsData), order.by=index(returnsData)) #equal weight returns of universe
    cors <- cor(returnsData, mktIndex) #correlations to market index
    
    weightedRets <- periodReturn ^ wR
    weightedCors <- (1 - as.numeric(cors)) ^ wC
    weightedVols <- (vols + errorJitter) ^ wV
    wS <- wS + errorJitter
    
    z <- (weightedRets * weightedCors / weightedVols) ^ wS #compute z_i and zero out negative returns
    z[periodReturn < 0] <- 0
    crashProtection <- sum(z==0)/length(z) #compute crash protection cash cushion
    
    orderedZ <- sort(as.numeric(z), decreasing=TRUE)
    selectedSecurities <- z >= orderedZ[bestN]
    preNormalizedWeights <- z*selectedSecurities #select top N securities, keeping z_i scores
    periodWeights <- preNormalizedWeights/sum(preNormalizedWeights) #normalize
    if (enableCrashProtection) {
      periodWeights <- periodWeights * (1-crashProtection) #CP rule
    }
    weights[[i]] <- periodWeights
  }
  
  weights <- do.call(rbind, weights)
  weights[, cashCol] <- weights[, cashCol] + 1-rowSums(weights) #add to risk-free asset all non-invested weight
  strategyReturns <- Return.rebalancing(R = returns, weights = weights) #compute strategy returns
  if(returnWeights) {
    return(list(weights, strategyReturns))
  } else {
    return(strategyReturns)
  }
}

Essentially, the key new block of code is this:

    if(!is.null(monthlyRiskFree)) {
      rfData <- returnsRF[i:(i+11),]
      rfReturn <- ((rfData[12,] + Return.cumulative(rfData[10:12,]) + 
                    Return.cumulative(rfData[7:12,]) + Return.cumulative(rfData)))/22
      periodReturn <- periodReturn - as.numeric(rfReturn)
    }

Which does exactly as I mentioned above — computes the EAA variant of the returns for the period for the risk-free asset and subtracts it from the other returns.

So how does using this new innovation compare to simply looking at absolute returns?

Let’s find out.

offensive <- EAA(prices, cashAsset="VBMFX", bestN=3)
defensive <- EAA(prices, cashAsset="VBMFX", bestN=3, wS=.5, wC=1)
offRF <- EAA(prices, cashAsset="VBMFX", bestN=3, monthlyRiskFree = threeMoPrice)
defRF <- EAA(prices, cashAsset="VBMFX", bestN=3, wS=.5, wC=1, monthlyRiskFree = threeMoPrice)
compare <- cbind(offensive, defensive, offRF, defRF)
colnames(compare) <- c("Offensive", "Defensive", "OffRF", "DefRF")
stats <- rbind(Return.annualized(compare)*100, StdDev.annualized(compare)*100, maxDrawdown(compare)*100, SharpeRatio.annualized(compare))
rownames(stats)[3] <- "Worst Drawdown"
charts.PerformanceSummary(compare)

Here’s the table of statistics:

                                Offensive Defensive     OffRF     DefRF
Annualized Return               12.206133 10.262766 11.415583 10.269146
Annualized Standard Deviation   11.352728  8.615134 10.551722  8.129250
Worst Drawdown                  12.629251  8.134785 14.351895  9.376533
Annualized Sharpe Ratio (Rf=0%)  1.075172  1.191249  1.081869  1.263234

And the corresponding chart:

In short, nope. No dice there. On the defensive portfolio, the change is negligible, and on the offensive side, it seems to encourage more risk than necessary, with…nothing to show for it, really.

That stated, just because this method didn’t pan out doesn’t mean that the actual mechanics of obtaining risk-free data are without value.

Thanks for reading.

NOTE: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.


To leave a comment for the author, please follow the link and comment on their blog: QuantStrat TradeR » R.

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.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)