The investment universe in this research consists of 4,141 firms that are or were listed on the NSE or BSE, as reported in the Worldscope India dataset with Datastream code WSCOPEIN. The data shows a steady growth in the number of firms covered annually during the period 2006-2021, increasing from 1,700 firms to around 2,500 firms.

I. STRATEGY IN A NUTSHELL

Using Worldscope India data (2006–2021), firms are sorted by asset growth from year t-2 to t-1 into Conservative, Neutral, and Aggressive portfolios, further split by size (Big/Small). Portfolios are value-weighted, formed each September, and rebalanced yearly. The investment factor is constructed by going long Conservative portfolios and short Aggressive ones.

II. ECONOMIC RATIONALE

The investment factor captures risks not explained by existing models. Conservative firms, with steadier earnings and cash flows, earn higher returns than aggressive firms, which rely on uncertain growth and carry higher risk. Including this factor improves pricing accuracy and portfolio allocation.

Four and Five-Factor Models in the Indian Equities Market [Click to Open PDF]

Rajan Raju, Invespar Pte Ltd

<Abstract>

We compute the Fama-French three- and five-factor and momentum factor returns for Indian equities between October 2006 and February 2022 using data from Refinitiv Datastream following two breakpoint schemes. We show a high correlation between our factor return estimates and those reported in the Data Library using the breakpoint scheme that closely follows the Indian Institute of Management, Ahmedabad (IIMA) Data Library for the Indian Market. In addition, we report four- and five-factor return estimates using the current breakpoint methodology of Fama-French and other international replication studies. We show the differences in the factor return estimates due to the methodology, thereby bridging the method adopted in the seminal work by IIMA and current international practice. We differ from international studies by building portfolios in September of each year to reflect the Indian fiscal reporting period, thereby providing factors that reflect the Indian circumstance. We use factor spanning tests to show that all five Fama-French and Momentum factors explain average returns in the Indian equity markets.

IV. BACKTEST PERFORMANCE

Annualised Return3.61%
Volatility8.89%
Beta0.017
Sharpe Ratio0.41
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate46%

V. FULL PYTHON CODE

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