Introduction

The momentum effect states that what was strongly going up in the near past will probably continue to go up shortly. It is one of the most used trading anomalies, but the strategy using only the momentum can suffer significant drawdowns sometimes.

Some research papers show that the return of momentum strategies depend on the overall market conditions. This state of the market can be defined in various ways like the investors' sentiment, prior market returns and so on. Therefore, this algorithm will combine the momentum effect with the market state filter to turn off the momentum trading in down market state times.

Method

As we know, a stock market index tracks the price changes of a select group of stocks and compiles those stock price changes into a single value. For example, S&P500 is composed of only 500 large-cap stocks. A broad market index is characterized by including stocks from companies of all sizes(large, mid and small-cap based on their values). The most popular U.S. broad market indexes include the Russell 3000, the Wilshire 5000 Total Market Index and the MSCI U.S. Broad Market Index. Those broad-based market indexes attempt to cover the entire market and their return can be a good benchmark of the current market state.

class MomentumandStateofMarkeFiltersAlgorithm(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2011, 1, 1)
        self.set_end_date(2018, 8, 1)
        self.set_cash(100000)
        # add Wilshire 5000 Total Market Index data
        self.wilshire_symbol = self.add_data(Fred, Fred.wilshire.price5000, Resolution.DAILY).symbol
        self.w5000_return = self.ROC(self.wilshire_symbol, 252)
        # initialize the RateOfChange indicator of Wilshire 5000 total market index
        history = self.history(self.wilshire_symbol, 500, Resolution.DAILY)
        for tuple in history.loc[self.wilshire_symbol].itertuples():
            self.w5000_return.update(tuple.index, tuple.value)

In this algorithm, we choose the Wilshire 5000 Total Market Index to be the market state measure. For the period of index return, longer horizons should capture more dramatic changes in the state of the market, but longer horizons also reduce the number of observations of changes in the market's state. Here we choose 12 months return according to the paper Market States and Momentum from Guttierez, Cooper and Hameed. The daily index price comes from the US Federal Reserve (FRED) dataset.

The investment universe contains all stocks on NYSE and NASDAQ with a price higher than $1. We use the momentum percent indicator to gauge the momentum effect. In the CoarseSelectionFunction, the MOMP indicator value for each Symbol in coarse is updated with the adjusted price and saved in the dictionary self.momp.

def coarse_selection_function(self, coarse):
    coarse = [x for x in coarse if (x.has_fundamental_data and x.adjusted_price > 1)]
    for i in coarse:
        if i.symbol not in self.momp:
            self.momp[i.symbol] = SymbolData(i.symbol, self.lookback, self)
        else:
            self.momp[i.symbol].MOMP.update(self.time, i.adjusted_price)

When the indicator is ready, stocks are then sorted based on the previous six months momentum percent value. Top 20 Stocks with the highest MOMP are in the long stock list, 20 Stocks with the lowest MOMP are in the short stock list.

self.m_o_m_p_ready = {symbol: SymbolData for symbol, SymbolData in self.momp.items() if SymbolData.MOMP.is_ready}
if self.m_o_m_p_ready:
    # sort stocks by 6 months' momentum
    sort_by_m_o_m_p = sorted(self.m_o_m_p_ready, key = lambda x: self.m_o_m_p_ready[x].MOMP.current.value, reverse = True)
    self.long = sort_by_m_o_m_p[:20]
    self.short = sort_by_m_o_m_p[-20:]
    return self.long+self.short

The strategy is rebalanced monthly. At the beginning of each month, we identify the state of the market. If the market’s one-year return is positive, we define the state of the market as "UP" otherwise the state is "DOWN". When the market is in "UP" state, we go long on the previous six-month winners (highest Momentum) and goes short on the last six-month losers (lowest Momentum). Stocks are equally weighted. If the market is in "DOWN" state, we liquidate all asset holdings and invest in the long-term Treasury bond ETF to control the downside risk.

def on_data(self, data):
    if self.month_start and self.selection: 
        self.month_start = False
        self.selection = False
        if self.long is None or self.short is None: return
        # if the previous 12 months return on the broad equity market was positive
        if self.w5000_return.current.value > 0: 
            stocks_invested = [x.key for x in self.portfolio if x.value.INVESTED]
            for i in stocks_invested:
                if i not in self.long+self.short:
                    self.liquidate(i)

            short = [symbol for symbol in self.short if symbol in data.bars]
            short_weight = 0.5/len(short)
            # goes short on the prior six-month losers (lowest decile) 
            for short_symbol in short:
                self.set_holdings(short_symbol, -short_weight)

            # goes long on the prior six-month winners (highest decile)
            long = [symbol for symbol in self.long if symbol in data.bars]
            long_weight = 0.5/len(long)
            for long_symbol in long:
                self.set_holdings(long_symbol, long_weight)
        else:
            self.liquidate()
            self.set_holdings(self.tlt, 1)


Reference

  1. Quantpedia Premium - Momentum and State of Market (Sentiment) Filters