Earning Money in Cryptocurrency Markets by Spotting Statistical Arbitrage Opportunities

When you come in contact with cryptocurrencies, e.g. Bitcoin (BTC), you quickly realise that there is no single price of BTC at any given moment. The reason is that Bitcoin is traded on different markets. It can be worth more on Coinbase exchange and less on Kraken exchange. In particular, the Coindesk Bitcoin Price Index (XBP) aims to unify the BTC price into a single number based on four markets that meet specific criteria (see here for details on XBP calculation). In practice, the use of a cryptocurrency index seems to be valuable for press and newspapers, not for traders and investors. They usually look at the exchange where their positions are opened and track the exchange-related prices.

The idea of trading the same cryptocurrency on different crypto-markets at the same time is not new. It dates back to trading of a pair of stocks (equities) which prices are highly correlated and cointegrated and is known as statistical arbitrage (Stat Arb). The basic statistical arbitrage is a trading strategy that assumes that the price-series of two assets put into a pair (stocks or, in our case, cryptocurrencies) are non-stationary and mean reverting over time. In other words, when one coin in a pair outperforms the other, the poorer performing coin is bought long with the expectation that it will climb towards its outperforming partner, the other is sold short.




In this post we will see how to combine theory with real market data, identify statistical arbitrage opportunities to buy/sell, create a simple backtest in Python to prove that Stat Arb is not dead and, in fact, that you can make money in cryptocurrency markets! Ready for launch? Let’s go!

1. Markets where a given Cryptocurrency is Traded

Building an algo-trading model employing Stat Arb starts with building your knowledge on data available. Naturally, first thing you would like to know are the names of crypto-markets where the coin of your choice is traded. In all what follows, let’s assume we will be interested in trading Ether (ETH) or, more precisely, the ETH/USD ratio.

Again, we make use of CryptoComapre.com’s API which blended with Python can act promptly to deliver required information:

# Earning Money in Cryptocurrency Markets by Spotting 
# Statistical Arbitrage Opportunities
#
# tested in Python 3.5.4
# (c) 2017, Dr. Pawel Lachowicz
 
import numpy as np
import pandas as pd
from scipy import stats
from matplotlib import pyplot as plt
from datetime import datetime
import json
from bs4 import BeautifulSoup
import requests

%matplotlib inline
grey = .6, .6, .6


# define a pair
fsym = "ETH"
tsym = "USD"

url = "https://www.cryptocompare.com/api/data/coinsnapshot/?fsym=" + \
       fsym + "&tsym=" + tsym
response = requests.get(url)
soup = BeautifulSoup(response.content, "html.parser")
dic = json.loads(soup.prettify())
print(dic)

Above, we fetch the coin snapshot (ETH/USD) that returns a JSON-formatted data structure which we grab in a Python’s dictionary, dic. At the first glance, the variable of dic contains a wealth of information:

