Quant BuffetRelax, Not Over Thinking

Investment Factor in Indian Stocks

Log in to collect

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.

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

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.

Backtest performance

Annualised return3.61%
Volatility8.89%
Beta0.017
Sharpe ratio0.41
Win rate46%

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)