Salience Theory and Cryptocurrency Returns
Log in to collectAcademic paper
Salience Theory and Cryptocurrency Returns
Charlie X. Cai; Ran Zhao
- University of Liverpool
- ?University of Liverpool Management School
- Claremont Graduate University
- ?Claremont Graduate University, Drucker School of Management
Strategy in a nutshell
The strategy invests in cryptocurrencies with a market capitalization above $1 million, using data from coinmarketcap.com. It calculates a Salience Measure (ST) for each crypto, which quantifies how much a crypto’s daily return deviates from the market average, adjusted by salience weights. Cryptos are then ranked based on ST values and sorted into quintiles. The strategy goes long on the lowest ST quintile (least salient) and short on the highest ST quintile (most salient). Portfolios are value-weighted and rebalanced regularly to capture the expected price reversals driven by salience effects.
Economic rationale
Cryptocurrency markets are dominated by retail investors who are highly susceptible to behavioral biases, particularly salience and attention-driven trading. Cryptos with unusually high salience often experience overreactions, while low-salience cryptos are underappreciated. As market prices revert to fundamental equilibrium, this creates predictable profit opportunities. The strategy leverages these behavioral patterns, exploiting the temporary mispricing caused by trend amplification, hype, or community-driven sentiment in the crypto market.
Backtest performance
Full Python code
from AlgorithmImports import *
from typing import List, Dict
class SalienceTheoryandCryptocurrencyReturns(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.cryptos: Dict[str, str] = {
"XLMUSD": "XLM", # Stellar
"XMRUSD": "XMR", # Monero
"XRPUSD": "XRP", # XRP
"ADAUSD": "ADA", # Cardano
"DOTUSD": "DOT", # Polkadot
"UNIUSD": "UNI", # Uniswap
"LINKUSD": "LINK", # Chainlink
"ANTUSD": "ANT", # Aragon
"BATUSD": "BAT", # Basic Attention Token
"BTCUSD": "BTC", # Bitcoin
"BTGUSD": "BTG", # Bitcoin Gold
"DAIUSD": "DAI", # Dai
"DASHUSD": "DASH", # Dash
"DGBUSD": "DGB", # Dogecoin
"ETCUSD": "ETC", # Ethereum Classic
"ETHUSD": "ETH", # Ethereum
"FUNUSD": "FUN", # FUN Token
"LTCUSD": "LTC", # Litecoin
"MKRUSD": "MKR", # Maker
"NEOUSD": "NEO", # Neo
"PAXUSD": "PAX", # Paxful
"SNTUSD": "SNT", # Status
"TRXUSD": "TRX", # Tron
"XRPUSD": "XRP", # XRP
"XTZUSD": "XTZ", # Tezos
"XVGUSD": "XVG", # Verge
"ZECUSD": "ZEC", # Zcash
"ZRXUSD": "ZRX" # Ox
}
self.symbol_data: Dict[str, data_tools.SymbolData] = {}
self.ranking_period: int = 21
self.momentum_period: int = self.ranking_period * 2 # need n of daily prices
self.quantile: int = 5
self.portfolio_percentage: float = .2
self.leverage: int = 3
self.delta: float = .7
self.theta: float = .1
self.SetBrokerageModel(BrokerageName.Bitfinex)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
for ticker, crypto in self.cryptos.items():
data: Crypto = self.AddCrypto(ticker, Resolution.Daily, Market.Bitfinex)
data.SetFeeModel(data_tools.CustomFeeModel())
data.SetLeverage(self.leverage)
network_symbol: Symbol = self.AddData(data_tools.CryptoNetworkData, crypto, Resolution.Daily).Symbol
self.symbol_data[ticker] = data_tools.SymbolData(network_symbol, self.momentum_period)
self.recent_month: int = -1
def OnData(self, data: Slice) -> None:
crypto_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.CryptoNetworkData.get_last_update_date()
# daily updating of crypto prices and market capitalization(CapMrktCurUSD)
for crypto, symbol_obj in self.symbol_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.symbol_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
self.symbol_data[crypto].update_cap(cap_mrkt_cur_usd)
# monthly rebalance
if self.recent_month == self.Time.month:
return
prepared_symbols: List[Tuple[str, data_tools.CryptoNetworkData]] = [(crypto, symbol_obj) for crypto, symbol_obj in self.symbol_data.items() if symbol_obj.is_ready() and \
self.Securities[symbol_obj._network_symbol].GetLastData() and self.Time.date() < crypto_data_last_update_date[symbol_obj._network_symbol] and \
crypto in data and data[crypto]]
ST: Dict[str, float] = {}
if len(prepared_symbols) != 0:
self.recent_month = self.Time.month
daily_returns: np.ndarray = np.array([symbol_obj.daily_returns() for _, symbol_obj in prepared_symbols])
# salience
mean_returns: np.ndarray = np.mean(daily_returns, axis=0)
salience: np.ndarray = abs(daily_returns - mean_returns) / (abs(daily_returns) + abs(mean_returns) + self.theta)
# salience ranking
salience_order: np.ndarray = salience.argsort(axis=0)
salience_rank: np.ndarray = salience_order.argsort(axis=0)
pi: float = 1. / float(self.ranking_period)
salience_rank_sum: np.ndarray = salience_rank.sum(axis=0) * pi
# decision weights
weights: np.ndarray = salience_rank / salience_rank_sum
# salience effect
ST = { prepared_symbols[i] : np.cov(stack)[0][1] for i, stack in enumerate(np.stack((weights, daily_returns), axis=1)) }
long: List[str] = []
short: List[str] = []
if len(ST) >= self.quantile:
# sort the portfolio into quintiles, long the lowest quintile, short the highest
sorted_by_ST: List[Tuple[str, float]] = sorted(ST.items(), key=lambda x:x[1], reverse=True)
quantile: int = int(len(sorted_by_ST) / self.quantile)
long = [x[0] for x in sorted_by_ST[-quantile:]]
short = [x[0] for x in sorted_by_ST[:quantile]]
# value weighting
weight: Dict[str, float] = {}
for i, portfolio in enumerate([long, short]):
mc_sum:float = sum(list(map(lambda x: x[1]._cap_mrkt_cur_usd, portfolio)))
for ticker, symbol_obj in portfolio:
weight[ticker] = ((-1)**i) * symbol_obj._cap_mrkt_cur_usd / mc_sum
# trade execution
portfolio:List[PortfolioTarget] = [PortfolioTarget(symbol, self.portfolio_percentage * w) for symbol, w in weight.items() if symbol in data and data[symbol]]
self.SetHoldings(portfolio, True)