A Basic Logical Invest Global Market Rotation Strategy

This may be one of the simplest strategies I’ve ever presented on this blog, but nevertheless, it works, for some definition of “works”.

Here’s the strategy: take five global market ETFs (MDY, ILF, FEZ, EEM, and EPP), along with a treasury ETF (TLT), and every month, fully invest in the security that had the best momentum. While I’ve tried various other tweaks, none have given the intended high return performance that the original variant has.

Here’s the link to the original strategy.

While I’m not quite certain of how to best go about programming the variable lookback period, this is the code for the three month lookback.

require(quantmod)
require(PerformanceAnalytics)

symbols <- c("MDY", "TLT", "EEM", "ILF", "EPP", "FEZ")
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))
returns <- Return.calculate(prices)
returns <- na.omit(returns)

logicInvestGMR <- function(returns, lookback = 3) {
  ep <- endpoints(returns, on = "months") 
  weights <- list()
  for(i in 2:(length(ep) - lookback)) {
    retSubset <- returns[ep[i]:ep[i+lookback],]
    cumRets <- Return.cumulative(retSubset)
    rankCum <- rank(cumRets)
    weight <- rep(0, ncol(retSubset))
    weight[which.max(cumRets)] <- 1
    weight <- xts(t(weight), order.by=index(last(retSubset)))
    weights[[i]] <- weight
  }
  weights <- do.call(rbind, weights)
  stratRets <- Return.portfolio(R = returns, weights = weights)
  return(stratRets)
}

gmr <- logicInvestGMR(returns)
charts.PerformanceSummary(gmr)

And here’s the performance:

> rbind(table.AnnualizedReturns(gmr), maxDrawdown(gmr), CalmarRatio(gmr))
                          portfolio.returns
Annualized Return                  0.287700
Annualized Std Dev                 0.220700
Annualized Sharpe (Rf=0%)          1.303500
Worst Drawdown                     0.222537
Calmar Ratio                       1.292991

With the resultant equity curve:

While I don’t get the 34% advertised, nevertheless, the risk to reward ratio over the duration of the backtest is fairly solid for something so simple, and I just wanted to put this out there.

Thanks for reading.

Advertising a Few Systematic ETFs (Strictly Of My Own Volition)

This post will introduce several ETFs from Alpha Architect and Cambria Funds (run by Meb Faber) that I think readers should be aware of (if not so already) in order to capitalize on systematic investing without needing to lose a good portion of the return due to taxes and transaction costs.

So, as my readers know, I backtest lots of strategies on this blog that deal with monthly turnover, and many transactions. In all instances, I assume that A) slippage and transaction costs are negligible, =B) there is sufficient capital such that when a weighting scheme states to place 5.5% of a portfolio into an ETF with an expensive per-share price (EG a sector spider, SPY, etc.), that the issue of integer shares can be adhered to without issue, and C) that there are no taxes on the monthly transactions. For retail investors without millions of dollars to deploy, one or more of these assumptions may not hold. After all, if you have $20,000 to invest, and are paying $50 a month on turnover costs, that’s -3% to your CAGR, which would render quite a few of these strategies pretty terrible.

So, in this short blurb, I want to shine a light on several of these ETFs.

First off, a link to a post from Alpha Architect that essentially states that there are only two tried-and-true market “anomalies” when correcting for data-mining: value, and momentum. Well, that and the durable consumption goods factor. The first, I’m not quite sure how to rigorously test using only freely available data, and the last, I’m not quite sure why it works. Low volatility, perhaps?

In any case, for people who don’t have institutional-grade investing capabilities, here are some ETFs that aim to intelligently capitalize on the value and momentum factors, along with one “permanent portfolio” type of ETF.

Momentum:
GMOM: Global Momentum. Essentially, spread your bets, and go with the trend. Considering Meb Faber is a proponent of momentum (see his famous Ivy Portfolio book), this is the way to capitalize on that.

Value:
QVAL: Alpha Architect’s (domestic) Quantitative Value ETF. The team at Alpha Architect are proponents of value investing, and with a team of several PhDs dedicated to a systematic value investing research process, this may be a way for retail investors to buy-and-hold one product and outsource the meticulous value research necessary for the proper implementation of such a strategy.

IVAL: an international variant of the above.

GVAL: The Cambria Funds quantitative value fund.

Asset Allocation (permanent portfolio):

GAA: Global Asset Allocation. My interpretation? Take the good old stocks, bonds, and real assets portfolio, and spread it out across the globe.

Now, let’s just do a quick rundown and see how these strategies have performed over the small time horizon the latest one has been in existence.

symbols <- c("GMOM", "QVAL", "IVAL", "GVAL", "GAA")

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))

coolEtfReturns <- Return.calculate(prices)
coolEtfReturns <- na.omit(coolEtfReturns)
charts.PerformanceSummary(coolEtfReturns, main = "Quant investing for retail people.")

stats <- rbind(table.AnnualizedReturns(coolEtfReturns),
               maxDrawdown(coolEtfReturns),
               CalmarRatio(coolEtfReturns),
               SortinoRatio(coolEtfReturns) * sqrt(252))
round(stats, 3)
                           GMOM  QVAL  IVAL  GVAL   GAA
