Quant BuffetRelax, Not Over Thinking

Trading Volume in Cryptocurrency Markets and Reversals

Log in to collect

Academic paper

Trading Volume in Cryptocurrency Markets

AuthorsDaniele Bianchi; Alexander Dickerson

Institute
  • Queen Mary University of London
  • ?School of Economics and Finance, Queen Mary University of London
  • University of Warwick
  • ?Warwick Business School

Strategy in a nutshell

Trades 26 crypto pairs daily by sorting them into groups based on past returns and volume shocks. Goes long on low-return, low-volume pairs and short on high-return, low-volume pairs, with daily rebalancing and optional CFD execution.

Economic rationale

Profits from reversal patterns: past low returns combined with low volume predict future positive reversals, while high-return, low-volume pairs tend to decline. Strategy is robust to transaction costs and not driven by traditional risk factors or illiquidity.

Backtest performance

Annualised return41.18%
Volatility7.84%
Beta-0.01
Sharpe ratio5.25
Sortino ratio-0.247
Win rate50%

Full Python code

from AlgorithmImports import *
class TradingVolumeInCryptocurrencyMarketsAndReversals(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)

self.cryptos = [
    "BTCUSD", # Bitcoin
    "ETHUSD", # Ethereum
    "XRPUSD", # XRP
    # "BCHUSD", # Bitcoin cash
    "LTCUSD", # Litecoin
    "BSVUSD", # Bitcoin SV
    "EOSUSD", # EOS
    "XMRUSD", # Monero
    "TRXUSD", # Tron
    "XTZUSD", # Tezos
    "XLMUSD", # Stellar
    "NEOUSD", # Neo
    "DAIUSD", # Dai
    "ZECUSD", # Zcash
    "VETUSD", # VeChain
    "ETCUSD", # Ethereum Classic
    "MKRUSD", # Maker
    "OMGUSD", # OMG Network
    # "DGBUSD", # Dogecoin
    # "BATUSD", # Basic Attention Token
    # "ZRXUSD", # Ox
]

self.data = {}
self.period = 61
self.traded_percentage = 0.1
self.quantile = 3

self.SetBrokerageModel(BrokerageName.Bitfinex)

for crypto in self.cryptos:
    # GDAX is coinmarket, but it doesn't support this many cryptos, so we choose Bitfinex
    data = self.AddCrypto(crypto, Resolution.Minute, Market.Bitfinex)
    data.SetFeeModel(CustomFeeModel())
    data.SetLeverage(10)
    
    self.data[crypto] = SymbolData(crypto, self.period)

self.last_day = -1

def OnData(self, data):
performance = {}
volume_shock = {}

if self.last_day == self.Time.day: return
self.last_day = self.Time.day

for crypto in self.cryptos:
    if crypto in data.Bars and data[crypto]:
        # Volume can be taken only from TradeBar and data[crypto] returns QuoteBar by default
        price = data.Bars[crypto].Value
        volume = data.Bars[crypto].Volume
        self.data[crypto].update(price, volume)
        
        if self.data[crypto].is_ready():
            result_volume_shock = self.data[crypto].volume_shock()
            if result_volume_shock:
                performance[crypto] = self.data[crypto].performance()
                volume_shock[crypto] = result_volume_shock
if len(performance) < self.quantile:
    self.Liquidate()
    return
sorted_by_performance = [x[0] for x in sorted(performance.items(), key=lambda item: item[1])]
sorted_by_volume_shock = [x[0] for x in sorted(volume_shock.items(), key=lambda item: item[1])]

quantile = int(len(sorted_by_performance) / self.quantile)
lowest_performance = sorted_by_performance[:quantile]
lowest_volume_shock = sorted_by_volume_shock[:quantile]

highest_performance = sorted_by_performance[-quantile:]

long = [x for x in lowest_performance if x in lowest_volume_shock]
short = [x for x in highest_performance if x in lowest_volume_shock]

# Trade execution
invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
    if symbol not in long + short:
        self.Liquidate(symbol)

long_length = len(long)
short_length = len(short)

for crypto in long:
    if crypto in data and data[crypto]:
        self.SetHoldings(crypto, self.traded_percentage / long_length)
    
for crypto in short:
    if crypto in data and data[crypto]:
        self.SetHoldings(crypto, -self.traded_percentage / short_length)

class SymbolData():
def __init__(self, symbol, period):
self.Symbol = symbol
self.Closes = RollingWindow[float](period)
self.Volumes = RollingWindow[float](period)
    
def update(self, close, volume):
self.Closes.Add(close)
self.Volumes.Add(volume)
    
def is_ready(self):
return self.Closes.IsReady and self.Volumes.IsReady

def performance(self):
closes = [x for x in self.Closes]
return closes[0] / closes[-1] - 1

# Log deviation, 10 base log of current day volume - 10 base log of sum of 30 days volumes before current day devided by 30    
def volume_shock(self):
volumes = [x for x in self.Volumes]
current_day_volume = volumes[0]
avg_volume = sum(volumes[1:]) / len(volumes[1:]) # sum of 30 days volumes before current day devided by 30
if (current_day_volume <= 0):
    return None
else:
    return np.log(volumes[0]) - np.log(avg_volume)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))