本研究的投资范围包括4,141家在NSE或BSE上市的公司,数据来源于Worldscope India数据库,覆盖公司数量从2006年的1,700家增加至2021年的2,500家。投资组合基于从t-2年到t-1年财政年度结束的总资产变动,计算出保守、弱势和中性三个投资组合。构建六个双排序投资组合,包括大/保守、大小盘积极等,每年9月30日重新平衡,按市值加权。投资因子组合通过做多大小盘保守组合,做空大小盘积极组合。

策略概述

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

投资组合基于从t-2年财政年度结束到t-1年财政年度结束的总资产变动来创建,并以t-2年9月底的总资产为分母计算比例。这创建了三个投资组合,分别称为保守、弱势和中性。六个双排序的投资组合分别为大/保守(BC)、大/中性(BNI)、大/积极(BA)及其小盘对应组合SC、SNI和SA。

投资组合每年9月30日构建,并持有到下一年9月30日。投资因子投资组合通过做多大小盘保守组合,并做空大小盘积极组合来构建。股票按市值加权,最终投资组合按年度重新平衡。

策略合理性

投资因子被添加到四因子或五因子模型中,因为它捕捉了其他因子无法完全解释的重要风险因素。具体而言,投资因子旨在捕捉保守投资的公司(即资本支出、研发等投资较少的公司)相比于积极投资的公司赚取更高回报的倾向。

这一风险因素很重要,因为积极投资的公司通常风险较大,依赖于不确定的未来增长前景。相反,保守投资的公司收益和现金流更为可预测,可能不太容易受到经济下滑或其他外部冲击的影响。

因此,通过将投资因子纳入资产定价模型,投资者可以更好地解释不同投资策略公司相关的风险,并通过倾向于保守投资的公司获得更高的回报。

论文来源

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

<摘要>

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

回测表现

年化收益率3.61%
波动率8.89%
Beta0.017
夏普比率0.41
索提诺比率N/A
最大回撤N/A
胜率46%

完整python代码

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

class InvestmentFactorInIndianStocks(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(1e9) # 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 

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

    def OnData(self, data: Slice) -> None:
        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()

        for symbol, symbol_data in self.data.items():
            # store price data
            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

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

                assets_field:str = 'totalAssets'
                shares_field:str = 'commonStockSharesOutstanding'
                if assets_field in bs_statement and bs_statement[assets_field] is not None \
                    and shares_field in bs_statement and bs_statement[shares_field] is not None:
                    date:datetime.date = self.Time
                    total_assets:float = float(bs_statement[assets_field])
                    shares:float = float(bs_statement[shares_field])
                    # store fundamentals
                    symbol_data.update_fundamentals(date, total_assets, 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]:
                    total_assets:Tuple[datetime.date, float] = symbol_data.get_total_assets()
                    market_cap:float = symbol_data.get_marketcap()
                    if market_cap != 0:
                        total_assets_t2:List[float] = [x[1] for x in total_assets if x[0].year == self.Time.year - 2]
                        total_assets_t1:List[float] = [x[1] for x in total_assets if x[0].year == self.Time.year - 1]
                        
                        if len(total_assets_t2) > 0 and len(total_assets_t1) > 0:
                            change:float = (total_assets_t1[0] / total_assets_t2[0]) - 1
                            metric_by_symbol[symbol] = (change, market_cap)

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

            if len(metric_by_symbol) >= self.quantile:
                # sort by investment 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