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.

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 Return5.28%
Volatility6.43%
Beta0.002
Sharpe Ratio0.82
Sortino Ratio-0.447
Maximum DrawdownN/A
Win Rate49%

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"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading