Quant BuffetRelax, Not Over Thinking

Institutional Equity Momentum in China

Log in to collect

Academic paper

Retail Investors and Momentum

AuthorsJun Du; Dashan Huang; Yu-Jane Liu; Yushui Shi; Avanidhar Subrahmanyam; Huacheng Zhang

Institute
  • Peking University
  • ?Peking University - Guanghua School of Management
  • SGSingapore Management University
  • ?Singapore Management University - Lee Kong Chian School of Business
  • Monash University
  • ?Monash University - Department of Banking and Finance
  • HUInternational Business School
  • University of California, Los Angeles
  • Research Network (United States)
  • ?Financial Research Network (FIRN)
  • ?Institute of Global Finance, UNSW Business School
  • ?University of California, Los Angeles (UCLA) - Finance Area
  • University of Nottingham
  • ?Nottingham University Business School

Strategy in a nutshell

The strategy invests in Chinese stocks by independently sorting them into deciles based on nominal prices and quintiles based on cumulative 12-month returns. It goes long the winners from the top-priced decile and short the losers from the same decile. Portfolios are equally weighted and rebalanced monthly.

Economic rationale

Stock prices influence retail investor participation due to financial constraints, particularly the round-lot (100-share minimum) rule. Reduced participation of small investors in high-priced stocks can create short-term momentum. Trading halts, corporate events, and stock splits further amplify these effects, producing predictable return patterns.

Backtest performance

Annualised return21.6%
Volatility24.22%
Beta0.032
Sharpe ratio0.89
Win rate69%

Full Python code

from AlgorithmImports import *
from data_tools import CustomFeeModel, SymbolData, ChineseStocks
# endregion

class InstitutionalEquityMomentumInChina(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)

self.perf_quantile:int = 5
self.price_quantile:int = 10

self.leverage:int = 5

self.period:int = 21 * 12
self.max_missing_days:int = 5

self.data:dict[Symbol, SymbolData] = {}

top_size_symbol_count:int = 400
ticker_file_str:str = self.Download('data.quantpedia.com/backtesting_data/equity/chinese_stocks/large_cap_500.csv')
tickers:List[str] = ticker_file_str.split('\r\n')[:top_size_symbol_count]

for t in tickers:
    # price data
    data = self.AddData(ChineseStocks, t, Resolution.Daily)
    data.SetFeeModel(CustomFeeModel())
    data.SetLeverage(self.leverage)

    self.data[data.Symbol] = SymbolData(self.period)

self.recent_month:int = -1
self.exclusion_flag:bool = False

def OnData(self, data):
curr_date:datetime.date = self.Time.date()

for symbol, symbol_data in self.data.items():
    if symbol in data and data[symbol] and data[symbol].Value and data[symbol].GetProperty('price_data'):
        price:float = data[symbol].Value 
        symbol_data.update_prices(price)
        symbol_data.set_last_update(curr_date)

        price_data:dict = data[symbol].GetProperty('price_data')
        if self.exclusion_flag and 'marketValue' in price_data and price_data['marketValue'] != 0:
            market_cap:float = float(price_data['marketValue'])
            symbol_data.set_market_cap(market_cap)

if self.recent_month == self.Time.month:
    return
self.recent_month = self.Time.month

if self.exclusion_flag:
    market_caps:dict[Symbol, float] = { sym: sym_data.get_market_cap() for sym, sym_data in self.data.items() if sym_data.market_cap_ready() }
    sorted_by_cap:list[Symbol] = [x[0] for x in sorted(market_caps.items(), key=lambda item: item[1])]
    # exclude lowest 30%
    active_universe:list[Symbol] = sorted_by_cap[int(len(sorted_by_cap) * 0.3):]
else:
    active_universe:list[Symbol] = list(self.data.keys())

performances:dict[Symbol, float] = {}
prices:dict[Symbol, float] = {}

for symbol in active_universe:
    symbol_data:SymbolData = self.data[symbol]

    if not symbol_data.data_still_coming(curr_date, self.max_missing_days):
        symbol_data.reset_data()

    if symbol_data.prices_ready():
        performances[symbol] = symbol_data.get_performance(self.period)
        prices[symbol] = symbol_data.get_last_price()

if len(performances) < self.perf_quantile or len(prices) < self.price_quantile:
    self.Liquidate()
    return

quantile:int = int(len(performances) / self.perf_quantile)
sorted_by_perf:list[Symbol] = [x[0] for x in sorted(performances.items(), key=lambda item: item[1])]
winners:list[Symbol] = sorted_by_perf[-quantile:]
losers:list[Symbol] = sorted_by_perf[:quantile]

quantile:int = int(len(prices) / self.price_quantile)
sorted_by_price:list[Symbol] = [x[0] for x in sorted(prices.items(), key=lambda item: item[1])]
top_price:list[Symbol] = sorted_by_price[-quantile:]

long_leg:list[Symbol] = [symbol for symbol in winners if symbol in top_price]
short_leg:list[Symbol] = [symbol for symbol in losers if symbol in top_price]

long_len:int = len(long_leg)
short_len:int = len(short_leg)

# Trade Execution
stocks_invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
    if symbol not in long_leg + short_leg:
        self.Liquidate(symbol)

for symbol in long_leg:
    self.SetHoldings(symbol, 1 / long_len)

for symbol in short_leg:
    self.SetHoldings(symbol, -1 / short_len)