本研究投资范围包括4,141家在NSE或BSE上市的公司,数据来源于Worldscope India,覆盖公司数量从2006年的1,700家增至2021年的约2,500家。投资组合基于每年9月底的运营盈利能力(OP)构建,形成稳健型、弱型和中立型三种组合。六个双排序组合包括大市值和小市值的稳健型与弱型。投资组合每年9月30日构建,持有至下年9月30日,盈利能力因子通过做多稳健型、做空弱型构建,并市值加权,每年重新平衡。

策略概述

本研究的投资范围包括4,141家曾在NSE或BSE上市的公司,这些公司数据来源于Worldscope India数据集,Datastream代码为WSCOPEIN。数据显示,在2006年至2021年期间,每年覆盖的公司数量稳步增长,从1,700家增加到约2,500家。

投资组合基于每年9月底的运营盈利能力(OP)进行构建。第t年的9月OP为年度收入减去商品成本、利息支出以及销售、管理和行政费用,再除以t-1年的账面股本。这形成了三种投资组合,分别为稳健型(盈利能力等于或超过70百分位)、弱型(盈利能力等于或低于30百分位)和中立型(其余公司)。六个双排序投资组合为大市值/稳健型(BR)、大市值/中立型(BNP)、大市值/弱型(BW)及其小市值对应的SR、SNP和SW。

每年9月30日构建投资组合,并持有至下一年的9月30日。

盈利能力因子投资组合通过做多大市值和小市值稳健型组合,并做空大市值和小市值弱型组合进行构建。股票基于市值加权,并在每年重新平衡。

策略合理性

作者将盈利能力因子纳入分析,是为了测试该因子是否能为股票回报提供超出其他因子的解释力。盈利能力因子,也被称为“质量”因子,衡量公司基于资产回报率、经营现金流与资产的比率以及资产周转率变化的盈利能力。

作者认为盈利能力因子可以捕捉公司财务健康状况和质量的重要信息,这可能会影响其股票回报。例如,高盈利的公司可能拥有更强的财务状况,能够更好地应对经济衰退。通过将盈利能力因子纳入分析,作者可以测试该因子在预测股票回报时是否增加了其他因子的解释力。

论文来源

Four and Five-Factor Models in the Indian Equities Market [点击浏览原文]

<摘要>

我们使用来自Refinitiv Datastream的数据,遵循两种断点方案,计算了2006年10月至2022年2月期间印度股票的Fama-French三因素、五因素和动量因子回报。我们展示了我们的因子回报估算与数据库中报告的回报估算之间的高度相关性,这些数据使用了与印度管理学院艾哈迈达巴德(IIMA)数据库紧密相关的断点方案。此外,我们使用Fama-French的当前断点方法和其他国际复制研究报告了四因素和五因素的回报估算。我们展示了由于方法不同导致的因子回报估算的差异,从而弥合了IIMA的开创性工作与当前国际实践之间的方法差异。我们与国际研究的不同之处在于,每年9月构建投资组合,以反映印度的财政报告期,从而提供反映印度情况的因子。我们使用因子跨越测试,表明所有五个Fama-French和动量因子都解释了印度股票市场的平均回报。

回测表现

年化收益率5.14%
波动率11.54%
Beta-0.019
夏普比率0.45
索提诺比率N/A
最大回撤N/A
胜率53%

完整python代码

from AlgorithmImports import *
import data_tools
from typing import List, Dict, Tuple
from datetime import datetime
# endregion