Annualized Return         0.038 0.237 0.315 0.323 0.106
Annualized Std Dev        0.082 0.138 0.123 0.192 0.066
Annualized Sharpe (Rf=0%) 0.466 1.709 2.556 1.680 1.595
Worst Drawdown            0.039 0.046 0.046 0.069 0.028
Calmar Ratio              0.981 5.189 6.816 4.678 3.737
Sortino Ratio (MAR = 0%)  0.665 2.598 3.742 2.274 2.407

In other words, aside from momentum, which is having a flat-ish series of months, the performances are overall fairly strong, in this tiny sample (not at all significant).

The one caveat I’d throw out there, however, is that these instruments are not foolproof. For fun, here’s a plot of GVAL (that is, Cambria’s global value fund) since its inception.

And the statistics for it for the whole duration of its inception.

                          GVAL.Adjusted
Annualized Return                -0.081
Annualized Std Dev                0.164
Annualized Sharpe (Rf=0%)        -0.494
Worst Drawdown                    0.276
Calmar Ratio                     -0.294

Again, tiny sample, so nothing conclusive at all, but it just means that these funds may occasionally hurt (no free lunch). That stated, I nevertheless think that Dr. Wesley Gray and Mebane Faber, at Alpha Architect and Cambria Funds, respectively, are about as reputable of money managers as one would find, and the idea that one can invest with them, as opposed to god knows with what mutual fund, to me, is something I think that’s worth not just pointing out, but drawing some positive attention to.

That stated, if anyone out there has hypothetical performances for these funds that goes back to a ten year history in a time-series, I’d love to run some analysis on those. After all, if there were some simple way to improve the performances of a portfolio of these instruments even more, well, I believe Newton had something to say about standing on the shoulders of giants.

Thanks for reading.

NOTE: I will be giving a quick lightning talk at R in finance in Chicago later this month (about two weeks). The early bird registration ends this Friday.

The JP Morgan SCTO strategy

This strategy goes over JP Morgan’s SCTO strategy, a basic XL-sector/RWR rotation strategy with the typical associated risks and returns with a momentum equity strategy. It’s nothing spectacular, but if a large bank markets it, it’s worth looking at.

Recently, one of my readers, a managing director at a quantitative investment firm, sent me a request to write a rotation strategy based around the 9 sector spiders and RWR. The way it works (or at least, the way I interpreted it) is this:

Every month, compute the return (not sure how “the return” is defined) and rank. Take the top 5 ranks, and weight them in a normalized fashion to the inverse of their 22-day volatility. Zero out any that have negative returns. Lastly, check the predicted annualized vol of the portfolio, and if it’s greater than 20%, bring it back down to 20%. The cash asset–SHY–receives any remaining allocation due to setting securities to zero.

For the reference I used, here’s the investment case document from JP Morgan itself.

Here’s my implementation:

Step 1) get the data, compute returns.

require(quantmod)
require(PerformanceAnalytics)
symbols <- c("XLB", "XLE", "XLF", "XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "RWR", "SHY")
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))
returns <- na.omit(Return.calculate(prices))

Step 2) The function itself.

sctoStrat <- function(returns, cashAsset = "SHY", lookback = 4, annVolLimit = .2,
                      topN = 5, scale = 252) {
  ep <- endpoints(returns, on = "months")
  weights <- list()
  cashCol <- grep(cashAsset, colnames(returns))
  
  #remove cash from asset returns
  cashRets <- returns[, cashCol]
  assetRets <- returns[, -cashCol]
  for(i in 2:(length(ep) - lookback)) {
    retSubset <- assetRets[ep[i]:ep[i+lookback]]
    
    #forecast is the cumulative return of the lookback period
    forecast <- Return.cumulative(retSubset)
    
    #annualized (realized) volatility uses a 22-day lookback period
    annVol <- StdDev.annualized(tail(retSubset, 22))
    
    #rank the forecasts (the cumulative returns of the lookback)
    rankForecast <- rank(forecast) - ncol(assetRets) + topN
    
    #weight is inversely proportional to annualized vol
    weight <- 1/annVol
    
    #zero out anything not in the top N assets
    weight[rankForecast <= 0] <- 0
    
    #normalize and zero out anything with a negative return
    weight <- weight/sum(weight)
    weight[forecast < 0] <- 0
    
    #compute forecasted vol of portfolio
    forecastVol <- sqrt(as.numeric(t(weight)) %*% 
                          cov(retSubset) %*% 
                          as.numeric(weight)) * sqrt(scale)
    
    #if forecasted vol greater than vol limit, cut it down
    if(as.numeric(forecastVol) > annVolLimit) {
      weight <- weight * annVolLimit/as.numeric(forecastVol)
    }
    weights[[i]] <- xts(weight, order.by=index(tail(retSubset, 1)))
  }
  
  #replace cash back into returns
  returns <- cbind(assetRets, cashRets)
  weights <- do.call(rbind, weights)
  
  #cash weights are anything not in securities
  weights$CASH <- 1-rowSums(weights)
  
  #compute and return strategy returns
  stratRets <- Return.portfolio(R = returns, weights = weights)
  return(stratRets)      
}

In this case, I took a little bit of liberty with some specifics that the reference was short on. I used the full covariance matrix for forecasting the portfolio variance (not sure if JPM would ignore the covariances and do a weighted sum of individual volatilities instead), and for returns, I used the four-month cumulative. I’ve seen all sorts of permutations on how to compute returns, ranging from some average of 1, 3, 6, and 12 month cumulative returns to some lookback period to some two period average, so I’m all ears if others have differing ideas, which is why I left it as a lookback parameter.

Step 3) Running the strategy.

scto4_20 <- sctoStrat(returns)
getSymbols("SPY", from = "1990-01-01")
spyRets <- Return.calculate(Ad(SPY))
comparison <- na.omit(cbind(scto4_20, spyRets))
colnames(comparison) <- c("strategy", "SPY")
charts.PerformanceSummary(comparison)
apply.yearly(comparison, Return.cumulative)
stats <- rbind(table.AnnualizedReturns(comparison),
               maxDrawdown(comparison),
               CalmarRatio(comparison),
               SortinoRatio(comparison)*sqrt(252))
round(stats, 3)

Here are the statistics:

                          strategy   SPY
Annualized Return            0.118 0.089
Annualized Std Dev           0.125 0.193
Annualized Sharpe (Rf=0%)    0.942 0.460
Worst Drawdown               0.165 0.552
Calmar Ratio                 0.714 0.161
Sortino Ratio (MAR = 0%)     1.347 0.763

               strategy         SPY
2002-12-31 -0.035499564 -0.05656974
2003-12-31  0.253224759  0.28181559
2004-12-31  0.129739794  0.10697941
2005-12-30  0.066215224  0.04828267
2006-12-29  0.167686936  0.15845242
2007-12-31  0.153890329  0.05146218
2008-12-31 -0.096736711 -0.36794994
2009-12-31  0.181759432  0.26351755
2010-12-31  0.099187188  0.15056146
2011-12-30  0.073734427  0.01894986
2012-12-31  0.067679129  0.15990336
2013-12-31  0.321039353  0.32307769
2014-12-31  0.126633020  0.13463790
2015-04-16  0.004972434  0.02806776

And the equity curve:

To me, it looks like a standard rotation strategy. Aims for the highest momentum securities, diversifies to try and control risk, hits a drawdown in the crisis, recovers, and slightly lags the bull run on SPY. Nothing out of the ordinary.

So, for those interested, here you go. I’m surprised that JP Morgan itself markets this sort of thing, considering that they probably employ top-notch quants that can easily come up with products and/or strategies that are far better.

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.

The Logical Invest Enhanced Bond Rotation Strategy (And the Importance of Dividends)

This post will display my implementation of the Logical Invest Enhanced Bond Rotation strategy. This is a strategy that indeed does work, but is dependent on reinvesting dividends, as bonds pay coupons, which means bond ETFs do likewise.

The strategy is fairly simple — using four separate fixed income markets (long-term US government bonds, high-yield bonds, emerging sovereign debt, and convertible bonds), the strategy aims to deliver a low-risk, high Sharpe profile. Every month, it switches to two separate securities, in either a 60-40 or 50-50 split (that is, a 60-40 one way, or the other). My implementation for this strategy is similar to the ones I’ve done for the Logical Invest Universal Investment Strategy, which is to maximize a modified Sharpe ratio in a walk-forward process.

Here’s the code:

LogicInvestEBR <- function(returns, lowerBound, upperBound, period, modSharpeF) {
  count <- 0
  configs <- list()
  instCombos <- combn(colnames(returns), m = 2)
  for(i in 1:ncol(instCombos)) {
    inst1 <- instCombos[1, i]
    inst2 <- instCombos[2, i]
    rets <- returns[,c(inst1, inst2)]
    weightSeq <- seq(lowerBound, upperBound, by = .1)
    for(j in 1:length(weightSeq)) {
      returnConfig <- Return.portfolio(R = rets, 
                      weights = c(weightSeq[j], 1-weightSeq[j]), 
                      rebalance_on="months")
      colnames(returnConfig) <- paste(inst1, weightSeq[j], 
                                inst2, 1-weightSeq[j], sep="_")
      count <- count + 1
      configs[[count]] <- returnConfig
    }
  }
  
  configs <- do.call(cbind, configs)
  cumRets <- cumprod(1+configs)
  
  #rolling cumulative 
  rollAnnRets <- (cumRets/lag(cumRets, period))^(252/period) - 1
  rollingSD <- sapply(X = configs, runSD, n=period)*sqrt(252)
  
  modSharpe <- rollAnnRets/(rollingSD ^ modSharpeF)
  monthlyModSharpe <- modSharpe[endpoints(modSharpe, on="months"),]
  
  findMax <- function(data) {
    return(data==max(data))
  }
  
  #configs$zeroes <- 0 #zeroes for initial periods during calibration
  weights <- t(apply(monthlyModSharpe, 1, findMax))
  weights <- weights*1
  weights <- xts(weights, order.by=as.Date(rownames(weights)))
  weights[is.na(weights)] <- 0
  weights$zeroes <- 1-rowSums(weights)
  configCopy <- configs
  configCopy$zeroes <- 0
  
  stratRets <- Return.portfolio(R = configCopy, weights = weights)
  return(stratRets)  
}

The one thing different about this code is the way I initialize the return streams. It’s an ugly piece of work, but it takes all of the pairwise combinations (that is, 4 choose 2, or 4c2) along with a sequence going by 10% for the different security weights between the lower and upper bound (that is, if the lower bound is 40% and upper bound is 60%, the three weights will be 40-60, 50-50, and 60-40). So, in this case, there are 18 configurations. 4c2*3. Do note that this is not at all a framework that can be scaled up. That is, with 20 instruments, there will be 190 different combinations, and then anywhere between 3 to 11 (if going from 0-100) configurations for each combination. Obviously, not a pretty sight.

Beyond that, it’s the same refrain. Bind the returns together, compute an n-day rolling cumulative return (far faster my way than using the rollApply version of Return.annualized), divide it by the n-day rolling annualized standard deviation divided by the modified Sharpe F factor (1 gives you Sharpe ratio, 0 gives you pure returns, greater than 1 puts more of a focus on risk). Take the highest Sharpe ratio, allocate to that configuration, repeat.

So, how does this perform? Here’s a test script, using the same 73-day lookback with a modified Sharpe F of 2 that I’ve used in the previous Logical Invest strategies.

symbols <- c("TLT", "JNK", "PCY", "CWB", "VUSTX", "PRHYX", "RPIBX", "VCVSX")
suppressMessages(getSymbols(symbols, from="1995-01-01", src="yahoo"))
etfClose <- Return.calculate(cbind(Cl(TLT), Cl(JNK), Cl(PCY), Cl(CWB)))
etfAdj <- Return.calculate(cbind(Ad(TLT), Ad(JNK), Ad(PCY), Ad(CWB)))
mfClose <- Return.calculate(cbind(Cl(VUSTX), Cl(PRHYX), Cl(RPIBX), Cl(VCVSX)))
mfAdj <- Return.calculate(cbind(Ad(VUSTX), Ad(PRHYX), Ad(RPIBX), Ad(VCVSX)))
colnames(etfClose) <- colnames(etfAdj) <- c("TLT", "JNK", "PCY", "CWB")
colnames(mfClose) <- colnames(mfAdj) <- c("VUSTX", "PRHYX", "RPIBX", "VCVSX")

etfClose <- etfClose[!is.na(etfClose[,4]),]
etfAdj <- etfAdj[!is.na(etfAdj[,4]),]
mfClose <- mfClose[-1,]
mfAdj <- mfAdj[-1,]

etfAdjTest <- LogicInvestEBR(returns = etfAdj, lowerBound = .4, upperBound = .6,
                             period = 73, modSharpeF = 2)

etfClTest <- LogicInvestEBR(returns = etfClose, lowerBound = .4, upperBound = .6,
                             period = 73, modSharpeF = 2)

mfAdjTest <- LogicInvestEBR(returns = mfAdj, lowerBound = .4, upperBound = .6,
                            period = 73, modSharpeF = 2)

mfClTest <- LogicInvestEBR(returns = mfClose, lowerBound = .4, upperBound = .6,
                           period = 73, modSharpeF = 2)

fiveStats <- function(returns) {
  return(rbind(table.AnnualizedReturns(returns), 
               maxDrawdown(returns), CalmarRatio(returns)))
}

etfs <- cbind(etfAdjTest, etfClTest)
colnames(etfs) <- c("Adjusted ETFs", "Close ETFs")
charts.PerformanceSummary((etfs))

mutualFunds <- cbind(mfAdjTest, mfClTest)
colnames(mutualFunds) <- c("Adjusted MFs", "Close MFs")
charts.PerformanceSummary(mutualFunds)
chart.TimeSeries(log(cumprod(1+mutualFunds)), legend.loc="topleft")

fiveStats(etfs)
fiveStats(mutualFunds)

So, first, the results of the ETFs:

Equity curve:

Five statistics:

> fiveStats(etfs)
                          Adjusted ETFs Close ETFs
Annualized Return            0.12320000 0.08370000
Annualized Std Dev           0.06780000 0.06920000
Annualized Sharpe (Rf=0%)    1.81690000 1.20980000
Worst Drawdown               0.06913986 0.08038459
Calmar Ratio                 1.78158934 1.04078405

In other words, reinvesting dividends makes up about 50% of these returns.

Let’s look at the mutual funds. Note that these are for the sake of illustration only–you can’t trade out of mutual funds every month.

Equity curve:

Log scale:

Statistics:

                          Adjusted MFs Close MFs
Annualized Return           0.11450000 0.0284000
Annualized Std Dev          0.05700000 0.0627000
Annualized Sharpe (Rf=0%)   2.00900000 0.4532000
Worst Drawdown              0.09855271 0.2130904
Calmar Ratio                1.16217559 0.1332706

In this case, day and night, though how much of it is the data source may also be an issue. Yahoo isn’t the greatest when it comes to data, and I’m not sure how much the data quality deteriorates going back that far. However, the takeaway seems to be this: with bond strategies, dividends will need to be dealt with, and when considering returns data presented to you, keep in mind that those adjusted returns assume the investor stays on top of dividend maintenance. Fail to reinvest the dividends in a timely fashion, and, well, the gap can be quite large.

To put it into perspective, as I was writing this post, I wondered whether or not most of this was indeed due to dividends. Here’s a plot of the difference in returns between adjusted and close ETF returns.

chart.TimeSeries(etfAdj - etfClose, legend.loc="topleft", date.format="%Y-%m",
                 main = "Return differences adjusted vs. close ETFs")

With the resulting image:

While there may be some noise to the order of the negative fifth power on most days, there are clear spikes observable in the return differences. Those are dividends, and their compounding makes a sizable difference. In one case for CWB, the difference is particularly striking (Dec. 29, 2014). In fact, here’s a quick little analysis of the effect of the dividend effects.

dividends <- etfAdj - etfClose
divReturns <- list()
for(i in 1:ncol(dividends)) {
  diffStream <- dividends[,i]
  divPayments <- diffStream[diffStream >= 1e-3]
  divReturns[[i]] <- Return.annualized(divPayments)
}
divReturns <- do.call(cbind, divReturns)
divReturns

