Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

In this post, I’ll attempt to address a question I’ve seen tossed around time and again regarding quantstrat.

“How do I model executions on individual underlying instruments in spread trading?”

First off, a disclaimer: this method is a bit of a kludge, and in using it, you’ll lose out on quantstrat’s inbuilt optimization functionality. Essentially, it builds upon the pre-computed signal methodology I described in a previous post.

Essentially, by appending a column with the same name but with different values to two separate instruments, I can “trick” quantstrat into providing me desired behavior by modeling trading on two underlying instruments.

SO here’s the strategy:

Go long 3 shares of the UNG (natural gas) ETF against 1 share of UGAZ (3x bull) when the spread crosses above its 20-day exponential moving average, otherwise, do nothing. Here’s the reasoning as to why:

require(quantmod)
require(quantstrat)

getSymbols("UNG", from="1990-01-01")
getSymbols("DGAZ", from="1990-01-01")
getSymbols("UGAZ", from="1990-01-01")
UNG <- UNG["2012-02-22::"]
UGAZ <- UGAZ["2012-02-22::"]

nEMA=20

legend(x=5, y=50, legend=c("EMA 20"),
fill=c("blue"), bty="n")


With the corresponding plot:

So, as you can see, we have a spread that drifts upward (something to do with the nature of the leveraged ETF)? So, let’s try and capture that with a strategy.

The way I’m going to do that is to precompute a signal–whether or not the spread’s close is above its EMA20, and append that signal to UNG, with the negative of said signal appended to UGAZ, and then encapsulate it in a quantstrat strategy. In this case, there’s no ATR order sizing function or initial equity–just a simple 3 UNG to 1 UGAZ trade.

signal <- Cl(spread) > EMA(Cl(spread), n=nEMA)
UNG$precomputedSig <- signal UGAZ$precomputedSig <- signal*-1

initDate='1990-01-01'
currency('USD')
Sys.setenv(TZ="UTC")
symbols <- c("UNG", "UGAZ")
stock(symbols, currency="USD", multiplier=1)

strategy.st <- portfolio.st <- account.st <-"spread_strategy"

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')
initOrders(portfolio.st, initDate=initDate)
strategy(strategy.st, store=TRUE)

#long rules
arguments=list(column="precomputedSig", threshold=.5,
relationship="gt", cross=TRUE),
label="longEntry")

arguments=list(column="precomputedSig", threshold=.5,
relationship="lt", cross=TRUE),
label="longExit")

#short rules
arguments=list(column="precomputedSig", threshold=-.5,
relationship="lt", cross=TRUE),
label="shortEntry")

arguments=list(column="precomputedSig", threshold=-.5,
relationship="gt", cross=TRUE),
label="shortExit")

arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market",
orderside="long", replace=FALSE, prefer="Open", orderqty=3),
type="enter", path.dep=TRUE)

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

#short 1
arguments=list(sigcol="shortEntry", sigval=TRUE, ordertype="market",
orderside="short", replace=FALSE, prefer="Open", orderqty=-1),
type="enter", path.dep=TRUE)

arguments=list(sigcol="shortExit", sigval=TRUE, orderqty="all", ordertype="market",
orderside="short", replace=FALSE, prefer="Open"),
type="exit", path.dep=TRUE)

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)  So, did our spread trade work? #trade statistics tStats <- tradeStats(Portfolios = portfolio.st, use="trades", inclZeroDays=FALSE) tStats[,4:ncol(tStats)] <- round(tStats[,4:ncol(tStats)], 2) print(data.frame(t(tStats[,-c(1,2)]))) (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses)) (aggCorrect <- mean(tStats$Percent.Positive))
(numTrades <- sum(tStats$Num.Trades)) (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE)) > (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses)) [1] 1.101303 > (aggCorrect <- mean(tStats$Percent.Positive))
[1] 51.315
> (numTrades <- sum(tStats$Num.Trades)) [1] 76 > (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE)) [1] 1.065  Sort of. However, when you think about it–looking at the statistics on a per-instrument basis in a spread trade is a bit of a red herring. After all, outside of a small spread, what one instrument makes, another will lose, so the aggregate numbers should be only slightly north of 1 or 50% in most cases, which is what we see here. A better way of looking at whether or not the strategy performs is to look at the cumulative sum of the daily P&L. #portfolio cash PL portString <- paste0("portfolio.", portfolio.st) portPL <- .blotter[[portString]]$summary\$Net.Trading.PL
portPL <- portPL[-1,] #remove initialization date
plot(cumsum(portPL))


With the following equity curve:

Is this the greatest equity curve? Probably not. In fact, after playing around with the strategy a little bit, it’s better to actually get in at the close of the next day than the open (apparently there’s some intraday mean-reversion).

Furthermore, one thing to be careful of is that in this backtest, I made sure that for UNG, my precomputedSig would only take values 1 and 0, and vice versa for the UGAZ variant, such that I could write the rules I did. If it took the values 1, 0, and -1, or 1 and -1, the results would not make sense.

In conclusion, the method I showed was essentially a method building on a previous technique of pre-computing signals. Doing this will disallow users to use quantstrat’s built-in optimization functionality, but will allow users to backtest individual leg execution.

To answer one last question, if one wanted to short the spread as well, the thing to do using this methodology would be to pre-compute a second column called, say, precomputedSig2, that behaved the opposite way.