
The strategy sorts stocks by negative daytime reversals and market capitalization, going long on the top AB_NR quintile and short on the bottom, with equally-weighted portfolios rebalanced monthly.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Reversal
I. 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.
II. 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.
III. SOURCE PAPER
Overnight Returns, Daytime Reversals, and Future Stock Returns: The Risk of Investing in a Tug of War With Noise Traders [Click to Open PDF]
Ferhat Akbas, Ekkehart Boehmer, Chao Jiang, and Paul D. Koch — University of Illinois at Chicago – College of Business Administration; Singapore Management University – Lee Kong Chian School of Business; University of South Carolina – Department of Finance; Iowa State University – Finance Department
<Abstract>
A higher frequency of positive overnight returns followed by negative trading day reversals during a month suggests a more intense daily tug of war between opposing investor clienteles, who are likely composed of noise traders overnight and arbitrageurs during the day. We show that a more intense daily tug of war predicts higher future returns in the cross section. Additional tests support the conclusion that, in a more intense tug of war, daytime arbitrageurs are more likely to discount the possibility that positive news arrives overnight and thus overcorrect the persistent upward overnight price pressure.


IV. BACKTEST PERFORMANCE
| Annualised Return | 5.28% |
| Volatility | 6.43% |
| Beta | 0.002 |
| Sharpe Ratio | 0.82 |
| Sortino Ratio | -0.447 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
V. 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"))