Quant BuffetRelax, Not Over Thinking

24-Hour Reversal in Cryptocurrencies

Log in to collect

Academic paper

Pure Momentum in Cryptocurrency Markets

AuthorsCesare Fracassi; Shimon Kogan

Institute
  • 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)