Disposition Effect in China
Log in to collectAcademic paper
Behavioural Factors in China Stock Market
Jinpeng Liu
- Southwestern University of Finance and Economics
- ?Southwestern University of Finance and Economics (SWUFE) - China Center for Behavior Economics and Finance
Strategy in a nutshell
The strategy targets A-share stocks listed on the Shanghai and Shenzhen stock markets. Using daily trading volume and price data from the CSMAR database, it estimates the disposition effect through regressions of abnormal trading volume. Stocks are first sorted by size (small vs. big) and then by their disposition effect measure. Stocks with the highest disposition effect are shorted, while those with minimal disposition effect are taken long. The portfolio is value-weighted and rebalanced yearly, aiming to exploit long-term mispricing caused by investors’ tendency to realize gains prematurely and hold onto losses.
Economic rationale
The disposition effect arises because investors are eager to take profits but reluctant to realize losses, causing upward momentum in winners to slow and downward momentum in losers to be suppressed. Stocks most affected by this behavioral bias deviate from their fundamental values, creating mispricing opportunities. By shorting stocks with pronounced disposition effects and going long on minimally affected stocks, the strategy aims to capture the correction toward fundamental value over time.
Backtest performance
Full Python code
from AlgorithmImports import *
from data_tools import SymbolData, CustomFeeModel, ChineseStocks, MultipleLinearRegression
import numpy as np
# endregion
class DispositionEffectinChina(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
# chinese stock universe
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')
self.tickers:List[str] = ticker_file_str.split('\r\n')[:self.top_size_symbol_count]
self.period = 365 # daily period
self.data:dict[str, SymbolData] = {} # symbol data
self.max_missing_days:int = 5 # max missing n of price data entries in a row for custom data
self.value_weighted:bool = True # True - value weighted; False - equally weighted
self.min_symbols:int = 2
self.leverage:int = 5
self.rebalance_month:int = 5 # May rebalance
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):
total_trading_volume:float = 0
included_symbols:List[Symbol] = []
disposition: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)
volume:float = float(price_data['turnoverVol'])
included_symbols.append(symbol)
total_trading_volume += volume
symbol_data.update_price(close)
symbol_data.update_volume(volume)
mc:float = float(price_data['marketValue'])
symbol_data.update_market_cap(mc)
# update second regression variable
if symbol_data.is_ready():
symbol_data.update_disposition_regression_x()
if self.recent_month != self.Time.month and self.Time.month == self.rebalance_month and not self.IsWarmingUp:
if symbol_data.cumulative_raw_volume_is_ready():
# first regression - abnormal trading volume
regr_data:np.ndarray = symbol_data.get_abnormal_trading_volume_regression_data()
ATV_regression_model = MultipleLinearRegression(regr_data[0], regr_data[1])
abnormal_trading_volume:np.ndarray = ATV_regression_model.resid
# second regression - disposition
regr_data:np.ndarray = symbol_data.get_disposition_regression_data()
disposition_regression_model = MultipleLinearRegression(regr_data, abnormal_trading_volume.T)
# negative beta_1 or positive beta_2 indicates a disposition effect
if disposition_regression_model.params[1] < 0 or disposition_regression_model.params[2] > 0:
disposition[symbol] = True
else:
disposition[symbol] = False
# update total market volume for included stocks in todays data structure
for symbol in included_symbols:
self.data[symbol].update_total_market_volume(total_trading_volume)
# rebalance yearly
if self.recent_month == self.Time.month:
return
self.recent_month = self.Time.month
if self.Time.month != self.rebalance_month:
return
if self.IsWarmingUp:
return
long:List[str] = []
short:List[str] = []
if len(disposition) >= self.min_symbols:
# according to the paper, long the no disposition small and big size portfolios and short the disposition small and big size portfolios
long = [symbol for symbol, disp in disposition.items() if disp == False]
short = [symbol for symbol, disp in disposition.items() if disp == True]
# weight calculation
weights_to_trade = {}
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])
for symbol in long:
if symbol in data and data[symbol]:
weights_to_trade[symbol] = self.data[symbol].recent_market_cap() / total_market_cap_long
for symbol in short:
if symbol in data and data[symbol]:
weights_to_trade[symbol] = -self.data[symbol].recent_market_cap() / total_market_cap_short
else:
long_c:int = len(long)
short_c:int = len(short)
for symbol in long:
if symbol in data and data[symbol]:
weights_to_trade[symbol] = 1 / long_c
for symbol in short:
if symbol in data and data[symbol]:
weights_to_trade[symbol] = -1 / short_c
# trade execution
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
if symbol not in weights_to_trade:
self.Liquidate(symbol)
for symbol, w in weights_to_trade.items():
self.SetHoldings(symbol, w)