### Archive

Archive for the ‘Backtesting’ Category

## Probabilistic Momentum

David Varadi has recently discussed an interesting strategy in the
Are Simple Momentum Strategies Too Dumb? Introducing Probabilistic Momentum post. David also provided the Probabilistic Momentum Spreadsheet if you are interested in doing computations in Excel. Today I want to show how you can test such strategy using 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)
#*****************************************************************
#******************************************************************

tickers = spl('SPY,TLT')

data <- new.env()
getSymbols(tickers, src = 'yahoo', from = '1980-01-01', env = data, auto.assign = T)
bt.prep(data, align='remove.na', dates='2005::')

#*****************************************************************
# Setup
#******************************************************************
lookback.len = 60

prices = data\$prices

models = list()

#*****************************************************************
# Simple Momentum
#******************************************************************
momentum = prices / mlag(prices, lookback.len)
data\$weight[] = NA
data\$weight\$SPY[] = momentum\$SPY > momentum\$TLT
data\$weight\$TLT[] = momentum\$SPY <= momentum\$TLT
models\$Simple  = bt.run.share(data, clean.signal=T)
```

The Simple Momentum strategy invests into SPY if SPY’s momentum if greater than TLT’s momentum, and invests into TLT otherwise.

```	#*****************************************************************
# Probabilistic Momentum
#******************************************************************
confidence.level = 60/100
ret = prices / mlag(prices) - 1

ir = sqrt(lookback.len) * runMean(ret\$SPY - ret\$TLT, lookback.len) / runSD(ret\$SPY - ret\$TLT, lookback.len)
momentum.p = pt(ir, lookback.len - 1)

data\$weight[] = NA
data\$weight\$SPY[] = iif(cross.up(momentum.p, confidence.level), 1, iif(cross.dn(momentum.p, (1 - confidence.level)), 0,NA))
data\$weight\$TLT[] = iif(cross.dn(momentum.p, (1 - confidence.level)), 1, iif(cross.up(momentum.p, confidence.level), 0,NA))
models\$Probabilistic  = bt.run.share(data, clean.signal=T)
```

The Probabilistic Momentum strategy is using Probabilistic Momentum measure and Confidence Level to decide on allocation. Strategy invests into SPY if SPY vs TLT Probabilistic Momentum is above Confidence Level and invests into TLT is SPY vs TLT Probabilistic Momentum is below 1 – Confidence Level.

To make Strategy a bit more attractive, I added a version that can leverage SPY allocation by 50%

```	#*****************************************************************
# Probabilistic Momentum + SPY Leverage
#******************************************************************
data\$weight[] = NA
data\$weight\$SPY[] = iif(cross.up(momentum.p, confidence.level), 1, iif(cross.up(momentum.p, (1 - confidence.level)), 0,NA))
data\$weight\$TLT[] = iif(cross.dn(momentum.p, (1 - confidence.level)), 1, iif(cross.up(momentum.p, confidence.level), 0,NA))
models\$Probabilistic.Leverage = bt.run.share(data, clean.signal=T)

#*****************************************************************
# Create Report
#******************************************************************
strategy.performance.snapshoot(models, T)
``` The back-test results look very similar to the ones reported in the Are Simple Momentum Strategies Too Dumb? Introducing Probabilistic Momentum post.

However, I was not able to exactly reproduce the transition plots. Looks like my interpretation is producing more whipsaw when desired.

```	#*****************************************************************
# Visualize Signal
#******************************************************************
cols = spl('steelblue1,steelblue')
prices = scale.one(data\$prices)

layout(1:3)

plota(prices\$SPY, type='l', ylim=range(prices), plotX=F, col=cols, lwd=2)
plota.lines(prices\$TLT, type='l', plotX=F, col=cols, lwd=2)
plota.legend('SPY,TLT',cols,as.list(prices))

highlight = models\$Probabilistic\$weight\$SPY > 0
plota.control\$col.x.highlight = iif(highlight, cols, cols)
plota(models\$Probabilistic\$equity, type='l', plotX=F, x.highlight = highlight | T)
plota.legend('Probabilistic,SPY,TLT',c('black',cols))

