该策略涵盖S&P 500指数和一个月期国库券。首先,构建黄金-石油价格比(GO)预测器,将黄金价格除以石油价格并取自然对数。然后,用过去240个月数据进行回归分析,预测S&P 500的超额回报率。根据回归预测和方差,计算最优S&P 500分配比例(0%至150%),剩余资金投资国库券。投资比例每月根据新预测调整,风险厌恶系数为3。

策略概述

投资范围包括S&P 500指数和一个月期国库券。首先,构建黄金-石油价格比(GO)预测器,方法是将黄金价格除以石油价格并取自然对数。其次,使用最近240个月的月度观察样本,将S&P 500指数(包括股息)的对数回报率(相对于一个月期国库券的超额回报率,t+1月的因变量)对t月的GO值(自变量)进行回归分析。第三,在t月末,使用估算的回归模型预测S&P 500在t+1月的超额回报率。第四,在t月末,计算t+1月期间S&P 500指数的最优投资组合分配,计算公式为:S&P 500分配比例 = (1 / 风险厌恶系数) * (S&P 500超额回报率的回归预测值 / S&P 500超额回报率方差的预测值)。S&P 500超额回报率的方差预测基于过去十年的滚动回报窗口计算,使用风险厌恶系数为3。S&P 500的投资比例限制在0%到150%之间。剩余的资金则分配到一个月期国库券。每月计算新的最优投资组合权重。

策略合理性

黄金-石油比(GO)预测未来整体股票回报的经济解释有两个方面。首先,估值模型表明,资产价格应等于预期贴现现金流,这意味着资产价格由未来预期现金流和折现率决定(Cochrane,2011)。因此,GO预测整体回报的能力可能源于现金流渠道、折现率渠道或两者兼具。通过Campbell(1991)和Campbell及Ammer(1993)的向量自回归(VAR)框架进行的股票回报分解显示,GO的预测能力主要来自于对整体现金流新闻的预期。其次,GO已被证明是经济变量的有力预测指标。具体而言,GO负相关且显著预测违约利差、金融压力以及宏观和金融不确定性。因此,较高的GO表明更好的经济状况。

论文来源

Gold price ratios and aggregate stock returns [点击浏览原文]

<摘要>

我们发现大多数黄金价格比,代表黄金的相对估值,能够正向且显著地预测整体股票回报。然而,在控制Welch和Goyal(2008)描述的一系列回报预测因子后,这些比率未能表现出显著的预测能力,除黄金-石油价格比(GO)外。GO是最强的预测指标。一标准差的增加与下个月年化超额回报率增加6.60%相关。GO在样本外的R^2和效用方面表现出最显著的预测能力。

回测表现

年化收益率7.02%
波动率9.7%
Beta0.498
夏普比率0.72
索提诺比率0.14
最大回撤N/A
胜率62%

完整python代码

from AlgorithmImports import *
import statsmodels.api as sm
import data_tools
# endregion
class GoldToOilRatioPredictsAggregateStockReturns(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.min_monthly_prices:int = 15
        self.regression_period:int = 5 * 12 + 1 # need n months of data
        self.min_market_alloc:float = 0.
        self.max_market_alloc:float = 1.5
        self.risk_aversion_coefficient:int = 3
        self.spy_daily_prices:list[float] = []
        self.latest_go_predictor:float = None
        self.regression_data:RegressionData = data_tools.RegressionData(self.regression_period)
        security = self.AddEquity('SPY', Resolution.Daily)
        security.SetLeverage(5)
        self.spy_symbol:Symbol = security.Symbol
        security = self.AddEquity('BIL', Resolution.Daily)
        security.SetLeverage(5)
        self.bil_symbol:Symbol = security.Symbol
        self.oil_symbol:Symbol = self.AddCfd('WTICOUSD', Resolution.Daily).Symbol
        self.gold_symbol:Symbol = self.AddCfd('XAUUSD', Resolution.Daily).Symbol
        self.recent_month:int = -1
    def OnData(self, data: Slice):
        # rebalance monthly
        if self.recent_month != self.Time.month:
            self.recent_month = self.Time.month
        
            if len(self.spy_daily_prices) >= self.min_monthly_prices and self.latest_go_predictor:
                monthly_return:float = (self.spy_daily_prices[-1] - self.spy_daily_prices[0]) / self.spy_daily_prices[0]
                self.regression_data.update(monthly_return, self.latest_go_predictor)
                if self.regression_data.is_ready():
                    x_train, x_predict = self.regression_data.get_x_data()
                    y_train:list[float] = self.regression_data.get_y_data()
                    regression_model = self.MultipleLinearRegression(x_train, y_train)
                    market_return_prediction:float = regression_model.predict([1, x_predict])[0]
                    sse:float = np.sum(regression_model.resid ** 2) # regression_model.ssr
                    variance:float = sse / ((self.regression_period - 1) - 2)
                    market_allocation:float = (1 / self.risk_aversion_coefficient) * (market_return_prediction / variance)
                    market_allocation:float = max(self.min_market_alloc, min(market_allocation, self.max_market_alloc))
                    if self.bil_symbol in data and self.spy_symbol in data and data[self.bil_symbol] and data[self.spy_symbol]:
                        self.SetHoldings(self.spy_symbol, market_allocation)
                        self.SetHoldings(self.bil_symbol, 1 - market_allocation)
            else:
                # reset regresion data, because they stopped being consecutive
                self.regression_data.reset_data()
                self.Liquidate()
            # reset
            self.latest_go_predictor = None
            self.spy_daily_prices.clear()
        # update GO predictor
        if self.oil_symbol in data and self.gold_symbol in data and data[self.oil_symbol] and data[self.gold_symbol]:
            oil_price:float = data[self.oil_symbol].Value
            gold_price:float = data[self.gold_symbol].Value
            go_predictor:float = np.log(gold_price / oil_price)
            self.latest_go_predictor = go_predictor
        # update spy daily prices
        if self.spy_symbol in data and data[self.spy_symbol]:
            price:float = data[self.spy_symbol].Value
            self.spy_daily_prices.append(price)
    def MultipleLinearRegression(self, x:list, y:list):
        x:np.array = 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