Back to Community
Strategy: Momentum at a fair price + reversal in bear markets

This is my First strategy of +1 Sharpe Ratio. I would like to hear what you guys think about it and maybe some advise.

Idea behind the strategy
Going long on top momentum stocks which are not highly overvalued. Also in bearish markets going short on top gainers which are overvalued.
Value of the stock is defined by the Peg_ratio

Rules for The Strategy:
- Universe = US Equities
- Regime filter = Only goes long when SPY.close > SPY.100day_moving_average, otherwise goes short
- Longs = get the Top 50 momentum stocks, only buy ones with Peg_ratio < 1.5
- Shorts = get the Top 50 gainers, only short ones with Peg_ratio > 3
- This strategy is rebalanced once a month, at the beginning of the month
- Weighting method = equally distributed weights, no more than 20% to a single stock
- No cash reallocation

It seems to work nicely, Im not able to backtest it in a longer period since there is no peg_ratio data before 7-1-2014
I know there is lots of room for improvement such as there is a lot of cash that is not being used, and maybe I could use a more sophisticated weighting method.
Also im concerned of the small number of stocks the strategy buys

This is my first strategy, surely there are lots of flaws, I'm here to learn and will appreciate any type of feedback

Clone Algorithm
4
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import quantopian.algorithm as algo    
from quantopian.pipeline import Pipeline  
from quantopian.pipeline.data.builtin import USEquityPricing 
from quantopian.pipeline.filters import QTradableStocksUS,Q500US,Q1500US  
from quantopian.pipeline.factors import Returns, CustomFactor, Latest

from quantopian.pipeline.data import Fundamentals

import numpy as np  
import pandas as pd

def initialize(context): 
    algo.schedule_function( 
        rebalance,  
        algo.date_rules.month_start(),
        algo.time_rules.market_open(minutes=15),   
    )  
    schedule_function(my_record_vars,
                      date_rules.month_start(),
                      time_rules.market_close())
    
    set_commission(commission.PerShare(cost=0.013, min_trade_cost=1.3))
    
    # Create our dynamic stock selector.
    algo.attach_pipeline(long_pipeline(),'long')    
    algo.attach_pipeline(short_pipeline(),'short')
       
    context.number_of_stocks = 10 # max number of stock to buy.
    context.index_id = sid(8554) # identifier for the SPY. used for trend filter.   
    context.index_average_window = 100  # moving average periods for index filter
    
class momentum(CustomFactor): #momentum definition
        inputs = [USEquityPricing.close,Returns(window_length=126)]    
        window_length = 252  
        def compute(self, today, assets, out, prices, returns):
            out[:] = ((prices[-21] - prices[-252])/prices[-252] -
                      (prices[-1] - prices[-21])/prices[-21]) / np.nanstd(returns, axis=0)  
            
class Retur(CustomFactor):     
    inputs = [USEquityPricing.close]   
    window_length = 21   
    def compute(self, today, assets, out, close):    
        out[:] = (close[-1] - close[0])/close[-1] 
    
def long_pipeline():
    mom = momentum()
    peg_ratio = Fundamentals.peg_ratio.latest
    
    universe = mom.top(50) & (peg_ratio < 1.5)#Buy top momentum stocks wich are at fair value 
    
    return Pipeline(  
        columns={'Factor': peg_ratio}, 
        screen = universe 
    )     

def short_pipeline():  
    ret = Retur()   
    peg_ratio = Fundamentals.peg_ratio.latest
    
    universe = ret.top(50) & (peg_ratio > 3)#Short best monthly performing stock wich are Expensive
    
    return Pipeline(  
        columns={'Factor': peg_ratio},   
        screen = universe      
    )

def before_trading_start(context, data):    
    context.long_pipe = algo.pipeline_output('long')   
    context.short_pipe = algo.pipeline_output('short')

    
def rebalance(context, data): 
    index_history = data.history(context.index_id,"close",context.index_average_window,"1d")#get index data 
    index_sma = index_history.mean()  # Average of index history  
    current_index = index_history[-1]  # get last element
    
    bull_market = current_index > index_sma  #crete regime filter 
    
    if bull_market: #Only goes long on bull markets
        longs = context.long_pipe.Factor.sort_values(ascending=False,) 
        buy_list = longs[:context.number_of_stocks]
        
        for security in context.portfolio.positions:
            if (security not in buy_list): #Sells current positions
                order_target(security, 0.0)
                
        for security in buy_list.index:
            if len(buy_list) > 5:#Doesn't allocate more than 20% to a single stock
                weight = (1.0 / len(buy_list)) 
            else:
                weight = 0.2   
            order_target_percent(security, weight) 
            
    else:#Only goes Short on bear markets
        shorts = context.short_pipe.Factor.sort_values(ascending=False,)
        sell_list = shorts[:context.number_of_stocks]
        
        for security in context.portfolio.positions:
            if(security not in sell_list): #Sells current positions
                order_target(security, 0.0)
                
        for security in sell_list.index: 
           if len(sell_list) > 5:#Doesn't allocate more than 20% to a single stock
                weight = (1.0 / len(sell_list)) 
           else:
                weight = 0.2 
           order_target_percent(security, -weight)
        

