Introduction
In this tutorial we implemented a long/short Equity strategy based on fundamental factors. The idea comes from AQR white book: A New Core Equity Paradigm. The original version is a long only strategy. We developed it into a long/short version. The paper strategy used some fundamental data as measures of value, quality and momentum, and then ranked all the stocks in the universe according to the factors. The strategy only long the stocks ranking at the top, but our algorithm would at the same time short the stocks ranking at the bottom. This strategy consistently beats the market and has solid economic intuition.
Factors
The paper strategy used three factors together to rank stocks: value, quality and momentum.
Value: The most commonly used measure for value is P/B ratio (price-to-book value). Intuitively, the stocks with high P/B ratio are likely to be overpriced, and those stocks are labeled as growth stock. On the other hand, the stocks with low P/B value are considered to be value stocks. Following this logic, we use book value per share as a measure for value in our algorithm: the stocks with high book value per share rank high.
Quality: Quality is a comprehensive factor. The paper used total profits over asset, gross margins, and free cash flow over assets. For simplicity, we used only operation margin as our quality factor. Here we assume that the companies with high operation margin are profitable, and their stocks are the quality ones.
Momentum: The paper strategy is quarterly rebalanced, so it used recent one-year return, three-month and returns around earning events as measures for momentum. While our algorithm is monthly rebalanced, we simply use recent monthly return as our momentum factor.
Ranking
Ranking is the core process for stock selection. We first rank all the stocks according to each factor, then assign weights to each factor to get the final rank. Specifically, in our algorithm we have 250 stocks in total. We rank them according to their book values per share, operation margins and one-month returns by descending order. For each stock, its index in each sorted list is its score on each factor. e.g. If stock A ranks 1st by value, 10th by quality and 30th by momentum, its scores on the value, quality and momentum are 1, 10 and 30 respectively.
The last step is to calculate the final score of each stock. We use the same weight as the paper does: 40% on value, 40% on quality and 20% on momentum. In this way, stock A's final score is \(1*0.4 + 10*0.4 + 30*0.2 = 10.4\). Finally, we can rank all the stocks by their scores by ascending order. It worth attention that the stock with the lowest score is the best and it ranks 1st, and the one with the highest score is the worst.
Implementation
In this implementation, the FineSelectionFunction would be the core part because we have to rank the stocks in this process. We also need a Scheduled Event handler to rebalance the portfolio every month. We would introduce the process step by step.
CoarseSelectionFunction
This function is a filter for the whole asset universe (around 8,000 stocks). We select the top 250 with highest dollar volume to ensure liquidity. We also filter out the stocks without fundamental information or with a too low price (less than $5).
def coarse_selection_function(self, coarse):
# if the rebalance flag is not 1, return null list to save time.
if self.reb != 1:
return return self.long + self.short
# make universe selection once a month
# drop stocks which have no fundamental data or have too low prices
selected = [x for x in coarse if (x.has_fundamental_data)
and (float(x.price) > 5)]
sorted_by_dollar_volume = sorted(selected, key=lambda x: x.dollar_volume, reverse=True)
top = sorted_by_dollar_volume[:self.num_coarse]
return [i.symbol for i in top]
FineSelectionFunction
Here is the core function. The process is that we make three sorted list to store the stocks, and then use a dictionary to store the score information. For the dictionary, the keys are Symbol objects and the values are their scores. Finally we sort the dictionary to get the final rank. we store the top 20 stocks to long in the list self.long
and the bottom 20 stocks to short in the list self.short
.
def fine_selection_function(self, fine):
# return the same symbol list if it's not time to rebalance
if self.reb != 1:
return self.long+self.short
self.reb = 0
# drop stocks which don't have the information we need.
# you can try replacing those factor with your own factors here
filtered_fine = [x for x in fine if x.operation_ratios.operation_margin.value
and x.valuation_ratios.price_change1_m
and x.valuation_ratios.book_value_per_share]
self.log('remained to select %d'%(len(filtered_fine)))
# rank stocks by three factor.
sorted_byfactor1 = sorted(filtered_fine, key=lambda x: x.operation_ratios.operation_margin.value, reverse=True)
sorted_byfactor2 = sorted(filtered_fine, key=lambda x: x.valuation_ratios.price_change1_m, reverse=True)
sorted_byfactor3 = sorted(filtered_fine, key=lambda x: x.valuation_ratios.book_value_per_share, reverse=True)
stock_dict = {}
# assign a score to each stock, you can also change the rule of scoring here.
for i,ele in enumerate(sorted_byfactor1):
rank1 = i
rank2 = sorted_byfactor2.index(ele)
rank3 = sorted_byfactor3.index(ele)
score = sum([rank1*0.2,rank2*0.4,rank3*0.4])
stock_dict[ele] = score
# sort the stocks by their scores
self.sorted_stock = sorted(stock_dict.items(), key=lambda d:d[1],reverse=False)
sorted_symbol = [x[0] for x in self.sorted_stock]
# sort the top stocks into the long_list and the bottom ones into the short_list
self.long = [x.symbol for x in sorted_symbol[:self.num_fine]]
self.short = [x.symbol for x in sorted_symbol[-self.num_fine:]]
return self.long+self.short
Rebalance
Our portfolio is rebalanced monthly, so first of all we should write a Scheduled Event handler in the Initialize function:
def initialize(self):
# Use SPY as a benchmark for market open
self.schedule.on(self.date_rules.month_start(self.spy), self.time_rules.after_market_open(self.spy,5), Action(self.rebalance))
# Our rebalanced method is straightforward: We first liquidate the stocks that are no longer in the long/short list, and then assign equal weight to the stocks we are going to long or short.
def rebalance(self):
# if this month the stock are not going to be long/short, liquidate it.
long_short_list = self.long + self.short
for i in self.portfolio.values:
if (i.invested) and (i.symbol not in long_short_list):
self.liquidate(i.symbol)
# Assign each stock equally. Always hold 10% cash to avoid margin call
for i in self.long:
self.set_holdings(i,0.9/self.num_fine)
for i in self.short:
self.set_holdings(i,-0.9/self.num_fine)
Summary
The strategy was backtest at a 60-year timespan. It's persistent, systematic, and intuitive. Although our version is very different from the paper one, the logic and intuition behind are the same. It has a better performance than the paper strategy because we added short positions. For further development, we can try to rank a large number of stocks, and do some portfolio optimization instead of holding stocks equally.
Reference
- A New Core Equity ParadigmOnline Copy
Derek Melchin
See the attached backtest for an updated version of the algorithm with the following changes:
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Jing Wu
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!