Impact of Overnight Returns and Daytime Reversals to Future Stock Returns
Log in to collectAcademic paper
Ferhat Akbas; Ekkehart Boehmer; Chao Jiang; Paul D. Koch
- University of Illinois Chicago
- ?University of Illinois at Chicago
- SGSingapore Management University
- ?Singapore Management University - Lee Kong Chian School of Business
- University of South Carolina
- ?University of South Carolina - Department of Finance
- Iowa State University
- ?Iowa State University - Finance Department
Strategy in a nutshell
Targets large-cap U.S. stocks, ranking them by frequency of negative daytime reversals (positive overnight returns followed by negative daytime returns). Goes long on the top AB_NR quintile and short on the bottom quintile, with monthly rebalancing.
Economic rationale
Negative daytime reversals reflect overnight noise-trader price pressure corrected by daytime investors. High reversal frequency predicts future returns, providing a risk premium for trading against noise traders.
Backtest performance
Annualised return5.28%
Volatility6.43%
Beta0.002
Sharpe ratio0.82
Sortino ratio-0.447
Win rate49%
Full Python code
import numpy as np
from AlgorithmImports import *
import pandas as pd
from pandas.core.frame import DataFrame
class ImpactOfOvernightReturnsDaytimeReversals(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)
market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.fundamental_count:int = 1000
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.period:int = 13 * 21
self.quantile:int = 10
self.leverage:int = 5
self.min_share_price:float = 5.
self.exchange_codes:List[str] = ['NYS', 'NAS', 'ASE']
self.long:List[Symbol] = []
self.short:List[Symbol] = []
self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.FundamentalSelectionFunction)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.Schedule.On(self.DateRules.MonthEnd(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
self.settings.daily_precise_end_time = False
def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(self.leverage)
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
selected:List[Fundamental] = [
x for x in fundamental if x.HasFundamentalData and x.Price >= self.min_share_price and \
x.Market == 'usa' and x.MarketCap != 0
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
AB_NR:Dict[Fundamental, float] = {}
for stock in selected:
symbol:Symbol = stock.Symbol
hist:DataFrame = self.History([symbol], self.period, Resolution.Daily)
if 'close' in hist.columns and 'open' in hist.columns:
closes:pd.Series = hist['close']
opens:pd.Series = hist['open']
if len(closes) == self.period and len(opens) == self.period:
# Calculate overnight and daily returns
RET_OC:pd.Series = pd.Series(closes / opens - 1) # Open to close return
RET:pd.Series = pd.Series(closes).pct_change() # Close to close return
RET_CO:pd.Series = ((1 + RET) / (1 + RET_OC)) - 1
# Negative daytime reversal signal for last year
reversal_vector:List = [1 if co > 0 and oc < 0 else 0 for co, oc in zip(RET_CO, RET_OC)]
# Slice it for every month
reversal_separate_months:List = [reversal_vector[x:x+21] for x in range(0, len(reversal_vector),21)]
NRIT:List = [month.count(1) / len(month) for month in reversal_separate_months]
NRIT_current_month:float = NRIT[-1]
NRTI_avg:float = np.average(NRIT[:-2])
# AB_NR calc
AB_NR[stock] = NRIT_current_month / NRTI_avg
if len(AB_NR) != 0:
# Sort by market cap and AB_NR
market_cap_values:List[float] = [x.MarketCap for x in AB_NR.keys()]
high_by_market_cap:List[Fundamental] = [x[0] for x in AB_NR.items() if x[0].MarketCap >= np.percentile(market_cap_values, 66)]
abnr_values:List[float] = list(AB_NR.values())
high_by_abnr:List[Fundamental] = [x[0] for x in AB_NR.items() if x[1] >= np.percentile(abnr_values, 80)]
low_by_abnr:List[Fundamental] = [x[0] for x in AB_NR.items() if x[1] <= np.percentile(abnr_values, 20)]
self.long = [x.Symbol for x in high_by_market_cap if x in high_by_abnr]
self.short = [x.Symbol for x in high_by_market_cap if x in low_by_abnr]
return self.long + self.short
def OnData(self, data: Slice) -> None:
if not self.selection_flag:
return
self.selection_flag = False
# order execution
targets:List[PortfolioTarget] = []
for i, portfolio in enumerate([self.long, self.short]):
for symbol in portfolio:
if symbol in data and data[symbol]:
targets.append(PortfolioTarget(symbol, ((-1) ** i) / len(portfolio)))
self.SetHoldings(targets, True)
self.long.clear()
self.short.clear()
def Selection(self) -> None:
self.selection_flag = True
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))