def my_record_vars(context, data):
    record(leverage = context.account.leverage)
There was a runtime error.
2 responses

Hi Marcos,

Nice one, thank you for sharing!

I tried to recreate the peg ratio manually, so it's possible to run a longer backtest. Only somewhat successfully, as there still seems to be missing data. Maybe try some other fields for earnings growth, e.g. just Net Income, or EBIT? Also, negative growth in earnings should probably be set to null I'm thinking. There might be other mistakes as well. Have a look?

Clone Algorithm
3
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import quantopian.algorithm as algo    
from quantopian.pipeline import Pipeline  
from quantopian.pipeline.data.builtin import USEquityPricing 
from quantopian.pipeline.filters import QTradableStocksUS,Q500US,Q1500US  
from quantopian.pipeline.factors import Returns, CustomFactor, Latest
from quantopian.pipeline.factors import PercentChange

from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data import Fundamentals as msf

import numpy as np  
import pandas as pd

def initialize(context): 
    algo.schedule_function( 
        rebalance,  
        algo.date_rules.month_start(),
        algo.time_rules.market_open(minutes=15),   
    )  
    schedule_function(my_record_vars,
                      date_rules.month_start(),
                      time_rules.market_close())
    
    set_commission(commission.PerShare(cost=0.013, min_trade_cost=1.3))
    
    # Create our dynamic stock selector.
    algo.attach_pipeline(long_pipeline(),'long')    
    algo.attach_pipeline(short_pipeline(),'short')
       
    context.number_of_stocks = 10 # max number of stock to buy.
    context.index_id = sid(8554) # identifier for the SPY. used for trend filter.   
    context.index_average_window = 100  # moving average periods for index filter
    
class momentum(CustomFactor): #momentum definition
        inputs = [USEquityPricing.close,Returns(window_length=126)]    
        window_length = 252  
        def compute(self, today, assets, out, prices, returns):
            out[:] = ((prices[-21] - prices[-252])/prices[-252] -
                      (prices[-1] - prices[-21])/prices[-21]) / np.nanstd(returns, axis=0)  
            
class Retur(CustomFactor):     
    inputs = [USEquityPricing.close]   
    window_length = 21   
    def compute(self, today, assets, out, close):    
        out[:] = (close[-1] - close[0])/close[-1] 
    
def long_pipeline():
    mom = momentum()
    peg_ratio = Fundamentals.peg_ratio.latest
    
    universe = mom.top(50) & (peg_ratio < 1.5)#Buy top momentum stocks wich are at fair value 
    
    return Pipeline(  
        columns={'Factor': peg_ratio}, 
        screen = universe 
    )     

def short_pipeline():  
    ret = Retur()   
    
    pe_ratio = msf.pe_ratio.latest
    
    eps_growth = PercentChange(inputs=[msf.normalized_diluted_eps_earnings_reports], window_length=252)
        
    peg_ratio = pe_ratio / eps_growth            # Fundamentals.peg_ratio.latest
    
    universe = ret.top(50) & (peg_ratio > 3)#Short best monthly performing stock wich are Expensive
    
    return Pipeline(  
        columns={'Factor': peg_ratio},   
        screen = universe      
    )

def before_trading_start(context, data):    
    context.long_pipe = algo.pipeline_output('long')   
    context.short_pipe = algo.pipeline_output('short')

    
def rebalance(context, data): 
    index_history = data.history(context.index_id,"close",context.index_average_window,"1d")#get index data 
    index_sma = index_history.mean()  # Average of index history  
    current_index = index_history[-1]  # get last element
    
    bull_market = current_index > index_sma  #crete regime filter 
    
    if bull_market: #Only goes long on bull markets
        longs = context.long_pipe.Factor.sort_values(ascending=False,) 
        buy_list = longs[:context.number_of_stocks]
        
        for security in context.portfolio.positions:
            if (security not in buy_list): #Sells current positions
                order_target(security, 0.0)
                
        for security in buy_list.index:
            if len(buy_list) > 5:#Doesn't allocate more than 20% to a single stock
                weight = (1.0 / len(buy_list)) 
            else:
                weight = 0.2   
            order_target_percent(security, weight) 
            
    else:#Only goes Short on bear markets
        shorts = context.short_pipe.Factor.sort_values(ascending=False,)
        sell_list = shorts[:context.number_of_stocks]
        
        for security in context.portfolio.positions:
            if(security not in sell_list): #Sells current positions
                order_target(security, 0.0)
                
        for security in sell_list.index: 
           if len(sell_list) > 5:#Doesn't allocate more than 20% to a single stock
                weight = (1.0 / len(sell_list)) 
           else:
                weight = 0.2 
           order_target_percent(security, -weight)
        

def my_record_vars(context, data):
    record(leverage = context.account.leverage)
There was a runtime error.

Thanks Joakim for your feedback, I'll keep working on it and post any updates.
Sadly the algo didn't pass a robustness test. If instead of algo.date_rules.month_start() you run the algo with .month_start(days_offset=1) it stops having positive results.