The Limit of ATR Order Sizing

Before beginning this post, I’d like to notify readers that I have a webcast tomorrow (Wednesday, Sep. 3) at 4:30 EST for Big Mike’s Trading. Those that can follow the code and the analytics on this blog will see nothing new, but for those that effectively “nod and wait for the punchline” in the form of the equity curve, I’ll demonstrate how to build a strategy “in real time”.

Here’s the link.

Now onto the post:

While the last post showed how ATR did a better job than raw dollar positions of equalizing risk in the form of standard deviations across instruments, it isn’t the be-all, end-all method of order sizing. Something I learned about recently was portfolio component expected shortfall (along with portfolio component standard deviation). The rabbit hole on these methods runs very deep, including to a paper in the Journal of Risk. To give a quick summary of this computation, it’s one that takes into account not just the well-known mean and covariance, but also interactions between higher order moments, such as co-skewness, and co-kurtosis. The actual details of the math behind this is quite extensive, but luckily, it’s already programmed into the PerformanceAnalytics package, so computing it is as simple as calling a pre-programmed procedure. This demo will, along the way of making yet another comparison between ATR and dollar order sizes, demonstrate one way of doing this.

For those that are unfamiliar with the terminology, expected shortfall is also known as conditional value-at-risk (aka CVaR), which is a coherent risk measure, while regular value at risk is not (for instance, take the example of two bonds each with a default probability of less than 5%, say, 4.95% — the 5% VaR of either of them is 0, but the 5% VaR of the two bond portfolio is greater than zero (or less, depending on how you express the quantity–as a portfolio value, or loss value)).

In any case, here’s the code:

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
atrOrder <- TRUE

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
if(atrOrder) {
  
  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)
} else { 
  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)
}


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)


#Portfolio comparisons to SPY
instRets <- PortfReturns(account.st)

if(atrOrder) {  
  atrSd <- StdDev(na.omit(instRets), portfolio_method="component")
  t1  <- Sys.time()
  atrES <- ES(na.omit(instRets), portfolio_method="component")
  t2 <- Sys.time()
  print(t2-t1)
} else {
  dollarSd <- StdDev(na.omit(instRets), portfolio_method="component")
  t1  <- Sys.time()
  dollarES <- ES(na.omit(instRets), portfolio_method="component")
  t2 <- Sys.time()
  print(t2-t1)
}

if(("atrSd" %in% ls()) & ("dollarSd" %in% ls())){  
  boxPlotFrame <- cbind(atrSd$pct_contrib_StdDev, 
                        atrES$pct_contrib_MES,
                        dollarSd$pct_contrib_StdDev,
                        dollarES$pct_contrib_MES)
  colnames(boxPlotFrame) <- c("atrSd", "atrES", "dollarSd", "dollarES")
  boxplot(boxPlotFrame)
}

rownames(boxPlotFrame) <- gsub(".DailyEndEq", "", rownames(boxPlotFrame))

This is the resulting image:

And the corresponding data which was used to generate the box plot:

> boxPlotFrame
          atrSd       atrES     dollarSd      dollarES
EFA 0.049378400 0.032829510 0.0412678995  0.0414725387
EPP 0.053737115 0.061714539 0.0509966059  0.0701035651
EWA 0.044784653 0.064515175 0.0510242998  0.0728805941
EWC 0.036487179 0.019929189 0.0439868019  0.0422535671
EWG 0.043172662 0.028698773 0.0455195612  0.0505451367
EWH 0.039433573 0.038858046 0.0449196754  0.0362996367
EWJ 0.030736566 0.037979817 0.0257488355  0.0257318748
EWS 0.041005553 0.016891054 0.0453268353  0.0240447623
EWT 0.029300684 0.058477463 0.0378874688  0.0599281716
EWU 0.043907517 0.025657106 0.0403600522  0.0366783005
EWY 0.039628602 0.028507044 0.0550089098  0.0370010120
EWZ 0.039586224 0.057278661 0.0721037108  0.0867982223
EZU 0.042050678 0.026106675 0.0412176610  0.0288231226
IEF 0.008465791 0.027757444 0.0008097627  0.0046390295
IGE 0.038329663 0.062596052 0.0487744244  0.0797431561
IYR 0.029283546 0.009068168 0.0299819881 -0.0088073708
IYZ 0.034378964 0.042472847 0.0265562205  0.0285952033
LQD 0.008845486 0.020600278 0.0013406959  0.0007059662
RWR 0.027775214 0.014710031 0.0301350434 -0.0063985075
SHY 0.007692137 0.026727026 0.0001618876  0.0009506367
TLT 0.008471822 0.015863044 0.0025502684  0.0031127512
XLB 0.037847498 0.069639008 0.0407311722  0.0788174726
XLE 0.034833476 0.040059687 0.0463448900  0.0602607692
XLF 0.036103344 0.031322842 0.0306424885  0.0337304540
XLI 0.036248065 0.003602306 0.0324606725  0.0018744509
XLK 0.039230708 0.022105404 0.0330037953  0.0276545899
XLP 0.029559398 0.006160133 0.0161629448 -0.0067567464
XLU 0.024703361 0.047065790 0.0191419001  0.0393038102
XLV 0.028872857 0.027564044 0.0168619029  0.0173169198
XLY 0.036149264 0.035242843 0.0289716256  0.0326969108

As can be seen in the image, which is a box plot of the various ways of computing the percentage of portfolio component risk for the two order types, ATR order sizing still does a better job than raw dollar order sizing in terms of controlling risk. However, as evidenced by the atrES box plot, there still is a somewhat wide distribution in terms of contributions to portfolio risk between the various instruments. However, even in this instance of portfolio component risk, it’s readily visible how the ATR order sizing improves on dollar order sizing. However, this also demonstrates how ATR order sizing isn’t the be-all, end-all method of portfolio allocations.

For future note, the application of portfolio component risk metrics is to optimize them in one of two ways–by minimizing the difference between them, or by striving to set them as close to equal to each other as possible (that is, portfolio component risk balance). The PortfolioAnalytics package provides methods on how to do that, which I’ll visit in the future.

Thanks for reading.

Comparing ATR order sizing to max dollar order sizing

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.

VCI — The Value Charts Indicator

So recently, I was made known of the Value Charts Indicator , which was supposed to be some form of alternative to the RSI. I decided to investigate it, and see if it’s worth using.

Before diving into a strategy, here’s how the indicator works:

"VCI" <- function(OHLC, nLookback=40, nRange=8, pctRank=FALSE) {
  if(nLookback > 7) {
    varA <- runMax(Hi(OHLC), nRange) - runMin(Lo(OHLC), nRange)
    varB <- lag(varA, nRange+1)
    varC <- lag(varA, nRange*2)
    varD <- lag(varA, nRange*3)
    varE <- lag(varA, nRange*4)
    LRange <- (varA+varB+varC+varD+varE)/25    
  }
  if(nLookback <=7) {
    absDiff <- abs(diff(Cl(OHLC)))
    dailyRange <- Hi(OHLC) - Lo(OHLC)
    tmp <- cbind(absDiff, dailyRange)
    maxTmp <- pmax(tmp)
    LRange <- SMA(maxTmp, 5)*.16
  }
  hilo <- (Hi(OHLC)+Lo(OHLC))/2
  VO <- (Op(OHLC)-SMA(hilo, nLookback))/LRange
  VH <- (Hi(OHLC)-SMA(hilo, nLookback))/LRange
  VL <- (Lo(OHLC)-SMA(hilo, nLookback))/LRange
  VC <- (Cl(OHLC)-SMA(hilo, nLookback))/LRange
  out <- cbind(VO=VO, VH=VH, VL=VL, VC=VC)
  colnames(out) <- c("VO", "VH", "VL", "VC")
  return(out)
}

Long story short, if the lookback period is 8 bars or more, it is something akin to an average of various five lagged ranges, over five times the specified range. That is, define your first range computation as the difference between the highest high and lowest low, and then average that with that same computation lagged by nRange+1 bars, nRange*2 bars, and so on. At a shorter frame than 8 bars (that is, a special case), the computation is a moving average of the daily maximum between the daily range and the close-to-close range (E.G. with a high of 4 and low of 2, with close of 3 and previous close of 2, that daily value will be equal to 4-2=2), and then take a 5 period SMA of that, and multiply by .16. Although the initial indicator had the range dependent on the lookback period, I chose to decouple it for greater flexibility to the user.

This range calculation is then used as a denominator of a computation that is the difference of the current price minus the SMA value of the average of an (H+L)/2 price proxy. In short, it’s a variation on a theme of the classical z-score from statistics. In other words, (X-Xbar)/(normalizing value).

This z-score is computed for all four price strands.

In my current implementation, I have not yet implemented the functionality for zero-movement bars (though that can be done by request) if anyone sees value with this indicator.

To put this indicator through its paces, I threw about as plain-standard-vanilla strategy around it. The strategy activates upon the close price greater than SMA200 (the “conventional wisdom”), and buys when the indicator crosses under -2 and exits above 2, using a lookback period of 10 days, with a range period of 2 days (the settings the indicator creator(s) had in mind were that -4/+4 was relatively oversold/overbought, with -8/+8 being extremely so). The idea here was to get a bunch of relatively short-term trades going, and use the advantage of large numbers to see how well this indicator performs.

Here’s the strategy code:

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

initDate="1990-01-01"
from="2003-01-01"
to=as.character(Sys.Date())
options(width=70)

source("demoData.R")

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

strategy.st <- portfolio.st <- account.st <- "VCI_test"
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

nRange=2
nLookback=10
pctRank=FALSE

buyThresh=-2
sellThresh=2

nSMA=200

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

add.indicator(strategy.st, name="VCI",
              arguments=list(OHLC=quote(OHLC(mktdata)), nLookback=nLookback,
                             nRange=nRange, pctRank=pctRank),
              label="vci")

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.sma"), relationship="gt"),
           label="filter")

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

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

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

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "SMA.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)

And here are the results:

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

> print(t(durStats))
      [,1]
Min      1
Q1       5
Med      9
Mean    11
Q3      14
Max     57
WMin     1
WQ1      5
WMed     8
WMean    9
WQ3     12
WMax    41
LMin     1
LQ1      5
LMed    15
LMean   15
LQ3     22
LMax    57

> SharpeRatio.annualized(portfRets)
                                     [,1]
Annualized Sharpe Ratio (Rf=0%) 0.8951308
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.06821319
> maxDrawdown(portfRets)
[1] 0.108064

> round(apply.yearly(dailyRetComparison, Return.cumulative),3)
           strategy    SPY
2003-12-31    0.058  0.066
2004-12-31    0.056  0.079
2005-12-30    0.034  0.025
2006-12-29    0.148  0.132
2007-12-31    0.094  0.019
2008-12-31   -0.022 -0.433
2009-12-31    0.149  0.192
2010-12-31   -0.055  0.110
2011-12-30    0.072 -0.028
2012-12-31    0.072  0.126
2013-12-31    0.057  0.289
2014-08-22    0.143  0.075
> round(apply.yearly(dailyRetComparison, SharpeRatio.annualized),3)
           strategy    SPY
2003-12-31    2.379  3.641
2004-12-31    0.751  0.706
2005-12-30    0.476  0.238
2006-12-29    2.083  1.312
2007-12-31    0.909  0.123
2008-12-31   -0.943 -1.050
2009-12-31    2.023  0.719
2010-12-31   -0.548  0.614
2011-12-30    0.854 -0.122
2012-12-31    1.015  0.990
2013-12-31    0.655  2.594
2014-08-22    2.869  1.137
> round(apply.yearly(dailyRetComparison, maxDrawdown),3)
           strategy   SPY
2003-12-31    0.014 0.025
2004-12-31    0.079 0.085
2005-12-30    0.058 0.074
2006-12-29    0.068 0.077
2007-12-31    0.073 0.102
2008-12-31    0.029 0.520
2009-12-31    0.041 0.280
2010-12-31    0.108 0.167
2011-12-30    0.052 0.207
2012-12-31    0.043 0.099
2013-12-31    0.072 0.062
2014-08-22    0.047 0.058

In short, it has the statistical profile of a standard mean-reverting strategy–lots of winners, losers slightly larger than winners, losers last longer in the market than winners as well. In terms of Sharpe Ratio, it’s solid but not exactly stellar. Overall, the strategy generally sports much better risk control than the raw SPY, but the annualized return to drawdown ratio isn’t quite up to the same level as some strategies tested on this blog in the past.

This is the equity curve comparison.

The equity profile seems to be pretty standard fare–winners happen over time, but a drawdown can wipe some of them (but not all) pretty quickly, as the system continues to make new equity highs. Solid, but not stellar.

Here’s an example of the equity curve of an individual instrument (the usual XLB):

Something to note is that the indicator is fairly choppy, and does best in a strong uptrend, when terms like oversold, pullback, and so on, are actually that, as opposed to a change in trend, or a protracted cyclic downtrend in a sideways market.

Here’s a picture of the strategy on XLB in 2012.

As you can see, the indicator at the 2-bar range isn’t exactly the smoothest, but with proper position-sizing rules (I use position sizing based on a 10-day ATR), the disadvantage of chopping across a threshold can be largely mitigated.

OVerall, while this indicator doesn’t seem to be much better than the more conventional RSIs, it nevertheless seems to be an alternative, and for those that want to use it, it’s now in my IKTrading package.

Thanks for reading.

A Hammer Trading System — Demonstrating Custom Indicator-Based Limit Orders in Quantstrat

So several weeks ago, I decided to listen on a webinar (and myself will be giving one on using quantstrat on Sep. 3 for Big Mike’s Trading, see link). Among some of those talks was a trading system called the “Trend Turn Trade Take Profit” system. This is his system:

Define an uptrend as an SMA10 above an SMA30.
Define a pullback as an SMA5 below an SMA10.

Define a hammer as a candle with an upper shadow less than 20% of the lower shadow, and a body less than 50% of the lower shadow. Enter on the high of the hammer, with the stop loss set at the low of the hammer and an additional one third of the range. The take profit target is 1.5 to 1.7 times the distance between the entry and the stop price.

Additionally (not tested here) was the bullish engulfing pattern, which is a two-bar pattern with the conditions of a down day followed by an up day on which the open of the up day was less than the close of the down day, and the close of the up day was higher than the previous day’s open, with the stop set to the low of the pattern, and the profit target in the same place.

This system was advertised to be correct about 70% of the time, with trades whose wins were 1.6 times as much as the losses, so I decided to investigate it.

The upside to this post, in addition to investigating someone else’s system, is that it will allow me to demonstrate how to create more nuanced orders with quantstrat. The best selling point for quantstrat, in my opinion, is that it provides a framework to do just about anything you want, provided you know how to do it (not trivial). In any case, the salient thing to take from this strategy is that it’s possible to create some interesting custom orders with some nuanced syntax.

Here’s the syntax for this strategy:

hammer <- function(OHLC, profMargin=1.5) {
  dailyMax <- pmax(Op(OHLC), Cl(OHLC))
  dailyMin <- pmin(Op(OHLC), Cl(OHLC))
  upShadow <- Hi(OHLC) - dailyMax
  dnShadow <- dailyMin - Lo(OHLC)
  body <- dailyMax-dailyMin
  hammerDay <- dnShadow/body > 2 & dnShadow/upShadow > 5
  hammers <- OHLC[hammerDay==1,]
  hammers$stopLoss <- 4/3*Lo(hammers)-1/3*Hi(hammers)
  hammers$takeProfit <- Hi(hammers) + (Hi(hammers)-hammers$stopLoss)*profMargin
  hammers <- cbind(hammerDay, hammers$stopLoss, hammers$takeProfit)
  hammers$stopLoss <- na.locf(hammers$stopLoss)
  hammers$takeProfit <- na.locf(hammers$takeProfit)
  colnames(hammers) <- c("hammer", "SL", "TP")
  return(hammers)
}

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

initDate="1990-01-01"
from="2003-01-01"
to=as.character(Sys.Date())
options(width=70)
verbose=TRUE

source("demoData.R")

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

strategy.st <- portfolio.st <- account.st <- "Hammer_4TP"
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
nSMA1=10
nSMA2=30
nSMA3=5
profMargin=1.5

period=10
pctATR=.1


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

add.indicator(strategy.st, name="hammer",
              arguments=list(OHLC=quote(OHLC(mktdata)), 
                             profMargin=profMargin),
              label="hammer")

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

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

add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), 
                             n=nSMA3),
              label="sma3")
#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("SMA.sma1", "SMA.sma2"), 
                          relationship="gt"),
           label="upTrend")

add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("SMA.sma3", "SMA.sma1"), 
                          relationship="lt"),
           label="pullback")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="hammer.hammer", threshold=.5, 
                          relationship="gt", cross=TRUE),
           label="hammerDay")

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

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("SMA.sma1", "SMA.sma2"), 
                          relationship="lt"),
           label="SMAexit")
#rules
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        osFUN=osDollarATR,
                        tradeSize=tradeSize, 
                        prefer="High",
                        pctATR=pctATR,
                        atrMod="X",
                        orderset="orders"), 
         type="enter", path.dep=TRUE,
         label="hammerEntry")
 
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$SL.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="stopLossLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="limit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$TP.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="takeProfitLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal",
         arguments=list(sigcol="SMAexit",
                        sigval=TRUE,
                        ordertype="market",
                        orderside="long",
                        replace=TRUE,
                        orderqty='all',
                        prefer='Open',
                        orderset='orders'
                        ),
         type='exit',
         label='SMAexitLong',
         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)

I added one additional rule to the strategy in that if the trend reverses (SMA10 < SMA30), to get out of the trade.

First off, let's take a closer look at the entry and exit rules.

#rules
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        osFUN=osDollarATR,
                        tradeSize=tradeSize, 
                        prefer="High",
                        pctATR=pctATR,
                        atrMod="X",
                        orderset="orders"), 
         type="enter", path.dep=TRUE,
         label="hammerEntry")
 
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$SL.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="stopLossLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="limit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$TP.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="takeProfitLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal",
         arguments=list(sigcol="SMAexit",
                        sigval=TRUE,
                        ordertype="market",
                        orderside="long",
                        replace=TRUE,
                        orderqty='all',
                        prefer='Open',
                        orderset='orders'
                        ),
         type='exit',
         label='SMAexitLong',
         path.dep=TRUE)

The rules used here use a few new concepts that I haven't used in previous blog posts. First off, the argument of orderset puts all the orders within one order set as a one-canceling-the-other mechanism. Next, the order.price syntax works similarly to the market data syntax on specifying indicators — EG add.indicator(strategy.st, name=”SMA”, arguments=list(x=quote(Cl(mktdata)), etc…), except this time, it specifies a certain column in the market data (which is, in fact, what Cl(mktdata) does, or HLC(mktdata), and so on), but also, the [timestamp] syntax is necessary so it knows what specific quantity in time is being referred to.

For take-profit orders, as you want to sell above the market, or buy below the market, the correct type of order (that is, the ordertype argument) is a limit order. With stop-losses or trailing stops (not shown here), since you want to sell below the market or buy above the market, the correct ordertype is a stoplimit order.

Finally, the rule I added (the SMA exit) actually improves the strategy's performance (I wanted to give this system the benefit of the doubt).

Here are the results, with the strategy leveraged up to .1 pctATR (the usual strategies I test range between .02 and .04):

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

print(t(durStats))
      [,1]
Min      1
Q1       1
Med      4
Mean     5
Q3       7
Max     56
WMin     1
WQ1      2
WMed     4
WMean    6
WQ3      7
WMax    56
LMin     1
LQ1      1
LMed     3
LMean    5
LQ3      6
LMax    42

> print(mktExposure)
   Symbol MktExposure
