We present comprehensive evidence in support of giving liquidity equal standing to size, value/growth, and momentum as investment styles, as defined by Sharpe (1992). First, we show that financial market liquidity, as identified by stock turnover, is an economically significant indicator of long-term returns. Then, we show that liquidity, as a characteristic, is not merely a substitute for size, value, and/or momentum. Finally, we show that liquidity has historically been a relatively stable characteristic of stocks, and that changes in liquidity are associated with changes in valuations.
Keywords: liquidity, investment style, size, value, momentum, turnover, stock returns, factors, Sharpe
- Choose the largest 3,500 firms on NYSE, AMEX, and NASDAQ by market capitalization.
- Ensure the stock price per share is no less than $2.
- Maintain a minimum market capitalization threshold of $10 million for inclusion.
- Exclude REITs, ADRs, ETFs, warrants, Americus Trust Components, and closed-end funds.
- Segment these stocks into four groups based on their market capitalization.
- Focus on stocks within the smallest market capitalization group.
- Further categorize these stocks into quartiles using their past year's turnover.
- Initiate long positions in the least traded stocks.
- Begin short positions in the most frequently traded stocks.
- Annually adjust the portfolio composition (every December).
- Weight stocks equally in the portfolio.
import backtrader as bt class LowLiquidityStrategy(bt.Strategy): def __init__(self): self.rank_market_cap = None self.rank_turnover = None def next(self): if self.datetime.date().month == 12 and self.datetime.date().day == 31: # Filter stocks by market cap and per-share price filtered_stocks = [d for d in self.datas if d.close >= 2 and d.market_cap >= 10000000 and not d.is_excluded] # Sort by market cap and select top 3500 sorted_by_market_cap = sorted(filtered_stocks, key=lambda d: d.market_cap, reverse=True)[:3500] # Divide stocks into quartiles by market cap quartile_size = len(sorted_by_market_cap) // 4 lowest_market_cap_quartile = sorted_by_market_cap[-quartile_size:] # Divide lowest market cap quartile stocks into quartiles based on turnover sorted_by_turnover = sorted(lowest_market_cap_quartile, key=lambda d: d.turnover) turnover_quartile_size = len(sorted_by_turnover) // 4 lowest_turnover_quartile = sorted_by_turnover[:turnover_quartile_size] highest_turnover_quartile = sorted_by_turnover[-turnover_quartile_size:] # Go long on stocks with the lowest turnover for data in lowest_turnover_quartile: self.order_target_percent(data, target=1.0 / len(lowest_turnover_quartile)) # Go short on stocks with the highest turnover for data in highest_turnover_quartile: self.order_target_percent(data, target=-1.0 / len(highest_turnover_quartile)) # Close positions for the stocks not in the lowest or highest turnover quartile for data in self.datas: if data not in lowest_turnover_quartile and data not in highest_turnover_quartile: self.close(data) cerebro = bt.Cerebro() cerebro.addstrategy(LowLiquidityStrategy) # Add your data feeds to cerebro here cerebro.run()
Note that this code is a starting point for implementing the trading rules you provided. You will need to add your data feeds to
cerebro and ensure that the data feeds have the necessary information, such as market capitalization, turnover, and any exclusion criteria (REITs, warrants, ADRs, ETFs, Americus Trust Components, and closed-end funds). Additionally, the code assumes the data feeds are daily. You may need to adjust it if you are using a different frequency.