Quantlandian

New Strategy - Presenting the “Quality Companies in an Uptrend” Model

Copyright reserved by Chris Cain - posted Nov 23, 2019 in Quantopian

We wanted to share with the Quantopian community an algorithm named “Quality Companies in an Uptrend”.

This non-optimized, long-only strategy has produced returns of 18.0% using the Q500US universe since 2003 with a Sharpe Ratio of 1.05 and 12% of Alpha and a Beta of 0.53.

We’d appreciate your input and feedback on the strategy.

Combining Quality With Momentum

This is a “quantamental” strategy, combining both fundamental factors (in this case, the quality factor) with technical factors (in this case, the cross-sectional momentum factor) in a quantitative, rules-based way.

The idea of this strategy is to first identify high-quality companies then tactically rotate into the high-quality companies with the best momentum.

What is Quality?

The characteristics of “quality” companies are rather broad. Quality is typically defined as companies that have some combination of:

  • stable earnings

  • strong balance sheets (low debt)

  • high profitability

  • high earnings growth

  • high margins.

How Will We Measure Quality?

For our strategy, we focus on companies with a high return on equity (ROE) ratio.

ROE is calculated by dividing the net income of a company by the average shareholder equity. Higher ROE companies indicate higher quality stocks. High ROE companies have historically produced strong returns.

Rules for The “Quality Companies in an Uptrend” Strategy:

  1. Universe = Q500US

  2. Quality (ROE) Filter. We then take the 50 stocks (top decile) with the highest ROE. This is our quality screen, we are now left with 50 high-
    quality stocks.

  3. Quality Stocks With Strong Momentum. We then buy the 20 stocks (of our 50 quality stocks) with the strongest relative momentum, skipping the last 10 days (to account for mean reversion over this shorter time frame).

  4. Trend Following Regime Filter. We only enter new positions if the trailing 6-month total return for the S&P 500 is positive. This is measured by the trailing 6-month total return of “SPY”.

  5. This strategy is rebalanced once a month, at the end of the month. We sell any stocks we currently hold that are no longer in our high ROE/high momentum list and replace them with stocks that have since made the list. We only enter new long positions if the trend-following regime filter is passed (SPY’s 6-month momentum is positive).

  6. Any cash not allocated to stocks gets allocated the IEF (7-10yr US Treasuries)

Potential Improvements?

What potential improvements do you think we can add to this strategy?

Some of our ideas include:

  • A composite to measure Quality, not just ROE

  • Adding a value component

  • Another way to measure momentum?

  • A better/different trend following filter?

We’d love to see what you guys come up with. Given the simple nature of this strategy, the performance is strong over the last 16+ years and should provide a good base for further testing.

Christopher Cain, CMT & Larry Connors
Connors Research LLC

import quantopian.algorithm as algo
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.filters import Q500US

def initialize(context):
    
    #set_slippage(slippage.FixedSlippage(spread = 0.0)) 
    algo.attach_pipeline(make_pipeline(), 'pipeline')    
    
    #Schedule Functions
    schedule_function(trade, date_rules.month_end() , time_rules.market_close(minutes=30))
    schedule_function(trade_bonds, date_rules.month_end(), time_rules.market_close(minutes=20))
    
    #This is for the trend following filter
    context.spy = sid(8554)
    context.TF_filter = False
    context.TF_lookback = 126
    
    #Set number of securities to buy and bonds fund (when we are out of stocks)
    context.Target_securities_to_buy = 20.0
    context.bonds = sid(23870)
    
    #Other parameters
    context.top_n_roe_to_buy = 50 #First sort by ROE
    context.relative_momentum_lookback = 126 #Momentum lookback
    context.momentum_skip_days = 10
    context.top_n_relative_momentum_to_buy = 20 #Number to buy
    
 
def make_pipeline():

    # Base universe set to the Q500US
    universe = Q500US()

    roe = Fundamentals.roe.latest

    pipe = Pipeline(columns={'roe': roe},screen=universe)
    return pipe

def before_trading_start(context, data):
    
    context.output = algo.pipeline_output('pipeline')
    context.security_list = context.output.index
        
def trade(context, data):

    ############Trend Following Regime Filter############
    TF_hist = data.history(context.spy , "close", 140, "1d")
    TF_check = TF_hist.pct_change(context.TF_lookback).iloc[-1]

    if TF_check > 0.0:
        context.TF_filter = True
    else:
        context.TF_filter = False
    ############Trend Following Regime Filter End############
    
    #DataFrame of Prices for our 500 stocks
    prices = data.history(context.security_list,"close", 180, "1d")      
    #DF here is the output of our pipeline, contains 500 rows (for 500 stocks) and one column - ROE
    df = context.output  
    
    #Grab top 50 stocks with best ROE
    top_n_roe = df['roe'].nlargest(context.top_n_roe_to_buy)
    #Calculate the momentum of our top ROE stocks   
    quality_momentum = prices[top_n_roe.index][:-context.momentum_skip_days].pct_change(context.relative_momentum_lookback).iloc[-1]
    #Grab stocks with best momentum    
    top_n_by_momentum = quality_momentum.nlargest(context.top_n_relative_momentum_to_buy)
           
    for x in context.portfolio.positions:
        if (x.sid == context.bonds):
            pass
        elif x not in top_n_by_momentum:
            order_target_percent(x, 0)
            print('GETTING OUT OF',x)
    
    for x in top_n_by_momentum.index:
        if x not in context.portfolio.positions and context.TF_filter==True:
            order_target_percent(x, (1.0 / context.Target_securities_to_buy))
            print('GETTING IN',x)
            

            
            
def trade_bonds(context , data):
    amount_of_current_positions=0
    if context.portfolio.positions[context.bonds].amount == 0:
        amount_of_current_positions = len(context.portfolio.positions)
    if context.portfolio.positions[context.bonds].amount > 0:
        amount_of_current_positions = len(context.portfolio.positions) - 1
    percent_bonds_to_buy = (context.Target_securities_to_buy - amount_of_current_positions) * (1.0 / context.Target_securities_to_buy)
    order_target_percent(context.bonds , percent_bonds_to_buy)


BUILD OUR QUANTLAND

So the momentum here is simply calculated by taking the percent change for the last 126 days? Why 126 days? Is that the result of some optimization?

@APTrade

Hi Andy

The momentum factor used here is the percent change of last 126 days minus last 10 days, defined as momentum_skip_days by the author. He is trying to weight down the effect of mean reversion happened in short term (10 day would be 2 weeks).

The choice of 126 days thus refers to half yearly, where 252 days is usually defined as the number of trading day in one year.

I believe there is no parameter optimization in such although I have seen WML (winner-minus-loser) factor, a 4-factor extension from Fama French’s, usually picks a whole trading year (252 days), some studies would adopt half-yearly and the logic is not far-fetched.

Kyo

1 Like

Ah yes, I might have been a little confused by the fact that there is a TF_lookback variable set to a 126 and a relative_momentum_lookback variable set to the same amount. And the momentum skip is only applied to the latter apparently.

Anyhow, since you say “the logic is not far-fetched”… what is the logic? Why not take a quarter, or a single month or one 1 1/2 half years? I mean I would simply try it out if I could right now, yet I can’t so I have to ask :slight_smile:

Hi Andy

I think you asked the right question :sweat_smile:

The term ‘logic’ here I meant ‘to use momentum factor in our strategy, and this phenomenon should happen in a period of something in medium term, say, 3 ~12 months’.

This is sort of supported by some traders’ gut feelings of “this stock is on uptrend” that happens for about a few months until some bad news or sentiment specific to the industry/market drive it down. And also more importantly some paper studies seem to mostly use 12-month return as the factor. 6-month is half of the period, but doesn’t sound off-mark (for myself) to a definition of stock price action in medium term.

I think what I am trying to tell here is exactly what your inquiry is: there might always be a margin of choices of parameters, that goes with the thinking: if 12-month works, 6-month may probably work so let’s try it out, and then why not testing 3-month or 1-month as well?

Well, its really up to further investigation! (and as we discussed in another post, we have to have the infrastructure and data!)

What was the performance of this version of algo?

Hi Valery,

Welcome!

According to the Author, this algo has produced returns of 18.0% using the Q500US universe since 2003 with a Sharpe Ratio of 1.05 and 12% of Alpha and a Beta of 0.53.