Backtest Period
Markets Traded
Equities
Maximum Drawdown
Period of Rebalancing
Monthly
Return (Annual)
Sharpe Ratio
Standard Deviation (Annual)
Original paper
SSRN-id1982100.pdf3171.4KB
Abstract
This article explores an alternative definition of momentum that is calculated using the idiosyncratic returns from market regressions. By removing the return component due to market beta exposure, this new definition of momentum reduces the volatility of momentum strategies and generates sizeable four-factor alphas. These results hold in a sample of 21 countries, in addition to U.S. data. Most interestingly, the findings also hold in Japan, where previous studies have failed to find any significant power for traditional momentum strategies.
Keywords:Â Momentum
Trading rules
- Target investment universe: Focus on NYSE, AMEX, NASDAQ companies that have a market capitalization greater than the 20th percentile among NYSE-listed entities (considered as large cap stocks).
- Derive the excess monthly return over the risk-free return for each of these stocks.
- Utilize a 3-year dataset to perform a CAPM analysis for every stock, using the market factor from the Kenneth French data library as the key explanatory variable.
- Compute idiosyncratic momentum: Sum up the idiosyncratic returns (which are the regression residuals) spanning an 11-month window (from month t-12 until month t-2).
- Group the stocks into five categories depending on their idiosyncratic momentum.
- Go long on top quintile stocks; go short on bottom quintile stocks.
- Equally weighted portfolio.
- Adjust the portfolio's composition on a monthly cycle.
Python code
Backtrader
import backtrader as bt
import pandas as pd
import numpy as np
import statsmodels.api as sm
from scipy.stats import percentileofscore
class IdiosyncraticMomentum(bt.Strategy):
params = dict(
momentum_period=11,
rebalance_interval=21
)
def __init__(self):
self.rf_rate = 0.02 # risk-free rate (assuming 2% annual rate)
self.rebalance_days = 0
def next(self):
if self.rebalance_days % self.params.rebalance_interval == 0:
self.rebalance_portfolio()
self.rebalance_days += 1
def rebalance_portfolio(self):
market_caps = {d: d.market_cap[0] for d in self.datas}
large_cap_threshold = np.percentile(list(market_caps.values()), 80)
large_cap_stocks = [d for d, cap in market_caps.items() if cap > large_cap_threshold]
excess_returns = {}
for d in large_cap_stocks:
adj_close = d.close_adj
monthly_return = (adj_close[0] - adj_close[-21]) / adj_close[-21]
excess_return = (monthly_return - (1 + self.rf_rate) ** (1/12) + 1) - 1
excess_returns[d] = excess_return
capm_regressions = {}
for d in large_cap_stocks:
excess_returns_stock = excess_returns[d].get(size=-36) # 3 years of data
excess_returns_market = d.market_factor.get(size=-36)
excess_returns_market = sm.add_constant(excess_returns_market)
model = sm.OLS(excess_returns_stock, excess_returns_market).fit()
residuals = model.resid
capm_regressions[d] = residuals
idiosyncratic_momentum = {}
for d in large_cap_stocks:
cum_idiosyncratic_return = np.sum(capm_regressions[d][-11])
idiosyncratic_momentum[d] = cum_idiosyncratic_return
sorted_stocks = sorted(large_cap_stocks, key=lambda x: idiosyncratic_momentum[x], reverse=True)
long_stocks = sorted_stocks[:len(sorted_stocks) // 5]
short_stocks = sorted_stocks[-len(sorted_stocks) // 5:]
for d in self.datas:
if d in long_stocks:
target_value = self.broker.getvalue() / len(long_stocks)
size = target_value / d.close[0]
self.order_target_size(target=d, size=size)
elif d in short_stocks:
target_value = -self.broker.getvalue() / len(short_stocks)
size = target_value / d.close[0]
self.order_target_size(target=d, size=size)
else:
self.order_target_size(target=d, size=0)
if __name__ == '__main__':
cerebro = bt.Cerebro()
cerebro.addstrategy(IdiosyncraticMomentum)
cerebro.broker.setcash(100000.0)
cerebro.run()