1     EFA       0.023
2     EPP       0.019
3     EWA       0.026
4     EWC       0.015
5     EWG       0.019
6     EWH       0.023
7     EWJ       0.017
8     EWS       0.024
9     EWT       0.022
10    EWU       0.025
11    EWY        0.02
12    EWZ       0.019
13    EZU       0.023
14    IEF        0.01
15    IGE       0.022
16    IYR        0.02
17    IYZ       0.024
18    LQD       0.022
19    RWR       0.023
20    SHY       0.017
21    TLT       0.007
22    XLB       0.016
23    XLE       0.021
24    XLF       0.012
25    XLI       0.022
26    XLK       0.019
27    XLP       0.023
28    XLU       0.022
29    XLV        0.02
30    XLY       0.018
> print(mean(as.numeric(as.character(mktExposure$MktExposure))))
[1] 0.01976667

> SharpeRatio.annualized(portfRets)
                                    [,1]
Annualized Sharpe Ratio (Rf=0%) 1.027048
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.06408888
> maxDrawdown(portfRets)
[1] 0.09036151

> round(apply.yearly(dailyRetComparison, Return.cumulative),3)
           strategy    SPY
2003-12-31    0.179  0.369
2004-12-31    0.075  0.079
2005-12-30   -0.036  0.025
2006-12-29    0.143  0.132
2007-12-31    0.121  0.019
2008-12-31   -0.042 -0.433
2009-12-31    0.066  0.192
2010-12-31    0.135  0.110
2011-12-30    0.057 -0.028
2012-12-31    0.039  0.126
2013-12-31   -0.023  0.289
2014-08-06    0.048  0.036
> round(apply.yearly(dailyRetComparison, SharpeRatio.annualized),3)
           strategy    SPY
2003-12-31    2.971  3.100
2004-12-31    1.039  0.706
2005-12-30   -0.774  0.238
2006-12-29    2.355  1.312
2007-12-31    2.024  0.123
2008-12-31   -0.925 -1.050
2009-12-31    1.026  0.719
2010-12-31    2.504  0.614
2011-12-30    0.644 -0.122
2012-12-31    0.640  0.990
2013-12-31   -0.520  2.594
2014-08-06    1.171  0.586
> round(apply.yearly(dailyRetComparison, maxDrawdown),3)
           strategy   SPY
2003-12-31    0.030 0.056
2004-12-31    0.058 0.085
2005-12-30    0.046 0.074
2006-12-29    0.035 0.077
2007-12-31    0.039 0.102
2008-12-31    0.061 0.520
2009-12-31    0.044 0.280
2010-12-31    0.029 0.167
2011-12-30    0.069 0.207
2012-12-31    0.057 0.099
2013-12-31    0.071 0.062
2014-08-06    0.032 0.058

In short, looking at the trade stats, this system is…far from what was advertised. In fact, here's the equity curve.

Anything but spectacular the past several years, which is why I suppose it was free to give it away in a webinar. Overall, however, the past several years have just seen the S&P just continue to catch up to this strategy. At the end of the day, it’s a highly unimpressive system in my opinion, and I won’t be exploring the other aspects of it further. However, as an exercise in showing some nuanced features of quantstrat, I think this was a worthwhile endeavor.

Thanks for reading.

A John Ehlers oscillator — Cycle RSI(2)

Since I’ve hit a rut in trend following (how do you quantify rising/falling/flat? What even defines those three terms in precise, machine definition? How do you avoid buying tops while not getting chopped by whipsaws?), I decided to look the other way, with oscillators. Certainly, I’m not ready to give up on Dr. Ehlers just yet. So, in this post, I’ll introduce a recent innovation of the RSI by Dr. John Ehlers.

The indicator is Dr. Ehlers’s modified RSI from Chapter 7 of Cycle Analytics for Traders.

For starters, here’s how the Ehlers RSI is different than the usual ones: it gets filtered with a high-pass filter and then smoothed with a supersmoother filter. While Michael Kapler also touched on this topic a while back, I suppose it can’t hurt if I attempted to touch on it myself.

Here is the high pass filter and the super smoother, from the utility.R file in DSTrading. They’re not exported since as of the moment, they’re simply components of other indicators.

highPassFilter <- function(x) {
  alpha1 <- (cos(.707*2*pi/48)+sin(.707*2*pi/48)-1)/cos(.707*2*pi/48)
  HP <- (1-alpha1/2)*(1-alpha1/2)*(x-2*lag(x)+lag(x,2))
  HP <- HP[-c(1,2)]
  HP <- filter(HP, c(2*(1-alpha1), -1*(1-alpha1)*(1-alpha1)), method="recursive")
  HP <- c(NA, NA, HP)
  HP <- xts(HP, order.by=index(x))
  return(HP)
}

superSmoother <- function(x) {
  a1 <- exp(-1.414*pi/10)
  b1 <- 2*a1*cos(1.414*pi/10)
  c2 <- b1
  c3 <- -a1*a1
  c1 <- 1-c2-c3
  filt <- c1*(x+lag(x))/2
  leadNAs <- sum(is.na(filt))
  filt <- filt[-c(1:leadNAs)]
  filt <- filter(filt, c(c2, c3), method="recursive")
  filt <- c(rep(NA,leadNAs), filt)
  filt <- xts(filt, order.by=index(x))
}

In a nutshell, both of these functions serve to do an exponential smoothing on the data using some statically computed trigonometric quantities, the rationale of which I will simply defer to Dr. Ehlers’s book (link here).

Here’s the modified ehlers RSI, which I call CycleRSI, from the book in which it’s defined:

"CycleRSI" <- function(x, n=20) {
  filt <- superSmoother(highPassFilter(x))
  diffFilt <- diff(filt)
  posDiff <- negDiff <- diffFilt
  posDiff[posDiff < 0] <- 0
  negDiff[negDiff > 0] <- 0
  negDiff <- negDiff*-1
  posSum <- runSum(posDiff, n)
  negSum <- runSum(negDiff, n)
  denom <- posSum+negSum
  rsi <- posSum/denom
  rsi <- superSmoother(rsi)*100
  colnames(rsi) <- "CycleRSI"
  return(rsi)
}

Here’s a picture comparing four separate RSIs.

The first is the RSI featured in this post (cycle RSI) in blue. The next is the basic RSI(2) in red. The one after that is Larry Connors’s Connors RSI , which may be touched on in the future, and the last one, in purple, is the generalized Laguerre RSI, which is yet another Dr. Ehlers creation (which I’ll have to test sometime in the future).

To start things off with the Cycle RSI, I decided to throw a simple strategy around it:

Buy when the CycleRSI(2) crosses under 10 when the close is above the SMA200, which is in the vein of a Larry Connors trading strategy from “Short Term ETF Trading Strategies That Work” (whether they work or not remains debatable), and sell when the CycleRSI(2) crosses above 70, or when the close falls below the SMA200 so that the strategy doesn’t get caught in a runaway downtrend.

Since the strategy comes from an ETF Trading book, I decided to use my old ETF data set, from 2003 through 2010.

Here’s the strategy code, as usual:

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

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

source("demoData.R")

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

strategy.st <- portfolio.st <- account.st <- "Cycle_RSI_I"
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
nRSI=2
RSIentry=10
RSIexit=70

nSMA=200

period=10
pctATR=.04

#indicators
add.indicator(strategy.st, name="lagATR", 
              arguments=list(HLC=quote(HLC(mktdata)), n=period), 
              label="atrX")
add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), n=nSMA),
              label="SMA")
add.indicator(strategy.st, name="CycleRSI",
              arguments=list(x=quote(Cl(mktdata)), n=nRSI),
              label="RSI")

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

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="CycleRSI.RSI", threshold=RSIentry, 
                          relationship="lt", cross=FALSE),
           label="RSIltEntryThresh")

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

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

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

#rules
#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="exitSMA", 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)

And here are the results:

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

> print(t(durStats))
      [,1]
Min      1
Q1       6
Med      9
Mean    11
Q3      14
Max     43
WMin     1
WQ1      7
WMed     9
WMean   11
WQ3     13
WMax    40
LMin     1
LQ1      6
LMed    11
LMean   12
LQ3     15
LMax    43

> print(mean(as.numeric(as.character(mktExposure$MktExposure))))
[1] 0.2806

> mean(corMeans)
[1] 0.2763

> SharpeRatio.annualized(portfRets)
                                    [,1]
Annualized Sharpe Ratio (Rf=0%) 1.215391
> Return.annualized(portfRets)
                       [,1]
Annualized Return 0.1634448
> maxDrawdown(portfRets)
[1] 0.1694307

Overall, the statistics don’t look bad. However, the 1:1 annualized returns to max drawdown isn’t particularly pleasing, as it means that this strategy can’t be leveraged effectively to continue getting outsized returns in this state. Quite irritating. Here’s the equity curve.

In short, as with other mean reverters, when drawdowns happen, they happen relatively quickly and brutally.

Here’s an individual instrument position chart.

By the looks of things, the strategy does best in a market that grinds upwards, rather than a completely choppy sideways market.

Finally, here’s some code for charting all of the different trades.

