Quant BuffetRelax, Not Over Thinking

Monthly Reversal in Chinese Equities

Log in to collect

Academic paper

Daily Momentum and New Investors in an Emerging Stock Market

AuthorsZhenyu Gao; Wenxi Jiang; Wei Xiong

Institute
  • HKChinese University of Hong Kong
  • ?The Chinese University of Hong Kong (CUHK) - CUHK Business School
  • Yale University
  • ?CUHK Business School, The Chinese University of Hong Kong
  • Shenzhen Stock Exchange
  • National Bureau of Economic Research
  • Princeton University
  • ?National Bureau of Economic Research (NBER)
  • ?Princeton University - Department of Economics

Strategy in a nutshell

Universe: Chinese stocks (Shanghai and Shenzhen exchanges), excluding daily limit-hitting stocks. Monthly, sort stocks by past three-month returns into quintiles. Construct a reversal portfolio: long bottom quintile, short top quintile. Portfolios are value-weighted and rebalanced monthly.

Economic rationale

New investors’ trading predicts future return declines, trading against experienced investors. Their activity captures noise trading more accurately, which professional investors arbitrage, supporting the profitability of a monthly reversal strategy in China.

Backtest performance

Annualised return13.09%
Volatility22.25%
Beta-0.036
Sharpe ratio0.59
Win rate47%

Full Python code

from AlgorithmImports import *
import data_tools
import numpy as np
from typing import Dict, List
# endregion

class MonthlyReversalinChineseEquities(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2015, 1, 1)   # Chinese data starts in 2015
self.SetCash(100000)

self.leverage:int = 5
self.quantile:int = 5
self.period:int = 3 + 1

self.data:Dict[Symbol, SymbolData] = {}
self.weight:Dict[Symbol, float] = {}

self.top_size_symbol_count:int = 300
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')[:self.top_size_symbol_count]

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

    stock_symbol:Symbol = data.Symbol

    self.data[stock_symbol] = data_tools.SymbolData(data.Symbol, self.period)

self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.curr_month:int = -1

def OnData(self, data: Slice) -> None:
if self.Time.month == self.curr_month:
    return
self.curr_month = self.Time.month

price_last_update_date:Dict[Symbol, datetime.date] = data_tools.ChineseStocks.get_last_update_date()
asset_returns:Dict[Symbol, float] = {}

for symbol, symbol_data in self.data.items():
    # store monthly data
    if symbol in data and data[symbol]:
        price_data:dict[str, str] = data[symbol].GetProperty('price_data')
        # valid price data
        if data[symbol].Value != 0. and price_data:
            close_price:float = data[symbol].Value
            symbol_data.update_price(close_price)

            mc:float = float(price_data['marketValue'])
            symbol_data.update_market_cap(mc)
    
    # calculate momentum
    if symbol_data.is_ready() and self.Time.date() < price_last_update_date[symbol]:
        asset_returns[symbol] = symbol_data.get_momentum()

weight:Dict[Symbol, float] = {}

# sort and divide to quantiles
if len(asset_returns) >= self.quantile:
    sorted_stocks:List[Symbol] = sorted(asset_returns, key=asset_returns.get)
    quantile:int = int(len(sorted_stocks) / self.quantile)
    long:List[Symbol] = sorted_stocks[:quantile]
    short:List[Symbol] = sorted_stocks[-quantile:]

    # calculate weights based on marketcap
    for i, portfolio in enumerate([long, short]):
        mc_sum:float = sum(list(map(lambda symbol: self.data[symbol].get_market_cap(), portfolio)))
        for symbol in portfolio:
            weight[symbol] = ((-1)**i) * self.data[symbol].get_market_cap() / mc_sum

# trade execution
invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
    if symbol not in weight:
        self.Liquidate(symbol)

for price_symbol, weight in weight.items():
    if price_symbol in data and data[price_symbol]:
        self.SetHoldings(price_symbol, weight)