highlight = models\$Simple\$weight\$SPY > 0
plota.control\$col.x.highlight = iif(highlight, cols, cols)
plota(models\$Simple\$equity, type='l', plotX=T, x.highlight = highlight | T)
plota.legend('Simple,SPY,TLT',c('black',cols))
``` David thank you very much for sharing your great ideas. I would encourage readers to play with this strategy and report back.

To view the complete source code for this example, please have a look at the bt.probabilistic.momentum.test() function in bt.test.r at github.

Mebane Faber posted another interesting blog post: Building a Simple Sector Rotation on Momentum and Trend that caught my interest. Today I want to show how you can test such strategy using 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)

#*****************************************************************
#******************************************************************

data = new.env()
ret = cbind(temp[]\$Mkt.RF + temp[]\$RF, temp[]\$RF)
price = bt.apply.matrix(ret / 100, function(x) cumprod(1 + x))
data\$SPY = make.stock.xts( price\$Mkt.RF )
data\$SHY = make.stock.xts( price\$RF )

ret = temp[]
price = bt.apply.matrix(ret[,1:9] / 100, function(x) cumprod(1 + x))
for(n in names(price)) data[[n]] = make.stock.xts( price[,n] )

# align dates
data\$symbolnames = c(names(price), 'SHY', 'SPY')
bt.prep(data, align='remove.na', dates='2000::')

# back-test dates
bt.dates = '2001:04::'

#*****************************************************************
# Setup
#******************************************************************
prices = data\$prices
n = ncol(data\$prices)

models = list()

#*****************************************************************
# Benchmark Strategies
#******************************************************************
data\$weight[] = NA
data\$weight\$SPY = 1
models\$SPY = bt.run.share(data, clean.signal=F, dates=bt.dates)

weight = prices
weight\$SPY = NA
weight\$SHY = NA

data\$weight[] = NA
data\$weight[] = ntop(weight[], n)
models\$EW = bt.run.share(data, clean.signal=F, dates=bt.dates)

#*****************************************************************
# Code Strategies
# http://www.mebanefaber.com/2013/12/04/square-root-of-f-squared/
#******************************************************************
sma = bt.apply.matrix(prices, SMA, 10)

# create position score
position.score = sma
position.score[ prices < sma ] = NA
position.score\$SHY = NA
position.score\$SPY = NA

# equal weight allocation
weight = ntop(position.score[], n)

# number of invested funds
n.selected = rowSums(weight != 0)

# cash logic
weight\$SHY[n.selected == 0,] = 1

weight[n.selected == 1,] = 0.25 * weight[n.selected == 1,]
weight\$SHY[n.selected == 1,] = 0.75

weight[n.selected == 2,] = 0.5 * weight[n.selected == 2,]
weight\$SHY[n.selected == 2,] = 0.5

weight[n.selected == 3,] = 0.75 * weight[n.selected == 3,]
weight\$SHY[n.selected == 3,] = 0.25

# cbind(round(100*weight,0), n.selected)

data\$weight[] = NA
data\$weight[] = weight
models\$strategy1 = bt.run.share(data, clean.signal=F, dates=bt.dates)

#*****************************************************************
# Create Report
#******************************************************************
strategy.performance.snapshoot(models, one.page = T)
``` Mebane thank you very much for sharing your great ideas. I would encourage readers to play with this strategy and report back.

Please note that I back-tested the strategy using the monthly observations. The strategy’s draw-down is around 17% using monthly data. If we switch to the daily data, the strategy’s draw-down goes to around 22%. There was one really bad month in 2002.

To view the complete source code for this example, please have a look at the bt.mebanefaber.f.squared.test() function in bt.test.r at github.

## Averaged Input Assumptions and Momentum

Today I want to share another interesting idea contributed by Pierre Chretien. Pierre suggested using Averaged Input Assumptions and Momentum to create reasonably quiet strategy. The averaging techniques are used to avoid over-fitting any particular frequency.

To create Averaged Input Assumptions we combine returns over different look-back periods, giving more weight to the recent returns, to form overall Input Assumptions.

```create.ia.averaged <- function(lookbacks, n.lag) {
lookbacks = lookbacks
n.lag = n.lag

function(hist.returns, index=1:ncol(hist.returns), hist.all)
{
nperiods = nrow(hist.returns)

temp = c()
for (n.lookback in lookbacks)
temp = rbind(temp, hist.returns[(nperiods - n.lookback - n.lag + 1):(nperiods - n.lag), ])
create.ia(temp, index, hist.all)
}
}
```

To create Averaged Momentum we take a look-back weighted avaerage of momentums computed over different look-back periods.

```momentum.averaged <- function(prices,
lookbacks = c(20,60,120,250) ,	# length of momentum look back
n.lag = 3
) {
momentum = 0 * prices
for (n.lookback in lookbacks) {
part.mom = mlag(prices, n.lag) / mlag(prices, n.lookback + n.lag) - 1
momentum = momentum + 252 / n.lookback * part.mom
}
momentum / len(lookbacks)
}
```

Next let’s compare using historical Input Assumptions vs Averaged Input Assumptions and Momentum vs Averaged Momentum. I will consider Absolute Momentum (not cross sectional), for more information about relative and absolute momentum, please see

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

#*****************************************************************
#******************************************************************

# 10 funds
tickers = spl('Us.Eq = VTI + VTSMX,
Eurpoe.Eq = IEV + FIEUX,
Japan.Eq = EWJ + FJPNX,
Emer.Eq = EEM + VEIEX,
Re = RWX + VNQ + VGSIX,
Com = DBC + QRAAX,
Gold = GLD + SCGDX,
Long.Tr = TLT + VUSTX,
Mid.Tr = IEF + VFITX,
Short.Tr = SHY + VFISX')

start.date = 1998

dates = paste(start.date,'::',sep='')

data <- new.env()
getSymbols.extra(tickers, src = 'yahoo', from = '1980-01-01', env = data, set.symbolnames = T, auto.assign = T)
bt.prep(data, align='keep.all', dates=paste(start.date-2,':12::',sep=''), fill.gaps = T)

#*****************************************************************
# Setup
#******************************************************************
prices = data\$prices
n = ncol(prices)
nperiods = nrow(prices)

periodicity = 'months'
period.ends = endpoints(prices, periodicity)
period.ends = period.ends[period.ends > 0]

max.product.exposure = 0.6

#*****************************************************************
# Input Assumptions
#******************************************************************
lookback.len = 40
create.ia.fn = create.ia

# input assumptions are averaged on 20, 40, 60 days using 1 day lag
ia.array = c(20,40,60)
avg.create.ia.fn = create.ia.averaged(ia.array, 1)

#*****************************************************************
# Momentum
#******************************************************************
universe = prices > 0

mom.lookback.len = 120
momentum = prices / mlag(prices, mom.lookback.len) - 1
mom.universe = ifna(momentum > 0, F)

# momentum is averaged on 20,60,120,250 days using 3 day lag
mom.array = c(20,60,120,250)
avg.momentum = momentum.averaged(prices, mom.array, 3)
avgmom.universe = ifna(avg.momentum > 0, F)

#*****************************************************************
# Algos
#******************************************************************
min.risk.fns = list(
EW = equal.weight.portfolio,
MV = min.var.portfolio,
MCE = min.corr.excel.portfolio,

MV.RSO = rso.portfolio(min.var.portfolio, 3, 100, const.ub = max.product.exposure),
MCE.RSO = rso.portfolio(min.corr.excel.portfolio, 3, 100, const.ub = max.product.exposure)
)

#*****************************************************************
# Code Strategies
#******************************************************************
make.strategy.custom <- function(name, create.ia.fn, lookback.len, universe, env) {
obj = portfolio.allocation.helper(data\$prices,
periodicity = periodicity,
universe = universe,
lookback.len = lookback.len,
create.ia.fn = create.ia.fn,
const.ub = max.product.exposure,
min.risk.fns = min.risk.fns,
)
env[[name]] = create.strategies(obj, data, prefix=paste(name,'.',sep=''))\$models
}

models <- new.env()
make.strategy.custom('ia.none'        , create.ia.fn    , lookback.len, universe       , models)
make.strategy.custom('ia.mom'         , create.ia.fn    , lookback.len, mom.universe   , models)
make.strategy.custom('ia.avg_mom'     , create.ia.fn    , lookback.len, avgmom.universe, models)
make.strategy.custom('avg_ia.none'    , avg.create.ia.fn, 252         , universe       , models)
make.strategy.custom('avg_ia.mom'     , avg.create.ia.fn, 252         , mom.universe   , models)
make.strategy.custom('avg_ia.avg_mom' , avg.create.ia.fn, 252         , avgmom.universe, models)

#*****************************************************************
# Create Report
#*****************************************************************
strategy.snapshot.custom <- function(models, n = 0, title = NULL) {
if (n > 0)
models = models[ as.vector(matrix(1:len(models),ncol=n, byrow=T)) ]

layout(1:3)
plotbt(models, plotX = T, log = 'y', LeftMargin = 3, main = title)
mtext('Cumulative Performance', side = 2, line = 1)
plotbt.strategy.sidebyside(models)
barplot.with.labels(sapply(models, compute.turnover, data), 'Average Annual Portfolio Turnover', T)
}

# basic vs basic + momentum => momentum filter has better results
models.final = c(models\$ia.none, models\$ia.mom)
strategy.snapshot.custom(models.final, len(min.risk.fns), 'Momentum Filter')

# basic vs basic + avg ia => averaged ia reduce turnover
models.final = c(models\$ia.none, models\$avg_ia.none)
strategy.snapshot.custom(models.final, len(min.risk.fns), 'Averaged Input Assumptions')

# basic + momentum vs basic + avg.momentum => mixed results for averaged momentum
models.final = c(models\$ia.mom, models\$ia.avg_mom)
strategy.snapshot.custom(models.final, len(min.risk.fns), 'Averaged Momentum')

# basic + momentum vs avg ia + avg.momentum
models.final = c(models\$ia.mom, models\$avg_ia.avg_mom)
strategy.snapshot.custom(models.final, len(min.risk.fns), 'Averaged vs Base')
```

Above, I compared results for the following 4 cases:
1. Adding Momentum filter: all algos perfrom better 2. Input Assumptions vs Averaged Input Assumptions: returns are very similar, but using Averaged Input Assumptions helps reduce portfolio turnover. 3. Momentum vs Averaged Momentum: returns are very similar, but using Averaged Momentum increases portfolio turnover. 4. historical Input Assumptions + Momentum vs Averaged Input Assumptions + Averaged Momentum: results are mixed, no consistent advantage of using Averaged methods Overall, the Averaged methods is a very interesting idea and I hope you will experiemtn with it and share your findings, like Pierre. Pierre, again thank you very much for sharing.

The full source code and example for the bt.averaged.test() function is available in bt.test.r at github.

## 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,
MCVaR = min.cvar.portfolio,
MCDaR = min.cdar.portfolio,
MRiskDown = min.risk.downside.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,
MCVaR = min.cvar.portfolio,
MCDaR = min.cdar.portfolio,
MRiskDown = min.risk.downside.portfolio,
)
)
```

Hopefully, I did not ruin your prolong lunch plans 🙂

## Commissions

November 5, 2013 1 comment

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

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

#*****************************************************************
#******************************************************************
tickers = spl('EEM')

data <- new.env()
getSymbols(tickers, src = 'yahoo', from = '1970-01-01', env = data, auto.assign = T)
bt.prep(data, align='keep.all', dates='2013:08::2013:09')

#*****************************************************************
# Code Strategies
#******************************************************************
sell.date = '2013:08:15'
day.after.sell.date = '2013:08:16'

capital = 100000
prices = data\$prices

# helper function to compute trade return

#*****************************************************************
# Zero commission
#******************************************************************
data\$weight[] = NA
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] )

#*****************************************************************
# 10c cps commission
# cents / share commission
#   trade cost = abs(share - mlag(share)) * commission\$cps
#******************************************************************
data\$weight[] = NA
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) )

#*****************************************************************
# \$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[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 )

#*****************************************************************
# % commission
# percentage commission
#   trade cost = price * abs(share - mlag(share)) * commission\$percentage
#******************************************************************
data\$weight[] = NA