agg.chart.ME <- function(Portfolio, Symbols, type=c("MAE", "MFE"), scale=c("cash", "percent", "tick")) {
  type=type[1]
  scale=scale[1]
  trades <- list()
  length(trades) <- length(Symbols)
  for(Symbol in Symbols) {
    trades[[Symbol]] <- pts <- perTradeStats(Portfolio=Portfolio, Symbol=Symbol, includeOpenTrade=FALSE)
  }
  trades <- do.call(rbind, trades)
  trades$Pct.Net.Trading.PL <- 100 * trades$Pct.Net.Trading.PL
  trades$Pct.MAE <- 100 * trades$Pct.MAE
  trades$Pct.MFE <- 100 * trades$Pct.MFE
  profitable <- (trades$Net.Trading.PL > 0)
  switch(scale, cash = {
    .ylab <- "Profit/Loss (cash)"
    if (type == "MAE") {
      .cols <- c("MAE", "Net.Trading.PL")
      .xlab <- "Drawdown (cash)"
      .main <- "Maximum Adverse Excursion (MAE)"
    } else {
      .cols <- c("MFE", "Net.Trading.PL")
      .xlab <- "Run Up (cash)"
      .main <- "Maximum Favourable Excursion (MFE)"
    }
  }, percent = {
    .ylab <- "Profit/Loss (%)"
    if (type == "MAE") {
      .cols <- c("Pct.MAE", "Pct.Net.Trading.PL")
      .xlab <- "Drawdown (%)"
      .main <- "Maximum Adverse Excursion (MAE)"
    } else {
      .cols <- c("Pct.MFE", "Pct.Net.Trading.PL")
      .xlab <- "Run Up (%)"
      .main <- "Maximum Favourable Excursion (MFE)"
    }
  }, tick = {
    .ylab <- "Profit/Loss (ticks)"
    if (type == "MAE") {
      .cols <- c("tick.MAE", "tick.Net.Trading.PL")
      .xlab <- "Drawdown (ticks)"
      .main <- "Maximum Adverse Excursion (MAE)"
    } else {
      .cols <- c("tick.MFE", "tick.Net.Trading.PL")
      .xlab <- "Run Up (ticks)"
      .main <- "Maximum Favourable Excursion (MFE)"
    }
  })
  .main <- paste("All trades", .main)
  plot(abs(trades[, .cols]), type = "n", xlab = .xlab, ylab = .ylab, 
       main = .main)
  grid()
  points(abs(trades[profitable, .cols]), pch = 24, col = "green", 
         bg = "green", cex = 0.6)
  points(abs(trades[!profitable, .cols]), pch = 25, col = "red", 
         bg = "red", cex = 0.6)
  abline(a = 0, b = 1, lty = "dashed", col = "darkgrey")
  legend(x = "bottomright", inset = 0.1, legend = c("Profitable Trade", 
                                                    "Losing Trade"), pch = c(24, 25), col = c("green", "red"), 
         pt.bg = c("green", "red"))
}

And the resulting plot:

One last thing to note…that $50,000 trade in the upper left hand corner? That was a yahoo data issue and is a false print. Beyond that, once again, this seems like standard fare for a mean reverter–when trades go bad, they’re *really* bad, but the puzzle of where to put a stop is a completely separate issue, as it usually means locking in plenty of losses that decrease in magnitude, along with possibly turning winners into losers. On the flip side, here’s the maximum favorable excursion plot.

In short, there are definitely trades that could have been stopped for a profit that turned into losers.

In conclusion, while the initial trading system seems to be a good start, it’s far from complete.

Thanks for reading.

Another Failed Volatility Histeresis: Ehlers’s Own Idea

This week, I attempted to use Ehlers’s own idea from this presentation.

Essentially, the idea is that when an indicator is flat, line crossings can produce whipsaws, so add a fraction of the daily range to the lagged indicator, and see if the non-lagged indicator crosses the threshold. In this case, it’s an exponentially smoothed daily range that’s used to compute the bands. I ran this from 2012 through the present day at the time of this writing (July 14, 2014), as the original link goes through most of the 2000s. (Also, be sure you’re using my most up-to-date IKTrading package, as I updated the quandClean function to deal with some intraday messy data issues that had gone unnoticed before.)

The settings I used were John Ehlers’s original settings — that is, a 20 day analysis period, a 10 day exponential band smoothing (that is, the band is computed as .1*(high-low)+.9*band), entered upon the percent B (that is, the current FRAMA minus the low band over the difference of the bands), and the fraction is 1/10th of the daily range.

Here’s the indicator used:

FRAMAbands <- function(HLC, n=126, FC=1, SC=300, nBands=n/2, bandFrac=10, ...) {
  frama <- FRAMA(HLC, n=n, FC=FC, SC=SC, ...)
  band <- Hi(HLC) - Lo(HLC)
  band <- xts(filter(1/nBands*band, 1-1/nBands, method="recursive"), order.by=index(frama))
  bandUp <- frama$trigger + band/bandFrac
  bandDn <- frama$trigger - band/bandFrac
  pctB <- (frama$FRAMA-bandDn)/(bandUp-bandDn)
  out <- cbind(frama, pctB)
  colnames(out) <- c("FRAMA", "trigger", "pctB")
  return(out)
}

And here’s the strategy code:

source("futuresData.R")

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

strategy.st <- portfolio.st <- account.st <- "FRAMA_BANDS_I"
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
FC = 1
SC = 300
n = 20
triggerLag = 1
nBands = 10
bandFrac=10
entryThreshPctB=1
exitThreshPctB=.5

period=10
pctATR=.06

#indicators
add.indicator(strategy.st, name="FRAMAbands",
              arguments=list(HLC=quote(HLC(mktdata)), FC=FC, SC=SC, 
                             n=n, triggerLag=triggerLag, nBands=nBands,
                             bandFrac=bandFrac),
              label="Fbands")

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

#signals
add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="pctB.Fbands", 
                          threshold=entryThreshPctB, 
                          relationship="gt", cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="pctB.Fbands", 
                          threshold=exitThreshPctB, 
                          relationship="lt", cross=TRUE),
           label="longExit")

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

#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 the results:

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

> print(t(durStats))
      [,1]
Min      1
Q1       2
Med      6
Mean     9
Q3      14
Max     65
WMin     1
WQ1      3
WMed    13
WMean   13
WQ3     19
WMax    65
LMin     1
LQ1      2
LMed     4
LMean    6
LQ3      8
LMax    57

mean(corMeans)
[1] 0.08232023

> SharpeRatio.annualized(portfRets)
                                      [,1]
Annualized Sharpe Ratio (Rf=0%) -0.2476826
> Return.annualized(portfRets)
                         [,1]
Annualized Return -0.03485231
> maxDrawdown(portfRets)
[1] 0.2632001

In short, it’s a loser over the past three years. Here’s the equity curve:

Now while it may have worked in the past (or something similar to it, using Ehlers’s filter indicator), it doesn’t seem to do so going forward.

I’ll leave this here for now as a demonstration of how to do Ehlers bands.

Thanks for reading.

Volatility Histeresis: A First Attempt

So the last time that a FRAMA strategy was tried with price crossovers, the problem was that due to counter-trending failures, the filter that was added missed a lot of good trades, and wound up losing a lot of money during flat markets that passed the arbitrary filter.

This trading system tries to rectify those issues by trading a rising FRAMA filtered on a 5-day standard deviation ratio.

The hypothesis is this: the FRAMA rises in legitimately trending markets, and stays flat in choppy markets. Therefore, the ratio of standard deviations (that is, a running standard deviation of the FRAMA over the standard deviation of the market close) should be higher during trending markets, and lower during choppy markets. Additionally, as this ratio bottoms out at zero and usually tops out at 1 (rarely gets higher), it can be used as an indicator across instruments of vastly different properties.

The data that will be used will be the quandl futures data file (without federal funds, coffee, or sugar, because of data issues).

Here’s the data file:

require(IKTrading)


currency('USD')
Sys.setenv(TZ="UTC")


t1 <- Sys.time()
if(!"CME_CL" %in% ls()) {
  #Energies
  CME_CL <- quandClean("CHRIS/CME_CL", start_date=from, end_date=to, verbose=verbose) #Crude
  CME_NG <- quandClean("CHRIS/CME_NG", start_date=from, end_date=to, verbose=verbose) #NatGas
  CME_HO <- quandClean("CHRIS/CME_HO", start_date=from, end_date=to, verbose=verbose) #HeatingOil
  CME_RB <- quandClean("CHRIS/CME_RB", start_date=from, end_date=to, verbose=verbose) #Gasoline
  ICE_B <- quandClean("CHRIS/ICE_B", start_date=from, end_date=to, verbose=verbose) #Brent
  ICE_G <- quandClean("CHRIS/ICE_G", start_date=from, end_date=to, verbose=verbose) #Gasoil
  
  #Grains
  CME_C <- quandClean("CHRIS/CME_C", start_date=from, end_date=to, verbose=verbose) #Chicago Corn
  CME_S <- quandClean("CHRIS/CME_S", start_date=from, end_date=to, verbose=verbose) #Chicago Soybeans
  CME_W <- quandClean("CHRIS/CME_W", start_date=from, end_date=to, verbose=verbose) #Chicago Wheat
  CME_SM <- quandClean("CHRIS/CME_SM", start_date=from, end_date=to, verbose=verbose) #Chicago Soybean Meal
  CME_KW <- quandClean("CHRIS/CME_KW", start_date=from, end_date=to, verbose=verbose) #Kansas City Wheat
  CME_BO <- quandClean("CHRIS/CME_BO", start_date=from, end_date=to, verbose=verbose) #Chicago Soybean Oil
  
  #Softs
  #ICE_SB <- quandClean("CHRIS/ICE_SB", start_date=from, end_date=to, verbose=verbose) #Sugar
  #Sugar 2007-03-26 is wrong
  #ICE_KC <- quandClean("CHRIS/ICE_KC", start_date=from, end_date=to, verbose=verbose) #Coffee
  #Coffee January of 08 is FUBAR'd
  ICE_CC <- quandClean("CHRIS/ICE_CC", start_date=from, end_date=to, verbose=verbose) #Cocoa
  ICE_CT <- quandClean("CHRIS/ICE_CT", start_date=from, end_date=to, verbose=verbose) #Cotton
  
  #Other Ags
  CME_LC <- quandClean("CHRIS/CME_LC", start_date=from, end_date=to, verbose=verbose) #Live Cattle
  CME_LN <- quandClean("CHRIS/CME_LN", start_date=from, end_date=to, verbose=verbose) #Lean Hogs
  
  #Precious Metals
  CME_GC <- quandClean("CHRIS/CME_GC", start_date=from, end_date=to, verbose=verbose) #Gold
  CME_SI <- quandClean("CHRIS/CME_SI", start_date=from, end_date=to, verbose=verbose) #Silver
  CME_PL <- quandClean("CHRIS/CME_PL", start_date=from, end_date=to, verbose=verbose) #Platinum
  CME_PA <- quandClean("CHRIS/CME_PA", start_date=from, end_date=to, verbose=verbose) #Palladium
  
  #Base
  CME_HG <- quandClean("CHRIS/CME_HG", start_date=from, end_date=to, verbose=verbose) #Copper
  
  #Currencies
  CME_AD <- quandClean("CHRIS/CME_AD", start_date=from, end_date=to, verbose=verbose) #Ozzie
  CME_CD <- quandClean("CHRIS/CME_CD", start_date=from, end_date=to, verbose=verbose) #Loonie
  CME_SF <- quandClean("CHRIS/CME_SF", start_date=from, end_date=to, verbose=verbose) #Franc
  CME_EC <- quandClean("CHRIS/CME_EC", start_date=from, end_date=to, verbose=verbose) #Euro
  CME_BP <- quandClean("CHRIS/CME_BP", start_date=from, end_date=to, verbose=verbose) #Cable
  CME_JY <- quandClean("CHRIS/CME_JY", start_date=from, end_date=to, verbose=verbose) #Yen
  CME_NE <- quandClean("CHRIS/CME_NE", start_date=from, end_date=to, verbose=verbose) #Kiwi
  
  #Equities
  CME_ES <- quandClean("CHRIS/CME_ES", start_date=from, end_date=to, verbose=verbose) #Emini
  CME_MD <- quandClean("CHRIS/CME_MD", start_date=from, end_date=to, verbose=verbose) #Midcap 400
  CME_NQ <- quandClean("CHRIS/CME_NQ", start_date=from, end_date=to, verbose=verbose) #Nasdaq 100
  CME_TF <- quandClean("CHRIS/CME_TF", start_date=from, end_date=to, verbose=verbose) #Russell Smallcap
  CME_NK <- quandClean("CHRIS/CME_NK", start_date=from, end_date=to, verbose=verbose) #Nikkei
  
  #Dollar Index and Bonds/Rates
  ICE_DX  <- quandClean("CHRIS/CME_DX", start_date=from, end_date=to, verbose=verbose) #Dixie
  #CME_FF  <- quandClean("CHRIS/CME_FF", start_date=from, end_date=to, verbose=verbose) #30-day fed funds
  CME_ED  <- quandClean("CHRIS/CME_ED", start_date=from, end_date=to, verbose=verbose) #3 Mo. Eurodollar/TED Spread
  CME_FV  <- quandClean("CHRIS/CME_FV", start_date=from, end_date=to, verbose=verbose) #Five Year TNote
  CME_TY  <- quandClean("CHRIS/CME_TY", start_date=from, end_date=to, verbose=verbose) #Ten Year Note
  CME_US  <- quandClean("CHRIS/CME_US", start_date=from, end_date=to, verbose=verbose) #30 year bond
}

