First off, for the hiring managers out there, after about a one-year contracting role at Bank of America doing some analytical reporting coding for them in Python, I am on the job market. Feel free to find my LinkedIn here.

This post will cover how to make tactical asset allocation strategies a bit more realistic with regards to execution. That is, by using next-day open-to-open rather than observe-the-close-get-the-close, it’s possible to see how much this change affects a strategy, and potentially, something that I think a site like AllocateSmartly could implement to actively display in their simulations (E.G. a checkbox that says “display next-day open to open returns”).

Now, onto the idea of the post: generally, when doing tactical asset allocation backtests, we tend to just use one set of daily returns. Get the adjusted close data, and at the end of every month, allocate to selected holdings. That is, most TAA backtests we’ve seen generally operate under the assumption of:

“Run the program at 3:50 PM EST. on the last day of the month, scrape the prices for the assets, allocate assets in the next ten minutes.”

This…can generally work for small accounts, but for institutions interested in scaling these strategies, maybe not so much.

So, the trick here, first, in English is this:

Compute open-to-open returns. Lag them by **negative** one. While this may sound really spooky at first, because a lag of negative one implies the potential for lookahead bias, in this case, it’s carefully done to use future returns. That is, the statement in English is (for example): “given my weights for data at the close of June 30, what would my open-to-open returns have been if I entered on the open of July 1st, instead of the June 30 close?”. Of course, this sets the return dates off by one day, so you’ll have to lag the total portfolio returns by a positive one to readjust for that.

This is how it works in code, using my KDA asset allocation algorithm. :

require(quadprog) # compute strategy statistics stratStats <- function(rets) { stats <- rbind(table.AnnualizedReturns(rets), maxDrawdown(rets)) stats[5,] <- stats[1,]/stats[4,] stats[6,] <- stats[1,]/UlcerIndex(rets) rownames(stats)[4] <- "Worst Drawdown" rownames(stats)[5] <- "Calmar Ratio" rownames(stats)[6] <- "Ulcer Performance Index" return(stats) } # required libraries require(quantmod) require(PerformanceAnalytics) require(tseries) # symbols symbols <- c("SPY", "VGK", "EWJ", "EEM", "VNQ", "RWX", "IEF", "TLT", "DBC", "GLD", "VWO", "BND") # get data rets <- list() prices <- list() op_rets <- list() for(i in 1:length(symbols)) { tmp <- getSymbols(symbols[i], from = '1990-01-01', auto.assign = FALSE, use.adjusted = TRUE) price <- Ad(tmp) returns <- Return.calculate(Ad(tmp)) op_ret <- Return.calculate(Op(tmp)) colnames(returns) <- colnames(price) <- colnames(op_ret) <- symbols[i] prices[[i]] <- price rets[[i]] <- returns op_rets[[i]] <- op_ret } rets <- na.omit(do.call(cbind, rets)) prices <- na.omit(do.call(cbind, prices)) op_rets <- na.omit(do.call(cbind, op_rets)) # algorithm KDA <- function(rets, offset = 0, leverageFactor = 1, momWeights = c(12, 4, 2, 1), op_rets = NULL, use_op_rets = FALSE) { # get monthly endpoints, allow for offsetting ala AllocateSmartly/Newfound Research ep <- endpoints(rets) + offset ep[ep < 1] <- 1 ep[ep > nrow(rets)] <- nrow(rets) ep <- unique(ep) epDiff <- diff(ep) if(last(epDiff)==1) { # if the last period only has one observation, remove it ep <- ep[-length(ep)] } # initialize vector holding zeroes for assets emptyVec <- data.frame(t(rep(0, 10))) colnames(emptyVec) <- symbols[1:10] allWts <- list() # we will use the 13612F filter for(i in 1:(length(ep)-12)) { # 12 assets for returns -- 2 of which are our crash protection assets retSubset <- rets[c((ep[i]+1):ep[(i+12)]),] epSub <- ep[i:(i+12)] sixMonths <- rets[(epSub[7]+1):epSub[13],] threeMonths <- rets[(epSub[10]+1):epSub[13],] oneMonth <- rets[(epSub[12]+1):epSub[13],] # computer 13612 fast momentum moms <- Return.cumulative(oneMonth) * momWeights[1] + Return.cumulative(threeMonths) * momWeights[2] + Return.cumulative(sixMonths) * momWeights[3] + Return.cumulative(retSubset) * momWeights[4] assetMoms <- moms[,1:10] # Adaptive Asset Allocation investable universe cpMoms <- moms[,11:12] # VWO and BND from Defensive Asset Allocation # find qualifying assets highRankAssets <- rank(assetMoms) >= 6 # top 5 assets posReturnAssets <- assetMoms > 0 # positive momentum assets selectedAssets <- highRankAssets & posReturnAssets # intersection of the above # perform mean-variance/quadratic optimization investedAssets <- emptyVec if(sum(selectedAssets)==0) { investedAssets <- emptyVec } else if(sum(selectedAssets)==1) { investedAssets <- emptyVec + selectedAssets } else { idx <- which(selectedAssets) # use 1-3-6-12 fast correlation average to match with momentum filter cors <- (cor(oneMonth[,idx]) * momWeights[1] + cor(threeMonths[,idx]) * momWeights[2] + cor(sixMonths[,idx]) * momWeights[3] + cor(retSubset[,idx]) * momWeights[4])/sum(momWeights) vols <- StdDev(oneMonth[,idx]) # use last month of data for volatility computation from AAA covs <- t(vols) %*% vols * cors # do standard min vol optimization minVolRets <- t(matrix(rep(1, sum(selectedAssets)))) n.col = ncol(covs) zero.mat <- array(0, dim = c(n.col, 1)) one.zero.diagonal.a <- cbind(1, diag(n.col), 1 * diag(n.col), -1 * diag(n.col)) min.wgt <- rep(.05, n.col) max.wgt <- rep(1, n.col) bvec.1.vector.a <- c(1, rep(0, n.col), min.wgt, -max.wgt) meq.1 <- 1 mv.port.noshort.a <- solve.QP(Dmat = covs, dvec = zero.mat, Amat = one.zero.diagonal.a, bvec = bvec.1.vector.a, meq = meq.1) min_vol_wt <- mv.port.noshort.a$solution names(min_vol_wt) <- rownames(covs) #minVolWt <- portfolio.optim(x=minVolRets, covmat = covs)$pw #names(minVolWt) <- colnames(covs) investedAssets <- emptyVec investedAssets[,selectedAssets] <- min_vol_wt } # crash protection -- between aggressive allocation and crash protection allocation pctAggressive <- mean(cpMoms > 0) investedAssets <- investedAssets * pctAggressive pctCp <- 1-pctAggressive # if IEF momentum is positive, invest all crash protection allocation into it # otherwise stay in cash for crash allocation if(assetMoms["IEF"] > 0) { investedAssets["IEF"] <- investedAssets["IEF"] + pctCp } # leverage portfolio if desired in cases when both risk indicator assets have positive momentum if(pctAggressive == 1) { investedAssets = investedAssets * leverageFactor } # append to list of monthly allocations wts <- xts(investedAssets, order.by=last(index(retSubset))) allWts[[i]] <- wts } # put all weights together and compute cash allocation allWts <- do.call(rbind, allWts) allWts$CASH <- 1-rowSums(allWts) # add cash returns to universe of investments investedRets <- rets[,1:10] investedRets$CASH <- 0 # compute portfolio returns out <- Return.portfolio(R = investedRets, weights = allWts) if(use_op_rets) { if(is.null(op_rets)) { stop("You didn't provide open returns.") } else { # cbind a cash return of 0 -- may not be necessary in current iterations of PerfA investedRets <- cbind(lag(op_rets[,1:10], -1), 0) out <- lag(Return.portfolio(R = investedRets, weights = allWts)) } } return(list(allWts, out)) }

