Quant BuffetRelax, Not Over Thinking

Low-Risk Anomaly in India

Log in to collect

Academic paper

Strategy in a nutshell

The strategy invests in Indian equities, overweighting low-risk, large-cap stocks and underweighting high-risk ones. Stocks are double-sorted by size and 3-year realized volatility, forming a beta-neutral long-short portfolio that is rebalanced monthly.

Economic rationale

Low-risk anomalies persist across different metrics and time periods. The approach captures statistically significant alpha by exploiting the tendency for low-volatility stocks to outperform, while beta-neutral factors ensure robust, market-independent performance.

Backtest performance

Annualised return42.63%
Volatility33.56%
Beta0.03
Sharpe ratio1.27
Win rate56%

Full Python code

from AlgorithmImports import *
from typing import List, Dict
import data_tools
import statsmodels.api as sm
import numpy as np
# endregion

class LowRiskAnomalyinIndia(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(10000000) # INR

self.price_period:int = 36 * 21

self.data:Dict[Symbol, data_tools.SymbolData] = {}
self.tickers_to_ignore:List[str] = ['TATAMTRDVR', 'LODHA']

ticker_file_str:str = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/nse_100_tickers.csv')
ticker_lines:List[str] = ticker_file_str.split('\r\n')
tickers = [ ticker_line.split(',')[0] for ticker_line in ticker_lines[1:] ]

self.quantile:int = 3
self.leverage:int = 5
self.beta_p_target:float = 1.
self.leverage_cap:float = 3.

for t in tickers:
    # price data subscription
    if t in self.tickers_to_ignore:
        continue
    data:Security = self.AddData(data_tools.IndiaStocks, t, Resolution.Daily)
    data.SetFeeModel(data_tools.CustomFeeModel())
    data.SetLeverage(self.leverage)
    stock_symbol:Symbol = data.Symbol

    self.data[stock_symbol] = data_tools.SymbolData(stock_symbol, self.price_period)

self.recent_month:int = -1

def OnData(self, data: Slice) -> None:
price_last_update_date:Dict[Symbol, datetime.date] = data_tools.IndiaStocks.get_last_update_date()

# custom data still comming in
if all([self.Securities[x].GetLastData() for x in list(self.data.keys())]) and any([self.Time.date() >= price_last_update_date[x] for x in price_last_update_date]):
    self.Liquidate()
    return

# store daily price data
for price_symbol, symbol_data in self.data.items():
    if price_symbol in data and data[price_symbol] and data[price_symbol].Value != 0:
        price:float = data[price_symbol].Value
        self.data[price_symbol].update_price(price)

# montly rebalance
if self.Time.month == self.recent_month:
    return
self.recent_month = self.Time.month

volatility_by_symbol:Dict[Symbol, float] = {symbol: symbol_data.get_volatility() for symbol, symbol_data in self.data.items() if symbol_data.prices_ready()}

asset_returns_dict:Dict[Symbol, np.ndarray] = {symbol : symbol_data.get_returns() for symbol, symbol_data in self.data.items() if symbol_data.prices_ready()}

asset_returns:List[float] = list(zip(*[[i for i in x] for x in asset_returns_dict.values()]))
market_returns:List[float] = [np.average(x) for x in asset_returns]

if len(asset_returns) == 0:
    return

# run stock regression
x:np.ndarray = np.array(market_returns)
y:np.ndarray = np.array(asset_returns)
model = self.multiple_linear_regression(x, y)
beta_values:np.ndarray = model.params[1]

# store betas
beta_by_symbol:Dict[Symbol, float] = {sym : beta_values[n] for n, sym in enumerate(list(asset_returns_dict.keys()))}

if len(volatility_by_symbol) < self.quantile:
    return

# sort by volatility and divide to quantiles
sorted_volatility:List[Symbol] = sorted(volatility_by_symbol, key=volatility_by_symbol.get)
quantile:int = len(volatility_by_symbol) // self.quantile
long:List[Symbol] = sorted_volatility[:quantile]
short:List[Symbol] = sorted_volatility[-quantile:]

# beta neutral portfolio
long_symbol_w:float = 1. / len(long)
short_symbol_w:float = 1. / len(short)
long_p_beta:float = sum([beta_by_symbol[x] * long_symbol_w for x in long if x in beta_by_symbol])
short_p_beta:float = sum([beta_by_symbol[x] * short_symbol_w for x in short if x in beta_by_symbol])
long_leverage:float = min(self.beta_p_target / long_p_beta, self.leverage_cap)
short_leverage:float = min(self.beta_p_target / short_p_beta, self.leverage_cap)

# 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 + short:
        self.Liquidate(symbol)

for symbol in long:
    if symbol in data and data[symbol]:
        self.SetHoldings(symbol, long_symbol_w * long_leverage)

for symbol in short:
    if symbol in data and data[symbol]:
        self.SetHoldings(symbol, -short_symbol_w * short_leverage)

def multiple_linear_regression(self, x:np.ndarray, y:np.ndarray):
x = sm.add_constant(x, has_constant='add')
result = sm.OLS(endog=y, exog=x).fit()
return result