Quant BuffetRelax, Not Over Thinking

Geopolitical Risk and the Cross-Section of Cryptocurrency Returns

Log in to collect

Academic paper

Is Geopolitical Risk Priced in the Cross-Section of Cryptocurrency Returns?

AuthorsHuaigang Long; Ender Demir; Barbara Będowska-Sójka; Adam Zaremba; Syed Jawad Hussain Shahzad

Institute
  • Zhejiang University
  • TRIstanbul Medeniyet University
  • Poznań University of Economics and Business
  • Montpellier Business School
  • ?Poznan University of Economics and Business

Strategy in a nutshell

The investment universe consists of all cryptocurrencies with daily price, volume, and capitalization data available on https://coinmarketcap.com/. Assets with a market cap of less than 1 million dollars and those with a trading history shorter than 60 days are excluded.

To proxy for geopolitical risk, the GPR index is constructed following Caldara and Iacoviello (2022). It is based on calculating the frequency of geopolitical event-related articles in major newspapers.

Now geopolitical beta is calculated using a rolling time-series regression of excess daily returns on a daily change in GPR and the following control variables: excess returns on the market, size, and momentum factors. The equation can be found on page 4 of the paper. The estimation period is 21 days, but it is robust to adjustments.

Sort the cryptocurrencies into value-weighted quintiles according to their geopolitical beta. Long the lowest geopolitical beta quintile, short the highest. Rebalance weekly.

Economic rationale

The GPR index, formerly constructed by Caldara & Iacoviello, is a measure of the geopolitical risk in the world. By approximating the geopolitical beta based on this index for a given cryptocurrency, its sensitivity to geopolitical events is measured. The results support a hypothesis that investors are likely to be willing to pay a premium for assets with low geopolitical beta. Therefore, price and geopolitical beta are negatively correlated, which is the base idea of this strategy.

Backtest performance

Annualised return187.05%
Volatility39.8%
Beta-0.001
Sharpe ratio4.7
Sortino ratio0.425
Win rate51%

Full Python code

from AlgorithmImports import *
import data_tools
from typing import List, Dict
# endregion

class CrawlingYellowBarracuda(QCAlgorithm):

def Initialize(self) -> None:
self.SetStartDate(2015, 1, 1)
self.SetCash(1000000)

self.period: int = 21 + 1 # need n of daily data
self.quantile: int = 5
self.leverage: int = 5
self.portfolio_percentage: float = .5

cryptos: Dict[str, str] = {
    "ANTUSD": "ANT", # Aragon
    "BATUSD": "BAT", # Basic Attention Token
    "BTCUSD": "BTC", # Bitcoin
    "BTGUSD": "BTG", # Bitcoin Gold
    "DAIUSD": "DAI", # Dai
    "DGBUSD": "DGB", # Dogecoin
    "EOSUSD": "EOS", # EOS
    "ETCUSD": "ETC", # Ethereum Classic
    "ETHUSD": "ETH", # Ethereum
    "FUNUSD": "FUN", # FUNToken
    "LTCUSD": "LTC", # Litecoin
    "MKRUSD": "MKR", # Maker
    "NEOUSD": "NEO", # Neo
    "OMGUSD": "OMG", # OMG Network
    "SNTUSD": "SNT", # Status
    "TRXUSD": "TRX", # Tron
    "XLMUSD": "XLM", # Stellar
    "XMRUSD": "XMR", # Monero
    "XRPUSD": "XRP", # XRP
    "XTZUSD": "XTZ", # Tezos
    "XVGUSD": "XVG", # Verge
    "ZECUSD": "ZEC", # Zcash
    "ZRXUSD": "ZRX", # Ox
}

self.data: Dict[str, data_tools.SymbolData] = {}

self.SetBrokerageModel(BrokerageName.Bitfinex)

for crypto, ticker in cryptos.items():
    # GDAX is coinmarket, but it doesn't support this many cryptos, so we choose Bitfinex
    data: Securities = self.AddCrypto(crypto, Resolution.Daily, Market.Bitfinex)
    data.SetFeeModel(data_tools.CustomFeeModel())
    data.SetLeverage(self.leverage)
    
    network_symbol: Symbol = self.AddData(data_tools.CryptoNetworkData, ticker, Resolution.Daily).Symbol
    
    self.data[crypto] = data_tools.SymbolData(network_symbol, self.period)

self.geo_risk_index: Symbol = self.AddData(data_tools.QuantpediaGeopoliticalRisk, 'GeopoliticalRiskIndex', Resolution.Daily).Symbol
self.geo_risk_index_values: RollingWindow = RollingWindow[float](self.period)
self.geo_risk_beta_value_index: int = 1

