Quant BuffetRelax, Not Over Thinking

Overnight Anomaly

Log in to collect

Academic paper

Return Differences between Trading and Non-Trading Hours: Like Night and Day

AuthorsMichael J. Cooper; Michael T. Cliff; Huseyin Gulen

Institute
  • University of Utah
  • ?University of Utah - David Eccles School of Business
  • Analysis Group (United States)
  • ?Analysis Group
  • ?Purdue University - Krannert School of Management

Strategy in a nutshell

The strategy involves buying the SPY ETF at its daily closing price and selling it at the next day's opening. While it generates frequent trades, making it prone to slippage and fees, it is best used as an input for a more sophisticated strategy or as a trade execution tool to optimize entry and exit points.

Economic rationale

This anomaly likely stems from companies publishing positive earnings surprises outside market hours starting in the mid-90s. Academic studies suggest high opening prices result from accumulated market orders, followed by a decline during the first trading hour. While an illiquidity premium may contribute to positive overnight returns, it explains only a small portion of the return difference between night and day.

Backtest performance

Annualised return14%
Beta0.341
Sortino ratio0.369
Win rate55%

Full Python code

from AlgorithmImports import *
#endregion
class OvernightAnomaly(QCAlgorithm):
def initialize(self):
self.set_start_date(1998, 1, 2)
self.set_cash(100_000)

data: Equity = self.add_equity("SPY", Resolution.MINUTE)
data.SetFeeModel(CustomFeeModel())
self.symbol: Symbol = data.Symbol

# NOTE: MarketOnClose orders must be placed with at least a 16 minute buffer before market close.
self.schedule.on(self.date_rules.every_day(self.symbol), self.time_rules.before_market_close(self.symbol, 16), self.day_close)
def day_close(self) -> None:
if not self.portfolio.invested:
    q: int = self.portfolio.total_portfolio_value // self.securities[self.symbol].price
    self.market_on_close_order(self.symbol, q)
    self.market_on_open_order(self.symbol, -q)
# Custom fee model (trading strategy is backtested without transaction costs to show a theoretical potential of overnight factor)
class CustomFeeModel(FeeModel):
def get_order_fee(self, parameters):
fee = parameters.security.price * parameters.order.absolute_quantity * 0
return OrderFee(CashAmount(fee, "USD"))