divReturns/Return.annualized(etfAdj)

And the result:

> divReturns
                         TLT        JNK        PCY        CWB
Annualized Return 0.03420959 0.08451723 0.05382363 0.05025999

> divReturns/Return.annualized(etfAdj)
                       TLT       JNK       PCY       CWB
Annualized Return 0.453966 0.6939243 0.5405922 0.3737499

In short, the effect of the dividend is massive. In some instances, such as with JNK, the dividend comprises more than 50% of the annualized returns for the security!

Basically, I’d like to hammer the point home one last time–backtests using adjusted data assume instantaneous maintenance of dividends. In order to achieve the optimistic returns seen in the backtests, these dividend payments must be reinvested ASAP. In short, this is the fine print on this strategy, and is a small, but critical detail that the SeekingAlpha article doesn’t mention. (Seriously, do a ctrl + F in your browser for the word “dividend”. It won’t come up in the article itself.) I wanted to make sure to add it.

One last thing: gaudy numbers when using monthly returns!

> fiveStats(apply.monthly(etfs, Return.cumulative))
                          Adjusted ETFs Close ETFs
Annualized Return            0.12150000   0.082500
Annualized Std Dev           0.06490000   0.067000
Annualized Sharpe (Rf=0%)    1.87170000   1.232100
Worst Drawdown               0.03671871   0.049627
Calmar Ratio                 3.30769620   1.662642

Look! A Calmar Ratio of 3.3, and a Sharpe near 2!*

*: Must manage dividends. Statistics reported are monthly.

Okay, in all fairness, this is a pretty solid strategy, once one commits to managing the dividends. I just felt that it should have been a topic made front and center considering its importance in this case, rather than simply swept under the “we use adjusted returns” rug, since in this instance, the effect of dividends is massive.

In conclusion, while I will more or less confirm the strategy’s actual risk/reward performance (unlike some other SeekingAlpha strategies I’ve backtested), which, in all honesty, I find really impressive, it comes with a caveat like the rest of them. However, the caveat of “be detail-oriented/meticulous/paranoid and reinvest those dividends!” in my opinion is a caveat that’s a lot easier to live with than 30%+ drawdowns that were found lurking in other SeekingAlpha strategies. So for those that can stay on top of those dividends (whether manually, or with machine execution), here you go. I’m basically confirming the performance of Logical Invest’s strategy, but just belaboring one important detail.

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.

The Logical Invest “Hell On Fire” Replication Attempt

This post is about my replication attempt of Logical Invest’s “Hell On Fire” strategy — which is its Universal Investment Strategy using SPXL and TMF (aka the 3x leveraged ETFs). I don’t match their results, but I do come close.

It seems that some people at Logical Invest have caught whiff of some of the work I did in replicating Harry Long’s ideas. First off, for the record, I’ve actually done some work with Harry Long in private, and the strategies we’ve worked on together are definitely better than the strategies he has shared for free, so if you are an institution hoping to vet his track record, I wouldn’t judge it by the very much incomplete frameworks he posts for free.

This post’s strategy is the Logical Invest Universal Investment Strategy leveraged up three times over. Here’s the link to their newest post. Also, I’m happy to see that they think positively of my work.

In any case, my results are worse than those on Logical Invest’s, so if anyone sees a reason for the discrepancy, please let me know.

Here’s the code for the backtest–most of it is old, from my first time analyzing Logical Invest’s strategy.

LogicalInvestUIS <- function(returns, period = 63, modSharpeF = 2.8) {
  returns[is.na(returns)] <- 0 #impute any NAs to zero
  configs <- list()
  for(i in 1:11) {
    weightFirst <- (i-1)*.1
    weightSecond <- 1-weightFirst
    config <- Return.portfolio(R = returns, weights=c(weightFirst, weightSecond), rebalance_on = "months")
    configs[[i]] <- config
  }
  configs <- do.call(cbind, configs)
  cumRets <- cumprod(1+configs)
  
  #rolling cumulative 
  rollAnnRets <- (cumRets/lag(cumRets, period))^(252/period) - 1
  rollingSD <- sapply(X = configs, runSD, n=period)*sqrt(252)
  
  modSharpe <- rollAnnRets/(rollingSD ^ modSharpeF)
  monthlyModSharpe <- modSharpe[endpoints(modSharpe, on="months"),]
  
  findMax <- function(data) {
    return(data==max(data))
  }
  
  #configs$zeroes <- 0 #zeroes for initial periods during calibration
  weights <- t(apply(monthlyModSharpe, 1, findMax))
  weights <- weights*1
  weights <- xts(weights, order.by=as.Date(rownames(weights)))
  weights[is.na(weights)] <- 0
  weights$zeroes <- 1-rowSums(weights)
  configCopy <- configs
  configCopy$zeroes <- 0
  
  stratRets <- Return.portfolio(R = configCopy, weights = weights)
  
  weightFirst <- apply(monthlyModSharpe, 1, which.max)
  weightFirst <- do.call(rbind, weightFirst)
  weightFirst <- (weightFirst-1)*.1
  align <- cbind(weightFirst, stratRets)
  align <- na.locf(align)
  chart.TimeSeries(align[,1], date.format="%Y", ylab=paste("Weight", colnames(returns)[1]), 
                                                           main=paste("Weight", colnames(returns)[1]))
  
  return(stratRets)
}

In this case, rather than steps of 5% weights, I used 10% weights after looking at the Logical Invest charts more closely.

