Notebook
In [2]:
import pandas as pd
import numpy as np
# Pipeline imports
from quantopian.pipeline import CustomFactor, CustomFilter, Pipeline
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import SimpleMovingAverage
# Import run_pipeline method
from quantopian.research import run_pipeline
In [36]:
#Custom factor to calculate number of days where close price < 50 day SMA
class LTUptrend(CustomFactor):
    inputs = [USEquityPricing.close]
    window_length =51
    
    def compute(self, today, assets, out, last_close_price):
        
        #*inputs are M x N numpy arrays, where M (axis=0) is the window_length and N (axis=1) is the number of securities. find mean of all the rows for each security
        #50 day SMA
        sma50cf = np.nanmean(last_close_price[0:-1], axis=0)
        
        # 'True' (=1) to detect bad days whenever last_close_price < sma50cf, otherwise it will be 0 (good day)
        # we need to find the last date for this event for each day
        #Since it is date descending, we want to find the last '1' in the series. or we can flip it upside down, and find the first 1 in series
        bad_day = last_close_price < sma50cf
        
        # Flip this array to now have the most recent date as row 0 (it's normally -1)  
        bad_day_flipped = np.flipud(bad_day)
        
        # Now find the first occurrence of a bad day for each stock (find first '1' so use argmax)
        days_since_last_bad_day = np.sum(bad_day_flipped, axis=0)

        # There is a special case where all days in the window are good days (all 0's)
        # The argmax method will return 0 for such day (so will make it look like it was a bad day when it was in fact all good days)
        # We want it to return the max days or the window_length for such cases where it is all 0's
        #use np.any to deal with such cases to just reutrn the max window length where its all good days
        all_good_days = np.any(bad_day_flipped, axis=0)
        days_since_last_bad_day = np.where(all_good_days==False, self.window_length, days_since_last_bad_day)
        
        out[:] = days_since_last_bad_day

#Just a custom filter so we can run it for AAPL only later on
class SidInList(CustomFilter):
    inputs = []
    window_length = 1
    params = ('sid_list',)

    def compute(self, today, assets, out, sid_list):
        out[:] = np.in1d(assets, sid_list)

#Make pipeline
def make_pipeline():
    
    sma50 = SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=50)
    last_close_price = USEquityPricing.close.latest
    
    #run through customfactor
    bad_day = last_close_price < sma50
    days_since_last_bad_day = LTUptrend()
    
    #filter for AAPL only
    include_filter = SidInList(sid_list = (24)) # SID for APPL

    return Pipeline(columns={'bad_day':bad_day,'bad_days_count': days_since_last_bad_day,'last_close_price':last_close_price,'sma50':sma50},screen=include_filter)

# Specify a time range to evaluate
period_start = '2020-02-04'
period_end = '2020-02-07'

# Execute pipeline over evaluation period
pipeline_output = run_pipeline(
    make_pipeline(),
    start_date=period_start,
    end_date=period_end
)

pipeline_output     

Pipeline Execution Time: 0.48 Seconds
Out[36]:
bad_day bad_days_count last_close_price sma50
2020-02-04 00:00:00+00:00 Equity(24 [AAPL]) False 25.0 308.71 290.391860
2020-02-05 00:00:00+00:00 Equity(24 [AAPL]) False 26.0 318.86 291.504660
2020-02-06 00:00:00+00:00 Equity(24 [AAPL]) False 25.0 321.58 292.696060
2020-02-07 00:00:00+00:00 Equity(24 [AAPL]) False 25.0 324.44 293.267442
In [ ]:
 
In [ ]:
 
In [ ]: