
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.
ASSET CLASS: stocks | REGION: United States | FREQUENCY:
Intraday | MARKET: equities | KEYWORD: Reversal
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 Return | 22.42% |
| Volatility | 5.41% |
| Beta | -0.005 |
| Sharpe Ratio | 4.15 |
| Sortino Ratio | -1.858 |
| Maximum Drawdown | N/A |
| Win Rate | 46% |
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