The strategy forecasts excess S&P returns using VIX volatility, hedging pressure, and open interest, optimizing monthly for a minimum variance portfolio and rebalancing equities and risk-free assets accordingly.

I. STRATEGY IN A NUTSHELL

Universe: S&P 500 and risk-free asset. Forecast excess market return via linear regression using VIX volatility, hedging pressure, and open interest. Optimize weights for minimum-variance portfolio, monthly rebalanced.

II. ECONOMIC RATIONALE

VIX and hedging pressure capture forward-looking market fear and hedging demand, providing predictive signals for risk-adjusted allocation.

III. SOURCE PAPER

 Global Tactical Cross-Asset Allocation: Exploiting the Informational Content of the Linkages between Spot and Derivatives Markets [Click to Open PDF]

Basu, Oomen, Stremme

<Abstract>

This paper focuses on the use of market variables that exploit the linkages between spot, futures and derivatives markets, as opposed to the business cycle indicators employed in most of the earlier studies. Spot and futures market linkages are exploited by using commercial and non-reportable hedging pressure as the predictive variables while the linkages between the derivatives and spot markets are exploited using the VIX index, a proxy for implied volatility. Using the S&P 500 and gold as our base assets, we study the performance of these variables by examining both the out-of-sample performance of unconditionally efficient portfolios based on our predictive variables as well as their in-sample performance using a statistical test. Our trading strategies can successfully time the market and avoid losses during the burst of the “dot.com” bubble in the second half of 2000, as well as during the bull run that followed. The in-sample results confirm our out-of-sample experiments with p-values of less than 1% in all cases. The predictive variables on their own do not perform nearly as well, indicating that it is linkages between these markets that are important for market timing. The VIX provides a signal to change the weight on the market while hedging pressure indicates the direction. We construct variables that combine both of these features and find that these variables provide the clearest signals for successful market timing.

IV. BACKTEST PERFORMANCE

Annualised Return8.39%
Volatility12.08%
Beta0.064
Sharpe Ratio0.44
Sortino Ratio-0.173
Maximum DrawdownN/A
Win Rate51%

V. FULL PYTHON CODE

from collections import deque
from AlgorithmImports import *
import numpy as np
import data_tools
import pandas as pd
from scipy.optimize import minimize
class MarketTimingSP500withVIXandCOTReport(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2005, 1, 1)
        self.SetCash(100000)
       
        # Market data.
        data = self.AddData(data_tools.QuantpediaFutures, 'CME_ES1', Resolution.Daily)
        data.SetFeeModel(data_tools.CustomFeeModel())
        data.SetLeverage(10)
        self.symbol = data.Symbol
        
        self.cash = self.AddEquity('BIL', Resolution.Daily).Symbol
        
        # Daily price data.
        self.data = {}
        self.period = 60
        self.data[self.symbol] = RollingWindow[float](self.period)
        self.data[self.cash] = RollingWindow[float](self.period)
        
        # COT data.
        self.cot_symbol = 'QEP'  # S&P500 E-mini COT symbol.
        self.AddData(data_tools.CommitmentsOfTraders, self.cot_symbol, Resolution.Daily)
        
        # Vix data.
        self.vix = self.AddData(CBOE, "VIX", Resolution.Daily).Symbol
        
        # Regression data.
        self.period = 12 * 4    # 1 year worth of weekly data.
        self.regression_data = deque(maxlen = self.period)
        self.Schedule.On(self.DateRules.MonthStart(self.symbol), self.TimeRules.At(0, 0), self.Rebalance)
        
    def OnData(self, data):
        # Store daily price data.
        for symbol in self.data:
            symbol_obj = self.Symbol(symbol)
            if symbol_obj in data and data[symbol_obj]:
                price = data[symbol_obj].Value
                self.data[symbol].Add(price)
            
        market_return = None
        cash_return = None
        hedging_pressure = 0
        open_interest = 0
        vix_price = 0
        # Market and cash asset daily data is ready.
        if self.data[self.symbol].IsReady and self.data[self.cash].IsReady:
            market_prices = [x for x in self.data[self.symbol]]
            market_return = market_prices[0] / market_prices[-1] - 1
            
            cash_prices =  [x for x in self.data[self.cash]]
            cash_return = cash_prices[0] / cash_prices[-1] - 1
        # Store COT data every wednesday.
        if self.cot_symbol in data and data[self.cot_symbol]:
            cot_data = self.Securities[self.cot_symbol].GetLastData()
            if cot_data:
                speculator_long_count = cot_data.GetProperty("LARGE_SPECULATOR_LONG")
                speculator_short_count = cot_data.GetProperty("LARGE_SPECULATOR_SHORT")
                commercial_long_count = cot_data.GetProperty("COMMERCIAL_HEDGER_LONG")
                commercial_short_count = cot_data.GetProperty("COMMERCIAL_HEDGER_SHORT")
                trader_long_count = cot_data.GetProperty("SMALL_TRADER_LONG")
                trader_short_count = cot_data.GetProperty("SMALL_TRADER_SHORT")
                
                if commercial_long_count != 0 and commercial_short_count != 0:
                    hedging_pressure = commercial_long_count / (commercial_long_count + commercial_long_count)
                    
                open_interest = speculator_long_count + speculator_short_count + commercial_long_count + commercial_short_count + trader_long_count+ trader_short_count
                    
                if self.vix in data and data[self.vix]:
                    vix_price = data[self.vix].Value
        
        if market_return and cash_return and hedging_pressure != 0 and open_interest != 0 and vix_price != 0:
            self.regression_data.append((market_return, cash_return, hedging_pressure, open_interest, vix_price))
    
    def Rebalance(self):
        # Regression data is ready.
        if len(self.regression_data) == self.regression_data.maxlen:
            # check futures and cot data arrival
            if any([self.securities[symbol].get_last_data() and self.time.date() > data_tools.LastDateHandler.get_last_update_date()[symbol] for symbol in [self.symbol, self.cot_symbol]]):
                self.liquidate()
                return
                
            # Linear regression calc.
            market_returns = [x[0] for x in self.regression_data]
            cash_returns = [x[1] for x in self.regression_data]
            hedging_pressures = [x[2] for x in self.regression_data]
            open_interests = [x[3] for x in self.regression_data]
            vix_prices = [x[4] for x in self.regression_data]
            
            x = [hedging_pressures[:-1], open_interests[:-1], vix_prices[:-1]]
            regression_model = data_tools.MultipleLinearRegresion(x, market_returns[1:])
            market_return_predicted = regression_model.predict([hedging_pressures[-1], open_interests[-1], vix_prices[-1]]) # returns list.
            
            x = [hedging_pressures[:-1], open_interests[:-1], vix_prices[:-1]]
            regression_model = data_tools.MultipleLinearRegresion(x, cash_returns[1:])
            cash_return_predicted = regression_model.predict([hedging_pressures[-1], open_interests[-1], vix_prices[-1]]) # returns list.
            
            # Create return df.
            returns = {
                self.symbol.Value : market_returns + market_return_predicted,
                self.cash.Value : cash_returns + cash_return_predicted
            }
            returns = pd.dataframe(returns, columns=returns.keys()).dropna()
            
            # The optimization method processes the data frame
            opt, weights = self.optimization_method(returns)
            
            for symbol in [self.symbol, self.cash]:
                w = weights[symbol.Value]
                if w > 0.001:
                    self.SetHoldings(symbol, w)
    def optimization_method(self, returns):
        '''Minimum variance optimization method'''
        # Objective function
        fun = lambda weights: np.dot(weights.T, np.dot(returns.cov(), weights))
        # Constraint #1: The weights can be negative, which means investors can short a security.
        constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
        # Constraint #2: Portfolio targets a given return
        # constraints.append({'type': 'eq', 'fun': lambda weights: np.dot(np.matrix(returns.mean()), np.matrix(weights).T).item() - self.target_return})
        size = returns.columns.size
        x0 = np.array(size * [1. / size])
        # bounds = tuple((self.minimum_weight, self.maximum_weight) for x in range(size))
        bounds = tuple((0, 1) for x in range(size))
        opt = minimize(fun,                         # Objective function
                       x0,                          # Initial guess
                       method='SLSQP',              # Optimization method:  Sequential Least SQuares Programming
                       bounds = bounds,             # Bounds for variables 
                       constraints = constraints)   # Constraints definition
        return opt, pd.Series(opt['x'], index = returns.columns)
class QuandlVix(PythonQuandl):
    def __init__(self):
        self.ValueColumnName = "close"

Leave a Reply

Discover more from Quant Buffet

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

Continue reading