Backtest Period
Markets Traded
Equities
Maximum Drawdown
Period of Rebalancing
6 Months
Return (Annual)
Sharpe Ratio
Standard Deviation (Annual)
Original paper
SSRN-id3346289.pdf688.3KB
Trading rules
- Investment universe: NYSE and AMEX common stocks (share codes 10 and 11)
- Exclude stocks priced less than $5 at the beginning of the holding period
- Sort stocks into three groups based on bid-ask spread; focus on the lowest spread group for liquidity
- Combine past returns momentum and I/A ratios to construct the InvMom strategy
- Momentum: based on past six-month returns (from month -5 to month 0)
- I/A ratio: annual change in gross property, plant, and equipment + annual change in inventories / lagged book value of assets
- Sort stocks into quintiles based on momentum and I/A ratios
- Double-sorting generates 5x5 momentum-investment portfolio combinations
- Take long position in winner portfolio with low investment; short position in loser portfolio with high investment
- Hold positions for the next six months (from month 2 to month 7), skipping the most recent month
- Equally weighted strategy, rebalanced every 6 months
Python code
Backtrader
import backtrader as bt
import pandas as pd
import numpy as np
class InvMom(bt.Strategy):
params = (
('lookback', 5),
('hold_period', 6),
)
def __init__(self):
self.inv_mom_data = dict()
def next(self):
if len(self.datas) < self.p.lookback + self.p.hold_period + 1:
return
if self.datetime.date(0).month != self.datetime.date(-1).month:
self.calculate_inv_mom()
for data, (position, weight) in self.inv_mom_data.items():
if position == 'long':
self.order_target_percent(data, target=weight)
elif position == 'short':
self.order_target_percent(data, target=-weight)
def calculate_inv_mom(self):
stocks_data = []
for data in self.datas:
if data.close[0] > 5 and data.share_code in (10, 11):
bid_ask_spread = data.bid[0] - data.ask[0]
momentum = data.close[0] / data.close[-self.p.lookback] - 1
ia_ratio = (data.gross_property_change[0] + data.inventory_change[0]) / data.book_value_assets[0]
stocks_data.append({
'data': data,
'bid_ask_spread': bid_ask_spread,
'momentum': momentum,
'ia_ratio': ia_ratio,
})
sorted_stocks = sorted(stocks_data, key=lambda x: x['bid_ask_spread'])
lowest_spread_group = sorted_stocks[:len(sorted_stocks) // 3]
momentum_quintiles = pd.qcut([stock['momentum'] for stock in lowest_spread_group], 5, labels=False)
ia_quintiles = pd.qcut([stock['ia_ratio'] for stock in lowest_spread_group], 5, labels=False)
long_portfolio = []
short_portfolio = []
for i, stock in enumerate(lowest_spread_group):
if momentum_quintiles[i] == 4 and ia_quintiles[i] == 0:
long_portfolio.append(stock['data'])
elif momentum_quintiles[i] == 0 and ia_quintiles[i] == 4:
short_portfolio.append(stock['data'])
long_weight = 1 / len(long_portfolio)
short_weight = 1 / len(short_portfolio)
for stock_data in lowest_spread_group:
data = stock_data['data']
if data in long_portfolio:
self.inv_mom_data[data] = ('long', long_weight)
elif data in short_portfolio:
self.inv_mom_data[data] = ('short', short_weight)
else:
self.inv_mom_data[data] = (None, 0)
if __name__ == '__main__':
cerebro = bt.Cerebro()
# Add stocks datafeeds to cerebro here
cerebro.addstrategy(InvMom)
cerebro.run()