CMEinsts <- c("CL", "NG", "HO", "RB", "C", "S", "W", "SM", "KW", "BO", "LC", "LN", "GC", "SI", "PL", 
              "PA", "HG", "AD", "CD", "SF", "EC", "BP", "JY", "NE", "ES", "MD", "NQ", "TF", "NK", #"FF",
              "ED", "FV", "TY", "US")

ICEinsts <- c("B", "G", #"SB", #"KC", 
              "CC", "CT", "DX")
CME <- paste("CME", CMEinsts, sep="_")
ICE <- paste("ICE", ICEinsts, sep="_")
symbols <- c(CME, ICE)
stock(symbols, currency="USD", multiplier=1)
t2 <- Sys.time()
print(t2-t1)

Here’s the strategy:

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

initDate="1990-01-01"
from="2000-03-01"
to="2011-12-31"
options(width=70)
verose=TRUE

FRAMAsdr <- function(HLC, n, FC, SC, nSD, ...) {
  frama <- FRAMA(HLC, n=n, FC=FC, SC=SC, ...)
  sdr <- runSD(frama$FRAMA, n=nSD)/runSD(Cl(HLC), n=nSD)
  sdr[sdr > 2]  <- 2
  out <- cbind(FRAMA=frama$FRAMA, trigger=frama$trigger, sdr=sdr)
  out
}

source("futuresData.R")

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

strategy.st <- portfolio.st <- account.st <- "FRAMA_SDR_I"
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
FC = 1
SC = 300
n = 126
triggerLag = 1
nSD = 5
sdThresh <- .3

period=10
pctATR=.02

#indicators
add.indicator(strategy.st, name="FRAMAsdr",
              arguments=list(HLC=quote(HLC(mktdata)), FC=FC, SC=SC, 
                             n=n, triggerLag=triggerLag, nSD=nSD),
              label="SDR")

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

#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("FRAMA.SDR", "trigger.SDR"), relationship="gt"),
           label="FRAMAup")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="sdr.SDR", threshold=sdThresh, 
                          relationship="gt",cross=FALSE),
           label="SDRgtThresh")

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

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("FRAMA.SDR", "trigger.SDR"), relationship="lt"),
           label="FRAMAdnExit")

#add.signal(strategy.st, name="sigThreshold",
#           arguments=list(column="sdr.SDR", threshold=sdThresh, relationship="lt", cross=TRUE),
#           label="SDRexit")

#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="FRAMAdnExit", 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="SDRexit", 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)

Notice that the exit due to the volatility filter had to have been commented out (as it caused the strategy to lose all its edge). In any case, the FRAMA is the usual 126 day FRAMA, and the running standard deviation is 5 days, in order to try and reduce lag. The standard deviation ratio threshold will be .2 or higher. Here are the results:

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

In other words, typical trend follower results. 40/60 wrong to right, with a 2:1 win to loss ratio. Far from spectacular.

Duration statistics:

print(t(durStats))
      [,1]
Min      1
Q1       2
Med      5
Mean    11
Q3      11
Max    158
WMin     1
WQ1      5
WMed    10
WMean   18
WQ3     21
WMax   158
LMin     1
LQ1      1
LMed     3
LMean    6
LQ3      6
LMax    93

In short, winners last longer than losers, which makes sense given that there are a lot of whipsaws, and that this is a trend-following strategy.

Market exposure:

> print(mean(as.numeric(as.character(mktExposure$MktExposure))))
[1] 0.3820789

38%. So how did it perform?

Like this. Not particularly great, considering it’s a 60% gain over 11 years. Here are the particular statistics:

> SharpeRatio.annualized(portfRets)
                                     [,1]
Annualized Sharpe Ratio (Rf=0%) 0.8124137
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.04229355
> maxDrawdown(portfRets)
[1] 0.07784351

In other words, about 10 basis points of returns per percent of market exposure, or a 10% annualized return. The problem being? The drawdown is much higher than the annualized return, meaning that leverage will only make things worse. Basically, for the low return on exposure and high drawdown to annualized return, this strategy is a failure. While the steadily ascending equity curve is good, it is meaningless when the worst losses take more than a year to recover from.

In any case, here’s a look at some individual instruments.

Here’s the equity curve for the E-minis.

