Reversal Effect in India
Log in to collectAcademic paper
Ali Fayyaz Munir; Mohd Edil Abd Sukor; Shahrin Saaid Shaharuddin
- MYUniversity of Malaya
- ?University of Malaya, Kuala Lumpur, Wilayah Persekutuan, Malaysia
https://journals.sagepub.com/doi/pdf/10.1177/21582440211068490
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
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)