Essentially, the salient part of the code is at the start, around line 32, when the algorithm gets the data from Yahoo, in that it creates a new set of returns using open adjusted data, and at the end, at around line 152, when the code lags the open returns by -1–I.E. lag(op_rets[,1:10], -1), and then lags the returns again to realign the correct dates–out – lag(Return.portfolio(R=investedRets, weights = allWts))

And here is the code for the results:

KDA_100 <- KDA(rets, leverageFactor = 1) KDA_100_open <- KDA(rets, leverageFactor = 1, op_rets = op_rets, use_op_rets = TRUE) compare <- na.omit(cbind(KDA_100[[2]], KDA_100_open[[2]])) charts.PerformanceSummary(KDA_100[[2]]) compare <- na.omit(cbind(KDA_100[[2]], KDA_100_open[[2]])) colnames(compare) <- c("Obs_Close_Buy_Close", "Buy_Open_Next_Day") charts.PerformanceSummary(compare) stratStats(compare)

With the following results:

> stratStats(compare) Obs_Close_Buy_Close Buy_Open_Next_Day Annualized Return 0.1069000 0.08610000 Annualized Std Dev 0.0939000 0.09130000 Annualized Sharpe (Rf=0%) 1.1389000 0.94300000 Worst Drawdown 0.0830598 0.09694208 Calmar Ratio 1.2870245 0.88815920 Ulcer Performance Index 3.8222753 2.41802066

As one can see, there’s definitely a bit of a performance deterioration to the tune of about 2% per year. While the strategy may still be solid, a loss of 20% of the CAGR means that the other risk/reward statistics suffer proportionally as well. In other words, this is a strategy that is fairly sensitive to the exact execution due to its fairly high turnover.

However, the good news is that there is a way to considerably reduce turnover as suggested by AllocateSmartly, which would be to reduce the impact of relative momentum on turnover. A new post on how to do *that* will be forthcoming in the near future.

One last thing–as I did pick up some Python skills, as evidenced by the way I ported the endpoints function into Python, and the fact that I completed an entire Python data science bootcamp in four instead of six months, I am also trying to port over the Return.portfolio function into Python as well, since that would allow a good way to compute turnover statistics as well. However, as far as I’ve seen with Python, I’m not sure there’s a well-maintained library similar to PerformanceAnalytics, and I do not know that there are libraries in Python that compare with rugarch, PortfolioAnalytics, and Quantstrat, though if anyone wants to share the go-to generally-accepted Python libraries to use beyond the usual numpy/pandas/matplotlib/cvxpy/sklearn (AKA the usual data science stack).

Thanks for reading.

NOTE: Lastly, some other news: late last year, I was contacted by the NinjaTrader folks to potentially become a vendor/give lectures on the NinjaTrader site about how to create quantitative/systematic strategies. While I’m not a C# coder, I can potentially give lectures on how to use R (and maybe Python in the future) to implement them.

Finally, if you wish to get in touch with me, my email is [email protected], and I can be found on my LinkedIn. Additionally, if you wish to subscribe to my volatility strategy, that I’ve been successfully trading since 2017, feel free to check it out here.

*Related*