Quant BuffetRelax, Not Over Thinking

Salience Theory and Cryptocurrency Returns

Log in to collect

Academic paper

Salience Theory and Cryptocurrency Returns

AuthorsCharlie X. Cai; Ran Zhao

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

Annualised return280.1%
Volatility45.3%
Beta-0.006
Sharpe ratio6.18
Sortino ratio-0.073
Win rate47%

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)