Quant BuffetRelax, Not Over Thinking

Market Sentiment and an Overnight Anomaly

Log in to collect

Academic paper

Market Sentiment and an Overnight Anomaly

AuthorsRadovan Vojtko; Daniela Hanicova

Institute
  • ?Quantpedia.com
  • ?Quantpedia

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

Annualised return15.58%
Volatility7.33%
Beta0.082
Sharpe ratio0.34
Maximum drawdown11%
Win rate53%

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"