Now, let’s look at the instruments.

getSymbols("SPY", from="1990-01-01")

getSymbols("TMF", from="1990-01-01")
TMFrets <- Return.calculate(Ad(TMF))
getSymbols("TLT", from="1990-01-01")
TLTrets <- Return.calculate(Ad(TLT))
tmf3TLT <- merge(TMFrets, 3*TLTrets, join='inner')
charts.PerformanceSummary(tmf3TLT)
Return.annualized(tmf3TLT[,2]-tmf3TLT[,1])
discrepancy <- as.numeric(Return.annualized(tmf3TLT[,2]-tmf3TLT[,1]))
tmf3TLT[,2] <- tmf3TLT[,2] - ((1+discrepancy)^(1/252)-1)
modifiedTLT <- 3*TLTrets - ((1+discrepancy)^(1/252)-1)

rets <- merge(3*Return.calculate(Ad(SPY)), modifiedTLT, join='inner')
colnames(rets) <- gsub("\\.[A-z]*", "", colnames(rets))

leveragedReturns <- rets
colnames(leveragedReturns) <- paste("Leveraged", colnames(leveragedReturns), sep="_")
leveragedReturns <- leveragedReturns[-1,]

Again, more of the same that I did from my work analyzing Harry Long’s strategies to get a longer backtest of SPXL and TMF (aka leveraged SPY and TLT).

Now, let’s look at some configurations.


hof <- LogicalInvestUIS(returns = leveragedReturns, period = 63, modSharpeF = 2.8)
hof2 <- LogicalInvestUIS(returns = leveragedReturns, period = 73, modSharpeF = 3)
hof3 <- LogicalInvestUIS(returns = leveragedReturns, period = 84, modSharpeF = 4)
hof4 <- LogicalInvestUIS(returns = leveragedReturns, period = 42, modSharpeF = 1.5)
hof5 <- LogicalInvestUIS(returns = leveragedReturns, period = 63, modSharpeF = 6)
hof6 <- LogicalInvestUIS(returns = leveragedReturns, period = 73, modSharpeF = 2)

hofComparisons <- cbind(hof, hof2, hof3, hof4, hof5, hof6)
colnames(hofComparisons) <- c("d63_F2.8", "d73_F3", "d84_F4", "d42_F1.5", "d63_F6", "d73_F2")
rbind(table.AnnualizedReturns(hofComparisons), maxDrawdown(hofComparisons), CalmarRatio(hofComparisons))

With the following statistics:

> rbind(table.AnnualizedReturns(hofComparisons), maxDrawdown(hofComparisons), CalmarRatio(hofComparisons))
                           d63_F2.8    d73_F3    d84_F4  d42_F1.5    d63_F6    d73_F2
Annualized Return         0.3777000 0.3684000 0.2854000 0.1849000 0.3718000 0.3830000
Annualized Std Dev        0.3406000 0.3103000 0.3010000 0.4032000 0.3155000 0.3383000
Annualized Sharpe (Rf=0%) 1.1091000 1.1872000 0.9483000 0.4585000 1.1785000 1.1323000
Worst Drawdown            0.5619769 0.4675397 0.4882101 0.7274609 0.5757738 0.4529908
Calmar Ratio              0.6721751 0.7879956 0.5845827 0.2541127 0.6457823 0.8455274

It seems that the original 73 day lookback, sharpe F of 2 had the best performance.

Here are the equity curves (log scale because leveraged or volatility strategies look silly at regular scale):

chart.TimeSeries(log(cumprod(1+hofComparisons)), legend.loc="topleft", date.format="%Y",
                 main="Hell On Fire Comparisons", ylab="Value of $1", yaxis = FALSE)
axis(side=2, at=c(0, 1, 2, 3, 4), label=paste0("$", round(exp(c(0, 1, 2, 3, 4)))), las = 1)

In short, sort of upwards from 2002 to the crisis, where all the strategies take a dip, and then continue steadily upwards.

Here are the drawdowns:

dds <- PerformanceAnalytics:::Drawdowns(hofComparisons)
chart.TimeSeries(dds, legend.loc="bottomright", date.format="%Y", main="Drawdowns Hell On Fire Variants", 
                 yaxis=FALSE, ylab="Drawdown", auto.grid=FALSE)
axis(side=2, at=seq(from=0, to=-.7, by = -.1), label=paste0(seq(from=0, to=-.7, by = -.1)*100, "%"), las = 1)

Basically, some regular bumps along the road given the CAGRs (that is, if you’re going to leverage something that has an 8% drawdown on the occasion three times over, it’s going to have a 24% drawdown on those same occasions, if not more), and the massive hit in the crisis when bonds take a hit, and on we go.

In short, this strategy is basically the same as the original strategy, just leveraged up, so for those with the stomach for it, there you go. Of course, Logical Invest is leaving off some details, since I’m not getting a perfect replica. Namely, their returns seem slightly higher, and their drawdowns slightly lower. I suppose that’s par for the course when selling subscriptions and newsletters.

One last thing, which I think people should be aware of–when people report statistics on their strategies, make sure to ask the question as to which frequency. Because here’s a quick little modification, going from daily returns to monthly returns:

> betterStatistics <- apply.monthly(hofComparisons, Return.cumulative)
> rbind(table.AnnualizedReturns(betterStatistics), maxDrawdown(betterStatistics), CalmarRatio(betterStatistics))
                           d63_F2.8    d73_F3    d84_F4  d42_F1.5    d63_F6   d73_F2
