该策略投资于MSCI股票市场分类列表中的79个股票市场,使用24个月数据回归计算超额回报与全球市场综合指数和布伦特原油价格回报的关系。根据每个市场的石油贝塔不确定性,将其分为五组,买入不确定性最高的市场,卖出不确定性最低的市场。每月按等权重重新平衡投资组合。

策略概述

投资范围包括MSCI股票市场分类列表中包含的79个股票市场。各个国家的股票市场列表见附录表A1。每个月t,使用t-1至t-24个月的数据,通过回归分析计算每个市场i在t月的超额回报(因变量)相对于当月的全球市场综合指数和布伦特原油价格回报(自变量),具体公式见方程2。全球市场综合指数使用MSCI世界指数计算,超额回报则使用三个月的美国国债利率。为了避免汇率波动问题,所有回报均以美元计算。接着,使用每个股票市场的贝塔参数点估计值及其相应的标准误,计算估计的石油贝塔值的95%置信区间。最后,根据每月计算的石油贝塔不确定性(即95%置信区间最高值与最低值之差),将股票市场按贝塔不确定性值排序并分成五个等权重的投资组合。每月的交易规则是:买入石油贝塔不确定性最高的五分位国家的股票市场,卖出石油贝塔不确定性最低的五分位国家的股票市场。该策略为等权重组合,并每月重新平衡。

策略合理性

大量文献表明,石油价格波动对股票市场回报具有重要影响。然而,全球股票市场对石油价格波动的反应,即石油贝塔,其符号和幅度充满不确定性。因此,石油价格波动对股票市场反应的不确定性代表了一种全球投资者无法完全分散的风险因素。根据Barry和Brown(1985,1986)的信息不确定性模型,理性的、厌恶不确定性的投资者在面临较大石油贝塔不确定性的股票市场时会要求更高的风险溢价。持有石油贝塔不确定性较高资产的投资者,因其参数不确定性较高,因此需要更高的溢价。

论文来源

Oil Beta Uncertainty and Global Stock Returns [点击浏览原文]

<摘要>

本文通过对全球股票回报横截面的分析,发现石油贝塔不确定性带来了显著的正溢价。通过一系列市场和投资组合层面的测试,我们表明,石油贝塔不确定性(由95%置信区间的跨度衡量)带来了年化高达9.72%的显著风险溢价,即使在控制全球系统性风险因素后仍然如此。大多数发达市场,如澳大利亚、加拿大、瑞士、美国和英国,通常处于低石油贝塔不确定性组合中,而新兴市场,如越南、埃及、中国、土耳其和巴基斯坦,则通常面临更高的石油贝塔不确定性。进一步分析表明,石油贝塔不确定性与全球经济条件的敏感性分歧(或不确定性)之间的关联,导致了这一风险溢价。研究结果揭示了石油市场不确定性如何通过新的行为渠道影响全球股票回报横截面。

回测表现

年化收益率10.17%
波动率15.38%
Beta0.109
夏普比率0.66
索提诺比率N/A
最大回撤N/A
胜率37%

完整python代码

from AlgorithmImports import *
import statsmodels.api as sm
import data_tools
# endregion

