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 ilya.kipnis@gmail.com, 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.