Annualized Return         0.3719000 0.3627000 0.2811000 0.1822000 0.3661000 0.377100
Annualized Std Dev        0.3461000 0.3014000 0.2914000 0.3566000 0.3159000 0.336700
Annualized Sharpe (Rf=0%) 1.0746000 1.2036000 0.9646000 0.5109000 1.1589000 1.119900
Worst Drawdown            0.4323102 0.3297927 0.4100792 0.6377512 0.4636949 0.311480
Calmar Ratio              0.8602366 1.0998551 0.6855148 0.2856723 0.7894636 1.210563

While the Sharpe ratios don’t improve too much, the Calmars (aka the return to drawdown) statistics increase dramatically. EG, imagine a month in which there’s a 40% drawdown, but it ends at a new equity high. A monthly return series will sweep that under the rug, or, for my fellow Jewish readers, pass over it. So, be wary.

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.

Rolling Sharpe Ratios

Similar to my rolling cumulative returns from last post, in this post, I will present a way to compute and plot rolling Sharpe ratios. Also, I edited the code to compute rolling returns to be more general with an option to annualize the returns, which is necessary for computing Sharpe ratios.

In any case, let’s look at some more code. First off, the new running cumulative returns:

"runCumRets" <- function(R, n = 252, annualized = FALSE, scale = NA) {
  R <- na.omit(R)
  if (is.na(scale)) {
    freq = periodicity(R)
    switch(freq$scale, minute = {
      stop("Data periodicity too high")
    }, hourly = {
      stop("Data periodicity too high")
    }, daily = {
      scale = 252
    }, weekly = {
      scale = 52
    }, monthly = {
      scale = 12
    }, quarterly = {
      scale = 4
    }, yearly = {
      scale = 1
    })
  }
  cumRets <- cumprod(1+R)
  if(annualized) {
    rollingCumRets <- (cumRets/lag(cumRets, k = n))^(scale/n) - 1 
  } else {
    rollingCumRets <- cumRets/lag(cumRets, k = n) - 1
  }
  return(rollingCumRets)
}

Essentially, a more general variant, with an option to annualize returns over longer (or shorter) periods of time. This is necessary for the following running Sharpe ratio code:

"runSharpe" <- function(R, n = 252, scale = NA, volFactor = 1) {
  if (is.na(scale)) {
    freq = periodicity(R)
    switch(freq$scale, minute = {
      stop("Data periodicity too high")
    }, hourly = {
      stop("Data periodicity too high")
    }, daily = {
      scale = 252
    }, weekly = {
      scale = 52
    }, monthly = {
      scale = 12
    }, quarterly = {
      scale = 4
    }, yearly = {
      scale = 1
    })
  }
  rollingAnnRets <- runCumRets(R, n = n, annualized = TRUE)
  rollingAnnSD <- sapply(R, runSD, n = n)*sqrt(scale)
  rollingSharpe <- rollingAnnRets/rollingAnnSD ^ volFactor
  return(rollingSharpe)
}

The one little innovation I added is the vol factor parameter, allowing users to place more or less emphasis on the volatility. While changing it from 1 will make the calculation different from the standard Sharpe ratio, I added this functionality due to the Logical Invest strategy I did in the past, and thought that I might as well have this function run double duty.

And of course, this comes with a plotting function.

"plotRunSharpe" <- function(R, n = 252, ...) {
  sharpes <- runSharpe(R = R, n = n)
  sharpes <- sharpes[!is.na(sharpes[,1]),]
  chart.TimeSeries(sharpes, legend.loc="topleft", main=paste("Rolling", n, "period Sharpe Ratio"),
                   date.format="%Y", yaxis=FALSE, ylab="Sharpe Ratio", auto.grid=FALSE, ...)
  meltedSharpes <- do.call(c, data.frame(sharpes))
  axisLabels <- pretty(meltedSharpes, n = 10)
  axisLabels <- unique(round(axisLabels, 1))
  axisLabels <- axisLabels[axisLabels > min(axisLabels) & axisLabels < max(axisLabels)]
  axis(side=2, at=axisLabels, label=axisLabels, las=1)
}

So what does this look like, in the case of a 252-day FAA vs. SPY test?

Like this:

par(mfrow = c (2,1))
plotRunSharpe(comparison, n=252)
plotRunSharpe(comparison, n=756)

Essentially, similar to what we saw last time–only having poor performance at the height of the crisis and for a much smaller amount of time than SPY, and always possessing a three-year solid performance. One thing to note about the Sharpe ratio is that the interpretation in the presence of negative returns doesn’t make too much sense. That is, when returns are negative, having a small variance actually works against the Sharpe ratio, so a strategy that may have lost only 10% while SPY lost 50% might look every bit as bad on the Sharpe Ratio plots due to the nature of a small standard deviation punishing smaller negative returns as much as it benefits smaller positive returns.

In conclusion, this is a fast way of computing and plotting a running Sharpe ratio, and this function doubles up as a utility for use with strategies such as the Universal Investment Strategy from Logical Invest.

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.

Introduction to my New IKReporting Package

This post will introduce my up and coming IKReporting package, and functions that compute and plot rolling returns, which are useful to compare recent performance, since simply looking at two complete equity curves may induce sample bias (EG SPY in 2008), which may not reflect the state of the markets going forward.