So first off, we can see one little feature of this strategy–due to the entry and exit not being symmetric (that is, it takes two conditions to enter–a rising FRAMA and a standard deviation ratio above .2–and only exits on one of them (falling FRAMA), price action that exhibits a steady grind upwards, due to the rapid change in ATR (it’s a 10-day figure) can actually slightly pyramid from time to time. This is a good feature in my opinion, since it can add onto a winning position. However, in times of extreme volatility, when even an adaptive indicator can get bounced around chasing “mini-trends”, we can see losses pile on.

Next, let’s look at a much worse situation. Here’s the equity curve for the Eurodollar/TED spread.

In this case, it’s clearly visible that the strategy has countertrend issues, as well as the fact that the 5-day standard deviation ratio can be relatively myopic when it comes to instruments that have protracted periods of complete inactivity–that is, the market is not even choppy so much as just still.

I’ll leave this here, and move onto other attempts at getting around this sort of roadblock next.

Thanks for reading.

Intermission: A Data File For Futures Data (from Quandl)

So between variations of different strategies, for those who have yet to come across it, my IKTrading package has a function called quandClean, which exists to get and clean daily futures data from quandl.com . The exact process can be found on the Revolution Analytics blog on this post.

While some of their futures data is quoted in other currencies, or has very short history, I’ve compiled a data file to get futures data that has long history.

Found there are price histories for ags, precious metals, forex, and more.

Here’s the code:

require(IKTrading)

currency('USD')
Sys.setenv(TZ="UTC")


t1 <- Sys.time()
if(!"CME_CL" %in% ls()) {
  #Energies
  CME_CL <- quandClean("CHRIS/CME_CL", start_date=from, end_date=to, verbose=verbose) #Crude
  CME_NG <- quandClean("CHRIS/CME_NG", start_date=from, end_date=to, verbose=verbose) #NatGas
  CME_HO <- quandClean("CHRIS/CME_HO", start_date=from, end_date=to, verbose=verbose) #HeatingOil
  CME_RB <- quandClean("CHRIS/CME_RB", start_date=from, end_date=to, verbose=verbose) #Gasoline
  ICE_B <- quandClean("CHRIS/ICE_B", start_date=from, end_date=to, verbose=verbose) #Brent
  ICE_G <- quandClean("CHRIS/ICE_G", start_date=from, end_date=to, verbose=verbose) #Gasoil
  
  #Grains
  CME_C <- quandClean("CHRIS/CME_C", start_date=from, end_date=to, verbose=verbose) #Chicago Corn
  CME_S <- quandClean("CHRIS/CME_S", start_date=from, end_date=to, verbose=verbose) #Chicago Soybeans
  CME_W <- quandClean("CHRIS/CME_W", start_date=from, end_date=to, verbose=verbose) #Chicago Wheat
  CME_SM <- quandClean("CHRIS/CME_SM", start_date=from, end_date=to, verbose=verbose) #Chicago Soybean Meal
  CME_KW <- quandClean("CHRIS/CME_KW", start_date=from, end_date=to, verbose=verbose) #Kansas City Wheat
  CME_BO <- quandClean("CHRIS/CME_BO", start_date=from, end_date=to, verbose=verbose) #Chicago Soybean Oil
  
  #Softs
  ICE_SB <- quandClean("CHRIS/ICE_SB", start_date=from, end_date=to, verbose=verbose) #Sugar
  ICE_KC <- quandClean("CHRIS/ICE_KC", start_date=from, end_date=to, verbose=verbose) #Coffee
  ICE_CC <- quandClean("CHRIS/ICE_CC", start_date=from, end_date=to, verbose=verbose) #Cocoa
  ICE_CT <- quandClean("CHRIS/ICE_CT", start_date=from, end_date=to, verbose=verbose) #Cotton
  
  #Other Ags
  CME_LC <- quandClean("CHRIS/CME_LC", start_date=from, end_date=to, verbose=verbose) #Live Cattle
  CME_LN <- quandClean("CHRIS/CME_LN", start_date=from, end_date=to, verbose=verbose) #Lean Hogs
  
  #Precious Metals
  CME_GC <- quandClean("CHRIS/CME_GC", start_date=from, end_date=to, verbose=verbose) #Gold
  CME_SI <- quandClean("CHRIS/CME_SI", start_date=from, end_date=to, verbose=verbose) #Silver
  CME_PL <- quandClean("CHRIS/CME_PL", start_date=from, end_date=to, verbose=verbose) #Platinum
  CME_PA <- quandClean("CHRIS/CME_PA", start_date=from, end_date=to, verbose=verbose) #Palladium
  
  #Base
  CME_HG <- quandClean("CHRIS/CME_HG", start_date=from, end_date=to, verbose=verbose) #Copper
  
  #Currencies
  CME_AD <- quandClean("CHRIS/CME_AD", start_date=from, end_date=to, verbose=verbose) #Ozzie
  CME_CD <- quandClean("CHRIS/CME_CD", start_date=from, end_date=to, verbose=verbose) #Loonie
  CME_SF <- quandClean("CHRIS/CME_SF", start_date=from, end_date=to, verbose=verbose) #Franc
  CME_EC <- quandClean("CHRIS/CME_EC", start_date=from, end_date=to, verbose=verbose) #Euro
  CME_BP <- quandClean("CHRIS/CME_BP", start_date=from, end_date=to, verbose=verbose) #Cable
  CME_JY <- quandClean("CHRIS/CME_JY", start_date=from, end_date=to, verbose=verbose) #Yen
  CME_NE <- quandClean("CHRIS/CME_NE", start_date=from, end_date=to, verbose=verbose) #Kiwi
  
  #Equities
  CME_ES <- quandClean("CHRIS/CME_ES", start_date=from, end_date=to, verbose=verbose) #Emini
  CME_MD <- quandClean("CHRIS/CME_MD", start_date=from, end_date=to, verbose=verbose) #Midcap 400
  CME_NQ <- quandClean("CHRIS/CME_NQ", start_date=from, end_date=to, verbose=verbose) #Nasdaq 100
  CME_TF <- quandClean("CHRIS/CME_TF", start_date=from, end_date=to, verbose=verbose) #Russell Smallcap
  CME_NK <- quandClean("CHRIS/CME_NK", start_date=from, end_date=to, verbose=verbose) #Nikkei
  
  #Dollar Index and Bonds/Rates
  ICE_DX  <- quandClean("CHRIS/CME_DX", start_date=from, end_date=to, verbose=verbose) #Dixie
  #CME_FF  <- quandClean("CHRIS/CME_FF", start_date=from, end_date=to, verbose=verbose) #30-day fed funds
  CME_ED  <- quandClean("CHRIS/CME_ED", start_date=from, end_date=to, verbose=verbose) #3 Mo. Eurodollar/TED Spread
  CME_FV  <- quandClean("CHRIS/CME_FV", start_date=from, end_date=to, verbose=verbose) #Five Year TNote
  CME_TY  <- quandClean("CHRIS/CME_TY", start_date=from, end_date=to, verbose=verbose) #Ten Year Note
  CME_US  <- quandClean("CHRIS/CME_US", start_date=from, end_date=to, verbose=verbose) #30 year bond
}

CMEinsts <- c("CL", "NG", "HO", "RB", "C", "S", "W", "SM", "KW", "BO", "LC", "LN", "GC", "SI", "PL", 
              "PA", "HG", "AD", "CD", "SF", "EC", "BP", "JY", "NE", "ES", "MD", "NQ", "TF", "NK", #"FF",
              "ED", "FV", "TY", "US")

ICEinsts <- c("B", "G", "SB", "KC", "CC", "CT", "DX")
CME <- paste("CME", CMEinsts, sep="_")
ICE <- paste("ICE", ICEinsts, sep="_")
symbols <- c(CME, ICE)
stock(symbols, currency="USD", multiplier=1)
t2 <- Sys.time()
print(t2-t1)

Note that you need your own quandl authorization token. However, beyond that, this process takes around 5 minutes or so to complete, so similarly to my demoData.R file, it functions based off of whether or not CME_CL (that is, the price history for crude oil) is present in your working environment.

The from (“yyyy-mm-dd”), to (same), and verbose (TRUE or FALSE) variables will be variables to set in a demo file, so you’ll have to input them yourself. Beyond that, you simply source this file, and you’ll have a large amount of futures data on which to run trading strategies. They don’t necessarily even have to be quantstrat types of trading strategies, as these are simply xts objects. I commented out CME_FF because it generally is something that is characterized by rare spikes as opposed to steady and consistent price movements.

Granted, I cannot vouch that this data will be perfect (probably a long way from it, considering that quandl isn’t the greatest source of it), but it *is* free, so for anyone who wishes to do any backtesting on futures data, well, here you go. Also, I may edit which exact instruments I use in the future if there are continuing data issues.

Thanks for reading.

FRAMA Part V: Wrap-Up on Confirmatory Indicator/Test Sample

So, it is possible to create a trading system that can correctly isolate severe and protracted downturns, without taking (too many) false signals.

Here are the rules:

126 day FRAMA, FC=4, SC=300 (we’re still modifying the original ETFHQ strategy).
A running median (somewhere between 150 days and 252 days–seemingly, all these configurations work).

Both the FRAMA and the confirmatory median must be moving in the same direction (up for a long trade, down for a short trade–the median rising is the new rule here), and the price must cross the FRAMA in that direction to enter into a trade, while exiting when the price crosses below the FRAMA. Presented as a set of quantstrat rules, it gets rather lengthy, since a rule needs to specify the three setup conditions, a rule to bind them together, and an exit rule (5 rules each side).

The strategy works on both long and short ends, though the short version seems more of an insurance strategy than anything else. Here’s the equity curve for a 150 day median:

Basically, it makes money in terrible periods, but gives some of it back during just about any other time. It’s there just to put it out there as something that can finally try and isolate the truly despicable conditions and give you a pop in those times. Other than that? Using it would depend on how often someone believes those sorts of drawdown conditions would occur–that is, a descending adaptive indicator, a descending 7-12 month median.

Here are the trade and portfolio statistics:

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

                                      [,1]
Annualized Sharpe Ratio (Rf=0%) 0.07606299
> Return.annualized(portfRets)
                         [,1]
Annualized Return 0.004247807
> maxDrawdown(portfRets)
[1] 0.09845553

In other words, it’s absolutely not a standalone strategy, but more of a little something to give a long-only strategy a boost during bad times. It’s certainly not as spectacular as it gets. For instance, here’s the equity curve for XLK in late 2008-2009. Mainly, the problem with the ETFHQ strategy (I’m still on that, yes) is that it does not at all take into account the magnitude of the direction of the indicator. This means that in a reverting market, this strategy has a potential to lose a great deal of money unnecessarily.

Basically, this strategy is highly conservative, meaning that it has a tendency to miss good trades, take unnecessary ones, and is generally flawed because it has no way of really estimating the slope of the FRAMA.

As the possible solution to this involves a strategy by John Ehlers, I think I’ll leave this strategy here for now.

So, to send off this original ETFHQ price cross strategy off, I’ll test it out of sample using a 200-day median, using both long and short sides (from 2010-03-01 to get the 200 day median burned in, to the current date as of the time of this writing, 2014-06-20).

Here are the trade stats and portfolio stats:

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

> SharpeRatio.annualized(portfRets)
                                     [,1]
Annualized Sharpe Ratio (Rf=0%) 0.3290298
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.02467234
> maxDrawdown(portfRets)
[1] 0.1354166

With the corresponding equity curve:

In short, definitely not good. Why?

Here’s a good symptom as to why:

This is the out-of-sample equity curve of SHY–that is, the ETF of short term bonds. The trend had ended, but the trading system didn’t pick up on that.

In this case, you can see that the magnitude of the trend makes no difference to the strategy–which is a major problem. Although the counter-trend trading was eliminated, forcing action was not, and trying to remain loyal to the price crossing the indicator strategy while sticking to more conventional methods (a confirming indicator) turns out to be flawed. Here is another side symptom of a flawed system:

In this instance, using such a conservative confirmatory indicator for the short trade and simply using that same indicator for the long side indicates that there may very well have been overfitting on the system. On a more general note, however, this picture makes one wonder whether a confirmatory indicator was even necessary. For instance, there were certainly protracted periods during which there was a long trend that were cut off due to the running median being slightly negative. There were both long and short opportunities missed.

In my opinion, I think this puts the kibosh on something as ham-handed as a long-running confirmatory indicator. Why? Because I think that it over-corrects for a flawed order logic system that doesn’t take into account the magnitude of the slope of the indicator. Obviously, trading in a countertrend (descending indicator) is a terrible idea. But what about a slight change of directional sign as part of a greater counter-trend? Suddenly, a robust, deliberately lagging confirmatory indicator no longer seems like such a bad idea. However, as you can see, the downside of a lagging indicator is that it may very well lag your primary indicator in a good portion of cases. And it does nothing to eliminate sideways trading.

Surely, a more elegant solution exists that attempts to quantify the fact that sometimes, the smooth-yet-adaptive FRAMA can trend rapidly (and such trades should be taken posthaste), and can also go flat. Ultimately, I think that while the indicator settings from ETFHQ have some merit, the simplistic order logic on its own can certainly hurt–and coupled with an order-sizing function that downsizes orders in times of trending while magnifying them in times of calm (a side-effect of ATR, which was created to equalize risk across instruments, but with the unintended consequence of very much not equalizing risk across market conditions) can cause problems.

The next strategy will attempt to rectify these issues.

Thanks for reading.

FRAMA Part IV: Continuing the Long/Short Filter Search

This post examines an n-day median filter for two desirable properties: robustness to outliers and an inherent trend-confirming lag. While this is an incomplete filter (or maybe even inferior), it offers some key insights into improving the trading system.

The strategy will be thus:

First and foremost, this will be a short-only strategy, due to the long bias within the sample period, so the stress-test of the system will be to attempt to capture the non-dominant trend (and only when appropriate).

Here’s the strategy: we will continue to use the same 126 day FRAMA with the fast constant set at 4, and a slow constant at 300 (that is, it can oscillate anywhere between an EMA4 and EMA300). We will only enter into a short position when this indicator is descending, below the 126-day median of the price action, and when the price action is lower than this indicator (usually this means a cross, not in all cases though). We will exit when the price action rises back above the indicator.

Here’s the strategy in R code:

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

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

#to rerun the strategy, rerun everything below this line
source("demoData.R") #contains all of the data-related boilerplate.

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

strategy.st <- portfolio.st <- account.st <- "FRAMA_III"
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

FC=4
SC=300
n=126
triggerLag=1

period=10
pctATR=.02

#indicators 

add.indicator(strategy.st, name="FRAMA",
              arguments=list(HLC=quote(HLC(mktdata)), n=n, 
                             SC=SC, FC=FC, triggerLag=triggerLag),
              label="primary")

add.indicator(strategy.st, name="runMedian",
              arguments=list(x=quote(Cl(mktdata)), n=n),
              label="confirmatory")

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

# #long signals
# 
# add.signal(strategy.st, name="sigComparison",
#            arguments=list(columns=c("FRAMA.primary", "X1.confirmatory"), 
#                           relationship="gte"),
#            label="FRAMAgteMedian")
# 
# add.signal(strategy.st, name="sigComparison",
#            arguments=list(columns=c("FRAMA.primary", "trigger.primary"), 
#                           relationship="gte"),
#            label="FRAMArising")
# 
# add.signal(strategy.st, name="sigComparison",
#            arguments=list(columns=c("Close", "FRAMA.primary"), 
#                           relationship="gte"),
#            label="ClGtFRAMA")
# 
# add.signal(strategy.st, name="sigAND",
#            arguments=list(columns=c("FRAMAgteMedian", 
#                                     "FRAMArising", "ClGtFRAMA"), 
#                           cross=TRUE),
#            label="longEntry")
# 
# add.signal(strategy.st, name="sigCrossover",
#            arguments=list(columns=c("Close", "FRAMA.primary"), 
#                           relationship="lt"),
#            label="longExit")
# 
# #long 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)

#short signals

add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("FRAMA.primary", "X1.confirmatory"), 
                          relationship="lt"),
           label="FRAMAltMedian")