class ProfitabilityFactorInIndianStocks(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(1000000000) # INR

        self.quantile:int = 3
        self.leverage:int = 20
        self.period:int = 12 # 3 years of quarters
        self.data:Dict[Symbol, data_tools.SymbolData] = {}

        # download tickers
        ticker_file_str:str = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/nse_500_tickers.csv')
        ticker_lines:List[str] = ticker_file_str.split('\r\n')
        tickers:List[str] = [ ticker_line.split(',')[0] for ticker_line in ticker_lines[1:] ]

        for t in tickers:
            # price data subscription
            data = self.AddData(data_tools.IndiaStocks, t, Resolution.Daily)
            data.SetFeeModel(data_tools.CustomFeeModel())
            data.SetLeverage(self.leverage)
            stock_symbol:Symbol = data.Symbol

            # fundamental data subscription
            balance_sheet = self.AddData(data_tools.IndiaBalanceSheet, t, Resolution.Daily).Symbol 
            income_statement = self.AddData(data_tools.IndiaIncomeStatement, t, Resolution.Daily).Symbol

            self.data[stock_symbol] = data_tools.SymbolData(stock_symbol, balance_sheet, income_statement, self.period)
        
        self.rebalance_month:int = 10
        self.rebalance_day:int = 1

    def OnData(self, data: Slice):
        rebalance_flag:bool = False
        metric_by_symbol:Dict[Symbol, Tuple(float, float)] = {}

        price_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaStocks.get_last_update_date()
        bs_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaBalanceSheet.get_last_update_date()
        is_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaIncomeStatement.get_last_update_date()

        for symbol, symbol_data in self.data.items():
            # store price
            if data.ContainsKey(symbol) and data[symbol] and data[symbol].Value != 0:
                price:float = data[symbol].Value
                self.data[symbol].update_price(price)
            bs_symbol:Symbol = symbol_data._balance_sheet_symbol
            is_symbol:Symbol = symbol_data._income_statement_symbol

            # check if BS an IS statement is present
            if bs_symbol in data and data[bs_symbol] and is_symbol in data and data[is_symbol]:
                bs_statement:Dict = data[bs_symbol].Statement
                is_statement:Dict = data[is_symbol].Statement

                revenue_field:str = 'totalRevenue'
                cost_of_revenue_field:str = 'costOfRevenue'
                assets_field:str = 'totalAssets'
                liab_field:str = 'totalLiab'
                shares_field:str = 'commonStockSharesOutstanding'

                if revenue_field in is_statement and is_statement[revenue_field] is not None \
                    and cost_of_revenue_field in is_statement and is_statement[cost_of_revenue_field] is not None \
                    and assets_field in bs_statement and bs_statement[assets_field] is not None \
                    and liab_field in bs_statement and bs_statement[liab_field] is not None \
                    and shares_field in bs_statement and bs_statement[shares_field] is not None:
                    date:datetime.date = self.Time
                    revenue:float = float(is_statement[revenue_field])
                    cost_of_revenue:float = float(is_statement[cost_of_revenue_field])
                    assets:float = float(bs_statement[assets_field])
                    liab:float = float(bs_statement[liab_field])
                    shares:float = float(bs_statement[shares_field])
                    # store fundamentals
                    symbol_data.update_fundamentals(date, revenue, cost_of_revenue, assets, liab, shares)
            
            if self.IsWarmingUp: 
                continue

            # rebalance on first of October
            if self.Time.month == self.rebalance_month and self.Time.day == self.rebalance_day:
                rebalance_flag = True

                # fundamental data are ready and still arriving
                if self.Securities[bs_symbol].GetLastData() and bs_symbol in bs_last_update_date and self.Time.date() <= bs_last_update_date[bs_symbol] and \
                    self.Securities[is_symbol].GetLastData() and is_symbol in is_last_update_date and self.Time.date() <= is_last_update_date[is_symbol]:
                    
                    market_cap:float = symbol_data.get_marketcap()
                    if market_cap != 0:
                        revenue:Tuple[datetime.date, float, float] = symbol_data.get_revenue()
                        total_assets_liab:Tuple[datetime.date, float, float] = symbol_data.get_total_assets_liab()
                        total_revenue:List[float] = [x[1] for x in revenue if x[0].year == self.Time.year]
                        cost_revenue:List[float] = [x[2] for x in revenue if x[0].year == self.Time.year]
                        total_assets:List[float] = [x[1] for x in total_assets_liab if x[0].year == self.Time.year - 1]
                        total_liab:List[float] = [x[2] for x in total_assets_liab if x[0].year == self.Time.year - 1]
                        
                        fundamentals:List[float] = [total_revenue, cost_revenue, total_assets, total_liab]

                        if all(len(fundamental) > 0 for fundamental in fundamentals):
                            if (total_assets[0] - total_liab[0]) > 0:
                                change:float = (sum(total_revenue) - sum(cost_revenue)) / (total_assets[0] - total_liab[0])
                                metric_by_symbol[symbol] = (change, market_cap)

        if rebalance_flag:
            weights:Dict[Symbol, float] = {}

            if len(metric_by_symbol) >= self.quantile:
                # sort by profitability factor
                sorted_changes:List = sorted(metric_by_symbol.items(), key=lambda x: x[1][0], reverse=True)
                quantile: int = int(len(sorted_changes) / self.quantile)

                # get top and bottom tercile
                long_tercile:List[Symbol] = [x[0] for x in sorted_changes][:quantile]
                short_tercile:List[Symbol] = [x[0] for x in sorted_changes][-quantile:]

                # calculate weights based on marketcap
                sum_long = sum([metric_by_symbol[i][1] for i in long_tercile])
                for asset in long_tercile:
                    weights[asset] = metric_by_symbol[asset][1] / sum_long

                sum_short = sum([metric_by_symbol[i][1] for i in short_tercile])
                for asset in short_tercile:
                    weights[asset] = -metric_by_symbol[asset][1] / sum_short
            
            # liquidate and rebalance
            invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
            for symbol in invested:
                if symbol not in weights:
                    self.Liquidate(symbol)
            
            for symbol, weight in weights.items():
                if symbol in data and data[symbol]:
                    self.SetHoldings(symbol, weight)

Leave a Reply

Discover more from Quant Buffet

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

Continue reading