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

  1. A New Core Equity ParadigmOnline Copy