24-Hour Reversal in Cryptocurrencies
Log in to collectAcademic paper
Pure Momentum in Cryptocurrency Markets
Cesare Fracassi; Shimon Kogan
- The University of Texas at Austin
- ?Coinbase Institute
- ?University of Texas at Austin
- Brandman University
- University of Pennsylvania
- ?Reichman University - Arison School of Business
- ?University of Pennsylvania - The Wharton School
Strategy in a nutshell
The strategy trades 138 cryptocurrencies hourly by ranking their past 24 one-hour returns. Investors go long on the two lowest-performing cryptos and short the two highest-performing ones. Positions are equally weighted and rebalanced every hour.
Economic rationale
The strategy exploits “pure momentum” in cryptocurrencies, capturing price movements driven solely by past returns rather than fundamentals. Investor underreaction, behavioral biases, and shifting reference points contribute to predictable short-term price patterns in the crypto market.
Backtest performance
Annualised return53.8%
Volatility15.28%
Beta0.301
Sharpe ratio3.52
Sortino ratio0.949
Win rate19%
Full Python code
from AlgorithmImports import *
from data_tools import CustomFeeModel, SymbolData
# endregion
class HourReversalInCryptocurrencies(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2016, 1, 1)
self.SetCash(100000)
self.cryptos:list[str] = [
#"ANTUSD", # Aragon
#"BATUSD", # Basic Attention Token
"BTCUSD", # Bitcoin
#"DAIUSD", # Dai
#"DGBUSD", # Dogecoin
#"EOSUSD", # EOS
#"ETCUSD", # Ethereum Classic
"ETHUSD", # Ethereum
#"FUNUSD", # FUN Token
"LTCUSD", # Litecoin
#"MKRUSD", # Maker
#"NEOUSD", # Neo
#"OMGUSD", # OMG Network
#"SNTUSD", # Status
#"TRXUSD", # Tron
#"XLMUSD", # Stellar
#"XMRUSD", # Monero
"XRPUSD", # XRP
#"XTZUSD", # Tezos
#"XVGUSD", # Verge
#"ZECUSD", # Zcash
#"ZRXUSD" # Ox
]
self.data:dict[Symbol, SymbolData] = {}
self.lag_period:int = 24 + 1
self.max_missing_hours:int = 0
self.leg_count:int = 2
self.leverage:int = 5
self.portfolio_percentage:float = 0.1
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.Hour, Market.Bitfinex)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(self.leverage)
self.data[data.Symbol] = SymbolData(self.lag_period)
def OnData(self, data):
curr_time:datetime = self.Time
performance:dict[Symbol, float] = {}
for symbol, symbol_data in self.data.items():
if symbol in data and data[symbol]:
price:float = data.Bars[symbol].Value
if not symbol_data.data_still_coming(curr_time, self.max_missing_hours):
symbol_data.reset_data()
if symbol_data.prev_price_ready():
symbol_data.calculate_perf(price)
if symbol_data.performances_ready():
performance[symbol] = symbol_data.get_first_perf()
symbol_data.update_price(curr_time, price)
if len(performance) < (2 * self.leg_count):
self.Liquidate()
return
sorted_by_perf:list[Symbol] = [x[0] for x in sorted(performance.items(), key=lambda item: item[1])]
long_leg:list[Symbol] = sorted_by_perf[:self.leg_count]
short_leg:list[Symbol] = sorted_by_perf[-self.leg_count:]
# trade execution
invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long_leg + short_leg:
self.Liquidate(symbol)
for symbol in long_leg:
q:float = self.CalculateOrderQuantity(symbol, (1 / self.leg_count) * self.portfolio_percentage)
if q > self.Securities[symbol].SymbolProperties.MinimumOrderSize:
self.MarketOrder(symbol, q)
for symbol in short_leg:
q:float = self.CalculateOrderQuantity(symbol, (1 / self.leg_count) * self.portfolio_percentage)
if q > self.Securities[symbol].SymbolProperties.MinimumOrderSize:
self.MarketOrder(symbol, -q)