Quant BuffetRelax, Not Over Thinking

Coreversal in Chinese Equities

Log in to collect

Academic paper

Coreversal: The Booms and Busts of Arbitrage Activities in China

AuthorsXin Liu; Zhigang Qiu; Luyao Shen; Weinan Zheng

Institute
  • Renmin University of China
  • ?School of Finance, Renmin University of China
  • Capital University of Economics and Business
  • ?International School of Economics and Management, Capital University of Economics and Business

Strategy in a nutshell

The strategy targets Chinese A-share stocks from the Wind Economic Database, excluding the smallest 30% of firms and those listed under seven months. At the end of each month, stocks are ranked into deciles based on the past twelve months’ cumulative returns. A standard reversal strategy is formed by buying the bottom decile (past losers) and selling the top decile (past winners), with monthly rebalancing. To enhance timing, the strategy computes the average pairwise abnormal correlations within the winner and loser deciles to derive the CoREV (coreversal) measure. If CoREV is in the top quintile at month-end, the long-short reversal portfolio is implemented over the next year; otherwise, the strategy holds cash.

Economic rationale

Empirical research shows that arbitrageurs can temporarily destabilize prices, causing overshoots and subsequent reversals. High reversal trading intensity (high CoREV) indicates crowded trades that exaggerate deviations from fundamentals. By timing the reversal strategy based on CoREV, the strategy captures short-term profits from these exaggerated mispricings before they revert in the long term. This approach combines traditional reversal trading with market timing, exploiting periods when arbitrage activity amplifies potential gains.

Backtest performance

Annualised return12.42%
Volatility13.75%
Beta0.011
Sharpe ratio0.9
Win rate52%

Full Python code

from AlgorithmImports import *
from data_tools import SymbolData, CustomFeeModel, ChineseStocks
import numpy as np
from scipy import stats
# endregion

class CoreversalinChineseEquities(QCAlgorithm):

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

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

# trenching
self.managed_queue:List[RebalanceQueueItem] = []
self.holding_period:int = 12            # months

# CoREV
self.CoREV_period:int = 12
self.CoREV_values:List[float] = []
self.CoREV_quantile:int = 5
self.quantile:int = 10

self.period = 52 * 5                    # daily period
self.data:dict[str, SymbolData] = {}    # symbol data
self.value_weighted:bool = False        # True - value weighted; False - equally weighted
self.leverage:int = 5

self.SetWarmUp(self.period, Resolution.Daily)

for t in self.tickers:
    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

def OnData(self, data: Slice):
performance:dict[Symbol, bool] = {}

# store daily data
for symbol, symbol_data in self.data.items():
    if data.ContainsKey(symbol):
        price_data:dict[str, str] = data[symbol].GetProperty('price_data')
        # valid price data
        if data[symbol].Value != 0. and price_data:
            # update price and market cap
            close:float = float(data[symbol].Value)
            symbol_data.update_price(close)

            mc:float = float(price_data['marketValue'])
            symbol_data.update_market_cap(mc)

            if symbol_data.is_ready():
                if self.recent_month != self.Time.month and not self.IsWarmingUp:
                    perf:float = symbol_data.performance()
                    performance[symbol] = perf

# rebalance monthly
if self.recent_month == self.Time.month:
    return
self.recent_month = self.Time.month

if self.IsWarmingUp:
    return

long:List[Symbol] = []
short:List[Symbol] = []

