A Way To Model Execution On Individual Legs Of A Spread In Quantstrat

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)
require(IKTrading)

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::"]

spread <- 3*OHLC(UNG) - OHLC(UGAZ)

nEMA=20

chart_Series(spread)
add_TA(EMA(Cl(spread), n=nEMA), on=1, col="blue", lwd=1.5)
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
add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputedSig", threshold=.5, 
                          relationship="gt", cross=TRUE),
           label="longEntry")

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

#short rules
add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="precomputedSig", threshold=-.5, 
                          relationship="lt", cross=TRUE),
           label="shortEntry")

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

#buy 3
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", 
                        orderside="long", replace=FALSE, prefer="Open", orderqty=3), 
         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 1
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="shortEntry", sigval=TRUE, ordertype="market", 
                        orderside="short", replace=FALSE, prefer="Open", orderqty=-1), 
         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)

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.

Thanks for reading.

NOTE: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.

11 thoughts on “A Way To Model Execution On Individual Legs Of A Spread In Quantstrat

  1. Pingback: Spreads in R (Quantstrat and FinancialInstrument) (Page 2) - Matlab, R project and Python | Big Mike Trading

  2. Pingback: The Whole Street’s Daily Wrap for 12/23/2014 | The Whole Street

  3. Hi Ilya Kipnis !

    Nice post.
    What is the built-in optimization functionality that is disallowed with the pre-computed indicator method ?
    Best,
    Fernando

      • Ilya,

        Thx for the reply.
        I managed to modify a bit your code, to create a Symbol dependent indicator.
        The good news is that everything works when i call applyStrategy.
        The bad news is walk.forward dont work properly, but it may be a bug in walk.forward function(see http://r.789695.n4.nabble.com/quantstrat-help-simple-combine-error-using-windows-and-walk-forward-td4697468.html – I’m getting exactly the same errors).

        Here is what I modified in your code:

        UNG <- UNG["2012-02-22::"]
        UGAZ <- UGAZ["2012-02-22::"]

        #move this to indicator function
        #spread <- 3*OHLC(UNG) – OHLC(UGAZ)
        #nEMA=20

        MyprecomputedSig <-function(OLHC,nEMA=20){
        spread <- 3*OHLC(UNG) – OHLC(UGAZ)
        signal EMA(Cl(spread), n=nEMA)
        #there may be a more elegant way to get the current symbol from the applyStrategy calling envir !
        symbol <- get("symbol", parent.frame(2))
        if(symbol == "UNG"){
        sg <- signal*1
        }
        else
        {
        sg <- signal*-1
        }
        colnames(sg)<-c("precomputedSig")
        return(sg)
        }

        and the the indicator :

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

        Best,
        Fernando

      • This does absolutely nothing. Your indicator is computed outside the strategy. Calling add.indicator on an object computed outside the strategy object does not change the fact that the strategy object does not know how the value you pass in with your add.indicator call was computed, and thus, any walk forward/optimization features will not work.

    • Ilya ,

      Thanks for your reply.
      I am not sure i understand fully your reply but
      i’m waiting anxious for a future post from you on the subject.

      Thx again,
      Fernando

      • My reply is this:

        Optimization/walk forward optimization only works when quantstrat knows how you’re computing your indicator. When you write add.indicator just to add your precomputed signal, the strategy does not know that the signal is coming from the close being greater than the EMA20. It has no idea how you actually obtained those values, so it can’t optimize on them.

  4. Now i understand ! Thx !

    Just to clarify, quantstrat knows how i am computing the indicator.
    All your posted code to compute it is inside the function “MyprecomputedSig” (with an nEMA input parameter with value of 20).
    Also, “MyprecomputedSig” will discover,via parent.frame() if “UNG” is the current symbol in the applyStrategy symbol loop (The return will be different if the current symbol is “UGAZ”).
    And of course, the return will also be different if you pass a nEMA not = 20 with add.rule.
    Anyway the *only* optimization not working with this code is walk.forward (for possible bug in his code).
    apply.paramset, for instance, works fine.

    Best,
    Fernando

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s