add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("FRAMA.primary", "trigger.primary"), 
                          relationship="lt"),
           label="FRAMAfalling")

add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "FRAMA.primary"), 
                          relationship="lt"),
           label="ClLtFRAMA")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("FRAMAltMedian", 
                                    "FRAMAfalling", "ClLtFRAMA"), 
                          cross=TRUE),
           label="shortEntry")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "FRAMA.primary"), 
                          relationship="gt"),
           label="shortExit")

#short rules

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="shortEntry", sigval=TRUE, ordertype="market", 
                        orderside="short", 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="shortExit", sigval=TRUE, orderqty="all", ordertype="market", 
                        orderside="short", 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)

The results aren’t pretty, meaning that the filter is still incomplete. Here are the trade stats:

                        EFA      EPP      EWA      EWC      EWG
Num.Txns              90.00    68.00    85.00    66.00    90.00
Num.Trades            44.00    34.00    41.00    33.00    45.00
Net.Trading.PL     -2030.68   -25.54 -1485.82 -2283.03  -356.83
Avg.Trade.PL         -46.15    -0.75   -36.24   -69.18    -7.93
Med.Trade.PL        -103.99   -44.95   -77.27  -100.40   -69.91
Largest.Winner      1238.56  1656.56  1106.59  2195.51  3197.29
Largest.Loser       -661.04  -786.27  -548.06  -823.55  -783.65
Gross.Profits       4336.92  4455.45  3246.48  3566.35  5948.76
Gross.Losses       -6367.61 -4480.99 -4732.30 -5849.38 -6305.59
Std.Dev.Trade.PL     364.75   419.42   288.36   487.05   557.98
Percent.Positive      29.55    32.35    43.90    24.24    37.78
Percent.Negative      70.45    67.65    56.10    75.76    62.22
Profit.Factor          0.68     0.99     0.69     0.61     0.94
Avg.Win.Trade        333.61   405.04   180.36   445.79   349.93
Med.Win.Trade         57.09   238.60    66.34   124.31   101.88
Avg.Losing.Trade    -205.41  -194.83  -205.75  -233.98  -225.20
Med.Losing.Trade    -156.80  -122.45  -170.49  -166.84  -184.70
Avg.Daily.PL         -46.15    -0.75   -36.24   -69.18    -7.93
Med.Daily.PL        -103.99   -44.95   -77.27  -100.40   -69.91
Std.Dev.Daily.PL     364.75   419.42   288.36   487.05   557.98
Ann.Sharpe            -2.01    -0.03    -2.00    -2.25    -0.23
Max.Drawdown       -5089.55 -3095.58 -3609.64 -4915.76 -4222.60
Profit.To.Max.Draw    -0.40    -0.01    -0.41    -0.46    -0.08
Avg.WinLoss.Ratio      1.62     2.08     0.88     1.91     1.55
Med.WinLoss.Ratio      0.36     1.95     0.39     0.75     0.55
Max.Equity           146.74  1473.19   834.54    78.43  1467.12
Min.Equity         -4942.81 -2562.72 -2775.10 -4837.32 -3960.49
End.Equity         -2030.68   -25.54 -1485.82 -2283.03  -356.83

                        EWH      EWJ      EWS      EWT      EWU
Num.Txns              74.00   102.00    72.00    66.00    72.00
Num.Trades            36.00    51.00    35.00    33.00    36.00
Net.Trading.PL       596.16 -1493.76   982.93  1354.40   439.00
Avg.Trade.PL          16.56   -29.29    28.08    41.04    12.19
Med.Trade.PL         -54.45   -89.63   -52.85   -40.22   -56.50
Largest.Winner      3436.43  1076.25  1165.10  1980.68  1680.33
Largest.Loser       -544.78  -781.15  -429.64  -441.47  -468.28
Gross.Profits       4519.53  5681.81  4763.28  4317.26  5105.73
Gross.Losses       -3923.37 -7175.57 -3780.35 -2962.87 -4666.73
Std.Dev.Trade.PL     610.29   368.61   353.59   408.77   415.23
Percent.Positive      38.89    37.25    37.14    42.42    33.33
Percent.Negative      61.11    62.75    62.86    57.58    66.67
Profit.Factor          1.15     0.79     1.26     1.46     1.09
Avg.Win.Trade        322.82   299.04   366.41   308.38   425.48
Med.Win.Trade         79.88    99.19   267.75   115.47   399.34
Avg.Losing.Trade    -178.33  -224.24  -171.83  -155.94  -194.45
Med.Losing.Trade    -137.71  -175.69  -141.06   -95.88  -180.13
Avg.Daily.PL          16.56   -29.29    28.08    41.04    12.19
Med.Daily.PL         -54.45   -89.63   -52.85   -40.22   -56.50
Std.Dev.Daily.PL     610.29   368.61   353.59   408.77   415.23
Ann.Sharpe             0.43    -1.26     1.26     1.59     0.47
Max.Drawdown       -2390.89 -2994.16 -1689.63 -2113.93 -3192.52
Profit.To.Max.Draw     0.25    -0.50     0.58     0.64     0.14
Avg.WinLoss.Ratio      1.81     1.33     2.13     1.98     2.19
Med.WinLoss.Ratio      0.58     0.56     1.90     1.20     2.22
Max.Equity          2534.61  1380.90  1938.04  2065.70  1845.45
Min.Equity         -2131.17 -2540.03 -1265.23 -1501.15 -3192.52
End.Equity           596.16 -1493.76   982.93  1354.40   439.00

                        EWY      EWZ      EZU      IEF      IGE
Num.Txns              68.00    80.00    96.00    63.00    56.00
Num.Trades            34.00    40.00    48.00    32.00    28.00
Net.Trading.PL      1359.59 -2763.77  -178.24 -5286.17  -588.44
Avg.Trade.PL          39.99   -69.09    -3.71  -165.19   -21.02
Med.Trade.PL          19.52  -103.71   -73.53  -253.57   -87.68
Largest.Winner      1799.34  2495.03  1423.73   908.54  2146.42
Largest.Loser       -467.07  -496.73  -847.67  -758.78  -466.57
Gross.Profits       4729.27  2790.33  5960.68  2309.18  2757.36
Gross.Losses       -3369.68 -5554.10 -6138.92 -7595.36 -3345.80
Std.Dev.Trade.PL     414.55   440.16   402.00   349.52   456.89
Percent.Positive      55.88    15.00    33.33    25.00    25.00
Percent.Negative      44.12    85.00    66.67    75.00    75.00
Profit.Factor          1.40     0.50     0.97     0.30     0.82
Avg.Win.Trade        248.91   465.05   372.54   288.65   393.91
Med.Win.Trade         58.75    43.02    67.59   156.68    43.03
Avg.Losing.Trade    -224.65  -163.36  -191.84  -316.47  -159.32
Med.Losing.Trade    -217.23  -110.98  -139.29  -284.87  -115.98
Avg.Daily.PL          39.99   -69.09    -3.71  -192.64   -21.02
Med.Daily.PL          19.52  -103.71   -73.53  -260.53   -87.68
Std.Dev.Daily.PL     414.55   440.16   402.00   318.33   456.89
Ann.Sharpe             1.53    -2.49    -0.15    -9.61    -0.73
Max.Drawdown       -2237.74 -3903.71 -3510.08 -6682.82 -2836.80
Profit.To.Max.Draw     0.61    -0.71    -0.05    -0.79    -0.21
Avg.WinLoss.Ratio      1.11     2.85     1.94     0.91     2.47
Med.WinLoss.Ratio      0.27     0.39     0.49     0.55     0.37
Max.Equity          3532.28   836.88  1270.40   669.24   709.44
Min.Equity          -790.83 -3066.84 -3222.22 -6013.57 -2127.36
End.Equity          1359.59 -2763.77  -178.24 -5286.17  -588.44

                        IYR      IYZ      LQD      RWR      SHY
