该策略投资于上海和深圳证券交易所的普通股,排除最小的30%公司。股票根据其市场贝塔值和行业基准贝塔值的差异计算IRSB值,并按十分位排序。构建一个多空组合,买入高IRSB值的股票组合,卖出低IRSB值的股票组合,按价值加权并每月末重新平衡。

策略概述

投资范围包括在上海证券交易所和深圳证券交易所交易的普通股。我们排除最小的30%公司(通常被视为壳公司)。股票价格、回报率和成交量数据可以从中国股票市场会计研究数据库(CSMAR)获取。行业分类和指数回报数据可以从申万宏源研究所(SWS Research)网站获取。

<交易步骤>

通过回归股票的原始回报率与市场回报率(公式1和2),估算每只股票(或行业)的市场贝塔值,其中因变量R_t为股票(或行业)在t月的原始回报率,独立变量R_M,t为市场回报率,涵盖t、t-1和t+1三个月。这些回报率通过CSMAR的加权回报率减去三个月的存款利率计算。为了获得一致的贝塔估计值(回归中的未知参数),我们通过将股票原始回报率对市场回报率的回归斜率系数汇总。

IRSB定义为股票相对于其行业同行的观察到的市场贝塔。具体来说,较高(较低)的IRSB反映了投资者对给定股票持有更多(更少)非理性观点,从而衡量投资者信念偏差的程度。公式(3)为:IRSB_s,t = (β_s,t − β_i,t−1)/β_i,t−1 ,其中β_s,t为股票s在t月的短期市场贝塔值,β_i,t−1为行业i在t−1月的长期市场贝塔值。

每月末,股票根据其IRSB值排序为十分位投资组合,按前述步骤构建。

低(高)投资组合包含IRSB最低(最高)的股票。最终投资组合通过买入高IRSB投资组合并卖出低IRSB投资组合构建。

最终投资组合计划每月(在月末)再平衡,并按价值加权。

策略合理性

投资者容易根据预期进行过度推断,并可能在股票层面形成偏颇的信念,这对股票回报产生较大的横截面影响。股票市场中的错误定价源于行为偏差,而IRSB指标表现出强大的预测能力,尤其是在那些难以套利的股票中。本文的独特贡献在于引入的衡量标准提供了独特的信息,通过将个别股票的公开数据与其行业同行进行比较,而不是基于公司特定特征的信念代理变量。公司的宏观经济风险暴露和财务风险对该变量的可预测性没有显著影响,使其成为深入分析和各种交易策略的客观选择。IRSB与投资者对近期正面新闻和涨停事件的推断性预期有关。

论文来源

An Asset Pricing Perspective [点击浏览原文]

<摘要>

我们基于中国股票市场中的行业相对股票市场贝塔(IRSB)构建了一个与信念偏差相关的新指标。我们发现,IRSB最高的十分位股票相对于IRSB最低的股票,年化回报率高出12.84%。IRSB溢价并不是由已知的风险因素或其他回报预测因子驱动的,在其他贝塔估计和大型公司中仍然持续存在。我们认为,高IRSB股票的优异表现主要源于投资者推断偏差导致的价格膨胀。

回测表现

年化收益率12.55%
波动率19.04%
Beta-0.004
夏普比率0.66
索提诺比率N/A
最大回撤N/A
胜率49%

完整python代码

