Quant BuffetRelax, Not Over Thinking

Cointegrated Cryptocurrency Portfolios

Log in to collect

Academic paper

Constructing Cointegrated Cryptocurrency Portfolios for Statistical Arbitrage

AuthorsTim Leung; Hung Cuong Nguyen

Institute
  • University of Washington
  • ?University of Washington - Department of Applied Math
  • BGRisk Engineering (Bulgaria)
  • ?Computational Finance and Risk Management

Strategy in a nutshell

Trades a spread of Bitcoin, Ethereum, Litecoin, and Bitcoin Cash based on cointegration. Long the spread when it falls below mean −0.5×SD and short when above mean +0.5×SD, holding one unit at a time with positions sized by OLS-estimated beta coefficients.

Economic rationale

Exploits mean reversion in cointegrated crypto prices: the spread, constructed from a stationary combination of non-stationary assets, reverts to its mean over time, allowing profits from buying low and selling high (or shorting high and covering low).

Backtest performance

Annualised return42.68%
Beta-0.148
Maximum drawdown-15.98%
Win rate46%

Full Python code

from AlgorithmImports import *
import numpy as np
import statsmodels.api as sm
#endregion
class CointegratedCryptocurrencyPortfolios(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)

self.cryptos = [
    "BTCUSD", # Bitcoin
    "ETHUSD", # Ethereum
    # "BCHUSD", # Bitcoin cash  # bchusd bitfinex quantconnect price ends in 2018.
    "LTCUSD", # Litecoin
]

self.data = {}
self.spread = []

self.c = 0.5 # Constant for this strategy, however there are other possible values in source paper
self.period = 21

self.last_day = -1
self.invested_long = None

self.SetBrokerageModel(BrokerageName.Bitfinex)

for crypto in self.cryptos:
    data = self.AddCrypto(crypto, Resolution.Daily, Market.Bitfinex)
    data.SetFeeModel(CustomFeeModel())
    data.SetLeverage(5)
    
    self.data[crypto] = RollingWindow[float](self.period)
def OnData(self, data):
if self.last_day == self.Time.day: return
self.last_day = self.Time.day

for crypto in self.cryptos:
    if crypto in data.Bars:
        if data[crypto]:
            price = data.Bars[crypto].Value
            self.data[crypto].Add(price)

used_cryptos = []            
bitcoin_prices = None
other_currencies_prices = []
for crypto in self.cryptos:
    if not self.data[crypto].IsReady:
        return
    else:
        if crypto is "BTCUSD":
            bitcoin_prices = np.array([x for x in self.data[crypto]])
        else:
            crypto_prices = [x for x in self.data[crypto]]
            
            if not crypto_prices[1:] == crypto_prices[:-1]: # If values in one list aren't same, then it can be use in MultipleLinearRegression
                other_currencies_prices.append([x for x in self.data[crypto]])
                used_cryptos.append(crypto)
                
if len(other_currencies_prices) == 0:
    self.Liquidate()
    return

regression_model = self.MultipleLinearRegression(other_currencies_prices, bitcoin_prices)

alpha = regression_model.params[0]
betas = [regression_model.params[index] for index in range(len(regression_model.params)) if index != 0]

beta_index = 0
current_spread_value = 0
for crypto in self.cryptos:
    if crypto is "BTCUSD":
        current_spread_value = current_spread_value + self.data[crypto][0]
    elif crypto in used_cryptos:
        current_speard_value = self.data[crypto][0] * betas[beta_index]
        beta_index = beta_index + 1
        
self.spread.append(current_spread_value)

if len(self.spread) < self.period: # We need at least one month values of spread
    return

threshold_long = np.mean(self.spread) - self.c * np.std(self.spread)
threshold_short = np.mean(self.spread) + self.c * np.std(self.spread)

if self.invested_long is None:
    if current_spread_value < threshold_long: # long or exit short
        self.InvestLong(betas, used_cryptos)
        self.invested_long = True
    elif current_spread_value > threshold_short: # short or exit long
        self.InvestShort(betas, used_cryptos)
        self.invested_long = False
else:
    if current_spread_value < threshold_long and self.invested_long: # long or exit short
        self.invested_long = True
        self.InvestLong(betas, used_cryptos)
    elif current_spread_value > threshold_short and not self.invested_long: # short or exit long
        self.invested_long = False
        self.InvestShort(betas, used_cryptos)
  
def InvestLong(self, betas, used_cryptos):
beta_index = 0
for crypto in self.cryptos:
    if self.Portfolio[crypto].Invested:
        self.Liquidate(crypto)
    if crypto is "BTCUSD":
        self.MarketOrder(crypto, 1)
    elif crypto in used_cryptos:
        self.MarketOrder(crypto, betas[beta_index])
        beta_index = beta_index + 1

def InvestShort(self, betas, used_cryptos):
beta_index = 0
for crypto in self.cryptos:
    if self.Portfolio[crypto].Invested:
        self.Liquidate(crypto)
    if crypto is "BTCUSD":
        self.MarketOrder(crypto, -1)
    elif crypto in used_cryptos:
        self.MarketOrder(crypto, -betas[beta_index])
        beta_index = beta_index + 1

def MultipleLinearRegression(self, x, y):
x = np.array(x).T
x = sm.add_constant(x)
result = sm.OLS(endog=y, exog=x).fit()
return result  
                    
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))