Quant BuffetRelax, Not Over Thinking

Low Value Factor in India

Log in to collect

Academic paper

'Long' Factors, not 'Short' Change : Long Only Factor Portfolios in India

AuthorsRajan Raju; Anish Teli

Institute
  • ?Invespar Pte Ltd
  • Healthcentric Advisors
  • ?QED Capital Advisors

Strategy in a nutshell

The strategy invests in S&P BSE 200 stocks with the lowest book-to-market ratios, sorted into deciles and rebalanced monthly, focusing on growth-oriented firms.

Economic rationale

Growth stocks tend to outperform in bull markets, while value stocks perform better in downturns. The strategy exploits these cycles by tilting toward growth when conditions favor them.

Backtest performance

Annualised return16.47%
Volatility22.65%
Beta0.142
Sharpe ratio0.73
Win rate88%

Full Python code

from AlgorithmImports import *
from datetime import datetime
import data_tools
# endregion

class LowValueFactorInIndia(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2006, 1, 1)
self.SetCash(100000)

self.leverage:int = 5
self.quantile:int = 10

self.max_missing_days:int = 5

self.PB_data:dict = {}
self.data:dict[str, data_tools.SymbolData] = {}

self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

# load price to book values
csv_string_file:str = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/price_to_book.csv')
lines:list = csv_string_file.split('\r\n')
tickers_with_PB_values:list = lines[0].split(';')[1:]

for line in lines[1:]: # skip header
    if line == '':
        continue

    line_split:list = line.split(';')

    date:datetime.date = datetime.strptime(line_split[0], '%Y-%m-%d').date()
    # init dictionary for this date
    self.PB_data[date] = {}

    length = len(line_split[1:])
    for i in range(length):
        PB_value:float = float(line_split[i + 1])

        if PB_value != 0:
            ticker:str = tickers_with_PB_values[i]

            self.PB_data[date][ticker] = PB_value

# subscribe india stocks
csv_string_file:str = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/india_nifty_100_tickers.csv')
lines:list = csv_string_file.split('\r\n')

for line in lines[:99]:
    line_split:list = line.split(';')

    for ticker in line_split:
        # subscribe india stock
        security:Security = self.AddData(data_tools.QuantpediaIndiaStocks, ticker, Resolution.Daily)
        security.SetFeeModel(data_tools.CustomFeeModel())
        security.SetLeverage(self.leverage)

        india_stock_symbol:Symbol = security.Symbol

        self.data[ticker] = data_tools.SymbolData(india_stock_symbol)

self.selection_flag = False
self.Schedule.On(self.DateRules.MonthEnd(self.market), self.TimeRules.BeforeMarketClose(self.market), self.Selection)

def OnData(self, data: Slice):
curr_date:datetime.date = self.Time.date()

if curr_date in self.PB_data:
    PB_values:dict[ticker, float] = self.PB_data[curr_date] 
    
    for ticker, PB_value in PB_values.items():
        self.data[ticker].update_PB_value(PB_value)

# rebalance monthly
if not self.selection_flag:
    return
self.selection_flag = False

book_to_market:dict[Symbol, float] = {}
all_book_to_markets:list = []

for _, symbol_obj in self.data.items():
    if not symbol_obj.is_ready():
        continue
    
    india_stock_symbol:Symbol = symbol_obj.india_stock_symbol
    if self.Securities[india_stock_symbol].GetLastData() and (self.Time.date() - \
        self.Securities[india_stock_symbol].GetLastData().Time.date()).days < self.max_missing_days:
        curr_PB_value:float = symbol_obj.PB_value
        book_to_market_value:float = 1 / curr_PB_value
        all_book_to_markets.append(book_to_market_value)
        book_to_market[india_stock_symbol] = book_to_market_value

    # clear stocks PB_value for last month
    symbol_obj.clear_PB_value()

# make sure there are enough stocks for selection
if len(book_to_market) <= self.quantile:
    self.Liquidate()
    return

# z-score calculation
z_score:dict[Symbol, float] = {}
mean_book_to_markets:float = np.mean(all_book_to_markets)
std_book_to_markets:float = np.std(all_book_to_markets)

# z-score transform
z_score:dict[Symbol, float] = {symbol:((value - mean_book_to_markets) / std_book_to_markets) for symbol, value in book_to_market.items()}
z_score_transform:dict[Symbol, float] = {symbol : (1+z) if z >= 0 else (1/ (1-z)) for symbol, z in z_score.items()}

quantile:int = int(len(z_score_transform) / self.quantile)
sorted_by_z_score:list[Symbol] = [x[0] for x in sorted(z_score_transform.items(), key=lambda item: item[1])]

# Buy the bottom decile (stocks with the lowest book-to-market ratios)
long_part:list[Symbol] = sorted_by_z_score[:quantile]
long_part_length:int = len(long_part)

# trade execution
invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in invested:
    if symbol not in long_part:
        self.Liquidate(symbol)

for symbol in long_part:
    self.SetHoldings(symbol, 1 / long_part_length)

def Selection(self):
self.selection_flag = True