self.value_weighted: bool = True
self.selection_flag: bool = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.WeekStart('BTCUSD'), self.TimeRules.At(9, 30), self.Selection)

def OnData(self, data: Slice) -> None:
curr_date: datetime.date = self.Time.date()
crypto_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.CryptoNetworkData.get_last_update_date()
qp_data_last_update_date: Dict[Symbol, datetime.date] = data_tools.QuantpediaGeopoliticalRisk.get_last_update_date()

# daily updating of crypto prices and market capitalization(CapMrktCurUSD)
if self.Securities[self.geo_risk_index].GetLastData() and self.Time.date() <= qp_data_last_update_date[self.geo_risk_index]:
    for crypto, symbol_obj in self.data.items():
    # if self.geo_risk_index in data and data[self.geo_risk_index]:
        network_symbol:Symbol = symbol_obj.network_symbol
        
        if data.ContainsKey(crypto):
            price: float = data[crypto].Value
            self.data[crypto].update_prices(price)

            # GPR_value:float = data[self.geo_risk_index].Value
            GPR_value:float = self.Securities[self.geo_risk_index].Price
            self.geo_risk_index_values.Add(GPR_value)
        
        if data.ContainsKey(network_symbol):
            cap_mrkt_cur_usd: float = data[network_symbol].Value
            self.data[crypto].update_cap(cap_mrkt_cur_usd)
else:
    self.geo_risk_index_values.Reset()

# weekly rebalance
if not self.selection_flag:
    return
self.selection_flag = False

if not self.geo_risk_index_values.IsReady:
    self.Liquidate()
    return

# calculate monthly performance series
monthly_returns_by_symbol: Dict[str, float] = {}
for crypto, symbol_obj in self.data.items():
    network_symbol: Symbol = symbol_obj.network_symbol
    if self.Securities[network_symbol].GetLastData() and self.Time.date() > crypto_data_last_update_date[network_symbol]:
        self.Liquidate()
        return

    if not symbol_obj.is_ready():
        continue

    monthly_returns_by_symbol[crypto] = symbol_obj.get_daily_returns()

if len(monthly_returns_by_symbol) < self.quantile:
    self.Liquidate()
    return

# create market factor
crypto_c: int = len(monthly_returns_by_symbol)
weights: np.ndarray = np.array([1/crypto_c] * crypto_c)
market_factor: np.ndarray = np.matmul(np.array(list(monthly_returns_by_symbol.values())).T, weights)

# create GPR factor
GPR_index_values: np.ndarray = np.array(list(self.geo_risk_index_values))
GPR_factor: np.ndarray = (GPR_index_values[:-1] - GPR_index_values[1:]) / GPR_index_values[1:]
regression_x: List[np.ndarray] = [
    GPR_factor,
    market_factor
]

beta_by_ticker: Dict[str, float] = {}

for crypto, monthly_return in monthly_returns_by_symbol.items():
    regression_y: np.ndarray = monthly_return
    regression_model: RegressionResultsWrapper = data_tools.MultipleLinearRegression(regression_x, regression_y)
    geo_risk_beat: float = regression_model.params[self.geo_risk_beta_value_index]

    beta_by_ticker[crypto] = geo_risk_beat

if len(beta_by_ticker) < self.quantile:
    self.Liquidate()
    return

# long and short selection
quantile: int = int(len(beta_by_ticker) / self.quantile)
sorted_by_beta: List[list[str, float]] = [x[0] for x in sorted(beta_by_ticker.items(), key=lambda item: item[1])]

long_leg: List[List[str, float]] = sorted_by_beta[:quantile]
short_leg: List[List[str, float]] = sorted_by_beta[-quantile:]

# weights calculation
weights: Dict[str, float] = {}

if self.value_weighted:
    for i, portfolio in enumerate([long_leg, short_leg]):
        mc_sum: float = sum(list(map(lambda ticker: self.data[ticker].cap_mrkt_cur_usd, portfolio)))
        for ticker in portfolio:
            weights[ticker] = ((-1)**i) * self.data[ticker].cap_mrkt_cur_usd / mc_sum

else:
    for i, portfolio in enumerate([long_leg, short_leg]):
        for ticker in portfolio:
            weights[ticker] = ((-1) ** i) / len(portfolio)

# trade execution
portfolio: List[PortfolioTarget] = [PortfolioTarget(ticker, self.portfolio_percentage * w) for ticker, w in weights.items() if ticker in data and data[ticker]]
self.SetHoldings(portfolio, True)

def Selection(self) -> None:
self.selection_flag = True