Trade BSE and NSE stocks by size, going long on the smallest quintile and short on the largest, using value-weighted portfolios rebalanced monthly based on market equity logarithms.

I. STRATEGY IN A NUTSHELL

The strategy trades BSE and NSE stocks sorted by market capitalization. It goes long the smallest quintile (small-cap stocks) and short the largest quintile (large-cap stocks). Portfolios are value-weighted and rebalanced monthly.

II. ECONOMIC RATIONALE

Small-cap stocks tend to outperform large-cap stocks, reflecting a size premium. This arises because smaller firms are generally riskier, more volatile, and have greater growth potential, leading to higher expected returns. The study confirms that this size effect holds in the Indian market.

III. SOURCE PAPER

Cross-sectional Return Predictability in Indian Stock Market: An Empirical Investigation [Click to Open PDF]

Goswami, Gautam, Fordham University – Finance Area

<Abstract>

This paper provides a comprehensive analysis of stock return predictability in the Indian stock market by employing both the portfolio and cross-sectional regressions methods using the data from January 1994 and ending in December 2018. We find strong predictive power of size, cash-flow-to-price ratio, momentum and short-term-reversal, and in some cases of book-to-market-ratio, price-earnings-ratio. The total volatility, idiosyncratic volatility, and beta are not consistent stock return predictors in the Indian stock market. In cross-sectional regression analysis, size, short-term reversal, momentum, and cash-flow-to-price ratio predict the future stock returns. Overall, the two variables momentum and cash flow to price ratio demonstrate reliable forecasting power under all methods and both small and large size samples.

IV. BACKTEST PERFORMANCE

Annualised Return10.3%
Volatility25.64%
Beta-0.025
Sharpe Ratio0.4
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate50%

V. FULL PYTHON CODE

from AlgorithmImports import *
from typing import Dict, List
import data_tools
# endregion
class TheSizeEffectinIndianMarket(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2010, 1, 1)
        self.SetCash(100000000) # INR
        self.quantile:int = 5
        self.leverage:int = 5
        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.recent_month:int = -1
    def OnData(self, data: Slice) -> None:
        rebalance_flag:bool = False
        metric_by_symbol:Dict[Symbol, 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 price_symbol, symbol_data in self.data.items():
            # store price data
            if data.ContainsKey(price_symbol) and data[price_symbol] and data[price_symbol].Value != 0:
                price: float = data[price_symbol].Value
                self.data[price_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
                shares_field:str = 'commonStockSharesOutstanding'
                if shares_field in bs_statement and bs_statement[shares_field] is not None:
                    shares_outstanding:float = float(bs_statement[shares_field])
                    # store fundamentals
                    symbol_data.update_fundamentals(shares_outstanding)
            
            if self.IsWarmingUp: 
                continue
            
            # monthly rebalance
            if self.Time.month != self.recent_month or rebalance_flag:
                self.recent_month = self.Time.month
                rebalance_flag = True
                # fundamental data are ready and still arriving
                if self.Securities[price_symbol].GetLastData() and price_symbol in price_last_update_date and self.Time.date() <= price_last_update_date[price_symbol]:
                    if self.Securities[bs_symbol].GetLastData() and bs_symbol in bs_last_update_date and self.Time.date() <= bs_last_update_date[bs_symbol]:
                        shares_outstanding:float = symbol_data.get_fundamentals()
                        price:float = symbol_data.get_price()
                        # calculate market capitalization
                        if price != -1 and shares_outstanding != -1:
                            metric_by_symbol[price_symbol] = shares_outstanding * price
        if rebalance_flag:
            weights:Dict[Symbol, float] = {}
            if len(metric_by_symbol) >= self.quantile:
                # sort by market capitalization
                sorted_market_cap = sorted(metric_by_symbol, key=metric_by_symbol.get)
                quantile: int = int(len(sorted_market_cap) / self.quantile)
                # get top and bottom quintile
                long_assets:List[Symbol] = sorted_market_cap[:quantile]
                short_assets:List[Symbol] = sorted_market_cap[-quantile:]
                # calculate weights based on values
                sum_long = sum([metric_by_symbol[i] for i in long_assets])
                for asset in long_assets:
                    weights[asset] = metric_by_symbol[asset] / sum_long
                sum_short = sum([metric_by_symbol[i] for i in short_assets])
                for asset in short_assets:
                    weights[asset] = -metric_by_symbol[asset] / sum_short
            
            # liquidate and rebalance
            invested:List[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
            for price_symbol in invested:
                if price_symbol not in weights:
                    self.Liquidate(price_symbol)
            
            for price_symbol, weight in weights.items():
                if price_symbol in data and data[price_symbol]:
                    self.SetHoldings(price_symbol, weight)

Leave a Reply

Discover more from Quant Buffet

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

Continue reading