Geopolitical Risk and the Cross-Section of Cryptocurrency Returns
Log in to collectAcademic paper
Is Geopolitical Risk Priced in the Cross-Section of Cryptocurrency Returns?
Huaigang Long; Ender Demir; Barbara Będowska-Sójka; Adam Zaremba; Syed Jawad Hussain Shahzad
- 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
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