{'Data': {'AggregatedData': {'FLAGS': '4',
   'FROMSYMBOL': 'ETH',
   'HIGH24HOUR': '301.14',
   'HIGHDAY': '300.65',
   'LASTMARKET': 'Exmo',
   'LASTTRADEID': '24442414',
   'LASTUPDATE': '1509915676',
   'LASTVOLUME': '0.00212',
   'LASTVOLUMETO': '0.61979896',
   'LOW24HOUR': '294.59',
   'LOWDAY': '295.2',
   'MARKET': 'CCCAGG',
   'OPEN24HOUR': '297.85',
   'OPENDAY': '300.04',
   'PRICE': '296.8',
   'TOSYMBOL': 'USD',
   'TYPE': '5',
   'VOLUME24HOUR': '206617.29367793005',
   'VOLUME24HOURTO': '61511364.75962282',
   'VOLUMEDAY': '181568.2062645673',
   'VOLUMEDAYTO': '53913839.277825184'},
  'Algorithm': 'Ethash',
  'BlockNumber': 4497207,
  'BlockReward': 3.0,
  'Exchanges': [{'FLAGS': '1',
    'FROMSYMBOL': 'ETH',
    'HIGH24HOUR': '305.45',
    'LASTTRADEID': '1060368',
    'LASTUPDATE': '1509915610',
    'LASTVOLUME': '0.027127',
    'LASTVOLUMETO': '8.23982625',
    'LOW24HOUR': '301.04300027',
    'MARKET': 'Cexio',
    'OPEN24HOUR': '301.66699999',
    'PRICE': '303.75',
    'TOSYMBOL': 'USD',
    'TYPE': '2',
    'VOLUME24HOUR': '2053.973031999999',
    'VOLUME24HOURTO': '622022.5332660044'},
   {'FLAGS': '1',
    'FROMSYMBOL': 'ETH',
    'HIGH24HOUR': '302',
    'LASTTRADEID': '14074021',
    'LASTUPDATE': '1509915662',
    'LASTVOLUME': '0.00003349',
    'LASTVOLUMETO': '0.0099672938',
    'LOW24HOUR': '296',
    'MARKET': 'Coinbase',
    'OPEN24HOUR': '298.75',
    'PRICE': '297.62',
    'TOSYMBOL': 'USD',
    'TYPE': '2',
    'VOLUME24HOUR': '48653.64503668011',
    'VOLUME24HOURTO': '14517318.477630273'},
...

where, for the purpose of this task, we solely aim to extract the names of all exchanges where ETH/USD is alive:

market = []
d = dic['Data']['Exchanges']  # a list
for i in range(len(d)):
    market.append(d[i]['MARKET'])
    print(market[-1])
Cexio
Coinbase
Coinroom
Gemini
Bitfinex
Cryptsy
Tidex
CCEX
Gatecoin
BTCE
Kraken
OKCoin
Exmo
WavesDEX
BitTrex
Remitano
Poloniex
HitBTC
Lykke
BitBay
LiveCoin
Yobit
Quoine

2. Cryptocurrency Markets according to the Volume traded within last 24 hours

Which market should we choose for setting up our Stat Arb trading strategy? It can be done by filtering the market data according to highest volume traded within last twenty four hours. Why? Simply because the more the better. With higher interest in a specific market we cut on liquidity risk and vouchsafe faster execution times (the spread becomes secondary problem for you to worry about).

In Python we use a special method (line #43) to filter the list and get a desired result:

vol = []
d = dic['Data']['Exchanges']  # a list
for i in range(len(d)):
    vol.append([d[i]['MARKET'], round(float(d[i]['VOLUME24HOUR']),2)])

# sort a list of sublists according to 2nd item in a sublist
vol = sorted(vol, key=lambda x: -x[1])

# Cryptocurrency Markets according to Volume traded within last 24 hours
for e in vol:
    print("%10s%15.2f" % (e[0], e[1]))
  Bitfinex       99874.91
  Coinbase       48653.65
    HitBTC       20080.36
  Poloniex       11928.00
    Gemini        7676.25
    Kraken        7354.39
   BitTrex        5720.76
     Cexio        2053.97
      Exmo        1621.00
    Quoine         963.88
    OKCoin         424.38
     Lykke         162.65
     Yobit         124.61
     Tidex         117.18
  LiveCoin          61.12
      CCEX          37.04
  Remitano           1.70
  Gatecoin           1.19
  Coinroom           0.78
   Cryptsy           0.00
      BTCE           0.00
  WavesDEX           0.00
    BitBay           0.00

Let’s select the Top 10 Markets trading ETH/USD for further consideration:

# Select Top 10 Cryptocurrency Markets
markets = [e[0] for e in vol][0:10]
print(markets)
['Bitfinex',
 'Coinbase',
 'HitBTC',
 'Poloniex',
 'Gemini',
 'Kraken',
 'BitTrex',
 'Cexio',
 'Exmo',
 'Quoine']

Just keep in mind that the order of that list may change if your rerun the script on a different day, different than Nov 7, 2017.

3. Downloading Cryptocurrency Price Time-Series by specifying Exchange

In my previous article on Cryptocurrency Time-Series for N-CryptoAsset Portfolio Analysis in Python I described you the code that enables fetching the OHLC time-series for any cryptocurrency using CryptoComapre.com’s API. Luckily enough, there is a chance to update that code (see line #68) in the way that from now on it would allow us to specify the market for which we wish to download the coin’s price-series:

def fetchCryptoOHLC_byExchange(fsym, tsym, exchange):
    # a function fetches a crypto OHLC price-series for fsym/tsym and stores
    # it in a pandas DataFrame; uses specific Exchange as provided
    # src: https://www.cryptocompare.com/api/
 
    cols = ['date', 'timestamp', 'open', 'high', 'low', 'close']
    lst = ['time', 'open', 'high', 'low', 'close']
 
    timestamp_today = datetime.today().timestamp()
    curr_timestamp = timestamp_today
 
    for j in range(2):
        df = pd.DataFrame(columns=cols)
        url = "https://min-api.cryptocompare.com/data/histoday?fsym=" + fsym + \
              "&tsym=" + tsym + "&toTs=" + str(int(curr_timestamp)) + \
              "&limit=2000" + "&e=" + exchange
        response = requests.get(url)
        soup = BeautifulSoup(response.content, "html.parser")
        dic = json.loads(soup.prettify())
        
        for i in range(1, 2001):
            tmp = []
            for e in enumerate(lst):
                x = e[0]
                y = dic['Data'][i][e[1]]
                if(x == 0):
                    # timestamp-to-date
                    td = datetime.fromtimestamp(int(y)).strftime('%Y-%m-%d')
                    tmp.append(td)  #(str(timestamp2date(y)))
                tmp.append(y)
            if(np.sum(tmp[-4::]) > 0):
                df.loc[len(df)] = np.array(tmp)
        df.index = pd.to_datetime(df.date)
        df.drop('date', axis=1, inplace=True)
        curr_timestamp = int(df.iloc[0][0])
        
        if(j == 0):
            df0 = df.copy()
        else:
            data = pd.concat([df, df0], axis=0)
 
    return data.astype(np.float64)

Having that, let’s write a code that allows us to not only download the ETH/USD time-series but also place them into pandas’ DataFrame (we will make the data to start from June 2 and end on Nov 5, 2017):

# if a variable 'cp' exists, delete it
if ('cp' in globals()) or ('cp' in locals()): del cp

# download daily OHLC price-series for ETH/USD for a given 'market'
# extract close-price (cp)

print("%s/%s" % (fsym, tsym))
for market in markets:
    print("%12s... " % market, end="")
    df = fetchCryptoOHLC_byExchange(fsym, tsym, market)
    ts = df[(df.index > "2017-06-01") & (df.index <= "2017-11-05")]["close"]
    ts.name = market
    if ('cp' in globals()) or ('cp' in locals()):
        cp = pd.concat([cp, ts], axis=1, ignore_index=False)
    else:
        cp = pd.DataFrame(ts)
    print("downloaded")
ETH/USD
    Bitfinex... downloaded
    Coinbase... downloaded
      HitBTC... downloaded
    Poloniex... downloaded
      Gemini... downloaded
      Kraken... downloaded
     BitTrex... downloaded
       Cexio... downloaded
        Exmo... downloaded
      Quoine... downloaded

what we can verify by displaying the first and the last ten lines of DataFrame:

print(cp.head(10))
print(cp.tail(10))


Therefore, we have daily close price time-series coming from 10 different markets in our hands. The numbers confirm the noticeable differences among these markets at the close. Now, let’s see how one can pick up a (some) good market(s) for our Stat Arb trading strategy.

4. Average Spread Estimation for ETH/USD among different Markets

By spread, here, I do not mean the distance between Bid and Ask for ETH/USD. Rather than that, given all time-serires, I mean the measurement of the average distance (in USD) between price-series (daily) for the time period covered (Jun 2 till Nov 5, 2018) and supplementing it with one standard deviation, i.e.

dist = []
for i in range(cp.shape[1]):
    for j in range(i):
        if(i != j):
            x = np.array(cp.iloc[:,i], dtype=np.float32)
            y = np.array(cp.iloc[:,j], dtype=np.float32)
            diff = np.abs(x-y)
            avg = np.mean(diff)
            std = np.std(diff, ddof=1)
            dist.append([cp.columns[i], cp.columns[j], avg, std])
            
dist = sorted(dist, key=lambda x: -x[2])
print("%10s%10s%10s%10s\n" % ("Coin1", "Coin2", "Mean", "Std Dev"))
for e in dist:
    print("%10s%10s%10.5f%10.2f" % (e[0], e[1], e[2], e[3]))

In our case, the code returns the following results:

     Coin1     Coin2      Mean   Std Dev

    Quoine      Exmo  31.18089     38.58
    Quoine     Cexio  31.07745     37.74
    Quoine  Poloniex  30.22300     40.33
    Quoine  Bitfinex  30.10771     40.25
    Quoine   BitTrex  30.05567     40.36
    Quoine  Coinbase  29.90038     40.07
    Quoine    Kraken  29.82280     40.04
    Quoine    HitBTC  29.82038     39.48
    Quoine    Gemini  29.71771     40.15
     Cexio    HitBTC   9.88497      8.28
     Cexio  Poloniex   9.68694      8.26
      Exmo     Cexio   9.58420      5.15
     Cexio   BitTrex   9.57796      8.27
     Cexio  Bitfinex   9.56936      7.96
     Cexio    Gemini   8.57503      8.20
     Cexio    Kraken   8.25248      6.54
     Cexio  Coinbase   7.84471      6.23
      Exmo   BitTrex   6.37057      5.34
      Exmo  Poloniex   6.23357      5.20
      Exmo    HitBTC   6.22764      5.00
      Exmo  Bitfinex   6.12720      5.01
      Exmo    Gemini   6.04459      6.21
      Exmo    Kraken   6.00204      4.35 *
      Exmo  Coinbase   5.74370      4.20
    HitBTC  Coinbase   2.77898      4.38
    Gemini    HitBTC   2.77732      5.61
    Kraken    HitBTC   2.63350      3.89
   BitTrex    Gemini   2.51720      4.78
   BitTrex    Kraken   2.48357      3.18
   BitTrex  Coinbase   2.46280      3.25
    Gemini  Poloniex   2.34541      5.04
    Kraken  Poloniex   2.30720      3.41
  Poloniex  Coinbase   2.27586      3.47
    Kraken  Bitfinex   2.16834      2.73
    Gemini  Bitfinex   2.16121      4.60
  Coinbase  Bitfinex   2.10006      2.81
    Kraken    Gemini   1.91146      5.16
   BitTrex    HitBTC   1.88293      3.52
  Poloniex    HitBTC   1.55051      3.61
    Kraken  Coinbase   1.51592      2.52
    HitBTC  Bitfinex   1.48223      3.33
   BitTrex  Poloniex   1.43484      2.39
   BitTrex  Bitfinex   1.23981      1.92
    Gemini  Coinbase   1.23809      4.75
  Poloniex  Bitfinex   0.68331      1.92

In theory, we want to select two markets with the highest mean spread at the lowest standard deviation (assuming that the distances calculated follow the normal distribution). In practice, given above numbers, you can check all combinations (in the backtest we are going to write below) in order to find out what what really works best.

5. Statistical Arbitrage Trading Strategy for ETH/USD

Let me choose Exmo and Kraken exchanges. The mean spread of 6 USD at $1\sigma$ of 4.25 USD gives a lot of hope for good fishing! We start from downloading the corresponding time-series (df1 and df2) followed by their quick visualisation:

market1 = "Exmo"
market2 = "Kraken"

df1 = fetchCryptoOHLC_byExchange(fsym, tsym, market1)
df2 = fetchCryptoOHLC_byExchange(fsym, tsym, market2)

# trim
df1 = df1[(df1.index > "2017-06-01") & (df1.index <= "2017-11-05")]
df2 = df2[(df2.index > "2017-06-01") & (df2.index <= "2017-11-05")]

# checkpoint
print(df1.close.shape[0], df2.close.shape[0])  # both sizes must be equal

# plotting
plt.figure(figsize=(20,10))
plt.plot(df1.close, '.-', label=market1)
plt.plot(df2.close, '.-', label=market2)
plt.legend(loc=2)
plt.title(fsym, fontsize=12)
plt.ylabel(tsym, fontsize=12)
plt.grid()

Every trading strategy requires some boundary conditions. Let’s summarise them all together before coding:

1. Algo-Strategy: Stat Arb for ETH/USD
2. Markets: Exmo, Kraken
3. Initial Investment: USD 10,000 split equally into two accounts
4. Trading Rules and Assumptions:
       (a) If asset1 close price (Exmo) $>$ asset2 close price (Kraken) then
               sell asset1 short and buy asset2 long;
       (b) If asset1 close price $<$ asset2 close price then close positions
               update accounts, sell asset2 short and buy asset1 long; etc.
       (c) Every time a new position is opened, invest a fixed amount of USD 2,500;
       (d) After the current trade, a new one is opened immediately at the same prices;
       (e) There is no slipage assumed;
       (f) The short selling is available on both markets;
       (g) There is no commission fee structure incorporated into trades;
       (h) The margin calls are ignored.

 

Therefore, we start trading with USD 5,000 on Exmo and Kraken account, respectively. That will allow us to monitor the “cash” level of both accounts after each trade. The full code to perform the backtest would look like this:

# Backtesting Stat Arb trading strategy for ETH/USD at Exmo and Kraken
#  cryptocurrency exchanges

# initial parameters
investment = 10000  # USD
account1, account2 = investment/2, investment/2  # USD
position = 0.5*(investment/2)  # USD

roi = []
ac1 = [account1]
ac2 = [account2]
money = []
pnl_exch1 = []
pnl_exch2 = []

trade = False
n = df1.close.shape[0]  # number of data points

# running the backtest
for i in range(n):
    p1 = float(df1.close.iloc[i])
    p2 = float(df2.close.iloc[i])
    if(p1 > p2):
        asset1 = "SHORT"
        asset2 = "LONG"
        if(trade == False):
            open_p1 = p1  # open prices
            open_p2 = p2
            open_asset1 = asset1
            open_asset2 = asset2
            trade = True
            print("new traded opened")
            new_trade = False
        elif(asset1 == open_asset1):
            new_trade = False  # flag
        elif(asset1 == open_asset2):
            new_trade = True   # flag
            
    elif(p2 > p1):
        asset1 = "LONG"
        asset2 = "SHORT"
        if(trade == False):
            open_p1 = p1  # open prices
            open_p2 = p2
            open_asset1 = asset1
            open_asset2 = asset2
            trade = True
            print("new traded opened")
            new_trade = False
        elif(asset1 == open_asset1):
            new_trade = False  # flag
        elif(asset1 == open_asset2):
            new_trade = True   # flag
            
    if(i == 0):
        print(df1.close.iloc[i], df2.close.iloc[i], \
              asset1, asset2, trade, "----first trade info")
    else:
        if(new_trade):
            
            # close current position
            if(open_asset1 == "SHORT"):
                # PnL of both trades
                pnl_asset1 = open_p1/p1 - 1
                pnl_asset2 = p2/open_p2 -1
                pnl_exch1.append(pnl_asset1)
                pnl_exch2.append(pnl_asset2)
                print(open_p1, p1, open_p2, p2, open_asset1, \
                      open_asset2, pnl_asset1, pnl_asset2)
                # update both accounts
                account1 = account1 + position*pnl_asset1
                account2 = account2 + position*pnl_asset2
                print("accounts [USD] = ", account1, account2)
                if((account1 <=0) or (account2 <=0)):
                    print("--trading halted")
                    break
                # return on investment (ROI)
                total = account1 + account2
                roi.append(total/investment-1)
                ac1.append(account1)
                ac2.append(account2)
                money.append(total)
                print("ROI = ", roi[-1])
                print("trade closed\n")
                trade = False
                
                # open a new trade
                if(asset1 == "SHORT"):
                    open_p1 = p1
                    open_p2 = p2
                    open_asset1 = asset1
                    open_asset2 = asset2
                else:
                    open_p1 = p1
                    open_p2 = p2
                    open_asset1 = asset1
                    open_asset2 = asset2
                trade = True
                print("new trade opened", asset1, asset2, \
                      open_p1, open_p2)
             
            # close current position
            if(open_asset1 == "LONG"):
                # PnL of both trades
                pnl_asset1 = p1/open_p1 -1
                pnl_asset2 = open_p2/p2 - 1
                pnl_exch1.append(pnl_asset1)
                pnl_exch2.append(pnl_asset2)
                print(open_p1, p1, open_p2, p2, open_asset1, \
                      open_asset2, pnl_asset1, pnl_asset2)
                # update both accounts
                account1 = account1 + position*pnl_asset1
                account2 = account2 + position*pnl_asset2
                print("accounts [USD] = ", account1, account2)
                if((account1 <=0) or (account2 <=0)):
                    print("--trading halted")
                    break
                # return on investment (ROI)
                total = account1 + account2
                roi.append(total/investment-1)
                ac1.append(account1)
                ac2.append(account2)
                money.append(total)
                trade_pnl.append(pnl_asset1+pnl_asset2)
                print("ROI = ", roi[-1])
                print("trade closed\n")
                trade = False
                
                # open a new trade
                if(open_asset1 == "SHORT"):
                    open_p1 = p1
                    open_p2 = p2
                    open_asset1 = asset1
                    open_asset2 = asset2
                else:
                    open_p1 = p1
                    open_p2 = p2
                    open_asset1 = asset1
                    open_asset2 = asset2
                new_trade = False
                trade = True
                print("new trade opened:", asset1, asset2, \
                      open_p1, open_p2)
                
        else:
            print("   ",df1.close.iloc[i], df2.close.iloc[i], \
                  asset1, asset2)

An exemplary trading log you should recover based on data sample selected above commences with:

new traded opened
214.49 221.54 LONG SHORT True ----first trade info
    216.75 224.23 LONG SHORT
    236.54 245.35 LONG SHORT
    239.0 247.6 LONG SHORT
    250.36 263.6 LONG SHORT
    253.74 256.19 LONG SHORT
    249.41 260.24 LONG SHORT
    269.0 279.85 LONG SHORT
214.49 326.42 221.54 326.0 LONG SHORT 0.5218425101403328 -0.32042944785276073
accounts [USD] =  6304.606275350832 4198.926380368098
ROI =  0.05035326557189301
trade closed

new trade opened: SHORT LONG 326.42 326.0
326.42 332.0 326.0 333.1 SHORT LONG -0.016807228915662553 0.021779141104294464
accounts [USD] =  6262.588203061676 4253.374233128834
ROI =  0.05159624361905091
trade closed

new trade opened LONG SHORT 332.0 333.1
332.0 332.0 333.1 333.1 LONG SHORT 0.0 0.0
accounts [USD] =  6262.588203061676 4253.374233128834
ROI =  0.05159624361905091
trade closed

new trade opened: LONG SHORT 332.0 333.1
332.0 390.0 333.1 385.0 LONG SHORT 0.17469879518072284 -0.1348051948051947
accounts [USD] =  6699.3351910134825 3916.3612461158473
ROI =  0.06156964371293294
trade closed

new trade opened: SHORT LONG 390.0 385.0
    393.2 386.13 SHORT LONG
    369.41 347.24 SHORT LONG
    357.3 345.99 SHORT LONG
    365.81 354.17 SHORT LONG
    370.61 368.95 SHORT LONG
    357.0 349.5 SHORT LONG
    359.82 356.48 SHORT LONG
    358.03 348.5 SHORT LONG
    334.0 324.48 SHORT LONG
    325.95 321.6 SHORT LONG
    334.0 325.5 SHORT LONG
    317.47 302.15 SHORT LONG
    295.0 277.0 SHORT LONG
    264.0 254.49 SHORT LONG
390.0 280.93 385.0 283.66 SHORT LONG 0.38824618232299857 -0.26322077922077913
accounts [USD] =  7669.950646820979 3258.3092980638994
ROI =  0.09282599448848772
trade closed

...

In the first trade we open a long position at Exmo at price of 214.49 and a short position at Kraken at 221.54. After 8 days, there has been a new trading signal generated what caused to close both positions. The close price at Exmo was 326.42 so we made a profit +52.18% while the concurrent short position at Kraken closed with a loss of 32.04% because of ETH/USD rose from 221.54 up to 326.0. In result, the cash at Exmo account went up from 5,000 to 6,304.61 while at Kraken account down from 5,000 to 4,198.93 (=5000 – 0.3204$\times$2500). A new trade has been opened with a short position at Exmo and long position at Kraken, etc.

Having the backtest code, there are many places you can take it and analyse its variants. If you build-in the commission fee structure, you may find the difference in profits with and without the fees. I strongly encourage to experiment with it. Who knows… maybe you will become a millionaire too?! :-)

Just to assist with the understanding of some numbers we captured along the backtest (see all Python lists; lines #163-168), first let’s have a look at the cash level in both accounts:

plt.figure(figsize=(12,7))

plt.subplot(2,3,1)
plt.plot(ac1)
plt.title("Exmo: Cash Level")
plt.xlabel("Trade No."); plt.ylabel("USD")
plt.grid()
plt.xlim([0, len(money)])
plt.ylim([0, 14001])

plt.subplot(2,3,2)
plt.plot(ac2)
plt.title("Kraken: Cash Level")
plt.xlabel("Trade No."); plt.ylabel("USD")
plt.grid()
plt.xlim([0, len(money)])
plt.ylim([0, 14001])

plt.subplot(2,3,3)
plt.plot(np.array(money))
plt.title("Total Cash")
plt.xlabel("Trade No."); plt.ylabel("USD")
plt.grid()
plt.xlim([0, len(money)])
plt.ylim([investment, 14000])

plt.tight_layout()
plt.savefig('cashlevels.png', bbox_inches='tight')


These charts indicate that within our Stat Arb strategy the performance of trades at Exmo was much much better than at Kraken. Only the final ca. 10 trades at Kraken brought some profits. Both characteristics have their conclusion as presented on the third chart: Total Cash.

In other words, after 5 months of running the strategy, we ended up with a nice +39.4% return on our initial investment of USD 10,000, i.e.

plt.figure(figsize=(8,5))
plt.plot(np.array(roi)*100, 'g')
plt.xlabel("Trade No."); 
plt.ylabel("Equity Growth [%]")
plt.title("ROI = %f.2%%" % (100*roi[-1]))
plt.xlim([0, len(money)])
plt.ylim([0, 40])
plt.grid()
plt.savefig('roi.png', bbox_inches='tight')

Final Note

Greed is good. Use this algo wisely! Don’t buy a jet. Instead, make your wife happy. Tiffany would do the job… :-)




7 comments
  1. Hey very cool blog!! Man .. Beautiful .. Wonderful .. I will bookmark your website and
    take the feeds additionally? I’m satisfied to search out numerous useful info right here within the post, we need work out more
    techniques on this regard, thanks for sharing. . . . . .

  2. the only thing you are missing, is the delirious cost to trade cryptos on those new pseudo exchanges…ROI < 0

  3. Hi Pawel,

    Very nice article! One tip can be using the request session to leverage the keep-alive connection.

    s = requests.session()

    trying something like:
    response = s.get(url)

    instead of
    response = request.get(url)

    Best,
    Ronaldo

  4. when running
    ====================
    # Select Top 10 Cryptocurrency Markets by their volume
    markets = [e[0] for e in vol][0:10]
    print(markets)
    ==================

    i am getting this (which is not as shown above):
    Bitfinex 220700.34
    [‘Bitfinex’, ‘Coinbase’, ‘Gemini’, ‘Poloniex’, ‘HitBTC’, ‘Kraken’, ‘BitTrex’, ‘Cexio’, ‘Exmo’, ‘Quoine’]
    Coinbase 144549.34
    [‘Bitfinex’, ‘Coinbase’, ‘Gemini’, ‘Poloniex’, ‘HitBTC’, ‘Kraken’, ‘BitTrex’, ‘Cexio’, ‘Exmo’, ‘Quoine’]
    Gemini 44508.90
    [‘Bitfinex’, ‘Coinbase’, ‘Gemini’, ‘Poloniex’, ‘HitBTC’, ‘Kraken’, ‘BitTrex’, ‘Cexio’, ‘Exmo’, ‘Quoine’]
    Poloniex 37787.74
    [‘Bitfinex’, ‘Coinbase’, ‘Gemini’, ‘Poloniex’, ‘HitBTC’, ‘Kraken’, ‘BitTrex’, ‘Cexio’, ‘Exmo’, ‘Quoine’]

    I will be happy if you could assist me what did i do wrong. thanks!

  5. Are your “prices” mid-point quotes, or trades? If trades, are you sure you are not simply fitting to stale trade noise? I’d wager this entire strategy is untradeable.

  6. Hello Pawel,

    Very good article :)

    I think you are missing declare trade_pnl = [] array in your code !

    Regards
    Ludo.

Leave a Reply

Your email address will not be published. Required fields are marked *