class CasualApricotSalamander(QCAlgorithm):

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

        tickers:list[str] = [
            'EWA',  # iShares MSCI Australia Index ETF
            'EWO',  # iShares MSCI Austria Investable Mkt Index ETF
            'EWK',  # iShares MSCI Belgium Investable Market Index ETF
            'EWZ',  # iShares MSCI Brazil Index ETF
            'EWC',  # iShares MSCI Canada Index ETF
            'FXI',  # iShares China Large-Cap ETF
            'EWQ',  # iShares MSCI France Index ETF
            'EWG',  # iShares MSCI Germany ETF 
            'EWH',  # iShares MSCI Hong Kong Index ETF
            'EWI',  # iShares MSCI Italy Index ETF
            'EWJ',  # iShares MSCI Japan Index ETF
            'EWM',  # iShares MSCI Malaysia Index ETF
            'EWW',  # iShares MSCI Mexico Inv. Mt. Idx
            'EWN',  # iShares MSCI Netherlands Index ETF
            'EWS',  # iShares MSCI Singapore Index ETF
            'EZA',  # iShares MSCI South Africe Index ETF
            'EWY',  # iShares MSCI South Korea ETF
            'EWP',  # iShares MSCI Spain Index ETF
            'EWD',  # iShares MSCI Sweden Index ETF
            'EWL',  # iShares MSCI Switzerland Index ETF
            'EWT',  # iShares MSCI Taiwan Index ETF
            'THD',  # iShares MSCI Thailand Index ETF
            'EWU',  # iShares MSCI United Kingdom Index ETF
            'SPY',  # SPDR S&P 500 ETF
        ]

        self.min_prices:int = 15
        self.regression_period:int = 24

        self.beta_index:int = 2 # relevant regression beta position
        self.t:float = 2.074    # https://www.sjsu.edu/faculty/gerstman/StatPrimer/t-table.pdf

        self.quantile:int = 5
        self.data:dict[Symbol, data_tools.SymbolData] = {}

        for ticker in tickers:
            security = self.AddEquity(ticker, Resolution.Daily)
            security.SetLeverage(5)

            self.data[security.Symbol] = data_tools.SymbolData(self.regression_period)
        
        self.msci_world:Symbol = self.AddEquity('URTH', Resolution.Daily).Symbol
        self.data[self.msci_world] = data_tools.SymbolData(self.regression_period)

        self.oil:Symbol = self.AddData(data_tools.QuantpediaFutures, 'CME_CL1', Resolution.Daily).Symbol
        self.data[self.oil] = data_tools.SymbolData(self.regression_period)

        self.recent_month:int = -1

    def OnData(self, data: Slice):
        # update prices
        for symbol, symbol_obj in self.data.items():
            if symbol in data and data[symbol] and data[symbol].Value != 0:
                symbol_obj.update_prices(data[symbol].Value)

        # monthly rebalance
        if self.recent_month != self.Time.month:
            self.recent_month = self.Time.month
            curr_date:datetime.date = self.Time.date()

            # update all regression data
            for symbol, symbol_obj in self.data.items():
                if symbol_obj.prices_ready(self.min_prices):
                    symbol_obj.update_regression_data(curr_date)
                else:
                    # reset regression data in case of missing month -> makes sure data are consecutive
                    symbol_obj.reset_regression_data()

                # clear space for next month prices
                symbol_obj.reset_prices()

            # get regression_x if possible
            regression_x:list[list[float]] = [
                self.data[self.msci_world].get_regression_data(),
                self.data[self.oil].get_regression_data()
            ] if self.data[self.msci_world].regression_data_ready() and \
                 self.data[self.oil].regression_data_ready() else None

            if regression_x == None:
                # liquidate when regression x isn't ready
                self.Liquidate()
                return 

            oil_beta_uncertainty:dict[Symbol, float] = {}

            for symbol, symbol_obj in self.data.items():
                if symbol_obj.regression_data_ready() and symbol not in [self.msci_world, self.oil]:
                    # calculate the measure of the oil beta uncertainty for stock market i in month t as a difference between the highest and lowest 95% confidence interval
                    regression_y:list[float] = symbol_obj.get_regression_data()

                    regression_model = self.MultipleLinearRegression(regression_x, regression_y)

                    oil_beta:float = regression_model.params[self.beta_index]
                    SE:float = regression_model.bse[self.beta_index]
                    
                    top_conf_interval:float = oil_beta + (self.t * SE)
                    bottom_conf_interval:float = oil_beta - (self.t * SE)

                    intervals_diff:float = top_conf_interval - bottom_conf_interval

                    oil_beta_uncertainty[symbol] = intervals_diff

            # perform selection and trade
            if len(oil_beta_uncertainty) >= self.quantile:
                quantile:int = int(len(oil_beta_uncertainty) / self.quantile)
                sorted_by_uncertainty:list[Symbol] = [x[0] for x in sorted(oil_beta_uncertainty.items(), key=lambda item: item[1])]

                # buy quantile with highest values
                long_leg:list[Symbol] = sorted_by_uncertainty[-quantile:]
                # short quantile with lowest values
                short_leg:list[Symbol] = sorted_by_uncertainty[:quantile]

                # trade execution
                invested = [x.Key for x in self.Portfolio if x.Value.Invested]
                for symbol in invested:
                    if symbol not in long_leg + short_leg:
                        self.Liquidate(symbol)

                long_length:int = len(long_leg)
                for symbol in long_leg:
                    if symbol in data and data[symbol]:
                        self.SetHoldings(symbol, 1 / long_length)

                short_length:int = len(short_leg)
                for symbol in short_leg:
                    if symbol in data and data[symbol]:
                        self.SetHoldings(symbol, -1 / short_length)
            else:
                self.Liquidate()

    def MultipleLinearRegression(self, x:list, y:list):
        x:np.ndarray = np.array(x).T
        x = sm.add_constant(x)
        result = sm.OLS(endog=y, exog=x).fit()
        return result

Leave a Reply

Discover more from Quant Buffet

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

Continue reading