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
15
Loading...
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.
3 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
10
Loading...
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.

Hi Marcos (and hi again to Joakim after my absence from the Q Forum this year).
Looking at your strategy, Marcos, and the parameters that you are using (e.g. account size and preference for being Long in the market), it seems evident that you intended this mainly for personal investment purposes. However it can also be generalized to become a good starting point for a "Long-Short Equity" strategy suitable for entry in Q's Contest. I'm a lousy python programmer, but i tried doing this for you by taking your code and then adding some bits in such a way that all Q's Contest rules would be fulfilled. Results aren't great (yet) but there is lots of scope for easy improvement and everything stays within the required constraints for Q's contest, but with the exception that leverage goes to zero briefly on 2 occasions (Dec 2016 & Sept 2017). I'm not sure what is causing this exactly, but maybe Joakim can help us. He's smarter than i am, and also considerably more skilled at python :-))

Clone Algorithm
2
Loading...
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
# MomAtFairPrice_EquityLongShort_TonyM_001
#  11 Dec 2019.
#  As per version _TonyM_000, but modify as follows for Quantopian contest entry:
#  - Increase initial trading capital from $100k to $ 10MM
#  - Increase number of trading positions (Long & Short) from 10 to 100.
#  - Reduce maximum gross exposure from 0.95 to 0.90
#  - Re-set rebalance to Weekly.
#  - Reverse sign on PEG zscore component in "Combined"



# MomAtFairPrice_BearStockReversal_TonyM_000
#  11 Dec 2019.

# This is a copy of " Momentum at a fair price + reversal in bear markets"
# Original algo by Marcos Wernicke
# Backtest from 2014-07-01 to 2018-11-08 with $100,000 initial capital

# But now convert to "Equity Long-Short" type algo for Quantopian,
# by considering Bear STOCK reversal, rather than Bear MARKET reversal.


# ---------------------------------------------------------------

# Import Quantopian Algorithm API functions
import quantopian.algorithm as algo    
#from quantopian.pipeline import Pipeline  
from quantopian.algorithm import (
    attach_pipeline,
    pipeline_output,
    order_optimal_portfolio
)

# Import Pipeline class and datasets
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing 
#from quantopian.pipeline.factors import AverageDollarVolume


import quantopian.optimize as opt

# Import other built-in factors. # Not used here
"""
from quantopian.pipeline.factors import (
    SimpleMovingAverage,
    RollingLinearRegressionOfReturns,
    SimpleBeta,
    Returns
)
"""


from quantopian.pipeline.filters import QTradableStocksUS  #,Q500US,Q1500US  

from quantopian.pipeline.experimental import risk_loading_pipeline

from quantopian.pipeline.factors import Returns, CustomFactor, Latest

import numpy as np  
import pandas as pd

# Morningstar fundamentals data (if required later)
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data import Fundamentals as income_statement
from quantopian.pipeline.data import Fundamentals as balance_sheet
from quantopian.pipeline.data import Fundamentals as operation_ratios  
from quantopian.pipeline.data import Fundamentals as valuation_ratios  
from quantopian.pipeline.classifiers.fundamentals import Sector

#Classify securities by sector to enforce sector neutrality later
sector = Sector()

#--------------------------------------------------------
# Define constraints that  target portfolio must satisfy:
#-------------------
MAX_GROSS_LEVERAGE = 1.00
MAX_GROSS_EXPOSURE = 0.90   # NOMINAL leverage = 1.00, constraint 0.8x to 1.1x 
MAX_BETA_EXPOSURE = 0.05    # Low beta-to-SPY 
MAX_SECTOR_EXPOSURE = 0.05  # Low sector exposure
#Dollar Neutral .05
#Position Concentration .10

# Set the Number of positions used
# --------------------------------
#  10 as per Marcos' original algo. Now increase to 100
NUM_LONG_POSITIONS = 100   
NUM_SHORT_POSITIONS = 100
# Modified the following 2 lines to ensure that the number of positions will actually be as specified above, 
#  irrespective of  nominal leverage setting. 

NUM_LONG_POSITIONS = int(NUM_LONG_POSITIONS / MAX_GROSS_EXPOSURE)
NUM_SHORT_POSITIONS = int(NUM_SHORT_POSITIONS / MAX_GROSS_EXPOSURE)

# Define maximum position size that can be held for any given stock. 
# The optimizer needs some leeway in order to operate. If maximum is too small, the optimizer may be overly-constrained.
MAX_SHORT_POSITION_SIZE = 2*1.0/(NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS)
MAX_LONG_POSITION_SIZE = 2*1.0/(NUM_LONG_POSITIONS + NUM_SHORT_POSITIONS)

