Cross-sectional Momentum in Large Cryptos
Log in to collectAcademic paper
Value Premium, Network Adoption, and Factor Pricing of Crypto Assets
Lin William Cong; George Andrew Karolyi; Ke Tang; Weiyi Zhao
- Cornell University
- ?Cornell University - Samuel Curtis Johnson Graduate School of Management
- Tsinghua University
- ?Institute of Economics, School of Social Sciences, Tsinghua University
Strategy in a nutshell
The investment universe consists of cryptocurrencies sourced from CoinMarketCap.com. Stablecoins, and coins with zero price, market capitalization, or trading volume across all periods, are excluded. First, identify the large-cap sample comprising cryptos with market capitalization above 1 million USD. Within this large-cap universe, sort cryptocurrencies by their past two-week returns (momentum) into decile portfolios. The strategy goes long the top decile and short the bottom decile. Portfolios are rebalanced weekly and weighted by market value.
Economic rationale
The momentum effect is observed through a portfolio-sorting approach and is statistically significant. While the exact economic or behavioral reason is not specified, traditional explanations include herding behavior and over- or underreaction. Short-term momentum strategies track recent trends and are particularly suited for highly dynamic markets such as cryptocurrencies, potentially capturing emerging price movements effectively.
Backtest performance
Full Python code
from AlgorithmImports import *
from typing import List, Dict
class CrosssectionalMomentumInLargeCryptos(QCAlgorithm):
def Initialize(self) -> None:
self.SetStartDate(2015, 1, 1)
self.SetCash(1_000_000)
self.period: int = 14 # need n of daily prices
self.quantile: int = 3
self.portfolio_percentage: float = .1
self.leverage: int = 10
self.cryptos: Dict[str, str] = {
"ANTUSD": "ANT", # Aragon
"BATUSD": "BAT", # Basic Attention Token
"BTCUSD": "BTC", # Bitcoin
"BTGUSD": "BTG", # Bitcoin Gold
"DAIUSD": "DAI", # Dai
"DGBUSD": "DGB", # Dogecoin
"EOSUSD": "EOS", # EOS
"ETCUSD": "ETC", # Ethereum Classic
"ETHUSD": "ETH", # Ethereum
"FUNUSD": "FUN", # FUN Token
"LTCUSD": "LTC", # Litecoin
"MKRUSD": "MKR", # Maker
"NEOUSD": "NEO", # Neo
"OMGUSD": "OMG", # OMG Network
"SNTUSD": "SNT", # Status
"TRXUSD": "TRX", # Tron
"XLMUSD": "XLM", # Stellar
"XMRUSD": "XMR", # Monero
"XRPUSD": "XRP", # XRP
"XTZUSD": "XTZ", # Tezos
"XVGUSD": "XVG", # Verge
"ZECUSD": "ZEC", # Zcash
"ZRXUSD": "ZRX" # Ox
}
self.data: Dict[str, data_tools.SymbolData] = {}
self.weight: Dict[str, float] = {}
self.SetBrokerageModel(BrokerageName.Bitfinex)
for crypto, ticker in self.cryptos.items():
# GDAX is coinmarket, but it doesn't support this many cryptos, so we choose Bitfinex
data: Securities = self.AddCrypto(crypto, Resolution.Daily, Market.Bitfinex)
data.SetLeverage(self.leverage)
network_symbol: Symbol = self.AddData(data_tools.CryptoNetworkData, ticker, Resolution.Daily).Symbol
self.data[crypto] = data_tools.SymbolData(network_symbol, self.period)
self.rebalance_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.WeekStart("BTCUSD"), self.TimeRules.At(0,0), self.Rebalance)
def OnData(self, data: Slice) -> None:
# daily updating of crypto prices and market capitalization(CapMrktCurUSD)
for crypto, symbol_obj in self.data.items():
network_symbol: Symbol = symbol_obj.network_symbol
if crypto in data.Bars and data[crypto]:
# get crypto price
price: float = data.Bars[crypto].Value
self.data[crypto].update(price)
if network_symbol in data and data[network_symbol]:
# get market capitalization
cap_mrkt_cur_usd: float = data[network_symbol].Value
if cap_mrkt_cur_usd != 0:
self.data[crypto].update_cap(cap_mrkt_cur_usd)
if not self.rebalance_flag:
return
# trade execution
invested: List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for ticker in invested:
if ticker not in self.weight:
self.Liquidate(ticker)
for ticker, w in self.weight.items():
self.SetHoldings(ticker, w)
self.rebalance_flag = False
self.weight.clear()
def Rebalance(self) -> None:
self.rebalance_flag = True
crypto_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.CryptoNetworkData.get_last_update_date()
performance: Dict[str, float] = {}
for crypto, symbol_obj in self.data.items():
network_symbol: Symbol = symbol_obj.network_symbol
if network_symbol not in crypto_data_last_update_date:
continue
# crypto doesn't have enough data
if self.Securities[network_symbol].GetLastData() and self.Time.date() > crypto_data_last_update_date[network_symbol]:
self.Liquidate()
return
if symbol_obj.is_ready():
# calculate performance for current crypto
performance[crypto] = symbol_obj.performance()
# not enough cryptos for selection
if len(performance) < self.quantile:
self.Liquidate()
return
# perform selection
quantile: int = int(len(performance) / self.quantile)
sorted_by_perf: List[str] = [x[0] for x in sorted(performance.items(), key=lambda item: item[1])]
# long top quantile
long: List[str] = sorted_by_perf[-quantile:]
# short bottom quantile
short: List[str] = sorted_by_perf[:quantile]
# value weighting
for i, portfolio in enumerate([long, short]):
mc_sum:float = sum(list(map(lambda ticker: self.data[ticker].cap_mrkt_cur_usd, portfolio)))
for ticker in portfolio:
self.weight[ticker] = ((-1) ** i) * (self.data[ticker].cap_mrkt_cur_usd / mc_sum)