from AlgorithmImports import *
from dateutil.relativedelta import relativedelta
from pandas.core.frame import DataFrame
from typing import Dict, List
import statsmodels.api as sm
# endregion
class IndustryRelativeStockBetainChineseEquities(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100000)
        self.weight:dict[Symbol, float] = {}
        self.leverage:float = 10
        self.industry_beta:Dict[str, RollingWindow] = {}
        self.stock_beta:Dict[Symbol, float] = {}
        
        self.period:int = 15
        self.rolling_period:int = 2
        self.traded_percentage:float = .2
    
        self.market:Symbol = self.AddEquity("FXI", Resolution.Daily).Symbol
        
        self.quantile:int = 10
        self.count:int = 100
        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.AfterMarketOpen(self.market), self.Selection)
        
    def OnSecuritiesChanged(self, changes:SecurityChanges):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)
            
    def CoarseSelectionFunction(self, coarse:List[CoarseFundamental]) -> List[Symbol]:
        # monthly selection
        if not self.selection_flag:
            return Universe.Unchanged
        # self.selection_flag = False
        selected:List[Symbol] = [x.Symbol
            for x in sorted([x for x in coarse if x.HasFundamentalData], 
            key = lambda x: x.DollarVolume, reverse = True)]
        return selected
    def FineSelectionFunction(self, fine: List[FineFundamental]) -> List[Symbol]:
        # filter chinese stocks by BusinessCountryID 
        fine:List[FineFundamental] = [x for x in fine if x.AssetClassification.MorningstarIndustryGroupCode != 0 and x.MarketCap != 0 and x.CompanyReference.BusinessCountryID == 'CHN']
        fine = sorted(fine, key=lambda x: x.MarketCap, reverse=True)[:self.count]
        # call history on fine and market
        history:DataFrame = self.History(list(map(lambda x:x.Symbol,fine)) + [self.market], start=self.Time.date() - relativedelta(months=self.period), end=self.Time.date())['close'].unstack(level=0)
        history = history.groupby(pd.Grouper(freq='M')).last()
        
        # sort stocks on industry numbers
        grouped_industries:Dict[MorningstarIndustryGroupCode, List[Symbol]] = {}
        for stock in fine:
            symbol:Symbol = stock.Symbol
            
            industry_group_code:str = str(stock.AssetClassification.MorningstarIndustryGroupCode)
            if not industry_group_code in grouped_industries:
                grouped_industries[industry_group_code] = []
            grouped_industries[industry_group_code].append(symbol)
        if len(history) >= self.period and self.market in history.columns and not history[self.market].isnull().values.any():
            history = history.iloc[-self.period:]
            history.index = history.index.to_pydatetime()
            asset_returns:DataFrame = history.pct_change().iloc[1:]
            stock_returns = asset_returns.loc[:, asset_returns.columns != self.market].tail(-1)
            asset_returns['market_lag_t-1'] = asset_returns[self.market].shift(1)
            asset_returns['market_lag_t+1'] = asset_returns[self.market].shift(-1)
            asset_returns = asset_returns.iloc[1:-1]
            stock_returns = stock_returns.iloc[:-1]
            industry_df:DataFrame = pd.DataFrame(index=asset_returns.index)
            for industry_code in grouped_industries:
                if len(grouped_industries[industry_code]) > 1:
                    ind_symbols:List[Symbol] = grouped_industries[industry_code]
                    if all(symbol in asset_returns.columns for symbol in ind_symbols):
                        industry_df[industry_code] = asset_returns[ind_symbols].mean(axis=1)
                else:
                    stock_returns = stock_returns.drop(stock_returns[grouped_industries[industry_code]], axis=1)
            # run industry regression
            x:np.ndarray = asset_returns[[self.market, 'market_lag_t-1', 'market_lag_t+1']].values
            y:np.ndarray = industry_df.values
            model = self.multiple_linear_regression(x, y)
            beta_values_industry:np.ndarray = sum(model.params[1:])
            for i, industry in enumerate(list(industry_df.columns)):
                if industry not in self.industry_beta:
                    self.industry_beta[industry] = RollingWindow[float](self.rolling_period)
                if not np.isnan(beta_values_industry[i]):
                    self.industry_beta[industry].Add(beta_values_industry[i])
            # run stock regression
            x:np.ndarray = asset_returns[[self.market, 'market_lag_t-1', 'market_lag_t+1']].values
            y:np.ndarray = stock_returns.values
            model = self.multiple_linear_regression(x, y)
            beta_values_stocks:np.ndarray = sum(model.params[1:])
            for i, asset in enumerate(list(stock_returns.columns)):
                asset_s:Symbol = self.Symbol(asset)
                if not np.isnan(beta_values_stocks[i]):
                    self.stock_beta[asset_s] = beta_values_stocks[i]
        irsb_values:Dict[FineFundamental, float] = {}
        # IRSB calculation
        for stock in fine:
            symbol:Symbol = stock.Symbol
            if symbol in self.stock_beta:
                for industry in grouped_industries:
                    if industry in self.industry_beta:
                        if symbol in grouped_industries[industry] and self.industry_beta[industry].IsReady:
                            irsb_values[stock] = (self.stock_beta[symbol] - self.industry_beta[industry][1]) / self.industry_beta[industry][1]
        # sorting by IRSB values
        if len(irsb_values) >= self.quantile:
            sorted_by_irsb:List[FineFundamental] = sorted(irsb_values, key=irsb_values.get, reverse=True)
            quantile:int = len(sorted_by_irsb) // self.quantile
            
            # portfolios
            long:List[FineFundamental] = sorted_by_irsb[:quantile]
            short:List[FineFundamental] = sorted_by_irsb[-quantile:]
            # calculate weights based on market cap
            sum_long:float = sum([x.MarketCap for x in long])
            for stock in long:
                self.weight[stock.Symbol] = (stock.MarketCap / sum_long) * self.traded_percentage 
            sum_short:float = sum([x.MarketCap for x in short])
            for stock in short:
                self.weight[stock.Symbol] = (-stock.MarketCap / sum_short) * self.traded_percentage 
        return list(self.weight.keys())
        
    def OnData(self, data):
        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.weight:
                self.Liquidate(symbol)
                
        for symbol, w in self.weight.items():
            if symbol in data and data[symbol]:
                self.SetHoldings(symbol, w)
            
        self.weight.clear()
        
    def Selection(self):
        self.selection_flag = True
    def multiple_linear_regression(self, x:np.ndarray, y:np.ndarray):
        # x:np.ndarray = np.array(x).T
        x = sm.add_constant(x, prepend=True)
        result = sm.OLS(endog=y, exog=x).fit()
        return result
# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading