Comparing ATR order sizing to max dollar order sizing

[This article was first published on QuantStrat TradeR » R, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

First off, it has come to my attention that some readers have trouble getting some of my demos to work because there may be different versions of TTR in use. If ever your demo doesn’t work, the first thing I would immediately recommend you do is this:

Only run the code through the add.indicator logic. And then, rather than adding the signals and rules, run the following code:

test <- applyIndicators(strategy.st, mktdata=OHLC(XLB))
head(test)

That should show you the exact column names of your indicators, and you can adjust your inputs accordingly.While one of my first posts introduced the ATR order-sizing function, I recently received a suggestion to test it in the context of whether or not it actually normalized risk across instruments. To keep things simple, my strategy is as plain vanilla as strategies come — RSI2 20/80 filtered on SMA200.

Here’s the code for the ATR order sizing version, for completeness’s sake.

require(IKTrading)
require(quantstrat)
require(PerformanceAnalytics)

initDate="1990-01-01"
from="2003-01-01"
to="2012-12-31"
options(width=70)

source("demoData.R")

#trade sizing and initial equity settings
tradeSize <- 100000
initEq <- tradeSize*length(symbols)

strategy.st <- portfolio.st <- account.st <- "DollarVsATRos"
rm.strat(portfolio.st)
rm.strat(strategy.st)
initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD')
initAcct(account.st, portfolios=portfolio.st, initDate=initDate, currency='USD',initEq=initEq)
initOrders(portfolio.st, initDate=initDate)
strategy(strategy.st, store=TRUE)

#parameters
pctATR=.02
period=10

nRSI <- 2
buyThresh <- 20
sellThresh <- 80
nSMA <- 200

add.indicator(strategy.st, name="lagATR", 
              arguments=list(HLC=quote(HLC(mktdata)), n=period), 
              label="atrX")

add.indicator(strategy.st, name="RSI",
              arguments=list(price=quote(Cl(mktdata)), n=nRSI),
              label="rsi")

add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), n=nSMA),
              label="sma")

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "sma"), relationship="gt"),
           label="filter")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=buyThresh, 
                          relationship="lt", cross=FALSE),
           label="rsiLtThresh")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("filter", "rsiLtThresh"), cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="rsi", threshold=sellThresh,
                          relationship="gt", cross=TRUE),
           label="longExit")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "sma"), relationship="lt"),
           label="filterExit")

#rules
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open", osFUN=osDollarATR,
                        tradeSize=tradeSize, pctATR=pctATR, atrMod="X"), 
         type="enter", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="filterExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open"), 
         type="exit", path.dep=TRUE)

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()
print(t2-t1)

#set up analytics
updatePortf(portfolio.st)
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]
updateAcct(portfolio.st,dateRange)
updateEndEq(account.st)

Here are some of the usual analytics, which don’t interest me in and of themselves as this strategy is rather throwaway, but to compare them to what happens when I use the max dollar order sizing function in a moment:

> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
[1] 1.659305
> (aggCorrect <- mean(tStats$Percent.Positive))
[1] 69.24967
> (numTrades <- sum(tStats$Num.Trades))
[1] 3017
> (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))
[1] 0.733

> SharpeRatio.annualized(portfRets)
                                     [,1]
Annualized Sharpe Ratio (Rf=0%) 0.9783541
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.07369592
> maxDrawdown(portfRets)
[1] 0.08405041

> round(apply.yearly(dailyRetComparison, Return.cumulative),3)
           strategy    SPY
2003-12-31    0.052  0.066
2004-12-31    0.074  0.079
2005-12-30    0.045  0.025
2006-12-29    0.182  0.132
2007-12-31    0.117  0.019
2008-12-31   -0.010 -0.433
2009-12-31    0.130  0.192
2010-12-31   -0.005  0.110
2011-12-30    0.069 -0.028
2012-12-31    0.087  0.126
> round(apply.yearly(dailyRetComparison, SharpeRatio.annualized),3)
           strategy    SPY
2003-12-31    1.867  3.641
2004-12-31    1.020  0.706
2005-12-30    0.625  0.238
2006-12-29    2.394  1.312
2007-12-31    1.105  0.123
2008-12-31   -0.376 -1.050
2009-12-31    1.752  0.719
2010-12-31   -0.051  0.614
2011-12-30    0.859 -0.122
2012-12-31    1.201  0.990
> round(apply.yearly(dailyRetComparison, maxDrawdown),3)
           strategy   SPY
2003-12-31    0.018 0.025
2004-12-31    0.065 0.085
2005-12-30    0.053 0.074
2006-12-29    0.074 0.077
2007-12-31    0.066 0.102
2008-12-31    0.032 0.520
2009-12-31    0.045 0.280
2010-12-31    0.084 0.167
2011-12-30    0.053 0.207
2012-12-31    0.050 0.099