In any case, the motivation for this package was brought about by one of my readers, who has reminded me in the past of the demand for the in-the-ditches work of pretty performance reports. This package aims to make creating such thing as painless as possible, and I will be updating it rapidly in the near future.

The strategy in use for this post will be Flexible Asset Allocation from my IKTrading package, in order to celebrate the R/Finance lightning talk I’m approved for on FAA, and it’ll be compared to SPY.

Here’s the code:

require(IKTrading)
require(quantmod)
require(PerformanceAnalytics)

options("getSymbols.warning4.0"=FALSE)

symbols <- c("XLB", #SPDR Materials sector
             "XLE", #SPDR Energy sector
             "XLF", #SPDR Financial sector
             "XLP", #SPDR Consumer staples sector
             "XLI", #SPDR Industrial sector
             "XLU", #SPDR Utilities sector
             "XLV", #SPDR Healthcare sector
             "XLK", #SPDR Tech sector
             "XLY", #SPDR Consumer discretionary sector
             "RWR", #SPDR Dow Jones REIT ETF

             "EWJ", #iShares Japan
             "EWG", #iShares Germany
             "EWU", #iShares UK
             "EWC", #iShares Canada
             "EWY", #iShares South Korea
             "EWA", #iShares Australia
             "EWH", #iShares Hong Kong
             "EWS", #iShares Singapore
             "IYZ", #iShares U.S. Telecom
             "EZU", #iShares MSCI EMU ETF
             "IYR", #iShares U.S. Real Estate
             "EWT", #iShares Taiwan
             "EWZ", #iShares Brazil
             "EFA", #iShares EAFE
             "IGE", #iShares North American Natural Resources
             "EPP", #iShares Pacific Ex Japan
             "LQD", #iShares Investment Grade Corporate Bonds
             "SHY", #iShares 1-3 year TBonds
             "IEF", #iShares 3-7 year TBonds
             "TLT" #iShares 20+ year Bonds
)

from="2003-01-01"

#SPDR ETFs first, iShares ETFs afterwards
if(!"XLB" %in% ls()) {
  suppressMessages(getSymbols(symbols, from="2003-01-01", src="yahoo", adjust=TRUE))
}

prices <- list()
for(i in 1:length(symbols)) {
  prices[[i]] <- Cl(get(symbols[i]))
}
prices <- do.call(cbind, prices)
colnames(prices) <- gsub("\\.[A-z]*", "", colnames(prices))

faa <- FAA(prices = prices, riskFreeName = "SHY", bestN = 6, stepCorRank = TRUE)

getSymbols("SPY", from="1990-01-01")

comparison <- merge(faa, Return.calculate(Cl(SPY)), join='inner')
colnames(comparison) <- c("FAA", "SPY")

And now here’s where the new code comes in:

This is a simple function for computing running cumulative returns of a fixed window. It’s a quick three-liner function that can compute the cumulative returns over any fixed period near-instantaneously.

"runCumRets" <- function(R, n = 252) {
  cumRets <- cumprod(1+R)
  rollingCumRets <- cumRets/lag(cumRets, k = n) - 1
  return(rollingCumRets)
}

So how does this get interesting? Well, with some plotting, of course.

Here’s a function to create a plot of these rolling returns.

"plotCumRets" <- function(R, n = 252, ...) {
  cumRets <- runCumRets(R = R, n = n)
  cumRets <- cumRets[!is.na(cumRets[,1]),]
  chart.TimeSeries(cumRets, legend.loc="topleft", main=paste(n, "day rolling cumulative return"),
                   date.format="%Y", yaxis=FALSE, ylab="Return", auto.grid=FALSE)
  
  meltedCumRets <- do.call(c, data.frame(cumRets))
  axisLabels <- pretty(meltedCumRets, n = 10)
  axisLabels <- round(axisLabels, 1)
  axisLabels <- axisLabels[axisLabels > min(axisLabels) & axisLabels < max(axisLabels)]
  axis(side=2, at=axisLabels, label=paste(axisLabels*100, "%"), las=1)
}

While the computation is done in the first line, the rest of the code is simply to make a prettier plot.

Here’s what the 252-day rolling return comparison looks like.

require(IKReporting)
plotCumRets(comparison)

So here’s the interpretation: assuming that there isn’t too much return degradation in the implementation of the FAA strategy, it essentially delivers most of the upside of SPY while doing a much better job protecting the investor when things hit the fan. Recently, however, seeing as to how the stock market has been on a roar, there’s a slight bit of underperformance over the past several years.

However, let’s look at a longer time horizon — the cumulative return over 756 days.

plotCumRets(comparison, n = 756)

With the following result:

This offers a much clearer picture–essentially, what this states is that over any 756-day period, the strategy has not lost money, ever, unlike SPY, which would have wiped out three years of gains (and then some) at the height of the crisis. More recently, as the stock market is in yet another run-up, there has been some short-term (well, if 756 days can be called short-term) underperformance, namely due to SPY having some historical upward mobility.

On another unrelated topic, some of you (perhaps from Seeking Alpha) may have seen the following image floating around:

This is a strategy I have collaborated with Harry Long from Seeking Alpha on. While I’m under NDA and am not allowed to discuss the exact rules of this particular strategy, I can act as a liaison for those that wish to become a client of ZOMMA, LLC. While the price point is out of the reach of ordinary retail investors (the price point is into the six figures), institutions that are considering licensing one of these indices can begin by sending me an email at ilya.kipnis@gmail.com. I can also set up a phone call.

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.