Quant BuffetRelax, Not Over Thinking

Gold to Oil Ratio Predicts Aggregate Stock Returns

Log in to collect

Academic paper

Gold price ratios and aggregate stock returns

AuthorsTong Fang

Institute
  • Shandong University of Finance and Economics
  • ?Shandong University - School of Economics

Strategy in a nutshell

The strategy dynamically allocates between the S&P 500 index and the one-month Treasury bill using the gold-oil price ratio (GO) as a predictor.

Process:

GO Predictor: Compute the natural log of the gold-to-oil price ratio.

Regression Forecasting: Regress the S&P 500’s excess return (vs. T-bill) on GO using a 240-month rolling window.

Forecasting Returns: At the end of month t, use the regression to forecast the S&P 500 excess return for t+1.

Portfolio Allocation Rule: \text{S&P 500 Allocation} = \frac{1}{\text{risk aversion}} \times \frac{\text{forecasted excess return}}{\text{forecasted variance}}

Variance forecast: 10-year rolling window of past returns.

Risk aversion coefficient = 3.

Allocation bounded between 0% and 150%.

Final Weights: S&P 500 weight determined by rule; remainder allocated to one-month T-bill.

Rebalancing: Monthly updates of regression, forecast, and weights.

Economic rationale

Asset prices reflect both expected cash flows and discount rates (Cochrane, 2011). GO’s predictive ability comes mainly from anticipating aggregate cash flow news.

GO also negatively predicts default spreads, financial stress, and uncertainty, making it a leading indicator of economic conditions.

A higher GO signals stronger economic outlooks, translating into higher expected equity returns

Backtest performance

Annualised return7.02%
Volatility9.7%
Beta0.498
Sharpe ratio0.72
Sortino ratio0.14
Win rate62%

Full Python code

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