
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.
ASSET CLASS: stocks | REGION: India | FREQUENCY:
Yearly | MARKET: equities | KEYWORD: Investment Factor, Indian Stocks
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 Return | 3.61% |
| Volatility | 8.89% |
| Beta | 0.017 |
| Sharpe Ratio | 0.41 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 46% |
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)