Skewness factor in Chinese Equities
Log in to collectAcademic paper
Market Volatility and Skewness Risks in China
Fang Zhen
- Central University of Finance and Economics
- ?Central University of Finance and Economics (CUFE) - China Economics and Management Academy
Strategy in a nutshell
Long stocks with high exposure to skewness innovations and short those with low exposure, using monthly value-weighted decile portfolios.
Economic rationale
Stocks sensitive to skewness innovations benefit from leverage effects, short-term reversals, and lottery demand, providing compensation for losses in bear markets.
Backtest performance
Annualised return22.58%
Volatility23.03%
Beta-0.197
Sharpe ratio0.98
Win rate49%
Full Python code
from AlgorithmImports import *
import data_tools
from statsmodels.tsa.arima.model import ARIMA
import numpy as np
from typing import List, Dict
# endregion
class SkewnessfactorinChineseEquities(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
self.quantile:int = 10
self.leverage:int = 10
self.period:int = 12 * 21
self.max_missing_days:int = 7
self.SetWarmUp(self.period, Resolution.Daily)
# market cap filtering
self.exclusion_flag:bool = True
self.market_cap_cutoff:float = .5
self.data:dict[str, data_tools.SymbolData] = {}
self.weights:dict[Symbol, float] = {}
# cboe data
self.cboe_skewness:Symbol = self.AddData(data_tools.CBOEData, 'CBOE_skew', Resolution.Daily).Symbol
self.cboe_skewness_data:RollingWindow = RollingWindow[float](self.period)
self.spy:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.selection_flag:bool = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(self.spy), self.TimeRules.AfterMarketOpen(self.spy), self.Selection)
def OnSecuritiesChanged(self, changes:SecurityChanges) -> None:
for security in changes.AddedSecurities:
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(self.leverage)
def CoarseSelectionFunction(self, coarse:List[CoarseFundamental]) -> List[Symbol]:
if not self.selection_flag:
return Universe.Unchanged
if not self.cboe_skewness_data.IsReady:
return Universe.Unchanged
else:
# custom data is still comming in
if self.Securities[self.cboe_skewness].GetLastData() and (self.Time.date() - self.Securities[self.cboe_skewness].GetLastData().Time.date()).days > self.max_missing_days:
return Universe.Unchanged
selected:List[Symbol] = [x.Symbol for x in coarse if x.HasFundamentalData and x.Price > 5]
return selected
def FineSelectionFunction(self, fine:List[FineFundamental]) -> List[Symbol]:
# filter chinese stocks by BusinessCountryID
fine:List[FineFundamental] = list(filter(lambda stock: stock.MarketCap != 0 and stock.CompanyReference.BusinessCountryID == 'CHN', fine))
if self.exclusion_flag:
# exclude 30% of lowest stocks by MarketCap
sorted_by_market_cap:List[FineFundamental] = sorted(fine, key = lambda x: x.MarketCap)
fine = sorted_by_market_cap[int(len(sorted_by_market_cap) * self.market_cap_cutoff):]
# calculate skewness beta
skew_beta:Dict[FineFundamental, float] = {}
# skewness innovations
skewness:np.ndarray = np.array(list(self.cboe_skewness_data))
arma_model = ARIMA(skewness, order=(1,0,1))
model_fit = arma_model.fit()
innovations:np.ndarray = model_fit.resid[::-1]
for stock in fine:
symbol:Symbol = stock.Symbol
market_cap:float = stock.MarketCap
history:DF = self.History(symbol, self.period, Resolution.Daily)
if history.empty or history.loc[symbol].close.size != self.period:
continue
closes:np.ndarray = history['close'].values
stock_returns:np.array = (closes[1:] - closes[:-1]) / closes[:-1]
model_fit = data_tools.multiple_linear_regression(innovations[2:], stock_returns[:-1])
skew_beta[stock] = model_fit.params[1]
if len(skew_beta) < self.quantile:
return Universe.Unchanged
quantile:int = int(len(skew_beta) / self.quantile)
sorted_skew_beta:List[FineFundamental] = [x[0] for x in sorted(skew_beta.items(), key=lambda item: item[1], reverse=True)]
long:List[FineFundamental] = sorted_skew_beta[:quantile]
short:List[FineFundamental] = sorted_skew_beta[-quantile:]
total_long_cap:float = sum(list(map(lambda stock: stock.MarketCap, long)))
for stock in long:
self.weights[stock.Symbol] = stock.MarketCap / total_long_cap
total_short_cap:float = sum(list(map(lambda stock: stock.MarketCap, short)))
for stock in short:
self.weights[stock.Symbol] = -stock.MarketCap / total_short_cap
return list(self.weights.keys())
def OnData(self, data: Slice) -> None:
# store CBOE data
if self.cboe_skewness in data and data[self.cboe_skewness]:
close:float = data[self.cboe_skewness].Value
self.cboe_skewness_data.Add(close)
if self.IsWarmingUp:
return
# monthly rebalance
if not self.selection_flag:
return
self.selection_flag = False
# 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 self.weights:
self.Liquidate(symbol)
for symbol, w in self.weights.items():
if symbol in data and data[symbol]:
self.SetHoldings(symbol, w)
self.weights.clear()
def Selection(self):
self.selection_flag = True