Num.Txns              96.00   108.00    63.00    98.00    51.00
Num.Trades            48.00    54.00    31.00    49.00    25.00
Net.Trading.PL     -3444.89 -2032.70  1532.27 -3740.29 -4049.16
Avg.Trade.PL         -71.77   -37.64    49.43   -76.33  -161.97
Med.Trade.PL        -129.99   -83.00   -84.44  -114.84  -141.20
Largest.Winner      1714.13  2673.04  2693.04  1455.78    86.02
Largest.Loser       -745.50  -463.08  -480.73  -578.29  -644.17
Gross.Profits       4652.33  4978.26  5114.62  3534.95   365.46
Gross.Losses       -8097.22 -7010.97 -3582.35 -7275.24 -4414.63
Std.Dev.Trade.PL     405.93   479.46   604.30   341.37   195.72
Percent.Positive      22.92    22.22    35.48    16.33    28.00
Percent.Negative      77.08    77.78    64.52    83.67    72.00
Profit.Factor          0.57     0.71     1.43     0.49     0.08
Avg.Win.Trade        422.94   414.86   464.97   441.87    52.21
Med.Win.Trade        110.81    29.28   139.76   188.82    44.33
Avg.Losing.Trade    -218.84  -166.93  -179.12  -177.44  -245.26
Med.Losing.Trade    -182.73  -134.02  -129.79  -138.78  -232.61
Avg.Daily.PL         -71.77   -37.64    45.47   -76.33  -162.83
Med.Daily.PL        -129.99   -83.00   -86.18  -114.84  -144.12
Std.Dev.Daily.PL     405.93   479.46   614.22   341.37   199.88
Ann.Sharpe            -2.81    -1.25     1.18    -3.55   -12.93
Max.Drawdown       -3857.85 -5575.45 -2876.51 -4695.60 -4049.16
Profit.To.Max.Draw    -0.89    -0.36     0.53    -0.80    -1.00
Avg.WinLoss.Ratio      1.93     2.49     2.60     2.49     0.21
Med.WinLoss.Ratio      0.61     0.22     1.08     1.36     0.19
Max.Equity           260.07   118.92  3375.06   302.96     0.00
Min.Equity         -3597.77 -5456.52 -2138.62 -4392.65 -4049.16
End.Equity         -3444.89 -2032.70  1532.27 -3740.29 -4049.16

                        TLT      XLB      XLE      XLF      XLI
Num.Txns              85.00   104.00    50.00   120.00    92.00
Num.Trades            43.00    51.00    25.00    60.00    46.00
Net.Trading.PL     -4037.97 -5591.16  -308.15 -3036.79 -2136.85
Avg.Trade.PL         -93.91  -109.63   -12.33   -50.61   -46.45
Med.Trade.PL        -133.03  -138.47   -96.47   -79.48  -108.98
Largest.Winner      1425.91  1831.45  1828.51  1058.03  1218.87
Largest.Loser       -543.28  -707.20  -430.13  -711.69  -632.77
Gross.Profits       3355.40  3130.31  2472.08  5282.27  4597.82
Gross.Losses       -7393.37 -8721.48 -2780.24 -8319.05 -6734.68
Std.Dev.Trade.PL     338.88   345.20   420.71   309.84   342.80
Percent.Positive      25.58    25.49    20.00    30.00    23.91
Percent.Negative      74.42    74.51    80.00    70.00    76.09
Profit.Factor          0.45     0.36     0.89     0.63     0.68
Avg.Win.Trade        305.04   240.79   494.42   293.46   417.98
Med.Win.Trade        168.50    83.98    33.87   135.46   294.74
Avg.Losing.Trade    -231.04  -229.51  -139.01  -198.07  -192.42
Med.Losing.Trade    -197.38  -207.82  -120.99  -171.67  -149.06
Avg.Daily.PL        -101.44  -109.63   -12.33   -50.61   -46.45
Med.Daily.PL        -140.48  -138.47   -96.47   -79.48  -108.98
Std.Dev.Daily.PL     339.33   345.20   420.71   309.84   342.80
Ann.Sharpe            -4.75    -5.04    -0.47    -2.59    -2.15
Max.Drawdown       -4926.34 -6711.79 -1938.05 -3451.10 -4068.90
Profit.To.Max.Draw    -0.82    -0.83    -0.16    -0.88    -0.53
Avg.WinLoss.Ratio      1.32     1.05     3.56     1.48     2.17
Med.WinLoss.Ratio      0.85     0.40     0.28     0.79     1.98
Max.Equity           459.78     0.00  1298.51   414.31     0.00
Min.Equity         -4466.56 -6711.79 -1329.01 -3036.79 -4068.90
End.Equity         -4037.97 -5591.16  -308.15 -3036.79 -2136.85

                        XLK      XLP      XLU      XLV      XLY
Num.Txns              86.00    92.00    82.00    94.00    82.00
Num.Trades            43.00    45.00    40.00    47.00    40.00
Net.Trading.PL     -1205.62 -4427.34 -3490.76 -4291.56  -230.80
Avg.Trade.PL         -28.04   -98.39   -87.27   -91.31    -5.77
Med.Trade.PL        -101.30  -153.01   -93.87   -98.69  -140.01
Largest.Winner      2403.16  1008.09  1805.03   842.35  2090.68
Largest.Loser       -806.29  -460.10  -462.68  -554.57  -698.45
Gross.Profits       4984.72  2839.42  2493.63  2959.31  6253.33
Gross.Losses       -6190.34 -7266.76 -5984.39 -7250.87 -6484.14
Std.Dev.Trade.PL     464.22   294.41   350.37   280.87   495.21
Percent.Positive      30.23    15.56    20.00    19.15    30.00
Percent.Negative      69.77    84.44    80.00    80.85    70.00
Profit.Factor          0.81     0.39     0.42     0.41     0.96
Avg.Win.Trade        383.44   405.63   311.70   328.81   521.11
Med.Win.Trade        191.31   116.12    61.87   266.16   307.13
Avg.Losing.Trade    -206.34  -191.23  -187.01  -190.81  -231.58
Med.Losing.Trade    -188.49  -191.04  -156.33  -161.51  -171.21
Avg.Daily.PL         -28.04   -98.39   -87.27   -91.31    -5.77
Med.Daily.PL        -101.30  -153.01   -93.87   -98.69  -140.01
Std.Dev.Daily.PL     464.22   294.41   350.37   280.87   495.21
Ann.Sharpe            -0.96    -5.30    -3.95    -5.16    -0.18
Max.Drawdown       -3448.99 -5384.93 -3540.13 -5186.60 -3964.07
Profit.To.Max.Draw    -0.35    -0.82    -0.99    -0.83    -0.06
Avg.WinLoss.Ratio      1.86     2.12     1.67     1.72     2.25
Med.WinLoss.Ratio      1.01     0.61     0.40     1.65     1.79
Max.Equity           646.11   651.59     0.00   895.04  2960.96
Min.Equity         -3003.57 -4733.34 -3540.13 -4291.56 -1003.11
End.Equity         -1205.62 -4427.34 -3490.76 -4291.56  -230.80

At this point, for the sake of brevity, I’ll leave off the equity curves and portfolio statistics (they’ll obviously be bad). However, let’s look at some images of what exactly is going on with individual trades.

Here is the full-backtest equity curve and corresponding indicators for XLP. The FRAMA is in purple, with the 126-day median in orange, along with the 10-day ATR (lagged by a day) on the bottom.

And here we can immediately see certain properties:

1) ATR order-sizing is not a be-all, end-all type of order. It was created for one purpose, which is to equalize risk across instruments (the original idea of which, I defer to Andreas Clenow’s article). However, that is only a base from which to begin, using other scaled order-sizing procedures which can attempt to quantify the confidence in any particular trade. As it currently stands, for short strategies in equities, the best opportunities happen in the depths of rapid falling price action, during which ATR will rise. One may consider augmenting the ATR order sizing function in order to accomplish this task (or merely apply leverage at the proper time, through modifying the pctATR parameter).

2) While the running median certainly has value as a filter to keep out obviously brainless trades (E.G. in the middle of an uptrend), once the FRAMA crosses the median, anything can happen, as the only logic is that the current FRAMA is just slightly lower than the previous day’s. This may mean that the running median itself is still rising, or that the FRAMA is effectively flat, and what is being traded on is purely noise. And furthermore, with ATR order sizing amplifying the consequences of that noise, this edge case can have disastrous consequences on an equity curve.

Here’s a zoom in on 2005, where we see a pretty severe drawdown (chart time series recolored for clarity).

As can be seen, even though the FRAMA seems to be slightly rising, a price crossing when the FRAMA is lower than the previous day by even an invisibly small amount (compare the purple–the FRAMA, to the red–the same quantity lagged a day) is enough to trigger a trade that will buy a sizable number of shares, even when the volatility is too small to justify such a trade. Essentially, most of the losses in this trading system arise as a result of trading during these flat periods during which the system attempts to force action.

This pattern repeats itself. Here is the equity curve for XLB.

Again, aside from maybe a bad trade in the end thanks to any trade being taken once all three conditions line up (decreasing FRAMA, FRAMA lower than median, price lower than FRAMA) too late due to a flat FRAMA/median relationship, most of the losers seem to be trades made during very flat and calm market action, even when the running median may be going in the opposite direction of the FRAMA, during which the ATR order-sizing function tried to force action. A second filter that serves to catch these edge-case situations (or maybe a filter that replaces the running median entirely) will be investigated in the future.

So, to recap this post:

The running median filter is an intrinsically lagging but robust indicator, chosen deliberately for these two properties. It is able to filter out trades that obviously go against the trend. However, due to some edge cases, there were still a great deal of losses that were incurred, which drown out the one good shorting opportunity over this sample period. This is an issue that needs addressing.

Thanks for reading.