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



Thank you. I enjoy the post a lot. A very good introduction to Pair tradings.
Never used zorro, how I can create synthetic index on zorro?