Market Sentiment and an Overnight Anomaly
Log in to collectAcademic paper
Strategy in a nutshell
The investment universe consists of SPY ETF, and the price of SPY, price of VIX and Brain Market Sentiment (BMS) indicator are used to identify the market sentiment. The investor buys SPY ETF and holds it overnight; when the price of SPY is above its 20-day moving average, the price of VIX is below its moving average, and the value of the BMS indicator is greater than its 20-day moving average.
Note that the authors suggest using this strategy as an overlay when deciding whether to make a trade rather than using this system on its own.
Economic rationale
There are numerous possible reasons, which can explain an overnight anomaly. Academic studies show that part of the reason for an overnight anomaly is the high opening prices derived from the accumulation of market orders from market participants, which subsequently decline in the first hour of trading. Some portion of positive overnight returns can be expected due to an illiquidity premium, but liquidity can explain only a small part of the night and day return difference.
Additionally, the explanation for market sentiment is pretty simple. When the sentiment is bullish the general mood on the market is good which means the investors tend to buy more, which makes the mood even better. On the other hand, when the sentiment is bearish the general mood on the market is not so good which means the investors tend to buy less, which makes the mood even worse.
Backtest performance
Full Python code
from AlgoLib import *
# endregion
class MarketSentimentAndAnOvernightAnomaly(XXX):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.period:int = 20 # sma period
self.weight:float = 0
self.price_data:dict = {}
self.spy_symbol:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
self.vix_symbol:Symbol = self.AddData(QuandlVix, 'CBOE/VIX', Resolution.Daily).Symbol # starts in 2004
self.bms_symbol:Symbol = self.AddData(QuantpediaBMS, 'BMS_GLOBAL', Resolution.Daily).Symbol # starts in 2018
for symbol in [self.spy_symbol, self.vix_symbol, self.bms_symbol]:
self.price_data[symbol] = RollingWindow[float](self.period)
def OnData(self, data: Slice):
# calculate signal from SPY 16 minutes before close
if self.spy_symbol in data and data[self.spy_symbol] and self.Time.hour == 15 and self.Time.minute == 44:
weight:float = 0.
for symbol in [self.spy_symbol, self.vix_symbol, self.bms_symbol]:
# trade only sub-strategies with underlying data available
if self.Securities[symbol].GetLastData() and (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days <= 3:
price:float = self.Securities[symbol].GetLastData().Price
rolling_window:RollingWindow = self.price_data[symbol]
if rolling_window.IsReady and self.GetSignal(price, rolling_window, True if symbol != self.vix_symbol else False):
weight += (1 / 3)
rolling_window.Add(price)
q:int = int((self.Portfolio.TotalPortfolioValue * weight) / data[self.spy_symbol].Value)
if q != 0:
self.MarketOnCloseOrder(self.spy_symbol, q)
self.MarketOnOpenOrder(self.spy_symbol, -q)
def GetSignal(self, curr_value:float, rolling_window:RollingWindow, signal_above_sma:bool) -> bool:
prices:list[float] = [x for x in rolling_window]
moving_average:float = sum(prices) / len(prices)
result:bool = False
if signal_above_sma and (curr_value > moving_average):
result = True
elif not signal_above_sma and (curr_value < moving_average):
result = True
return result
# Quantpedia data.
# NOTE: IMPORTANT: Data order must be ascending (datewise)
class QuantpediaBMS(PythonData):
def GetSource(self, config, date, isLiveMode):
return SubscriptionDataSource("data.quantpedia.com/backtesting_data/index/{0}.csv".format(config.Symbol.Value), SubscriptionTransportMedium.RemoteFile, FileFormat.Csv)
def Reader(self, config, line, date, isLiveMode):
data:QuantpediaBMS = QuantpediaBMS()
data.Symbol = config.Symbol
if not line[0].isdigit(): return None
split:list = line.split(',')
data.Time = datetime.strptime(split[0], "%Y-%m-%d") + timedelta(days=1)
data.Value = float(split[2])
return data
class QuandlVix(PythonQuandl):
def __init__(self):
self.ValueColumnName = "VIX Close"