Now here’s a new bit of analytics–comparing annualized standard deviations between securities:

> sdQuantile <- quantile(sapply(instRets, sd.annualized))
> sdQuantile
         0%         25%         50%         75%        100% 
0.004048235 0.004349390 0.004476377 0.004748530 0.005557765 
> (extremeRatio <- sdQuantile[5]/sdQuantile[1]-1)
    100% 
0.372886 
> (boxBorderRatio <- sdQuantile[4]/sdQuantile[2]-1)
      75% 
0.0917693 

In short, because the instrument returns are computed as a function of only the initial account equity (quantstrat doesn’t know that I’m “allocating” a notional cash amount to each separate ETF–because I’m really not–I just treat it as one pile of cash that I mentally think of as being divided “equally” between all 30 ETFs), that means that the returns per instrument also have already implicitly factored in the weighting scheme from the order sizing function. In this case, the most volatile instrument is about 37% more volatile than the least — and since I’m dealing with indices of small nations along with short-term treasury bills in ETF form, I’d say that’s impressive.

More impressive, in my opinion, is that the difference in volatility between the 25th and 75th percentile is about 9%. It means that our ATR order sizing seems to be doing its job.Here’s the raw computations in terms of annualized volatility:

> sapply(instRets, sd.annualized)
EFA.DailyEndEq EPP.DailyEndEq EWA.DailyEndEq EWC.DailyEndEq 
   0.004787248    0.005557765    0.004897699    0.004305728 
EWG.DailyEndEq EWH.DailyEndEq EWJ.DailyEndEq EWS.DailyEndEq 
   0.004806879    0.004782505    0.004460708    0.004618460 
EWT.DailyEndEq EWU.DailyEndEq EWY.DailyEndEq EWZ.DailyEndEq 
   0.004417686    0.004655716    0.004888876    0.004858743 
EZU.DailyEndEq IEF.DailyEndEq IGE.DailyEndEq IYR.DailyEndEq 
   0.004631333    0.004779468    0.004617250    0.004359273 
IYZ.DailyEndEq LQD.DailyEndEq RWR.DailyEndEq SHY.DailyEndEq 
   0.004346095    0.004101408    0.004388131    0.004585389 
TLT.DailyEndEq XLB.DailyEndEq XLE.DailyEndEq XLF.DailyEndEq 
   0.004392335    0.004319708    0.004515228    0.004426415 
XLI.DailyEndEq XLK.DailyEndEq XLP.DailyEndEq XLU.DailyEndEq 
   0.004129331    0.004492046    0.004369804    0.004048235 
XLV.DailyEndEq XLY.DailyEndEq 
   0.004148445    0.004203503 

And here’s a histogram of those same calculations:

In this case, the reason that the extreme computation gives us a 37% greater result is that one security, EPP (pacific ex-Japan, which for all intents and purposes is emerging markets) is simply out there a bit. The rest just seem very clumped up.

Now let’s remove the ATR order sizing and replace it with a simple osMaxDollar rule, that simply will keep a position topped off at a notional dollar value. In short, aside from a few possible one-way position rebalancing transactions (E.G. with the ATR order sizing rule, ATR may have gone up whereas total value of a position may have gone down, which may trigger the osMaxDollar rule but not the osDollarATR rule on a second RSI cross) Here’s the new entry rule, with the ATR commented out:

# add.rule(strategy.st, name="ruleSignal", 
#          arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
#                         orderside="long", replace=FALSE, prefer="Open", osFUN=osDollarATR,
#                         tradeSize=tradeSize, pctATR=pctATR, atrMod="X"), 
#          type="enter", path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open", osFUN=osMaxDollar,
                        tradeSize=tradeSize, maxSize=tradeSize), 
         type="enter", path.dep=TRUE)

Let’s look at the corresponding statistical results:

> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
[1] 1.635629
> (aggCorrect <- mean(tStats$Percent.Positive))
[1] 69.45633
> (numTrades <- sum(tStats$Num.Trades))
[1] 3019
> (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))
[1] 0.735

> SharpeRatio.annualized(portfRets)
                                     [,1]
Annualized Sharpe Ratio (Rf=0%) 0.8529713
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.04857159
> maxDrawdown(portfRets)
[1] 0.06682969
> 
> dailyRetComparison <- cbind(portfRets, SPYrets)
> colnames(dailyRetComparison)  <- c("strategy", "SPY")
> round(apply.yearly(dailyRetComparison, Return.cumulative),3)
           strategy    SPY
2003-12-31    0.034  0.066
2004-12-31    0.055  0.079
2005-12-30    0.047  0.025
2006-12-29    0.090  0.132
2007-12-31    0.065  0.019
2008-12-31   -0.023 -0.433
2009-12-31    0.141  0.192
2010-12-31   -0.010  0.110
2011-12-30    0.038 -0.028
2012-12-31    0.052  0.126
> round(apply.yearly(dailyRetComparison, SharpeRatio.annualized),3)
           strategy    SPY
