Quant BuffetRelax, Not Over Thinking

Overnight-Intraday Reversal in Futures

Log in to collect

Academic paper

Overnight-Intraday Reversal Everywhere

AuthorsRobert Kosowski; Chun Liu; Yang Liu; Tianyu Wang

Institute
  • University of Oxford
  • Quantitative BioSciences
  • Centre for Economic Policy Research
  • ?CEPR (Centre for Economic Policy Research)
  • ?Imperial College Business School
  • ?University of Oxford, Oxford-Man Institute of Quantitative Finance
  • CAUniversity of Toronto
  • Tsinghua University
  • ?Tsinghua University - School of Economics and Management
  • Hunan University of Finance and Economics
  • ?Hunan University - College of Finance and Statistics
  • ?Tsinghua University, School of Economics and Management

Strategy in a nutshell

Universe: Five CME equity index futures (DJIA, NASDAQ, NIKKEI 225, S&P400, S&P500). CO-OC reversal strategy: buy the two overnight losers and short the two overnight winners at the next day’s open, closing positions at day’s end. Portfolio is equally weighted and rebalanced daily.

Economic rationale

Returns are driven by market microstructure and liquidity frictions rather than standard asset pricing factors or macroeconomic news, highlighting persistent inefficiencies even in highly liquid futures markets.

Backtest performance

Annualised return59.98%
Volatility10.87%
Beta0.017
Sharpe ratio5.52
Sortino ratio-0.657
Win rate48%

Full Python code

from AlgorithmImports import *
# endregion

class OvernightIntradayReversalinFutures(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)

self.traded_percentage:float = 0.1
self.futures:List[Symbol] = []
self.last_close:Dict[Symbol, float] = {}
self.traded_count:int = 2

symbols:List[str] = [
    Futures.Indices.SP400MidCapEmini,
    Futures.Indices.SP500EMini,
    Futures.Indices.MicroDow30EMini,
    Futures.Indices.NASDAQ100EMini,
    Futures.Indices.Nikkei225Dollar,
]

for symbol in symbols:
    future:Future = self.AddFuture(symbol, Resolution.Minute, dataNormalizationMode=DataNormalizationMode.BackwardsRatio, contractDepthOffset=0)
    self.futures.append(future.Symbol)

self.day_close_flag:bool = False
self.day_open_flag:bool = False

self.Schedule.On(self.DateRules.EveryDay(self.futures[1]), self.TimeRules.BeforeMarketClose(self.futures[1], 1), self.DayClose)
self.Schedule.On(self.DateRules.EveryDay(self.futures[1]), self.TimeRules.AfterMarketOpen(self.futures[1], 1), self.DayOpen)

def OnData(self, data: Slice) -> None:
if self.day_open_flag:
    self.day_open_flag = False

    returns:Dict[Symbol, float] = {symbol: data[symbol].Open / self.last_close[symbol] - 1 for symbol in self.futures if symbol in self.last_close and symbol in data and data[symbol]}
    self.last_close.clear()

    traded_count:int = self.traded_count if len(returns) >= len(self.futures) else 1
    sorted_returns:List[Symbol] = sorted(returns, key=returns.get)
    long:List[Symbol] = sorted_returns[:traded_count]
    short:List[Symbol] = sorted_returns[-traded_count:]
    
    for i, portfolio in enumerate([long, short]):
        for symbol in portfolio:
            self.SetHoldings(self.Securities[symbol].Mapped, ((-1) ** i) / len(portfolio) * self.traded_percentage)

if self.day_close_flag:
    self.day_close_flag = False

    self.Liquidate()
    self.last_close = { symbol: data[symbol].Close for symbol in self.futures if symbol in data and data[symbol] }

def DayClose(self) -> None:
self.day_close_flag = True

def DayOpen(self) -> None:
self.day_open_flag = True