Quant BuffetRelax, Not Over Thinking

Financial Uncertainty Explains Cryptocurrency Returns

Log in to collect

Academic paper

Strategy in a nutshell

The strategy sorts cryptocurrencies into terciles each month based on their sensitivity to financial uncertainty (EFU). It goes long on low-beta cryptos and short on high-beta cryptos, forming value-weighted portfolios rebalanced monthly, with a suggested allocation of 10% of trading capital.

Economic rationale

Cryptocurrencies respond primarily to financial uncertainty rather than economic fundamentals. This creates a systematic, priced risk factor driven by institutional activity and cross-market spillovers, making uncertainty a key determinant in crypto asset pricing and portfolio strategies.

Backtest performance

Annualised return20.7%
Volatility10.69%
Beta0.001
Sharpe ratio1.94
Win rate50%

Full Python code

from AlgorithmImports import *
from collections import deque
from dateutil.relativedelta import relativedelta
from pandas.core.frame import DataFrame
import data_tools
import statsmodels.api as sm
# endregionx

class FinancialUncertaintyExplainsCryptocurrencyReturns(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)

self.period:int = 24
self.portion:float = 0.1
self.rebalance_hour:int = 0 # rebalance at 00:00
self.portfolio_percentage:float = .1
self.quantile:int = 3
# self.leverage:int = 2

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
    "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.data:Dict[Symbol, SymbolData] = {}

# data subscription
for pair, ticker in self.cryptos.items():
    data = self.AddCrypto(pair, Resolution.Hour, Market.Bitfinex)
    data.SetFeeModel(data_tools.CustomFeeModel())
    # data.SetLeverage(self.leverage)
    network_data = self.AddData(data_tools.DailyCustomData, ticker, Resolution.Daily).Symbol

    self.data[data.Symbol] = data_tools.SymbolData(network_data, self.period)

self.fin_uncertainty:Symbol = self.AddData(data_tools.FinancialUncertaintyIndex, 'FIN_UN', Resolution.Daily).Symbol

self.rebalance_flag:bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.

self.current_month:int = -1

def OnData(self, data: Slice):
# monthly rebalance
if self.Time.month == self.current_month:
    return

if not self.Time.hour == self.rebalance_hour:
    return
self.current_month = self.Time.month

# update monthly price
for symbol, symbol_data in self.data.items():
    network_symbol:Symbol = symbol_data._network_symbol

    if symbol in data and data[symbol]:
        self.data[symbol].update(data[symbol].Close, self.Time.date())

    if network_symbol in data and data[network_symbol]:
        cap_mrkt_cur_usd:float = data[network_symbol].Value
        self.data[symbol].update_cap(cap_mrkt_cur_usd, self.Time.date())

network_data_last_update_date:Dict[Symbol, datetime.date] = data_tools.DailyCustomData.get_last_update_date()
financial_uncertainty_last_update_date:datetime.date = data_tools.FinancialUncertaintyIndex.get_last_update_date()

# custom data still comming in
if all([self.Securities[x].GetLastData() for x in list(self.data.keys())]) and any([self.Time.date() >= network_data_last_update_date[x] for x in network_data_last_update_date]) \
    or self.Time.date() >= financial_uncertainty_last_update_date:
    self.Liquidate()
    return

history_financial_uncertainty:DataFrame = self.History([self.fin_uncertainty], start=self.Time.date() - relativedelta(months=self.period), end=self.Time.date()).value.unstack(level=0)

crypto_returns_dict:Dict[Symbol, np.ndarray] = {symbol : symbol_data.get_returns() for symbol, symbol_data in self.data.items() if symbol_data.is_ready()}
crypto_returns:List[float] = list(zip(*[[i for i in x] for x in crypto_returns_dict.values()]))

if len(crypto_returns) == 0:
    return

# run stock regression
y:np.ndarray = np.array(crypto_returns)
x:np.ndarray = history_financial_uncertainty[1:].values
model = self.multiple_linear_regression(x, y)
beta_values:np.ndarray = model.params[1]

# store betas
beta_by_symbol:Dict[Symbol, float] = {sym : beta_values[n] for n, sym in enumerate(list(crypto_returns_dict.keys()))}

long:List[Symbol] = []
short:List[Symbol] = []

# sort by beta and divide into quantiles
if len(beta_by_symbol) >= self.quantile:
    sorted_gamma:List[Symbol] = sorted(beta_by_symbol.items(), key=lambda x:x[1])
    quantile:int = int(len(beta_by_symbol) / self.quantile)
    long = [symbol for symbol, beta in sorted_gamma][:quantile]
    short = [symbol for symbol, beta in sorted_gamma][-quantile:]

# weight:Dict[Symbol, float] = {}

# total_cap_long:float = sum([symbol_data._cap_mrkt_cur_usd for symbol, symbol_data in self.data.items() if symbol in long])
# for symbol in long:
#     weight[symbol] = self.data[symbol]._cap_mrkt_cur_usd / total_cap_long

# total_cap_short:float = sum([symbol_data._cap_mrkt_cur_usd for symbol, symbol_data in self.data.items() if symbol in short])
# for symbol in short:
#     weight[symbol] = -self.data[symbol]._cap_mrkt_cur_usd / total_cap_short

invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
    if symbol not in long + short:
        self.Liquidate(symbol)

# trade execution
# for symbol, w in weight.items():
#     if symbol in data and data[symbol]:
#         self.SetHoldings(symbol, self.portfolio_percentage * w)

for symbol in long:
    if symbol in data and data[symbol]:
        self.SetHoldings(symbol, 1/len(long) * self.portfolio_percentage)

for symbol in short:
    if symbol in data and data[symbol]:
        self.SetHoldings(symbol, -1/len(short) * self.portfolio_percentage)

def multiple_linear_regression(self, x:np.ndarray, y:np.ndarray):
x = sm.add_constant(x, has_constant='add')
result = sm.OLS(endog=y, exog=x).fit()
return result