#=================================================================
# Define own factors here:

    
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 which 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  #create regime filter ... for MARKET (Marcos' original)
    """
    if bull_market: #Only goes long in 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 in 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 make_pipeline():
# A function to create and return our dynamic stock selector (pipeline). Documentation at https://www.quantopian.com/help#pipeline-title
# Break this piece of logic out into its own function to make it easier to test and modify in isolation. In particular, this function can be copy/pasted into research and run by itself.
    universe = QTradableStocksUS()
    
# Factor: Yesterday's close price
    yesterday_close = USEquityPricing.close.latest
    
#Factors:
#--------
    mom = momentum()
    ret = Retur() 
    peg_ratio = Fundamentals.peg_ratio.latest

# Factor: Combined Rank
#----------------------
# By applying a mask to the rank computations, remove any stocks that failed to meet our initial criteria **BEFORE** computing ranks.  This means that the stock with rank 10.0 is the 10th-lowest stock that was included in the Q1500US.

# Construct a Factor representing the rank of each asset. Aggregate them together using simple addition after zscoring each item
    
    combined_factor = (
        1.00 * mom.zscore()
      + 1.00 * ret.zscore()
      - 1.00 * peg_ratio.zscore()
    )
    
# Now, instead of bull & bear MARKETS, want to consider both long & short stocks simultaneously
#    longs = context.long_pipe.Factor.sort_values(ascending=False,) 
#    buy_list = longs[:context.number_of_stocks]
        
#    shorts = context.short_pipe.Factor.sort_values(ascending=False,)
#    sell_list = shorts[:context.number_of_stocks]


# FILTERS representing the top and bottom NUM_POSITIONS stocks by our combined ranking system. 
# Use these as our tradeable universe each day or other re-allocation period
    longs = combined_factor.top(NUM_LONG_POSITIONS, mask=universe)
    shorts = combined_factor.bottom(NUM_SHORT_POSITIONS, mask=universe)
    
    # The final output of our pipeline should only include the top/bottom  stocks by our criteria
    long_short_screen = (longs | shorts)

    # Create pipeline
    #----------------
    pipe = Pipeline(
    columns={
        'close': yesterday_close,
        'longs':longs,
        'shorts':shorts,
        'combined_factor':combined_factor,
    },
    screen = (long_short_screen) 
    )
    return pipe

#=====================================================================  
    
    

def initialize(context): 


    algo.schedule_function(
        rebalance,
        algo.date_rules.week_start(days_offset=0),
        algo.time_rules.market_open(hours=0,minutes=30)
    )



    # Create our dynamic stock selector.
    algo.attach_pipeline(make_pipeline(), 'pipeline_data')

    # attach pipeline for risk model factors to neutralize in optimization 
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_factors')
 
#=====================================================================

def before_trading_start(context, data):
# Called and runs every day before market open. This is where we get the securities that made it through the pipeline. Call pipeline_output to get the output. Note: this is a dataframe where the index is the SIDs for all securities to pass screen, and the columns are the factors added to the pipeline object above.

    context.pipeline_data = algo.pipeline_output('pipeline_data')
    
    print(context.pipeline_data.sort_values('combined_factor')[-10:])

    # These are the securities that we are interested in trading each day.
    context.security_list = context.pipeline_data.index

    # This dataframe will contain all of our risk loadings
    context.risk_loadings = algo.pipeline_output('risk_factors')
    
#=====================================================================
def recording_statements(context, data):
    # Plot the number of positions over time (each day).
    record(num_positions=len(context.portfolio.positions))
    

#def my_record_vars(context, data):
#    record(leverage = context.account.leverage)
    
#=====================================================================

def rebalance(context, data):
# Called at the start of every specified schedule period to execute orders according to our schedule_function() timing in order to rebalance the longs & shorts lists.
    
    ### Optimize API
    pipeline_data = context.pipeline_data
    
    risk_loadings = context.risk_loadings

    # Here we define our objective for the Optimize API. We have selected MaximizeAlpha because we believe our combined factor ranking to be proportional to expected returns. This routine will optimize the expected return of our algorithm, going long on the highest expected return and short on the lowest.
    objective = opt.MaximizeAlpha(pipeline_data.combined_factor)
    
    ### Define the list of constraints
    constraints = []
    # Constrain our maximum gross leverage
    constraints.append(opt.MaxGrossExposure(MAX_GROSS_LEVERAGE))

    # Require our algorithm to remain dollar neutral
    constraints.append(opt.DollarNeutral())

    # Add the RiskModelExposure constraint to make use of the default risk model constraints
    neutralize_risk_factors = opt.experimental.RiskModelExposure(
        risk_model_loadings=risk_loadings
    )
    constraints.append(neutralize_risk_factors)
    
    # With this constraint we enforce that NO position can make up greater than MAX_SHORT_POSITION_SIZE on the short side and no greater than MAX_LONG_POSITION_SIZE on the long side. This ensures that we do not overly concentrate our portfolio in one security or a small subset of securities.
    constraints.append(
        opt.PositionConcentration.with_equal_bounds(
            min=-MAX_SHORT_POSITION_SIZE,
            max=MAX_LONG_POSITION_SIZE
        ))

    ### Put together all the pieces we defined above by passing them into the algo.order_optimal_portfolio function. This handles all of our ordering logic, assigning appropriate weights to the securities in our universe to maximize our alpha with respect to the given constraints.
    algo.order_optimal_portfolio(
        objective=objective,
        constraints=constraints
    )

#======================================================================
"""
    # Called once at the start of the algorithm.
    # Set our slippage and commisions initially to zero to evaulate the signal-generating ability of the algorithm independent of these additional costs.
    set_commission(commission.PerShare(cost=0.0, min_trade_cost=0))
    set_slippage(slippage.VolumeShareSlippage(volume_limit=1, price_impact=0))
    context.spy = sid(8554)
    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
    
 """
There was a runtime error.