Pairs Trading in Zorro

In our previous post, we looked into implementing a Kalman filter in R for calculating the hedge ratio in a pairs trading strategy.

You know, light reading…

We saw that while R makes it easy to implement a relatively advanced algorithm like the Kalman filter, there are drawbacks to using it as a backtesting tool.

Setting up anything more advanced than the simplest possible vectorised backtesting framework is tough going and error-prone. Plus, it certainly isn’t simple to experiment with strategy design – for instance, incorporating costs, trading at multiple levels, using a timed exit, or incorporating other trade filters.

To be fair, there are good native R backtesting solutions, such as Quantstrat. But in my experience none of them let you experiment as efficiently as the Zorro platform.

And as an independent trader, the ability to move fast – writing proof of concept backtests, invalidating bad ideas, exploring good ones in detail, and ultimately moving to production efficiently – is quite literally a superpower.

I’ve already invalidated 3 ideas since starting this post

The downside with Zorro is that it would be pretty nightmarish implementing a Kalman filter in its native Lite-C code. But thanks to Zorro’s R bridge, I can use the R code for the Kalman filter that I’ve already written, with literally only a couple of minor tweaks. We can have the best of both worlds!

This post presents a script for a pairs trading algorithm using Zorro. We’ll stick with a static hedge ratio and focus on the pairs trading logic itself. In the next post, I’ll show you how to configure Zorro to talk to R and thus make use of the Kalman filter algorithm.

Let’s get to it. 

The Pairs Trading Zoo

Even the briefest scan of the pairs trading literature reveals many approaches to constructing spreads. For example, using:

  • Prices
  • Log-prices
  • Ratios
  • Factors
  • Cointegration
  • Least squares regression
  • Copulas
  • State space models

Ultimately, the goal is to find a spread that is both mean-reverting and volatile enough to make money from.

In my view, how you do that is much less important than it’s ability to make money. From personal experience, I know that the tendency is to get hung up on the “correct” way to implement a pairs trade. Such a thing doesn’t exist — I’ve seen money-printing pairs trading books that younger me, being more hung up on “correctness”, would have scoffed at.

Instead, understand that pairs trading is ultimately a numbers game and that universe selection is more important than the specifics of the algorithm. Sure, you can tweak your implementation to squeeze a little more out of it, and even find pockets of conditional or seasonal mean-reversion, but the specifics of the implementation are unlikely to be the ultimate source of alpha.

Anyway, that’s for you to mull over and keep in mind as you read this series. Right now, we’re just going to present one version of a pairs trade in Zorro.

Implementing a Pairs Trade in Zorro

This is a pairs trade that uses a price-based spread for its signals. First, here’s the code that calculates the spread, given two tickers Y and X (lines 5 – 6) and a hedge ratio, beta (line 39). The spread is simply (Y – \beta X)

Here’s the code:

/* 
Price-based spread in Zorro
*/

#define Y "GDX"
#define X "GLD"

var calculate_spread(var hedge_ratio)
{
  var spread = 0;
  asset(Y);
  spread += priceClose();
  
  asset(X); 
  spread -= hedge_ratio*priceClose();
  
  return spread;
}

function run()
{
    set(PLOTNOW);
    StartDate = 20100101;
    EndDate = 20191231;
    BarPeriod = 1440;
    LookBack = 100;
    
    // load data from Alpha Vantage in INITRUN
    if(is(INITRUN)) 
    {
        string Name;
        while(Name = loop(Y, X))
        {
            assetHistory(Name, FROM_AV);
        }
    }
    
    // calculate spread
    var beta = 0.4; 
    vars spread = series(calculate_spread(beta));
    
    // plot
    asset(Y);
    var asset1Prices = priceClose();
    asset(X);
    plot(strf("%s-LHS", Y), asset1Prices, MAIN, RED);
    plot(strf("%s-RHS", X), priceClose(), 0|AXIS2, BLUE);
    plot("spread", spread, NEW, BLACK);
}

Using GDX and GLD as our Y and X tickers respectively and a hedge ratio of 0.4, Zorro outputs the following plot:

The spread looks like it was reasonably stationary during certain subsets of the simulation, but between 2011 and 2013 it trended – not really a desirable property for a strategy based on mean-reversion.

Even this period in late 2013, where one could imagine profiting from a mean-reversion strategy, the spread hasn’t been very well behaved. The buy and sells levels are far from obvious in advance:

One way to tame the spread is to apply a rolling z-score transformation. That is, take a window of data, say 100 days, and calculate its mean and standard deviation. The z-score of the next point is the raw value less the window’s mean, divided by its standard deviation. Applying this in a rolling fashion is a one-liner in Zorro:

vars ZScore = series(zscore(spread[0], 100));

Our z-scored spread then looks like this:

The z-scored spread has some nice properties. In particular, it tends to oscillate between two extrema, eliminating the need to readjust buy and sell levels (although we’d need to decide on the actual values to use). On the other hand, it does introduce an additional parameter, namely the window length used in its calculation.

To implement the rest of our pairs trade, we need to decide the z-score levels at which to trade and implement the logic for buying and selling the spread.

Zorro makes that fairly easy for us. Here’s the complete backtesting framework code:

/* 
Price-based spread trading in Zorro
*/

#define Y "GDX"
#define X "GLD"
#define MaxTrades 5
#define Spacing 0.5
// #define COSTS

int ZSLookback = 100;
int Portfolio_Units = 100; //units of the portfolio to buy/sell (more --> better fidelity to dictates of hedge ratio)

