该策略投资于CoinMarketCap上列出的加密货币,排除市值低于100万美元和交易历史少于60天的资产。利用Caldara和Iacoviello的地缘政治风险(GPR)指数,通过21天滚动时间序列回归计算地缘政治贝塔。然后,将加密货币按贝塔值分组,做多地缘政治贝塔最低的五分位组,做空贝塔最高的五分位组,每周重新平衡投资组合。

策略概述

投资范围包括所有在CoinMarketCap上列出的加密货币,但市值低于100万美元和交易历史少于60天的资产被排除在外。为了衡量地缘政治风险,使用Caldara和Iacoviello(2022)提出的地缘政治风险(GPR)指数,该指数基于主要报纸中与地缘政治事件相关的文章频率计算得出。

地缘政治贝塔通过滚动时间序列回归计算,回归模型的自变量为每日超额回报、GPR每日变化,控制变量包括市场超额回报、规模和动量因子。具体方程见论文第4页。估算期为21天,但对调整具有鲁棒性。然后根据地缘政治贝塔将加密货币按价值加权分为五个五分位组,做多地缘政治贝塔最低的五分位组,做空地缘政治贝塔最高的五分位组,并每周重新平衡投资组合。

策略合理性

Caldara和Iacoviello创建的GPR指数是全球地缘政治风险的衡量指标。通过基于该指数近似计算每种加密货币的地缘政治贝塔,可以衡量其对地缘政治事件的敏感性。研究结果支持这样的假设:投资者愿意为低地缘政治贝塔的资产支付溢价。因此,价格与地缘政治贝塔呈负相关,这也是该策略的基本理念。

论文来源

Is Geopolitical Risk Priced in the Cross-Section of Cryptocurrency Returns? [点击浏览原文]

<摘要>

我们研究了地缘政治风险在加密货币定价横截面中的作用。我们计算了加密货币对地缘政治风险指数变化的敞口,并记录了地缘政治贝塔最低的加密货币相对于高地缘政治贝塔的加密货币表现更好。我们的研究结果表明,风险厌恶型投资者需要额外的补偿来激励其持有具有低或负地缘政治贝塔的加密货币,而他们愿意为具有高或正地缘政治贝塔的资产支付溢价。

回测表现

年化收益率187.05%
波动率39.8%
Beta-0.001
夏普比率4.7
索提诺比率0.425
最大回撤N/A
胜率51%

完整python代码

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading