Quant BuffetRelax, Not Over Thinking

Absolute Delta Beta Strategy in Chinese Equities

Log in to collect

Academic paper

Relative Strength Strategies for Investing

AuthorsMeb Faber

Institute
  • Institut Mines-Télécom Business School
  • ?Cambria Investment Management

Strategy in a nutshell

The strategy targets A-share stocks listed on the Shanghai and Shenzhen exchanges, using data from the CSMAR database and benchmarking against the CSI300 index. It estimates an asymmetric CAPM for each stock, separating market conditions into rising and declining states. The difference between the betas in these states defines the Absolute Delta Beta (ADB). Stocks are sorted into 50 groups based on their ADB values. The portfolio goes long on stocks with the lowest ADB (indicating minimal sensitivity to framing effects) and short on stocks with the highest ADB. The strategy is value-weighted and rebalanced monthly.

Economic rationale

Absolute Delta Beta serves as a proxy for the framing effect, a behavioral bias where investors alter risk preferences depending on market context. A low ADB indicates rational pricing, while a high ADB reflects stronger framing effects, which are associated with lower expected future returns. By taking long positions in low-ADB stocks and short positions in high-ADB stocks, the strategy aims to exploit mispricings generated by this behavioral bias.

Backtest performance

Annualised return8.23%
Volatility54.87%
Beta0.189
Sharpe ratio0.15
Win rate53%

Full Python code

from AlgorithmImports import *
from data_tools import QuantpediaCSI300, CustomFeeModel, SymbolData, MultipleLinearRegression
# endregion

class AbsoluteDeltaBetaStrategyInChineseEquities(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2010, 1, 1)   # CSI 300 data starts in 2005
self.SetCash(100000)

self.leverage:int = 10
self.quantile:int = 10

self.min_prices:int = 15

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

self.benchmark:Symbol = self.AddData(QuantpediaCSI300, 'CSI_300', Resolution.Daily).Symbol
self.benchmark_data = SymbolData()

self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.BeforeMarketClose(self.market, 0), self.Selection)

def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)

def CoarseSelectionFunction(self, coarse):
curr_date:datetime.date = self.Time.date()

for stock in coarse:
    symbol:Symbol = stock.Symbol
    
    if symbol in self.data:
        self.data[symbol].update_price_with_date(stock.AdjustedPrice, curr_date)

# monthly rebalance
if not self.selection_flag:
    return Universe.Unchanged

selected_symbols = [x.Symbol for x in coarse if x.HasFundamentalData]

return selected_symbols

def FineSelectionFunction(self, fine):
csi_rets_with_dates:list[datetime.date, float]|None = self.benchmark_data.get_rets_with_dates() \
    if self.benchmark_data.is_ready(self.min_prices) else None

if csi_rets_with_dates == None:
    return Universe.Unchanged

fine = list(filter(lambda stock: stock.MarketCap != 0 and stock.CompanyReference.BusinessCountryID == 'CHN', fine))

# Exclude 30% of lowest stocks by MarketCap
sorted_by_market_cap = sorted(fine, key = lambda x: x.MarketCap)
fine = sorted_by_market_cap[int(len(sorted_by_market_cap) * 0.3):]

ADB:dict[Symbol, float] = {}

for stock in fine:
    symbol:Symbol = stock.Symbol
    market_cap:float = stock.MarketCap

    if symbol not in self.data:
        self.data[symbol] = SymbolData()

    if self.data[symbol].is_ready(self.min_prices):
        stock_rets_with_dates:list[datetime.date, float] = self.data[symbol].get_rets_with_dates()
        rising_x, rising_y, declining_x, declining_y = ([] for i in range(4))

        for (csi_ret, csi_date), (stock_ret, stock_date) in zip(csi_rets_with_dates, stock_rets_with_dates):
            if csi_date == stock_date:
                if stock_ret > 0:
                    rising_x.append(csi_ret)
                    rising_y.append(stock_ret)
                else:
                    declining_x.append(csi_ret)
                    declining_y.append(stock_ret)

        if len(rising_x) != 0 and len(declining_x) != 0:
            regression_model = MultipleLinearRegression(rising_x, rising_y)
            rising_market_beta:float = regression_model.params[0]

            regression_model = MultipleLinearRegression(declining_x, declining_y)
            declining_market_beta:float = regression_model.params[0]

            ADB[stock] = abs(rising_market_beta - declining_market_beta)
    
    self.data[symbol].reset_data()

if len(ADB) < self.quantile:
    return Universe.Unchanged

quantile:int = int(len(ADB) / self.quantile)
sorted_by_ADB:list[Symbol] = [x[0] for x in sorted(ADB.items(), key=lambda item: item[1])]

long_leg:list[Symbol] = sorted_by_ADB[:quantile]
short_leg:list[Symbol] = sorted_by_ADB[-quantile:]

total_long_cap:float = sum(list(map(lambda stock: stock.MarketCap, long_leg)))
for stock in long_leg:
    self.weights[stock.Symbol] = stock.MarketCap / total_long_cap

total_short_cap:float = sum(list(map(lambda stock: stock.MarketCap, short_leg)))
for stock in short_leg:
    self.weights[stock.Symbol] = -stock.MarketCap / total_short_cap

return list(self.weights.keys())

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

if self.benchmark in data and data[self.benchmark] and data[self.benchmark].Value != 0:
    self.benchmark_data.update_price_with_date(data[self.benchmark].Value, curr_date)

if not self.selection_flag:
    return
self.selection_flag = False

self.benchmark_data.reset_data()

# trade execution
invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in 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