As we can see —test results are inconclusive. You can’t just take all your data, throw it against the algorithm, take the lowest p_value and say — I’ll trade this because test says this is the most cointegrating pair there is.

Side note: pair might be perfectly mean reverting, but the amount it reverts could be too small to profit, when commissions are included.

Now that we have cointegration results, what’s next? We need to solve some technical difficulties to make pair backtesting possible. First — we need a platform to execute.

Platforms

Since pair trading requires some relatively advanced features, not all platforms support them. Two main featured needed are:

Shorting — to bet on spread reverting to mean, you long one and short the other coin. If you can’t short, you can’t trade pairs.

— to bet on spread reverting to mean, you long one and short the other coin. If you can’t short, you can’t trade pairs. Multi asset — for obvious reasons.

Turns out, this is quite rare.

As you might know, I’ve previously written few posts about Gekko backtesting specifically, but Gekko lacks both features needed for shorting, so we need something else.

So I went on a little platform searching.

QuantConnect / LEAN

Written in C#. Very complex, tons of code, which means is much harder to customize source. But the code is very clean. Optimized mostly for more traditional assets, Crypto is an afterthought. Since it’s C#, runs best in Windows. I was able to get it running on Ubuntu with Mono but it was a struggle + performance penalty + no UI for non-Windows. Supports Python strats also, but brings debugging difficulties by being multi-language platform. Great documentation, but a bit fragmented. Good amount of free/public strategies. Very custom data format, hard to force to use data from DB. Supports multi-assets / universe selection, which is huge. Supports shorting.

Backtrader

Has great documentation with some nice witty jokes and a very clean code. One of the most feature rich platforms. One of the simplest to use and quickest to get started, which is huge. Has relatively good charting, although not interactive. There is a Bokeh plugin though, but haven’t tried. Has LIVE trading, but Crypto requires external engine, haven’t tried but seems legit, judging from this post. Supports multi-assets. Supports shorting. Only problem — it’s slow.

Catalyst

Based on Zipline, repurposed for Crypto. Way too slow. Almost no free/public strats that I’ve found. Supports shorting, at least on paper. Found the features to be quite lacking with this one.

Backtesting.py

I’ve rarely seen this mentioned anywhere, but I found it and it’s great for me. Best out of the box Charting, super extensible if you are a coder. Super fast. Very short and clean code, does few basic things and does them good. Doesn’t support shorting, but since it was so minimalistic — quite easy to develop yourself. If you wan’t to develop your own platform, this can serve as a good starting point.

Side note: none of these platforms have anywhere close the number of publicly available strategies like Gekko.

There are a ton of other platforms, but most of them aren’t actively developed. Here is the list of lists, of you are interested. Haven’t tried most of them personally.

Every platform lacked something very important for me, but in the end I chose backtrader — mainly because ease of use and ability to get started quickly.

Strategy Implementation

I tried to use the default pairs-trading.py strategy as starting point, but it didn’t work for me out of the box, so I had to make some adjustments.

EDIT (May 21st): I previously had two fixes here that involved making changes in backtrader source, but u/mementix (the author of backtrader) pointed out to me that there is a better way of doing things in the platform — by extending instead of changing source.

First, there was problem in ols.py file with statsmodels lib which API was changed since the strategy was created. Instead of changing source, you should copy the class OLS_Slope_InterceptN and class OLS_TransformationN as external indicator and fix this line:

p1 = sm.add_constant(p1, prepend=self.p.prepend_constant, has_constant=’add’)

Next there was issue with comminfo.py. getsize() tries to int parse all order sizes, but in Crypto that’s not realistic because you won’t be buying round numbers of BTC each time, if ever. Again, instead of changing source, you should create your own commission scheme. Here’s the one I used:

class CommInfo_Crypto(bt.CommInfoBase): params = ( ('stocklike', True), ('commtype', bt.CommInfoBase.COMM_PERC), ('percabs', True), ) def getsize(self, price, cash):

return self.p.leverage * (cash / price)

After the fixes, I played with the strategy and felt like some things could be simplified. To sum up, what I did was:

OLS Spread calculation was very slow, so I had to use my own simplified version (check SpreadZScore in github) which is basically spread = self.data0 / self.data1. While technically this is not the way how spread is calculated usually, for demonstration purposes I felt results it provided was good/close enough.

in github) which is basically While technically this is not the way how spread is calculated usually, for demonstration purposes I felt results it provided was good/close enough. Order Size calculations. I simply split the cash between assets and short/long:

self.order_target_percent(data=self.data0, target=0.5)

self.order_target_percent(data=self.data1, target=-0.5)

While not technically correct (you are borrowing funds when shorting + you need margin account + you need to pay rolling interest for borrowing), I think it’s OK for purposes of this post.

You can see the full code in my repo here.

Which exchanges support shorting?

To be able to short coin X, somebody has to be willing to lend you the coin (which implies interest rate that is often forgotten), so you can sell it now + buy and give back later when it’s hopefully cheaper.

As you can imagine, technically this comes with some additional complexity and risk, for both exchange and also you. Which means that being able to short is not something to be taken for granted, not a lot of exchanges support shorting and only for few selected coins. I did some research and found that most of the coins in my tests are supported, but:

None of the exchanges support all the coins, most of them support few (~5) coins and mostly the big ones.

Coins get delisted from margin trading (shorting) all the time because low volume etc. The supported list changes all the time.

I found 2 nice compiled lists, a bit old (mid 2018), but still a good overview:

If you want to dig in yourself and find the latest supported coins, I have compiled a list with exchange info pages that describe what is supported:

Note: I’m not 100% sure that all those support Perpetual Contracts (instead of Futures). To be able to execute pairs trading, you must be able to exit at any time, instead of specific fixed time in future, like with Future Contracts. If you are thinking of executing this LIVE, you must know what types of contracts your chosen pair supports and in what exchanges.

Testing Setup

17 coins gives us 16! combinations which is 136 pairs in total.

40 of those have coint < 0.05

20 of those have coint < 0.01

Instead of backtesting only pairs with low coint p_value, I’ll test them all and later show you the difference between profits/losses of higher and lower p_values.

To try to simulate real life and avoid look ahead bias (as much as possible):

I’ll be running coint test from May 1st (2018) to Jan 1st (2019) (already done in “Running the tests” section)

to (already done in “Running the tests” section) But backtests will be ran from Jan 1st (2019) to Apr 1st (2019) (3 months)

There are 2 main variables involved in the strategy:

spread period — period over which to calculate ZScore of spread (in my case — ratio between coins)

— period over which to calculate ZScore of spread (in my case — ratio between coins) threshold — when ZScore crosses this level (upper/lower), initiate signal to reverse current position

I won’t try to choose single value for each beforehand, but create a grid of possible values and run them all, to see which combinations give the best results:

spread period = [ 5d, 7d, 10d, 15d ]

threshold = [ 0.5, 0.7, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0]

Result Overview