Trading Volume in Cryptocurrency Markets and Reversals
Log in to collectAcademic paper
Trading Volume in Cryptocurrency Markets
Daniele Bianchi; Alexander Dickerson
- 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"))