Quant BuffetRelax, Not Over Thinking

Reversal Effect in India

Log in to collect

Academic paper

Adaptive Market Hypothesis and Time-varying Contrarian Effect: Evidence From Emerging Stock Markets of South Asia

AuthorsAli Fayyaz Munir; Mohd Edil Abd Sukor; Shahrin Saaid Shaharuddin

Institute
  • MYUniversity of Malaya
  • ?University of Malaya, Kuala Lumpur, Wilayah Persekutuan, Malaysia

Strategy in a nutshell

The investment universe comprises stocks traded on the Bombay Stock Exchange (BSE), excluding inconsistent or small-cap stocks. Monthly data are sourced from Thomson Reuters DataStream. The main variable of interest is the cumulative average return (CAR), defined as the sum of returns over the 12 months from (t–12) to (t–1). At the end of each month (t), stocks are sorted into three portfolios: winner, loser, or none. The winner portfolio includes stocks in the top 20% of prior 12-month CARs, while the loser portfolio includes stocks in the bottom 20%. The strategy goes long on the loser portfolio and short on the winner portfolio, with equal weighting, and is rebalanced monthly.

Economic rationale

The strategy’s effectiveness stems from the link between contrarian payoffs and stock market conditions. Contrarian returns are largely influenced by the state of the market, with reversal returns being stronger during negative market states, high volatility, or crisis periods. Even after controlling for standard risk factors, the relationship between contrarian profitability and changing market conditions persists. Time-varying market efficiency arises from evolving market circumstances, and in developing markets like India, investors often struggle to adapt promptly. Consequently, contrarian opportunities emerge over time, reflecting persistent weak-form market inefficiencies.

Backtest performance

Annualised return47.74%
Volatility36.21%
Beta-0.024
Sharpe ratio1.32
Win rate56%

Full Python code

from AlgorithmImports import *
from data_tools import QuantpediaIndiaStocks, SymbolData, CustomFeeModel
#endregion

class ReversalEffectInIndia(QCAlgorithm):

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

self.data:dict = {}

self.portfolio_percentage:float = 0.5
self.days_in_month:int = 21
self.period:int = 12 * self.days_in_month # 12 months of daily closes
self.SetWarmUp(self.period, Resolution.Daily)

self.quantile:int = 5
self.leverage:int = 5
self.perf_period:int = 12
self.max_missing_days:int = 5
self.symbol:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

csv_string_file = self.Download('data.quantpedia.com/backtesting_data/equity/india_stocks/india_nifty_500_tickers.csv')
line_split = csv_string_file.split(';')

# NOTE: Download method is rate-limited to 100 calls (https://github.com/QuantConnect/Documentation/issues/345)
for ticker in line_split[:99]:
    security = self.AddData(QuantpediaIndiaStocks, ticker, Resolution.Daily)
    security.SetFeeModel(CustomFeeModel())
    security.SetLeverage(self.leverage)

    symbol:Symbol = security.Symbol
    self.data[symbol] = SymbolData(self.period)

self.recent_month:int = -1

def OnData(self, data):
# store daily prices
for symbol in self.data:
    if symbol in data and data[symbol]:
        price:float = data[symbol].Value
        if price != 0 and not np.isnan(price):
            self.data[symbol].update(price)

if self.IsWarmingUp: return

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

performance:[Symbol, float] = {}

for symbol, symbol_obj in self.data.items():
    # prices are ready and data is still comming in
    if symbol_obj.is_ready() and self.Securities[symbol].GetLastData() and \
        (self.Time.date() - self.Securities[symbol].GetLastData().Time.date()).days <= self.max_missing_days:
        perf:float = self.data[symbol].performance(self.days_in_month)
        if perf != 0 and not np.isnan(perf):
            performance[symbol] = perf

long_part:list[Symbol] = []
short_part:list[Symbol] = []

if len(performance) >= self.quantile:
    quantile:int = int(len(performance) / self.quantile)
    sorted_by_performance = [x[0] for x in sorted(performance.items(), key=lambda item: item[1], reverse=True)]

    # Long the loser portfolio and short the winner portfolio
    # The winner portfolio consists of stocks with the prior 12-month CARs in the top 20%.
    short_part = sorted_by_performance[-quantile:]
    # The loser portfolio consists of stocks with the prior 12-month CARs in the bottom 20%.
    long_part = sorted_by_performance[:quantile]
        
# trade execution
long_count:int = len(long_part)
short_count:int = len(short_part)

stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
    if symbol not in long_part + short_part:
        self.Liquidate(symbol)

for symbol in long_part:
    self.SetHoldings(symbol, (1 / long_count) * self.portfolio_percentage)
for symbol in short_part:
    self.SetHoldings(symbol, (-1 / short_count) * self.portfolio_percentage)