if len(performance) >= self.quantile:
    # sort by performance
    sorted_by_performance:List = sorted(performance.items(), key=lambda x: x[1], reverse=True)
    quantile:int = int(len(sorted_by_performance) / self.quantile)
    top_quantile:List[Symbol] = [x[0] for x in sorted_by_performance[:quantile]]
    bottom_quantile:List[Symbol] = [x[0] for x in sorted_by_performance[-quantile:]]

    total_pairwise_abnormal_correlations:float = 0
    for quantile in [top_quantile, bottom_quantile]:
        total_quantile_correlation:float = 0

        for symbol in quantile:
            # excess return of each stock
            stock_weekly_returns:np.ndarray = self.data[symbol].returns_for_period_n()

            # equal-weighted average excess return of the bottom (top) decile excluding stock i
            relevant_symbols:List[Symbol] = [x for x in quantile if x != symbol]
            if len(relevant_symbols) != 0:
                quantile_ew_perf:float = np.mean(np.array([self.data[x].returns_for_period_n() for x in relevant_symbols]), axis=0)

            symbol_corr:float = np.corrcoef(stock_weekly_returns, quantile_ew_perf)[0, 1]

            total_quantile_correlation += symbol_corr
        
        # take the average of this sum
        total_quantile_correlation /= len(quantile)
        total_pairwise_abnormal_correlations += total_quantile_correlation
    
    # simple average of the average pairwise abnormal correlations in the loser decile and winner decile
    CoREV:float = total_pairwise_abnormal_correlations / 2
    if len(self.CoREV_values) >= self.CoREV_period:
        # if CoREV is at the month-end within its top quintile, buy the standard long-short reversal portfolio over the next year
        percentile:float = stats.percentileofscore(self.CoREV_values, CoREV) / 100
        top_percentile:float = 1 - (1 / self.CoREV_quantile)
        if percentile >= top_percentile:
            long = bottom_quantile
            short = top_quantile

    # append this month's corev value
    if CoREV != 0:
        # append this month's corev value
        self.CoREV_values.append(CoREV)

if long and short:
    # calculate quantities for long and short trenche
    if self.value_weighted:
        total_market_cap_long:float = sum([self.data[x].recent_market_cap() for x in long])
        total_market_cap_short:float = sum([self.data[x].recent_market_cap() for x in short])
        
        long_w:float = self.Portfolio.TotalPortfolioValue / self.holding_period
        short_w:float = self.Portfolio.TotalPortfolioValue / self.holding_period
        
        long_symbol_q:List[tuple[Symbol, float]] = [(x, np.floor(long_w * (self.data[x].recent_market_cap() / total_market_cap_long) / data[symbol].Value)) for x in long]
        short_symbol_q:List[tuple[Symbol, float]] = [(x, -np.floor(short_w * (self.data[x].recent_market_cap() / total_market_cap_short) / data[symbol].Value)) for x in short]
        
        self.managed_queue.append(RebalanceQueueItem(long_symbol_q + short_symbol_q))
    else:
        long_w:float = self.Portfolio.TotalPortfolioValue / self.holding_period / len(long)
        short_w:float = self.Portfolio.TotalPortfolioValue / self.holding_period / len(short)
        
        long_symbol_q:List[tuple[Symbol, float]] = [(x, np.floor(long_w / data[x].Value)) for x in long]
        short_symbol_q:List[tuple[Symbol, float]] = [(x, -np.floor(short_w / data[x].Value)) for x in short]
        
        self.managed_queue.append(RebalanceQueueItem(long_symbol_q + short_symbol_q))
    
# trade execution - rebalance portfolio
remove_item:RebalanceQueueItem|None = None

for item in self.managed_queue:
    # liquidate
    if item.holding_period == self.holding_period: # all portfolio parts are held for n months
        for symbol, quantity in item.opened_symbol_q:
            self.MarketOrder(symbol, -quantity)
                    
        remove_item = item
    
    # trade execution    
    if item.holding_period == 0: # all portfolio parts are held for n months
        opened_symbol_q:List[tuple[Symbol, float]] = []
        
        for symbol, quantity in item.opened_symbol_q:
            self.MarketOrder(symbol, quantity)
            opened_symbol_q.append((symbol, quantity))
                    
        # only opened orders will be closed        
        item.opened_symbol_q = opened_symbol_q
        
    item.holding_period += 1
    
# need to remove closed part of portfolio after loop. Otherwise it will miss one item in self.managed_queue
if remove_item:
    self.managed_queue.remove(remove_item)

class RebalanceQueueItem():
def __init__(self, symbol_q:List):
# symbol/quantity collections
self.opened_symbol_q:List[tuple[Symbol, float]] = symbol_q  
self.holding_period:int = 0