Comparing ATR order sizing to max dollar order sizing
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.
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.