Archive
Running Back-tests in parallel
Once you start experimenting with many different asset allocation algorithms, the computation time of running the back-tests can be substantial. One simple way to solve the computation time problem is to run the back-tests in parallel. I.e. if the asset allocation algorithm does not use the prior period holdings to make decision about current allocation, we can run many periods in parallel.
In the Update for Backtesting Asset Allocation Portfolios post, I show cased the portfolio.allocation.helper() function in strategy.r at github. The portfolio.allocation.helper() function is a user-friendly interface to evaluate multiple asset allocation algorithms over given asset universe in a sequential fashion.
Following is a sample code from the Update for Backtesting Asset Allocation Portfolios post:
#***************************************************************** # Code Strategies #****************************************************************** obj = portfolio.allocation.helper(data$prices, periodicity = 'months', lookback.len = 60, min.risk.fns = list( EW=equal.weight.portfolio, RP=risk.parity.portfolio, MD=max.div.portfolio, MV=min.var.portfolio, MVE=min.var.excel.portfolio, MV2=min.var2.portfolio, MC=min.corr.portfolio, MCE=min.corr.excel.portfolio, MC2=min.corr2.portfolio, MS=max.sharpe.portfolio(), ERC = equal.risk.contribution.portfolio, # target retunr / risk TRET.12 = target.return.portfolio(12/100), TRISK.10 = target.risk.portfolio(10/100), # rso RSO.RP.5 = rso.portfolio(risk.parity.portfolio, 5, 500), # others MMaxLoss = min.maxloss.portfolio, MMad = min.mad.portfolio, MCVaR = min.cvar.portfolio, MCDaR = min.cdar.portfolio, MMadDown = min.mad.downside.portfolio, MRiskDown = min.risk.downside.portfolio, MCorCov = min.cor.insteadof.cov.portfolio ) )
To run the same strategies in parallel, I created the portfolio.allocation.helper.parallel() function in strategy.r at github. There is one extra input that you need to specify: cores – number of CPU processors used for computations.
For example, the code below will use 2 CPU processors to run back-test computations. It will run faster than the portfolio.allocation.helper() function.
#***************************************************************** # Code Strategies #****************************************************************** obj = portfolio.allocation.helper.parallel(cores = 2, data$prices, periodicity = 'months', lookback.len = 60, min.risk.fns = list( EW=equal.weight.portfolio, RP=risk.parity.portfolio, MD=max.div.portfolio, MV=min.var.portfolio, MVE=min.var.excel.portfolio, MV2=min.var2.portfolio, MC=min.corr.portfolio, MCE=min.corr.excel.portfolio, MC2=min.corr2.portfolio, MS=max.sharpe.portfolio(), ERC = equal.risk.contribution.portfolio, # target retunr / risk TRET.12 = target.return.portfolio(12/100), TRISK.10 = target.risk.portfolio(10/100), # rso RSO.RP.5 = rso.portfolio(risk.parity.portfolio, 5, 500), # others MMaxLoss = min.maxloss.portfolio, MMad = min.mad.portfolio, MCVaR = min.cvar.portfolio, MCDaR = min.cdar.portfolio, MMadDown = min.mad.downside.portfolio, MRiskDown = min.risk.downside.portfolio, MCorCov = min.cor.insteadof.cov.portfolio ) )
Hopefully, I did not ruin your prolong lunch plans 🙂
Commissions
Today, I want to explain the commission’s functionality build in to Systematic Investor Toolbox(SIT) “share” back-test.
At each re-balance time the capital is allocated given the weight such that
share = weight * capital / price cash = capital - share * price
For example, if weight is 100% (i.e. fully invested) and capital = $100 and price = $10 then
share = 10 shares cash = $0
The period return is equal to
return = [share * price + cash] / [share * price.yesterday + cash] - 1
The total return is equal to
total.return = [1 + return.0] * [1 + return.1] * ... * [1 + return.n] - 1
The period returns constructed this way let me construct portfolio returns without using loops
I.e. if share, price, cash are matrices, then
portfolio.return = rowSums((share * price + cash) / (share * mlag(price) + cash) - 1)
and
equity = cumprod(1 + portfolio.return)
To introduce commissions into above framework, I had to make following assumptions.
Let’ assume that P0 and P1 are stock prices and com is commission that is very small relative to stock price then
[P0 - com] / P0 is close (equal) to P0 / [P0 + com] and [P0 - com] * P1 is close (equal) to P0 * [P1 - com]
Given commissions, the period return formula used in SIT is equal to
return = [share * price + cash - commission] / [share * price.yesterday + cash] - 1
Now let’s look at the example of trade with commissions:
Let's say we are fully invested (i.e. cash = 0 and capital = share * P0) opening trade cost = share * P0 + com closing trade cost = share * P1 - com return = [closing trade cost] / [opening trade cost] - 1 = [share * P1 - com] / [share * P0 + com] - 1
In SIT, these computations are equivalent to
([capital - com] / [capital]) * ([share * P1 + cash - com] / [share * P0 + cash]) - 1 < given that cash = 0 and capital = share * P0 > = ([share * P0 - com] / [share * P0]) * ([share * P1 - com] / [share * P0]) - 1 < given [P0 - com] / P0 ~ P0 / [P0 + com] > = ([share * P0] / [share * P0 + com] / ) * ([share * P1 - com] / [share * P0]) - 1 = [share * P1 - com] / [share * P0 + com] - 1
Hence, as long as commissions are small relative to whole trade the returns produced with SIT will be very close to the true returns.
SIT currently supports following 3 types of commissions
- cents / share commission
commission = abs(share - mlag(share)) * commission$cps
- fixed commission
commission = sign(abs(share - mlag(share))) * commission$fixed
- percentage commission
commission = price * abs(share - mlag(share)) * commission$percentage
You can mix and match these commission methods in any way,
the total.commission = cps.commission + fixed.commission + percentage.commission
and period return is equal to
return = (share * price + cash - total.commission) / (share * mlag(price) + cash) - 1
Next let’s see the impact of different type of commissions. There two ways to specify the commissions.
-
commission = 0.1
will be translated in
commission = list(cps = 0.1, fixed = 0.0, percentage = 0.0)
-
commission = list(cps = 0.0, fixed = 0.0, percentage = 0.0)
############################################################################### # Load Systematic Investor Toolbox (SIT) # https://systematicinvestor.wordpress.com/systematic-investor-toolbox/ ############################################################################### setInternet2(TRUE) con = gzcon(url('http://www.systematicportfolio.com/sit.gz', 'rb')) source(con) close(con) #***************************************************************** # Load historical data #****************************************************************** load.packages('quantmod') tickers = spl('EEM') data <- new.env() getSymbols(tickers, src = 'yahoo', from = '1970-01-01', env = data, auto.assign = T) for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T) bt.prep(data, align='keep.all', dates='2013:08::2013:09') #***************************************************************** # Code Strategies #****************************************************************** buy.date = '2013:08:14' sell.date = '2013:08:15' day.after.sell.date = '2013:08:16' capital = 100000 prices = data$prices share = as.double(capital / prices[buy.date]) # helper function to compute trade return comp.ret <- function(sell.trade.cost, buy.trade.cost) { round(100 * (as.double(sell.trade.cost) / as.double(buy.trade.cost) - 1), 2) } #***************************************************************** # Zero commission #****************************************************************** data$weight[] = NA data$weight[buy.date] = 1 data$weight[sell.date] = 0 commission = 0.0 model = bt.run.share(data, commission = commission, capital = capital, silent = T) comp.ret( share * prices[sell.date], share * prices[buy.date] ) comp.ret( model$equity[day.after.sell.date], model$equity[buy.date] ) #***************************************************************** # 10c cps commission # cents / share commission # trade cost = abs(share - mlag(share)) * commission$cps #****************************************************************** data$weight[] = NA data$weight[buy.date] = 1 data$weight[sell.date] = 0 commission = 0.1 model = bt.run.share(data, commission = commission, capital = capital, silent = T) comp.ret( share * (prices[sell.date] - commission), share * (prices[buy.date] + commission) ) comp.ret( model$equity[day.after.sell.date], model$equity[buy.date] ) #***************************************************************** # $5 fixed commission # fixed commission per trade to more effectively to penalize for turnover # trade cost = sign(abs(share - mlag(share))) * commission$fixed #****************************************************************** data$weight[] = NA data$weight[buy.date] = 1 data$weight[sell.date] = 0 commission = list(cps = 0.0, fixed = 5.0, percentage = 0.0) model = bt.run.share(data, commission = commission, capital = capital, silent = T) comp.ret( share * prices[sell.date] - commission$fixed, share * prices[buy.date] + commission$fixed ) comp.ret( model$equity[day.after.sell.date], model$equity[buy.date] ) #***************************************************************** # % commission # percentage commission # trade cost = price * abs(share - mlag(share)) * commission$percentage #****************************************************************** data$weight[] = NA data$weight[buy.date] = 1 data$weight[sell.date] = 0 commission = list(cps = 0.0, fixed = 0.0, percentage = 1/100) model = bt.run.share(data, commission = commission, capital = capital, silent = T) comp.ret( share * prices[sell.date] * (1 - commission$percentage), share * prices[buy.date] * (1 + commission$percentage) ) comp.ret( model$equity[day.after.sell.date], model$equity[buy.date] )
To view the complete source code for this example, please have a look at the bt.commission.test() function in bt.test.r at github.
Updates for Proportional Minimum Variance and Adaptive Shrinkage methods
I create supporting pages for two projects I have collaborated with David Varadi in 2013:
Please check the links to get more info, including supporting blog posts, back-tests, R code to reproduce the back-tests, and more to come in the near future.
I and David appreciate your feedback and comments.
Update for Backtesting Asset Allocation Portfolios post
It was over a year since my original post, Backtesting Asset Allocation portfolios. I have expanded the functionality of the Systematic Investor Toolbox both in terms of optimization functions and helper back-test functions during this period.
Today, I want to update the Backtesting Asset Allocation portfolios post and showcase new functionality. I will use the following global asset universe as: SPY,QQQ,EEM,IWM,EFA,TLT,IYR,GLD to form portfolios every month using different asset allocation methods.
############################################################################### # Load Systematic Investor Toolbox (SIT) # https://systematicinvestor.wordpress.com/systematic-investor-toolbox/ ############################################################################### setInternet2(TRUE) con = gzcon(url('http://www.systematicportfolio.com/sit.gz', 'rb')) source(con) close(con) #***************************************************************** # Load historical data #****************************************************************** load.packages('quantmod,quadprog,corpcor,lpSolve') tickers = spl('SPY,QQQ,EEM,IWM,EFA,TLT,IYR,GLD') data <- new.env() getSymbols(tickers, src = 'yahoo', from = '1980-01-01', env = data, auto.assign = T) for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T) bt.prep(data, align='remove.na', dates='1990::') #***************************************************************** # Code Strategies #****************************************************************** cluster.group = cluster.group.kmeans.90 obj = portfolio.allocation.helper(data$prices, periodicity = 'months', lookback.len = 60, min.risk.fns = list( EW=equal.weight.portfolio, RP=risk.parity.portfolio(), MD=max.div.portfolio, MV=min.var.portfolio, MVE=min.var.excel.portfolio, MV2=min.var2.portfolio, MC=min.corr.portfolio, MCE=min.corr.excel.portfolio, MC2=min.corr2.portfolio, MS=max.sharpe.portfolio(), ERC = equal.risk.contribution.portfolio, # target retunr / risk TRET.12 = target.return.portfolio(12/100), TRISK.10 = target.risk.portfolio(10/100), # cluster C.EW = distribute.weights(equal.weight.portfolio, cluster.group), C.RP = distribute.weights(risk.parity.portfolio, cluster.group), # rso RSO.RP.5 = rso.portfolio(risk.parity.portfolio, 5, 500), # others MMaxLoss = min.maxloss.portfolio, MMad = min.mad.portfolio, MCVaR = min.cvar.portfolio, MCDaR = min.cdar.portfolio, MMadDown = min.mad.downside.portfolio, MRiskDown = min.risk.downside.portfolio, MCorCov = min.cor.insteadof.cov.portfolio ) ) models = create.strategies(obj, data)$models #***************************************************************** # Create Report #****************************************************************** strategy.performance.snapshoot(models, T, 'Backtesting Asset Allocation portfolios')
I hope you will enjoy creating your own portfolio allocation methods or playing with a large variety of portfolio allocation techniques that are readily available for your experimentation.
To view the complete source code for this example, please have a look at the bt.aa.test.new() function in bt.test.r at github.
Modeling Couch Potato strategy
I first read about the Couch Potato strategy in the MoneySense magazine. I liked this simple strategy because it was easy to understand and easy to manage. The Couch Potato strategy is similar to the Permanent Portfolio strategy that I have analyzed previously.
The Couch Potato strategy invests money in the given proportions among different types of assets to ensure diversification and rebalances the holdings once a year. For example the Classic Couch Potato strategy is:
- 1) Canadian equity (33.3%)
- 2) U.S. equity (33.3%)
- 3) Canadian bond (33.3%)
I highly recommend reading following online resources to get more information about the Couch Potato strategy:
- MoneySense
- Canadian Couch Potato
- AssetBuilder
Today, I want to show how you can model and monitor the Couch Potato strategy with the Systematic Investor Toolbox.
############################################################################### # Load Systematic Investor Toolbox (SIT) # https://systematicinvestor.wordpress.com/systematic-investor-toolbox/ ############################################################################### setInternet2(TRUE) con = gzcon(url('http://www.systematicportfolio.com/sit.gz', 'rb')) source(con) close(con) # helper function to model Couch Potato strategy - a fixed allocation strategy couch.potato.strategy <- function ( data.all, tickers = 'XIC.TO,XSP.TO,XBB.TO', weights = c( 1/3, 1/3, 1/3 ), periodicity = 'years', dates = '1900::', commission = 0.1 ) { #***************************************************************** # Load historical data #****************************************************************** tickers = spl(tickers) names(weights) = tickers data <- new.env() for(s in tickers) data[[ s ]] = data.all[[ s ]] bt.prep(data, align='remove.na', dates=dates) #***************************************************************** # Code Strategies #****************************************************************** prices = data$prices n = ncol(prices) nperiods = nrow(prices) # find period ends period.ends = endpoints(data$prices, periodicity) period.ends = c(1, period.ends[period.ends > 0]) #***************************************************************** # Code Strategies #****************************************************************** data$weight[] = NA for(s in tickers) data$weight[period.ends, s] = weights[s] model = bt.run.share(data, clean.signal=F, commission=commission) return(model) }
The couch.potato.strategy() function creates a periodically rebalanced portfolio for given static allocation.
Next, let’s back-test some Canadian Couch Potato portfolios:
#***************************************************************** # Load historical data #****************************************************************** load.packages('quantmod') map = list() map$can.eq = 'XIC.TO' map$can.div = 'XDV.TO' map$us.eq = 'XSP.TO' map$us.div = 'DVY' map$int.eq = 'XIN.TO' map$can.bond = 'XBB.TO' map$can.real.bond = 'XRB.TO' map$can.re = 'XRE.TO' map$can.it = 'XTR.TO' map$can.gold = 'XGD.TO' data <- new.env() for(s in names(map)) { data[[ s ]] = getSymbols(map[[ s ]], src = 'yahoo', from = '1995-01-01', env = data, auto.assign = F) data[[ s ]] = adjustOHLC(data[[ s ]], use.Adjusted=T) } #***************************************************************** # Code Strategies #****************************************************************** models = list() periodicity = 'years' dates = '2006::' models$classic = couch.potato.strategy(data, 'can.eq,us.eq,can.bond', rep(1/3,3), periodicity, dates) models$global = couch.potato.strategy(data, 'can.eq,us.eq,int.eq,can.bond', c(0.2, 0.2, 0.2, 0.4), periodicity, dates) models$yield = couch.potato.strategy(data, 'can.div,can.it,us.div,can.bond', c(0.25, 0.25, 0.25, 0.25), periodicity, dates) models$growth = couch.potato.strategy(data, 'can.eq,us.eq,int.eq,can.bond', c(0.25, 0.25, 0.25, 0.25), periodicity, dates) models$complete = couch.potato.strategy(data, 'can.eq,us.eq,int.eq,can.re,can.real.bond,can.bond', c(0.2, 0.15, 0.15, 0.1, 0.1, 0.3), periodicity, dates) models$permanent = couch.potato.strategy(data, 'can.eq,can.gold,can.bond', c(0.25,0.25,0.5), periodicity, dates) #***************************************************************** # Create Report #****************************************************************** plotbt.custom.report.part1(models)
I have included a few classic Couch Potato portfolios and the Canadian version of the Permanent portfolio. The equity curves speak for themselves: you can call them by the fancy names, but in the end all variations of the Couch Potato portfolios performed similar and suffered a huge draw-down during 2008. The Permanent portfolio did a little better during 2008 bear market.
Next, let’s back-test some US Couch Potato portfolios:
#***************************************************************** # Load historical data #****************************************************************** tickers = spl('VIPSX,VTSMX,VGTSX,SPY,TLT,GLD,SHY') data <- new.env() getSymbols(tickers, src = 'yahoo', from = '1995-01-01', env = data, auto.assign = T) for(i in ls(data)) data[[i]] = adjustOHLC(data[[i]], use.Adjusted=T) # extend GLD with Gold.PM - London Gold afternoon fixing prices data$GLD = extend.GLD(data$GLD) #***************************************************************** # Code Strategies #****************************************************************** models = list() periodicity = 'years' dates = '2003::' models$classic = couch.potato.strategy(data, 'VIPSX,VTSMX', rep(1/2,2), periodicity, dates) models$margarita = couch.potato.strategy(data, 'VIPSX,VTSMX,VGTSX', rep(1/3,3), periodicity, dates) models$permanent = couch.potato.strategy(data, 'SPY,TLT,GLD,SHY', rep(1/4,4), periodicity, dates) #***************************************************************** # Create Report #****************************************************************** plotbt.custom.report.part1(models)
The US Couch Potato portfolios also suffered huge draw-downs during 2008. The Permanent portfolio hold it ground much better.
It has been written quite a lot about Couch Potato strategy, but looking at different variations I cannot really see much difference in terms of perfromance or draw-downs. Probably that is why in the last few years, I have seen the creation of many new ETFs to address that in one way or another. For example, now we have tactical asset allocation ETFs, minimum volatility ETFs, income ETFs with covered calls overlays.
To view the complete source code for this example, please have a look at the bt.couch.potato.test() function in bt.test.r at github.
Some additional references from the Canadian Couch Potato blog that are worth reading: