Market Timing with Relative Sentiment
Log in to collectAcademic paper
Want Smart Beta? Follow the Smart Money: Market and Factor Timing Using Relative Sentiment
Raymond Micaletti
- ?Relative Sentiment Technologies, LLC
Strategy in a nutshell
This is a weekly “long-or-flat” SPY strategy based on the relative sentiment of institutional versus individual investors (IIRS) from the CFTC CoT data. It combines normalized positioning in S&P 500 equities, 30-year U.S. Treasuries, and the yield curve. A composite z-score (SMI) determines the signal: go long SPY if the prior-week SMI is positive, stay flat if negative, with weekly rebalancing.
Economic rationale
Institutional investors typically outperform individuals over intermediate horizons. By mimicking institutional positions via the CoT report—using Commercials, Non-commercials, and Non-reportables—the strategy aims to capture market moves more reliably than following individual investor sentiment.
Backtest performance
Full Python code
from AlgorithmImports import *
import numpy as np
import data_tools
from typing import Dict, List
import pandas as pd
from pandas.core.series import Series as series
#endregion
class MarketTimingwithRelativeSentiment(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.cot_symbols:List[str] = [
'QEP', # S&P 500
'QTY', # 10 Yr Note Futures
'QUS' # 30 Yr Note Futures
]
self.SetTimeZone(TimeZones.NewYork)
self.max_missing_days:int = 7
self.missing_days:int = 0
self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.N:int = 78 # lookback period
self.M:int = 5 # extrema lookback
self.symbol_data:Dict[str, data_tools.SymbolData] = {}
for cot_symbol in self.cot_symbols:
# COT data
self.AddData(data_tools.CommitmentsOfTraders, cot_symbol, Resolution.Daily)
self.symbol_data[cot_symbol] = data_tools.SymbolData(self.N)
def OnData(self, data:Slice) -> None:
rebalance_flag:bool = False
# store most recent open interest for every symbol - weekly data
if all((cot_symbol in data and data[cot_symbol]) for cot_symbol in self.cot_symbols):
for cot_symbol in self.cot_symbols:
inst_long_count:float = data[cot_symbol].GetProperty("COMMERCIAL_HEDGER_LONG")
inst_short_count:float = data[cot_symbol].GetProperty("COMMERCIAL_HEDGER_SHORT")
indiv_long_count:float = data[cot_symbol].GetProperty("SMALL_TRADER_LONG")
indiv_short_count:float = data[cot_symbol].GetProperty("SMALL_TRADER_SHORT")
oi:float = data[cot_symbol].GetProperty("open_interest")
delta_cm:float = (inst_long_count + inst_short_count) / oi
delta_nr:float = (indiv_long_count + indiv_short_count) / oi
self.symbol_data[cot_symbol].update(delta_cm, delta_nr)
rebalance_flag = True
else:
self.missing_days += 1
# rebalance once a week
if rebalance_flag:
self.missing_days = 0
if all(self.symbol_data[cot_symbol].is_ready() for cot_symbol in self.cot_symbols):
a = self.symbol_data[self.cot_symbols[0]].z_score()
z_sp:series = pd.Series(self.symbol_data[self.cot_symbols[0]].z_score()[::-1])
z_ty:series = pd.Series(self.symbol_data[self.cot_symbols[1]].z_score()[::-1])
z_us:series = pd.Series(self.symbol_data[self.cot_symbols[2]].z_score()[::-1])
z_sp_max:np.ndarray = z_sp.rolling(self.M).max().values
z_ty_max:np.ndarray = z_ty.rolling(self.M).max().values
z_us_max:np.ndarray = z_us.rolling(self.M).max().values
z_us_min:np.ndarray = z_us.rolling(self.M).min().values
z_smi:np.ndarray = z_sp_max - z_us_min + (z_ty_max - z_us_max)
smi_index_value:float = z_smi[-1]
if smi_index_value > 0.:
# investor goes long SPY on the day when the prior-week SMI is positive
self.SetHoldings(self.market, 1.)
else:
# and is flat (staying in cash) otherwise
self.Liquidate(self.market)
else:
self.Liquidate(self.market)
else:
if self.missing_days == self.max_missing_days:
# log messages
for cot_symbol in self.cot_symbols:
self.symbol_data[cot_symbol].reset()
missing_days:int = int((self.Time.date() - self.Securities[cot_symbol].GetLastData().Time.date()).days)
self.Log(f'{cot_symbol} COT data missing days: {missing_days} on {self.Time.date()}')
self.Liquidate(self.market)
self.missing_days = 0