Betting Against Beta in India
Log in to collectAcademic paper
'Long' Factors, not 'Short' Change : Long Only Factor Portfolios in India
Rajan Raju; Anish Teli
- ?Invespar Pte Ltd
- Healthcentric Advisors
- ?QED Capital Advisors
Strategy in a nutshell
This strategy targets S&P BSE 200 stocks, ranks them by adjusted beta scores, and builds long positions in low-beta equities. Portfolios are rebalanced monthly to capture the low-risk anomaly.
Economic rationale
Empirical evidence shows high-beta stocks underperform while low-beta stocks deliver superior risk-adjusted returns. Constraints on leverage explain this anomaly, making “Betting Against Beta” a profitable and robust factor strategy.
Backtest performance
Annualised return18.88%
Volatility16.02%
Beta0.249
Sharpe ratio1.18
Win rate51%
Full Python code
from AlgorithmImports import *
import numpy as np
from scipy import stats
import data_tools
#endregion
class BettingAgainstBetainIndia(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2005, 1, 1)
self.SetCash(100000)
self.data:dict = {}
self.long_term_period:int = 5 * 12 * 21
self.short_term_period:int = 1 * 12 * 21
self.SetWarmUp(self.short_term_period, Resolution.Daily)
self.quantile:int = 5
self.max_missing_days:int = 5
self.market = self.AddData(data_tools.BSE_200, 'BSE_200', Resolution.Daily).Symbol
self.data[self.market] = data_tools.SymbolData(self.short_term_period)
csv_string_file = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/india_nifty_100_tickers.csv')
line_split = csv_string_file.split(';')
# NOTE: Download method is rate-limited to 100 calls (https://github.com/QuantConnect/Documentation/issues/345)
self.ticker:list[str] = line_split[:99]
for ticker in self.ticker:
security = self.AddData(data_tools.QuantpediaIndiaStocks, ticker, Resolution.Daily)
security.SetFeeModel(data_tools.CustomFeeModel())
security.SetLeverage(5)
self.data[ticker] = data_tools.SymbolData(self.short_term_period)
self.recent_month:int = -1
def OnData(self, data):
rebalance_flag:bool = False
# rebalance once a month
if not self.IsWarmingUp and self.Time.month != self.recent_month:
rebalance_flag = True
self.recent_month = self.Time.month
beta:dict[str, float] = {}
# store daily price data
if self.market in data and data[self.market]:
# market price data
self.data[self.market].update(data[self.market].Value)
# stock price data
for ticker in self.ticker:
if ticker in data and data[ticker]:
self.data[ticker].update(data[ticker].Value)
else:
if self.data[ticker].closes.Count != 0:
self.data[ticker].update(self.data[ticker].closes[0])
if self.IsWarmingUp: continue
if rebalance_flag:
if self.data[ticker].is_ready() and self.data[self.market].is_ready():
# stock price data is still comming in
if self.Securities[ticker].GetLastData() and (self.Time.date() - self.Securities[ticker].GetLastData().Time.date()).days < self.max_missing_days:
# market and stock returns
market_prices:np.ndarray = np.array([x for x in self.data[self.market].closes])
market_returns:np.ndarray = market_prices[:-1] / market_prices[1:] - 1
market_returns = np.nan_to_num(market_returns, nan=0.)
stock_prices:np.ndarray = np.array([x for x in self.data[ticker].closes])
stock_returns:np.ndarray = stock_prices[:-1] / stock_prices[1:] - 1
stock_returns = np.nan_to_num(stock_returns, nan=0.)
# NOTE source paper version of beta estimate
slope, intercept, r_value, p_value, std_err = stats.linregress(market_returns, stock_returns)
beta[ticker] = slope
if rebalance_flag:
# z-score normalization
beta_values:list[float] = [beta for ticker, beta in beta.items()]
beta_std:float = np.std(beta_values)
beta_mean:float = np.mean(beta_values)
z_score:dict = { ticker: (beta - beta_mean) / beta_std for ticker, beta in beta.items()}
z_transform:dict = { ticker : (1+z) if z>= 0 else (1/(1-z)) for ticker, z in z_score.items()}
long:list[Symbol] = []
short:list[Symbol] = []
if len(z_transform) >= self.quantile:
quantile:int = int(len(z_transform) / self.quantile)
sorted_by_zscore = sorted(z_transform.items(), key=lambda item: item[1], reverse=True)
long = [x[0] for x in sorted_by_zscore[:quantile]]
short = [x[0] for x in sorted_by_zscore[-quantile:]]
# trade execution
long_count:int = len(long)
short_count:int = len(short)
stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
if symbol not in long + short:
self.Liquidate(symbol)
for symbol in long:
self.SetHoldings(symbol, 1 / long_count)
for symbol in short:
self.SetHoldings(symbol, -1 / short_count)