var calculate_spread(var hedge_ratio)
{
  var spread = 0;
  asset(Y);
#ifndef COSTS  
  Spread = Commission = Slippage = 0;
#endif
  spread += priceClose();
  
  asset(X); 
#ifndef COSTS
  Spread = Commission = Slippage = 0;
#endif 
  spread -= hedge_ratio*priceClose();
  
  return spread;
}

function run()
{
    set(PLOTNOW);
    setf(PlotMode, PL_FINE);
    StartDate = 20100101;
    EndDate = 20191231;
    BarPeriod = 1440;
    LookBack = ZSLookback;
    MaxLong = MaxShort = MaxTrades;
    
    // load data from Alpha Vantage in INITRUN
    if(is(INITRUN)) 
    {
        string Name;
        while(Name = loop(Y, X))
        {
            assetHistory(Name, FROM_AV);
        }
    }
    
    // calculate spread
    var beta = 0.4; 
    vars spread = series(calculate_spread(beta));
    vars ZScore = series(zscore(spread[0], 100));
    
    // set up trade levels
    var Levels[MaxTrades]; 
    int i;
    for(i=0; i<MaxTrades; i++)
    {
        Levels[i] = (i+1)*Spacing;
    }
    
// -------------------------------
// trade logic 
// -------------------------------

    // exit on cross of zero line
    if(crossOver(ZScore, 0) or crossUnder(ZScore, 0)) 
    {
        asset(X);
        exitLong(); exitShort();
        asset(Y);
        exitLong(); exitShort();
    }
    
    // entering positions at Levels
    for(i=0; i<=MaxTrades; i++)
    {
        if(crossUnder(ZScore, -Levels[i])) // buying the spread (long Y, short X)
        {
            asset(Y);
            Lots = Portfolio_Units;
            enterLong();
            asset(X);
            Lots = Portfolio_Units * beta;
            enterShort();
        }
        if(crossOver(ZScore, Levels[i])) // shorting the spread (short Y, long X)
        {
            asset(Y);
            Lots = Portfolio_Units;
            enterShort();
            asset(X);
            Lots = Portfolio_Units * beta;
            enterLong();
        }
    }
    
    // exiting positions at Levels
    for(i=1; i<=MaxTrades-1; i++)
    {
        if(crossOver(ZScore, -Levels[i])) // covering long spread (exiting long Y, exiting short X)
        {
            asset(Y);
            exitLong(0, 0, Portfolio_Units);
            asset(X);
            exitShort(0, 0, Portfolio_Units * beta);
        }
        if(crossUnder(ZScore, Levels[1])) // covering short spread (exiting short Y, exiting long X)
        {
            asset(Y);
            exitShort(0, 0, Portfolio_Units);
            asset(X);
            exitLong(0, 0, Portfolio_Units * beta);
        }
    }
    

    // plots
    if(!is(LOOKBACK))
    {
        plot("zscore", ZScore, NEW, BLUE);
        int i;
        for(i=0; i<MaxTrades; i++)
        {
            plot(strf("#level_%d", i), Levels[i], 0, BLACK);
            plot(strf("#neglevel_%d", i), -Levels[i], 0, BLACK);
        }   
        plot("spread", spread, NEW, BLUE);
    }
}

The trade levels are controlled by the MaxTrades and Spacing variables (lines 7 – 8). These are implemented as #define statements to make it easy to change these values, enabling fast iteration.

As implemented here, with MaxTradesequal to 5 and Spacing equal to 0.5, Zorro will generate trade levels every 0.5 standard deviations above and below the zero line of our z-score.

The generation of the levels happens in lines 57 – 63.

The trade logic is quite simple:

  • Buy the spread if the z-score crosses under a negative level
  • Short the spread if the z-score crosses over a positive level
  • If we’re long the spread, cover a position if the z-score crosses over a negative level
  • If we’re short the spread, cover a position if the z-score crosses under a positive level
  • Cover whenever z-score crosses the zero line

By default, we’re trading 100 units of the spread at each level. We’re trading in and out of the spread as the z-score moves around and crosses our levels. If the z-score crosses more than one level in a single period, we’d be entering positions for each crossed level at market.

Essentially, it’s a bet on the mean-reversion of the z-scored spread translating into profitable buy and sell signals in the underlyings.

The strategy returns a Sharpe ratio of about 0.6 before costs (you can enable costs by uncommenting #define COSTS in line 9, but you’ll need to set up a Zorro assets list with cost details, or tell Zorro about costs via script) and the following equity curve:

Conclusion

There you have it – a Zorro framework for price-based pairs trading. More than this particular approach to pairs trading itself, I hope that I’ve demonstrated Zorro’s efficiency for implementing such frameworks quickly. And once they’re implemented, you can run experiments and iterate on the design, as well as the utility of the trading strategy, efficiently.

For instance, it’s trivial to:

  • Add more price levels, tighten them up, or space them out
  • Get feedback on the impact of changing the z-score window length
  • Explore what happens when you change the hedge ratio
  • Change the simulation period
  • Swap out GLD and GDX for other tickers

You can even run Zorro on the command line and pass most of the parameters controlling these variables as command line arguments – which means you can write a batch file to run hundreds of backtests and really get into some serious data mining – if that’s your thing.

All that aside, in the next post I want to show you how to incorporate the dynamic estimate of the hedge ratio into our Zorro pairs trading framework by calling the Kalman filter implemented in R directly from our Zorro script.

You can grab the code for the Kalman Filter we used in the previous post for free below:

[thrive_leads id=’11337′]

3 thoughts on “Pairs Trading in Zorro”

Leave a Comment