2003-12-31    1.639  3.641
2004-12-31    1.116  0.706
2005-12-30    0.985  0.238
2006-12-29    1.755  1.312
2007-12-31    0.785  0.123
2008-12-31   -0.856 -1.050
2009-12-31    1.774  0.719
2010-12-31   -0.134  0.614
2011-12-30    0.686 -0.122
2012-12-31    1.182  0.990
> round(apply.yearly(dailyRetComparison, maxDrawdown),3)
           strategy   SPY
2003-12-31    0.015 0.025
2004-12-31    0.035 0.085
2005-12-30    0.033 0.074
2006-12-29    0.058 0.077
2007-12-31    0.058 0.102
2008-12-31    0.036 0.520
2009-12-31    0.043 0.280
2010-12-31    0.062 0.167
2011-12-30    0.038 0.207
2012-12-31    0.035 0.099

And now for the kicker–to see just how much riskier using a naive order-sizing method that doesn’t take into account the different idiosyncratic of a security is:

> sdQuantile <- quantile(sapply(instRets, sd.annualized))
> sdQuantile
          0%          25%          50%          75%         100% 
0.0002952884 0.0026934043 0.0032690492 0.0037727970 0.0061480828 
> (extremeRatio <- sdQuantile[5]/sdQuantile[1]-1)
   100% 
19.8206 
> (boxBorderRatio <- sdQuantile[4]/sdQuantile[2]-1)
     75% 
0.400754 
> hist(sapply(instRets, sd.annualized))

In short, the ratio between the riskiest and least riskiest asset rises from less than 40% to 1900%. But in case, that’s too much of an outlier (E.G. dealing with treasury bill/note/bond ETFs vs. pacific ex-Japan aka emerging Asia), the difference between the third and first quartiles in terms of volatility ratio has jumped from 9% to 40%.

Here’s the corresponding histogram:As can be seen, a visibly higher variance in variances–in other words, a second moment on the second moment–meaning that to not use an order-sizing function that takes into account individual security risk therefore introduces unnecessary kurtosis and heavier tails into the risk/reward ratio, and due to this unnecessary excess risk, performance suffers measurably.Here are the individual security annualized standard deviations for the max dollar order sizing method:

> sapply(instRets, sd.annualized)
EFA.DailyEndEq EPP.DailyEndEq EWA.DailyEndEq EWC.DailyEndEq 
  0.0029895232   0.0037767697   0.0040222015   0.0036137500 
EWG.DailyEndEq EWH.DailyEndEq EWJ.DailyEndEq EWS.DailyEndEq 
  0.0037097070   0.0039615376   0.0030398638   0.0037608791 
EWT.DailyEndEq EWU.DailyEndEq EWY.DailyEndEq EWZ.DailyEndEq 
  0.0041140227   0.0032204771   0.0047719772   0.0061480828 
EZU.DailyEndEq IEF.DailyEndEq IGE.DailyEndEq IYR.DailyEndEq 
  0.0033176214   0.0013059712   0.0041621776   0.0033752435 
IYZ.DailyEndEq LQD.DailyEndEq RWR.DailyEndEq SHY.DailyEndEq 
  0.0026899679   0.0011777797   0.0034789117   0.0002952884 
TLT.DailyEndEq XLB.DailyEndEq XLE.DailyEndEq XLF.DailyEndEq 
  0.0024854557   0.0034895815   0.0043568967   0.0029546665 
XLI.DailyEndEq XLK.DailyEndEq XLP.DailyEndEq XLU.DailyEndEq 
  0.0027963302   0.0028882028   0.0021212224   0.0025802850 
XLV.DailyEndEq XLY.DailyEndEq 
  0.0020399289   0.0027037138

Is ATR order sizing the absolute best order-sizing methodology? Most certainly not.In fact, in the PortfolioAnalytics package (quantstrat’s syntax was modeled from this), there are ways to explicitly penalize the higher order moments and co-moments. However, in this case, ATR order sizing works as a simple yet somewhat effective demonstrator of risk-adjusted order-sizing, while implicitly combating some of the risks in not paying attention to the higher moments of the distributions of returns, and also still remaining fairly close to the shore in terms of ease of explanation to those without heavy quantitative backgrounds. This facilitates marketing to large asset managers that may otherwise be hesitant in investing with a more complex strategy that they may not so easily understand.

Thanks for reading.


To leave a comment for the author, please follow the link and comment on their blog: QuantStrat TradeR » R.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Never miss an update!
Subscribe to R-bloggers to receive
e-mails with the latest R posts.
(You will not see this message again.)

Click here to close (This popup will not appear again)