Trade NYSE, NASDAQ, and AMEX stocks daily by previous returns, going long on the lowest-return decile and short on the highest, holding value-weighted portfolios until market close.

I. STRATEGY IN A NUTSHELL

Trade U.S. stocks daily by buying the lowest-return decile and shorting the highest based on intraday returns (10:00 a.m. to 3:30 p.m.), excluding stocks with >5% moves or no trades. Portfolios are value-weighted and held for the final 30 minutes before market close.

II. ECONOMIC RATIONALE

The short-term reversal effect reflects illiquidity and institutional trading behavior, with reversals strongest near market close when institutions dominate trading. This effect persists after controlling for common risk factors, emphasizing market microstructure dynamics.

III. SOURCE PAPER

What Drives Intraday Reversal? Illiquidity or Liquidity Oversupply? [Click to Open PDF]

Junqing Kang, Shen Lin, Xiong Xiong, Sun Yat-sen University (SYSU) – Lingnan (University) College, Tianjin University – College of Management and Economics; PBCSF, Tsinghua University, College of Management and Economics and China Center for Social Computing and Analytics

<Abstract>

Previous studies of the U.S. market regard short-term reversal as compensation for liquidity provision. However, we find that intraday reversal has no significant dependence on stock liquidity in the Chinese market. Hence, based on a stylized framework, we propose an alternative explanation: irrational uninformed liquidity providers, who underestimate the information component in the equilibrium price due to physiological anchoring, trade against previous price movement, which generates an opposing price pressure. The empirical results confirm this explanation of liquidity oversupply (from irrational uninformed liquidity providers). The negative correlation between previous intraday returns and future returns in the Chinese market is reversed once we extend the holding period. This indicates that reversal is a pricing error due to excessive liquidity provision from uninformed retail traders instead of a price correction from a temporary price concession due to a lack of liquidity.

IV. BACKTEST PERFORMANCE

Annualised Return22.42%
Volatility5.41%
Beta-0.005
Sharpe Ratio4.15
Sortino Ratio-1.858
Maximum DrawdownN/A
Win Rate46%

V. FULL PYTHON CODE

from AlgorithmImports import *
from typing import List, Dict
from pandas.core.frame import dataframe
from pandas.core.series import Series
#endregion
class IntradayReversalInUS(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100_000)
        self.exchange_codes: List[str] = ['NYS', 'NAS', 'ASE']	 
        self.quantile: int = 10
        self.leverage: int = 5
        self.min_share_price: int = 5
        self.percentage_threshold: float = 0.05
        self.data: Dict[Symbol, SymbolData] = {} # Storing important data for each selected stock in this strategy
        self.selected_symbols: List[Symbol] = []
        
        self.fundamental_count: int = 100
        self.fundamental_sorting_key = lambda x: x.DollarVolume
        
        self.UniverseSettings.Leverage = self.leverage
        self.UniverseSettings.Resolution = Resolution.Minute
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.settings.daily_precise_end_time = False
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # Each day select stocks for trading
        selected: List[Fundamental] = [
            x for x in fundamental 
            if x.HasFundamentalData 
            and x.Market == 'usa' 
            and x.Price > self.min_share_price
            and x.MarketCap != 0
            and x.SecurityReference.ExchangeId in self.exchange_codes
        ]
        if len(selected) > self.fundamental_count:
            selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
    
        # Create list of selected symbols and create SymbolData object for each of these stocks
        for stock in selected:
            symbol: Symbol = stock.Symbol
            
            # Create SymbolData object and store stock's market capitalization
            self.data[symbol] = SymbolData(stock.MarketCap)
            
            self.selected_symbols.append(symbol)
        
        return self.selected_symbols
    def OnData(self, data: Slice) -> None:
        selected: List[Symbol] = []
        
        # Calculate overnight return
        if self.Time.hour == 10 and self.Time.minute == 0:
            
            # Select only stocks, which return in absolute value isn't greater than 5% 
            for symbol in self.selected_symbols:
                if symbol in data and data[symbol]:
                    # Get current price from data object
                    current_price: float = data[symbol].Value
                    
                    # Store current price, for next calculation
                    self.data[symbol].open_price = current_price
                    
                    # Use history to get last close
                    history: dataframe = self.History(symbol, 1, Resolution.Daily)
                    
                    if history.empty:
                        continue
                    
                    # Get last close from history
                    closes: Series = history.loc[symbol].close
                    close: float = closes[0]
                    
                    # Calculate over night return
                    over_night_return: float = (current_price - close) / close 
                    
                    # Select current stock, if stock's absolute value of over night return is equal or smaller than 5%                   
                    if abs(over_night_return) <= self.percentage_threshold:
                        self.data[symbol].over_night_return = over_night_return
                        selected.append(symbol)
                    
            self.selected_symbols = selected
            
        # Trade 30 minutes before market close
        if self.Time.hour == 15 and self.Time.minute == 30:
            
            over_night_returns: Dict[Symbol, float] = {}
            
            # Select stocks, which don't have return in absolute value greater than 5% since 10:00 a.m. to 3:30 p.m.
            for symbol in self.selected_symbols:
                if symbol in data and data[symbol]:
                    current_price: float = data[symbol].Value
                    open_price: float = self.data[symbol].open_price
                    
                    # Calculate return since since 10:00 a.m. to 3:30 p.m
                    daily_return: float = (current_price - open_price) / open_price
                    
                    # Select current stock, if stock's absolute value of daily return is equal or smaller than 5%  
                    if abs(daily_return) <= self.percentage_threshold:
                        # self.data[symbol].daily_return = daily_return
                        over_night_returns[symbol] = self.data[symbol].over_night_return
                        
            # Check if we have enough stocks for trade            
            if len(over_night_returns) < self.quantile:
                return 
            
            # Sort stocks based on overnight return
            quantile: int = int(len(over_night_returns) / self.quantile)
            sorted_by_overnight_ret: List[Symbol] = [x[0] for x in sorted(over_night_returns.items(), key=lambda item: item[1])]
            
            # Go long(short) on the decile of stocks with the lowest(highest) measure
            long: List[Symbol] = sorted_by_overnight_ret[:quantile]
            short: List[Symbol] = sorted_by_overnight_ret[-quantile:]
            
            # Trade execution
            for i, portfolio in enumerate([long, short]):
                mc_sum: float = sum(list(map(lambda symbol: self.data[symbol].market_cap, portfolio)))
                for symbol in portfolio:
                    self.SetHoldings(symbol, ((-1)**i) * self.data[symbol].market_cap / mc_sum)
            
        # Liquidate one minute before market close
        if self.Time.hour == 15 and self.Time.minute == 59:
            # Clear dictionaries for next intra day trading
            self.selected_symbols.clear()
            self.data.clear()                
            
            self.Liquidate() # liquidate all stocks
            
class SymbolData():
    def __init__(self, market_cap: float) -> None:
        self.market_cap: float = market_cap
        self.open_price: Union[None, float] = None
        self.over_night_return: Union[None, float] = None
        self.daily_return: Union[None, float] = None
        
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        fee: float = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0
        return OrderFee(CashAmount(fee, "USD"))

VI. Backtest Performance

Leave a Reply

Discover more from Quant Buffet

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

Continue reading