Time-Series Momentum Factor in Cryptocurrencies
Log in to collectAcademic paper
Stefan Hubrich
- T. Rowe Price Program for Charitable Giving
- ?T.Rowe Price
Strategy in a nutshell
The strategy trades 11 cryptocurrencies using a weekly momentum signal based on prior returns, standardized by z-scores. Positions are equally weighted with up to 10% total exposure, focusing on long-only implementation due to shorting limits. Portfolios are rebalanced weekly, with optional risk-based weighting.
Economic rationale
Weekly momentum captures short-term trends in volatile crypto markets, producing strong positive alphas. Results show momentum signals effectively predict returns, confirming that even in high-volatility environments, disciplined momentum strategies can deliver superior performance.
Backtest performance
Annualised return6.3%
Volatility8.1%
Beta0.035
Sharpe ratio0.78
Sortino ratio0.487
Win rate52%
Full Python code
from AlgorithmImports import *
import numpy as np
#endregion
class TimeSeriesMomentumCryptocurrencies(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.symbols = ['BTCUSD', 'ETCUSD', 'ETHUSD', 'LTCUSD', 'XMRUSD', 'ZECUSD']
self.data = {}
self.percentage_traded = 0.1
for symbol in self.symbols:
data = self.AddCrypto(symbol, Resolution.Daily, Market.Bitfinex)
data.SetFeeModel(CustomFeeModel())
data.SetLeverage(10)
self.data[symbol] = RollingWindow[float](5)
def OnData(self, data):
for symbol in self.data:
symbol_obj = self.Symbol(symbol)
if symbol_obj in data.Bars and data[symbol_obj]:
self.data[symbol].Add(data[symbol_obj].Value)
if self.Time.date().weekday() != 0:
return
perf_vol = {}
for symbol in self.symbols:
if self.data[symbol].IsReady:
prices = np.array([x for x in self.data[symbol]])
perf = prices[0] / prices[-1] - 1
daily_returns = prices[:-1] / prices[1:] - 1
vol = np.std(daily_returns)
perf_vol[symbol] = (perf, vol)
# Volatility weighting
total_vol = sum([1 / x[1][1] for x in perf_vol.items()])
if total_vol == 0: return
weight = {}
for symbol in perf_vol:
vol = perf_vol[symbol][1]
if vol != 0:
weight[symbol] = (1.0 / vol) / total_vol
else:
weight[symbol] = 0
# Trade execution.
long = [x[0] for x in perf_vol.items() if x[1][0] > 0]
invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in long:
self.Liquidate(symbol)
for symbol in long:
if symbol in data and data[symbol]:
self.SetHoldings(symbol, self.percentage_traded * weight[symbol])
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))