Notebook
In [1]:
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor
from quantopian.research import run_pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.pipeline.factors import AverageDollarVolume
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.data import morningstar as mstar
from quantopian.pipeline.filters.morningstar import IsPrimaryShare
from quantopian.pipeline.classifiers.morningstar import Sector
import numpy as np
from scipy.stats.mstats import gmean
from quantopian.research import run_pipeline

Initial Screen

This function defines our initial filter to create our universe

In [50]:
def filter_universe():  
    """
    9 filters:
        1. common stock
        2 & 3. not limited partnership - name and database check
        4. database has fundamental data
        5. not over the counter
        6. not when issued
        7. not depository receipts
        8. primary share
        9. high dollar volume
    Check Scott's notebook for more details.
    """
    common_stock = mstar.share_class_reference.security_type.latest.eq('ST00000001')
    not_lp_name = ~mstar.company_reference.standard_name.latest.matches('.* L[\\. ]?P\.?$')
    not_lp_balance_sheet = mstar.balance_sheet.limited_partnership.latest.isnull()
    have_data = mstar.valuation.market_cap.latest.notnull() # this knocks out ETFs, since ETFs won't have any fundamental data
    not_otc = ~mstar.share_class_reference.exchange_id.latest.startswith('OTC')
    not_wi = ~mstar.share_class_reference.symbol.latest.endswith('.WI')
    not_depository = ~mstar.share_class_reference.is_depositary_receipt.latest
    primary_share = IsPrimaryShare()
    not_financial = ~Sector().eq(103)
    #remove_nan_inf = np.isfinite(earning_yield) 
    
    # Combine the above filters.
    tradable_filter = (common_stock & not_lp_name & not_lp_balance_sheet &
                       have_data & not_otc & not_wi & not_depository & primary_share & not_financial)
    
    high_volume_tradable = AverageDollarVolume(
            window_length=21,
            mask=tradable_filter
        ).rank(ascending=False) < 1001 
    
    mask = high_volume_tradable
    
    return mask

Next we create the function to make the pipeline

In [51]:
def make_pipeline1():
    initial_screen = filter_universe()
    average_dollar_volume = AverageDollarVolume(window_length=21)
    sector= Sector()
    
    return Pipeline(
    columns={
            'avg_dollar_volume' : average_dollar_volume,
            'sector':sector
        },
        screen= initial_screen
    )

Now we make the pipeline, run it and print out a sample of the results

In [52]:
my_pipe = make_pipeline1()

result = run_pipeline(my_pipe, '2016-09-07','2016-09-07')
 
print 'Number of securities that passed the filter: %d' % len(result)
result.head(5)
Number of securities that passed the filter: 1000
Out[52]:
avg_dollar_volume sector
2016-09-07 00:00:00+00:00 Equity(2 [AA]) 1.150611e+08 101
Equity(24 [AAPL]) 2.239204e+09 311
Equity(53 [ABMD]) 2.791744e+07 206
Equity(62 [ABT]) 3.572440e+08 206
Equity(67 [ADSK]) 9.923147e+07 311

Forensic Screen

Next we introduce two classes to construct the forensic screen:

In [10]:
class STA(CustomFactor):
    inputs = [morningstar.cash_flow_statement.operating_cash_flow, 
              morningstar.cash_flow_statement.net_income,
              morningstar.balance_sheet.total_assets]
    window_length = 1
    
    def compute(self, today, assets, out, ocf, ni, ta):
        ta = np.where(np.isnan(ta), 0, ta)
        ocf = np.where(np.isnan(ocf), 0, ocf)
        ni = np.where(np.isnan(ni), 0, ni)
        out[:] = abs(ni[-1] - ocf[-1])/ ta[-1]
        
class SNOA(CustomFactor):
    inputs = [morningstar.balance_sheet.total_assets,
             morningstar.balance_sheet.cash_and_cash_equivalents,
             morningstar.balance_sheet.current_debt, # same as short-term debt?
             morningstar.balance_sheet.minority_interest,
             morningstar.balance_sheet.long_term_debt, # check same?
             morningstar.balance_sheet.preferred_stock # check same?
             ]
    window_length = 1
    
    def compute(self, today, assets, out, ta, cace, cd, mi, ltd, ps):
        ta = np.where(np.isnan(ta), 0, ta)
        cace = np.where(np.isnan(cace), 0, cace)
        cd = np.where(np.isnan(cd), 0, cd)
        mi = np.where(np.isnan(mi), 0, mi)
        ltd = np.where(np.isnan(ltd), 0, ltd)
        ps = np.where(np.isnan(ps), 0, ps)

        results = ((ta[-1]-cace[-1])-(ta[-1]-cace[-1]-ltd[-1]-cd[-1]-ps[-1]-mi[-1]))/ta[-1]      
        out[:] = np.where(np.isnan(results),0,results)

this next pipeline adds the forensic screen to the initial screen...

In [48]:
def make_pipeline2():
    initial_screen = filter_universe()

    sta = STA()
    sta_rank = sta.rank(mask=initial_screen, ascending=True) # high STA -> bad!, so we rank from lowest to highest
    snoa = SNOA()
    snoa_rank = snoa.rank(mask=initial_screen, ascending=True) # high SNOA -> bad!, so we rank from lowest to highest
    
    combined_forensic = (sta_rank+snoa_rank)/2.0
    combined_forensic_rank = combined_forensic.rank(ascending=True) # high combined rank -> bad, so we rank from lowest to highest
    
    # This filter screens out the worst 10% of stocks by forensic rank, i.e. the 10% with the lowest rank
    passes_forensic = combined_forensic_rank.percentile_between(0,90)
    
    return Pipeline(
    columns={
            'sta' : sta,
            'sta_rank' : sta_rank,
            'snoa' : snoa,
            'snoa_rank' : snoa_rank,
            #'combined_forensic' : combined_forensic,
            'comb_forensic_rank' : combined_forensic_rank
        },
        screen= passes_forensic
    )

again, we make the pipeline and run it, print the results...

In [49]:
my_pipe = make_pipeline2()

result = run_pipeline(my_pipe, '2016-09-07','2016-09-07')

print 'Number of securities that passed the filter: %d' % len(result)
result.head(50)
Number of securities that passed the filter: 896
Out[49]:
comb_forensic_rank snoa snoa_rank sta sta_rank
2016-09-07 00:00:00+00:00 Equity(2 [AA]) 138 0.313705 467 0.004261 119
Equity(24 [AAPL]) 217 0.277937 390 0.009287 294
Equity(53 [ABMD]) 359 0.001193 75 0.031376 775
Equity(62 [ABT]) 103 0.226758 302 0.007381 218
Equity(67 [ADSK]) 406 0.297331 427 0.014839 472
Equity(76 [TAP]) 172 0.198855 256 0.012002 377
Equity(114 [ADBE]) 340 0.157512 214 0.020025 615
Equity(122 [ADI]) 49 0.225369 297 0.003067 75
Equity(128 [ADM]) 285 0.184548 236 0.016884 539
Equity(161 [AEP]) 255 0.344168 540 0.006665 196
Equity(168 [AET]) 132 0.293619 419 0.005299 153
Equity(197 [AGCO]) 580 0.218290 283 0.033381 794
Equity(216 [HES]) 338 0.222235 294 0.016650 533
Equity(270 [AKRX]) 733 0.425679 670 0.018224 573
Equity(300 [ALK]) 206 0.088984 153 0.016205 514
Equity(301 [ALKS]) 133 0.194385 249 0.010259 324
Equity(337 [AMAT]) 623 0.244032 320 0.034747 804
Equity(353 [AME]) 210 0.307967 457 0.007296 215
Equity(357 [TWX]) 263 0.404962 636 0.004086 109
Equity(368 [AMGN]) 529 0.440196 688 0.010693 339
Equity(410 [AN]) 253 0.322824 480 0.008377 254
Equity(448 [APA]) 853 0.422000 665 0.029450 758
Equity(460 [APD]) 676 0.340592 526 0.022431 663
Equity(465 [APH]) 218 0.358067 561 0.004335 124
Equity(538 [ARW]) 31 0.215091 276 0.000972 17
Equity(559 [ASH]) 567 0.470440 738 0.010391 329
Equity(612 [ATO]) 308 0.321373 479 0.010037 314
Equity(624 [ATW]) 182 0.291610 415 0.007583 224
Equity(630 [ADP]) 54 0.045974 118 0.008635 262
Equity(659 [AMAG]) 856 0.398719 627 0.034124 801
Equity(660 [AVP]) 611 0.610346 874 0.007669 230
Equity(661 [AVT]) 283 0.221695 293 0.015091 479
Equity(663 [AVY]) 758 0.326395 489 0.033133 792
Equity(698 [BA]) 512 0.123615 183 0.038701 832
Equity(734 [BAX]) 608 0.270054 375 0.027343 727
Equity(739 [BBBY]) 276 0.256414 347 0.012954 415
Equity(754 [BBY]) 548 0.104421 166 0.045797 878
Equity(755 [BC]) 619 0.139536 199 0.060118 919
Equity(779 [BCR]) 400 0.311989 464 0.013370 429
Equity(794 [BDX]) 748 0.459069 719 0.017028 544
Equity(799 [BEAV]) 417 0.628701 893 0.000861 15
Equity(858 [BHI]) 692 0.153206 210 0.220926 990
Equity(913 [BKH]) 713 0.524135 806 0.013099 422
Equity(939 [BLL]) 317 0.458891 717 0.003350 85
Equity(975 [BMS]) 599 0.426455 672 0.013139 424
Equity(980 [BMY]) 461 0.210045 270 0.023667 684
Equity(1131 [BSX]) 518 0.304238 447 0.018107 570
Equity(1209 [CA]) 37 0.178784 229 0.003392 88
Equity(1218 [CACI]) 176 0.365201 574 0.002408 61
Equity(1228 [CAG]) 884 0.458097 715 0.029229 753

Value Screen

This class constructs the value factor:

In [68]:
class Value(CustomFactor):
    inputs = [morningstar.income_statement.ebitda,
              morningstar.valuation.enterprise_value]
    window_length = 1
    
    def compute(self, today, assets, out, ebitda, ev):
        ebitda = np.where(np.isnan(ebitda), 0, ebitda) # how do I 'clip' negative values?
        ev = np.where(np.isnan(ev), 0, ev)
        ev = np.clip(ev, a_min=1.0, a_max=None)  # Clip lower bound of the array to 1.0 
        ebitda = np.clip(ebitda, a_min=1.0, a_max=None)  # Clip lower bound of the array to 1.0 
        result = ebitda[-1] / ev[-1] # we do it this way round so that we can rank it in ascending order...
        out[:] = np.where(np.isnan(result),0,result)

and next we make another pipeline with initial, forensic and value screens...

In [71]:
def make_pipeline3():
    initial_screen = filter_universe()

    ###################################
    sta = STA()
    sta_rank = sta.rank(mask=initial_screen, ascending=True) # high STA -> bad!, so we rank from lowest to highest
    snoa = SNOA()
    snoa_rank = snoa.rank(mask=initial_screen, ascending=True) # high SNOA -> bad!, so we rank from lowest to highest
    
    combined_forensic = (sta_rank+snoa_rank)/2.0
    combined_forensic_rank = combined_forensic.rank(ascending=True) # high combined rank -> bad, so we rank from lowest to highest
    
    # This filter screens out the worst 10% of stocks by forensic rank, i.e. the 10% with the lowest rank
    passes_forensic = combined_forensic_rank.percentile_between(0,90)
    
    ##################################
    ebitda = morningstar.income_statement.ebitda.latest
    ev = morningstar.valuation.enterprise_value.latest
    
    value = Value() # the way we've setup the factor, a higher score (yield) is better
    value_pass_rank = value.rank(mask=passes_forensic, ascending=False) # therefore we rank from highest to lowest, i.e. descending
    #value_fail_rank = value.rank(mask=passes_forensic)
    
    passes_value = value_pass_rank.percentile_between(0,10) # take the 10% highest ranked stocks by value
    
    return Pipeline(
    columns={
            'comb_forensic_rank' : combined_forensic_rank,
            'value':value,
            'value_rank': value_pass_rank,
            'ev':ev,
            'ebitda':ebitda
            #'passes_value':passes_value,
            #'passes_forensic':passes_forensic,
        },
        screen= passes_value
    )

as before, we make, run and print the results of the pipeline

In [72]:
my_pipe = make_pipeline3()

result = run_pipeline(my_pipe, '2016-09-07','2016-09-07')

print 'Number of securities that passed the filter: %d' % len(result)
result.head(5)
Number of securities that passed the filter: 90
Out[72]:
comb_forensic_rank ebitda ev value value_rank
2016-09-07 00:00:00+00:00 Equity(2 [AA]) 138 772000000 20788700000 0.037136 81
Equity(168 [AET]) 132 1651000000 41384400000 0.039894 67
Equity(197 [AGCO]) 581 186400000 5036350000 0.037011 82
Equity(300 [ALK]) 207 514000000 7512210000 0.068422 16
Equity(624 [ATW]) 183 180874000 1705870000 0.106030 7

Quality Screen

And now some classes to construct the F-score (1st part of quality):

In [73]:
class ROA(CustomFactor):
    inputs = [ morningstar.operation_ratios.roa ]
    window_length = 1
    def compute(self, today, assets, out, roa):
        out[:] = np.where(roa[-1]>0,1,0)

class FCFTA(CustomFactor):
    inputs = [ morningstar.cash_flow_statement.free_cash_flow,
             morningstar.balance_sheet.total_assets]
    window_length = 1
    def compute(self, today, assets, out, fcf, ta):
        out[:] = np.where(fcf[-1]/ta[-1]>0,1,0)
        
class ROA_GROWTH(CustomFactor):
    inputs = [ morningstar.operation_ratios.roa  ]
    window_length = 252
    def compute(self, today, assets, out, roa):
        out[:] = np.where(roa[-1]>roa[-252],1,0)
        
class FCFTA_ROA(CustomFactor):
    inputs = [ morningstar.cash_flow_statement.free_cash_flow,
               morningstar.balance_sheet.total_assets,
             morningstar.operation_ratios.roa ]
    window_length = 1
    def compute(self, today, assets, out, fcf, ta, roa):
        out[:] = np.where(fcf[-1]/ta[-1]>roa[-1],1,0)
        
class FCFTA_GROWTH(CustomFactor):
    inputs = [ morningstar.cash_flow_statement.free_cash_flow,
             morningstar.balance_sheet.total_assets]
    window_length = 252
    def compute(self, today, assets, out, fcf, ta):
        out[:] = np.where(fcf[-1]/ta[-1]>fcf[-252]/ta[-252],1,0)          
        
class LTD_GROWTH(CustomFactor):
    inputs = [ morningstar.balance_sheet.total_assets,
              morningstar.balance_sheet.long_term_debt]
    window_length = 252
    def compute(self, today, assets, out, ta, ltd):
        out[:] = np.where(ltd[-1]/ta[-1]<ltd[-252]/ta[-252],1,0)

class CR_GROWTH(CustomFactor):
    inputs = [ morningstar.operation_ratios.current_ratio  ]
    window_length = 252
    def compute(self, today, assets, out, cr):
        out[:] = np.where(cr[-1]>cr[-252],1,0)   
        
class GM_GROWTH(CustomFactor):
    inputs = [ morningstar.operation_ratios.gross_margin ]
    window_length = 252
    def compute(self, today, assets, out, gm):
        out[:] = np.where(gm[-1]>gm[-252],1,0)
        
class ATR_GROWTH(CustomFactor):
    inputs = [ morningstar.operation_ratios.assets_turnover ]
    window_length = 252
    def compute(self, today, assets, out, atr):
        out[:] = np.where(atr[-1]>atr[-252],1,0)
        
class NEQISS(CustomFactor):
    inputs = [ morningstar.valuation.shares_outstanding ]
    window_length = 252
    def compute(self, today, assets, out, so):
        out[:] = np.where(so[-1]-so[-252]<1,1,0)
/usr/local/lib/python2.7/dist-packages/IPython/kernel/__main__.py:61: NotAllowedInLiveWarning: The fundamentals attribute valuation.shares_outstanding is not yet allowed in broker-backed live trading

we make a 4th pipeline that includes (1) initial, (2) forensic, (3) value and (4) financial strength (f-score) filters...

In [86]:
def make_pipeline4():
    initial_screen = filter_universe()
    
    # elements of forensic screen:
    sta = STA()
    sta_rank = sta.rank(mask=initial_screen, ascending=True) # high STA -> bad!, so we rank from lowest to highest
    snoa = SNOA()
    snoa_rank = snoa.rank(mask=initial_screen, ascending=True) # high SNOA -> bad!, so we rank from lowest to highest
    
    combined_forensic = (sta_rank+snoa_rank)/2.0
    combined_forensic_rank = combined_forensic.rank(ascending=True) # high combined rank -> bad, so we rank from lowest to highest
    
    # This filter screens out the worst 10% of stocks by forensic rank, i.e. the 10% with the lowest rank
    passes_forensic = combined_forensic_rank.percentile_between(0,90)
    
    # value screen
    value = Value() # the way we've setup the factor, a higher score (yield) is better
    value_pass_rank = value.rank(mask=passes_forensic, ascending=False) # therefore we rank from highest to lowest, i.e. descending
    #value_fail_rank = value.rank(mask=passes_forensic)
    
    passes_value = value_pass_rank.percentile_between(0,10) # take the 10% highest ranked stocks by value
    
    # elements of the f-score, or financial strength measure
    roa=ROA()
    fcfta=FCFTA()
    roa_growth=ROA_GROWTH()
    fcfta_roa=FCFTA_ROA()
    fcfta_growth=FCFTA_GROWTH()
    ltd_growth=LTD_GROWTH()
    cr_growth=CR_GROWTH()
    gm_growth=GM_GROWTH()
    atr_growth=ATR_GROWTH()
    neqiss = NEQISS()
    
    # here we add together the 10 elements of the f-score to get the total f-score, then we rank the results
    f_score = roa+fcfta+roa_growth+fcfta_roa+fcfta_growth+ltd_growth+cr_growth+gm_growth+atr_growth+neqiss
    f_score_rank = f_score.rank(mask=passes_value, ascending=False) # a higher f_score is better, so we rank highest to lowest
    
    passes_fin_strength = f_score_rank.percentile_between(0,50) # take the 50% highest ranked stocks by f_score
    
    return Pipeline(
    columns={
            'roa': roa,
            'fcfta':fcfta,
            'roa_growth':roa_growth,
            'fcfta_roa':fcfta_roa,
            'fcfta_growth':fcfta_growth,
            'ltd_growth':ltd_growth,
            'cr_growth':cr_growth,
            'gm_growth':gm_growth,
            'atr_growth':atr_growth,
            'neqiss' : neqiss,
            'f_score': f_score,
            'f_score_rank': f_score_rank
            #'passes_fin_strength': passes_fin_strength
        },
        screen= passes_fin_strength
    )

again, we make, run and print the results of the pipeline

In [87]:
my_pipe = make_pipeline4()

result = run_pipeline(my_pipe, '2016-09-07','2016-09-07')

print 'Number of securities that passed the filter: %d' % len(result)
result.head(45)
Number of securities that passed the filter: 45
Out[87]:
atr_growth cr_growth f_score f_score_rank fcfta fcfta_growth fcfta_roa gm_growth ltd_growth neqiss passes_fin_strength roa roa_growth
2016-09-07 00:00:00+00:00 Equity(300 [ALK]) 0 1 8 8 1 1 0 1 1 1 True 1 1
Equity(624 [ATW]) 0 1 8 9 1 1 1 1 1 1 True 1 0
Equity(734 [BAX]) 1 0 6 43 1 1 0 0 0 1 True 1 1
Equity(754 [BBY]) 1 0 7 25 1 1 1 0 0 1 True 1 1
Equity(755 [BC]) 1 0 8 10 1 1 1 1 1 1 True 1 0
Equity(1960 [CTL]) 1 0 8 11 1 1 1 1 0 1 True 1 1
Equity(2263 [DOW]) 0 1 8 12 1 1 0 1 1 1 True 1 1
Equity(2621 [ESV]) 1 1 9 2 1 1 0 1 1 1 True 1 1
Equity(3241 [GLW]) 1 0 7 26 1 1 0 0 1 1 True 1 1
Equity(3321 [GPS]) 0 0 6 44 1 1 1 0 1 1 True 1 0
Equity(3675 [EQC]) 0 1 7 27 1 0 0 1 1 1 True 1 1
Equity(3735 [HPQ]) 1 0 7 28 1 1 1 0 0 1 True 1 1
Equity(4010 [IR]) 1 1 8 13 1 1 0 1 0 1 True 1 1
Equity(4297 [KR]) 0 0 6 45 1 0 1 1 1 1 True 1 0
Equity(4313 [KSS]) 1 0 8 14 1 1 1 1 0 1 True 1 1
Equity(4589 [LUV]) 0 0 7 29 1 1 0 1 1 1 True 1 1
Equity(5249 [NE]) 1 1 10 1 1 1 1 1 1 1 True 1 1
Equity(5583 [ODP]) 0 1 8 15 1 1 1 0 1 1 True 1 1
Equity(5626 [OI]) 0 1 7 30 1 1 0 1 0 1 True 1 1
Equity(5719 [OSK]) 1 0 7 31 1 1 0 1 1 1 True 1 0
Equity(5787 [PCAR]) 0 1 7 32 1 1 0 1 0 1 True 1 1
Equity(6392 [RDC]) 1 1 9 3 1 1 0 1 1 1 True 1 1
Equity(8612 [CHS]) 1 0 7 33 1 1 1 0 0 1 True 1 1
Equity(9947 [HST]) 1 0 8 16 1 1 0 1 1 1 True 1 1
Equity(16108 [STLD]) 1 1 7 34 1 0 0 1 0 1 True 1 1
Equity(20306 [UTHR]) 0 1 7 35 1 1 0 0 1 1 True 1 1
Equity(20662 [TIVO]) 1 1 9 4 1 1 1 1 0 1 True 1 1
Equity(23599 [JBLU]) 0 1 9 5 1 1 1 1 1 1 True 1 1
Equity(24519 [SWHC]) 1 1 8 17 1 1 0 1 1 0 True 1 1
Equity(24799 [MTN]) 1 1 8 18 1 1 0 1 0 1 True 1 1
Equity(24814 [DF]) 0 1 7 36 1 0 1 1 0 1 True 1 1
Equity(25349 [MOH]) 0 1 7 37 1 1 1 1 0 1 True 1 0
Equity(26287 [XPO]) 1 0 8 19 1 1 1 1 0 1 True 1 1
Equity(28051 [UAL]) 0 0 7 38 1 1 1 1 1 1 True 1 0
Equity(32608 [OC]) 1 0 9 6 1 1 1 1 1 1 True 1 1
Equity(33729 [DAL]) 0 0 7 39 1 1 1 1 0 1 True 1 1
Equity(35359 [DAN]) 1 0 7 40 1 1 1 1 0 1 True 1 0
Equity(36714 [RAX]) 0 1 8 20 1 1 1 1 0 1 True 1 1
Equity(38921 [LEA]) 0 0 8 21 1 1 1 1 1 1 True 1 1
Equity(39047 [PEB]) 1 0 8 22 1 1 0 1 1 1 True 1 1
Equity(39665 [CPS]) 0 1 9 7 1 1 1 1 1 1 True 1 1
Equity(40606 [SWFT]) 0 0 7 41 1 1 1 1 1 1 True 1 0
Equity(41636 [MPC]) 0 1 7 42 1 1 1 1 0 1 True 1 0
Equity(44931 [NWSA]) 1 0 8 23 1 1 1 1 0 1 True 1 1
Equity(47098 [TSE]) 0 1 8 24 1 1 0 1 1 1 True 1 1

These new classes are for the second half of the quality score - economic strength or franchise power

In [89]:
class GM_GROWTH_2YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.gross_margin ]
    window_length = 504
    def compute(self, today, assets, out, gm):
        out[:] = gmean([gm[-1]+1, gm[-252]+1,gm[-504]+1])-1
        
class GM_STABILITY_2YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.gross_margin ]
    window_length = 504
    def compute(self, today, assets, out, gm):
        out[:] = np.std([gm[-1]-gm[-252],gm[-252]-gm[-504]],axis=0) #gm[-252]-gm[-504]#gm[-1]-gm[-252] # #np.nanstd([,gm[-252]-gm[-504]])
        
class ROA_GROWTH_2YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.roa ]
    window_length = 504
    def compute(self, today, assets, out, roa):
        out[:] = gmean([roa[-1]+1, roa[-252]+1,roa[-504]+1])-1
        
class ROIC_GROWTH_2YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.roic ]
    window_length = 504
    def compute(self, today, assets, out, roic):
        out[:] = gmean([roic[-1]+1, roic[-252]+1,roic[-504]+1])-1

and this 5th pipeline adds the two halves of the quality score together and adds that filter to all of the others (initial, forensic, value)

In [94]:
def make_pipeline5():
    initial_screen = filter_universe()
    
    ##################################
    # elements of forensic screen:
    sta = STA()
    sta_rank = sta.rank(mask=initial_screen, ascending=True) # high STA -> bad!, so we rank from lowest to highest
    snoa = SNOA()
    snoa_rank = snoa.rank(mask=initial_screen, ascending=True) # high SNOA -> bad!, so we rank from lowest to highest
    
    combined_forensic = (sta_rank+snoa_rank)/2.0
    combined_forensic_rank = combined_forensic.rank(ascending=True) # high combined rank -> bad, so we rank from lowest to highest
    
    # This filter screens out the worst 10% of stocks by forensic rank, i.e. the 10% with the lowest rank
    passes_forensic = combined_forensic_rank.percentile_between(0,90)
    
    ####################################
    # value screen
    value = Value() # the way we've setup the factor, a higher score (yield) is better
    value_pass_rank = value.rank(mask=passes_forensic, ascending=False) # therefore we rank from highest to lowest, i.e. descending
    #value_fail_rank = value.rank(mask=passes_forensic)
    
    passes_value = value_pass_rank.percentile_between(0,10) # take the 10% highest ranked stocks by value
    
    #####################################
    # elements of the f-score, or financial strength measure
    roa=ROA()
    fcfta=FCFTA()
    roa_growth=ROA_GROWTH()
    fcfta_roa=FCFTA_ROA()
    fcfta_growth=FCFTA_GROWTH()
    ltd_growth=LTD_GROWTH()
    cr_growth=CR_GROWTH()
    gm_growth=GM_GROWTH()
    atr_growth=ATR_GROWTH()
    neqiss = NEQISS()
    
    # here we add together the 10 elements of the f-score to get the total f-score, then we rank the results
    f_score = roa+fcfta+roa_growth+fcfta_roa+fcfta_growth+ltd_growth+cr_growth+gm_growth+atr_growth+neqiss
    f_score_rank = f_score.rank(mask=passes_value, ascending=False)
    
    #####################################
    # elements of the franchise power score
    gm_growth_2yr = GM_GROWTH_2YR()
    gm_growth_2yr_rank = gm_growth_2yr.rank(ascending=False, mask=passes_value)
    gm_stability_2yr = GM_STABILITY_2YR()
    gm_stability_2yr_rank = gm_stability_2yr.rank(ascending=True, mask=passes_value)
    roa_growth_2yr = ROA_GROWTH_2YR()
    roa_growth_2yr_rank = roa_growth_2yr.rank(ascending=False, mask=passes_value)
    roic_growth_2yr = ROIC_GROWTH_2YR()
    roic_growth_2yr_rank = roic_growth_2yr.rank(ascending=False, mask=passes_value)
    
    # here we add together the ranks of the elements of the franchise power score, average them and re-rank
    franchise_power = (gm_growth_2yr_rank + gm_stability_2yr_rank + roa_growth_2yr_rank + roic_growth_2yr_rank)/4.0 #
    franchise_power_rank = franchise_power.rank(ascending=True)
    
    # here we add together the ranks of the fin_strength and franchise power, average them and re-rank
    combined_quality = (f_score_rank+franchise_power_rank)/2.0
    combined_quality_rank = combined_quality.rank(ascending=True)
    
    # here we create the filter
    passes_quality = combined_quality_rank.percentile_between(0,50) # take the 10% highest ranked stocks by value
    
    return Pipeline(
    columns={

            'f_score': f_score,
            'f_score_rank': f_score_rank,
            #'combined_franchise':combined_franchise,
            'franchise_power_rank':franchise_power_rank,
            #'combined_quality':combined_quality,
            'combined_quality_rank':combined_quality_rank,
            'gm_growth':gm_growth_2yr,
            'gm_growth_rank':gm_growth_2yr_rank,
            'gm_stability':gm_stability_2yr,
            'gm_stability_rank':gm_stability_2yr_rank,
            'roa_growth':roa_growth_2yr,
            'roa_growth_rank':roa_growth_2yr_rank,
            'roic_growth':roic_growth_2yr,
            'roic_growth_rank':roic_growth_2yr_rank
        },
        screen= passes_quality
    )

Once again, we make, run and print out the results of this consolidated pipeline...

In [95]:
my_pipe = make_pipeline5()

result = run_pipeline(my_pipe, '2016-09-07','2016-09-07')

print 'Number of securities that passed the filter: %d' % len(result)
result.head(45)
Number of securities that passed the filter: 43
Out[95]:
combined_quality_rank f_score f_score_rank franchise_power_rank gm_growth gm_growth_rank gm_stability gm_stability_rank roa_growth roa_growth_rank roic_growth roic_growth_rank
2016-09-07 00:00:00+00:00 Equity(300 [ALK]) 2 8 8 10 0.670320 4 0.041760 72 0.033482 12 0.072045 7
Equity(624 [ATW]) 7 8 9 25 0.555366 20 0.014934 41 0.020936 27 0.024613 47
Equity(734 [BAX]) 32 6 43 28 0.448335 29 0.065856 82 0.034742 10 0.049975 16
Equity(754 [BBY]) 30 7 25 45 0.239539 50 0.009687 34 0.012496 58 0.030571 39
Equity(755 [BC]) 1 8 10 5 0.282254 45 0.002709 10 0.033993 11 0.064748 8
Equity(1960 [CTL]) 25 8 11 53 0.560480 18 0.005703 23 0.003613 80 0.011293 72
Equity(2263 [DOW]) 14 8 12 31 0.203313 58 0.015865 43 0.023504 21 0.045345 21
Equity(2621 [ESV]) 39 9 2 78 0.553270 21 0.042695 73 -0.002306 86 -0.000852 79
Equity(3212 [GILD]) 22 5 59 1 0.875400 2 0.005248 21 0.103368 1 0.205082 1
Equity(3241 [GLW]) 9 7 26 9 0.411618 32 0.006753 26 0.031608 14 0.044712 22
Equity(3321 [GPS]) 19 6 44 11 0.380403 39 0.009598 33 0.029683 15 0.053258 15
Equity(4010 [IR]) 6 8 13 17 0.312417 42 0.007956 30 0.021870 24 0.040398 26
Equity(4297 [KR]) 26 6 45 19 0.219680 56 0.001335 7 0.019197 35 0.040125 27
Equity(4313 [KSS]) 20 8 14 42 0.391318 37 0.002728 11 0.011970 61 0.017663 65
Equity(4589 [LUV]) 10 7 29 7 0.663556 6 0.033055 60 0.029399 16 0.061874 10
Equity(4654 [MAN]) 37 6 46 33 0.170284 68 0.000863 4 0.015111 45 0.033912 33
Equity(5249 [NE]) 12 10 1 40 0.597259 13 0.035835 64 0.016918 41 0.022529 53
Equity(5382 [JWN]) 33 6 47 26 0.365701 40 0.008287 31 0.019181 36 0.036421 28
Equity(5583 [ODP]) 42 8 15 70 0.232879 51 0.005618 22 -0.000850 84 -0.000335 78
Equity(5719 [OSK]) 23 7 31 30 0.178563 65 0.003628 14 0.019714 33 0.034982 30
Equity(5787 [PCAR]) 15 7 32 16 0.198601 61 0.002843 12 0.019764 32 0.053391 14
Equity(6392 [RDC]) 27 9 3 62 0.524586 22 0.040421 70 0.012985 55 0.016556 68
Equity(6624 [SAFM]) 41 5 62 22 0.186447 63 0.022043 54 0.051102 5 0.063228 9
Equity(9514 [BWA]) 34 6 50 23 0.213194 57 0.003094 13 0.021125 25 0.032164 37
Equity(9947 [HST]) 8 8 16 18 0.414598 31 0.003888 16 0.020018 30 0.026152 45
Equity(20306 [UTHR]) 11 7 35 4 0.928146 1 0.037939 67 0.067660 2 0.092554 2
Equity(20662 [TIVO]) 24 9 4 58 0.641860 8 0.040388 69 0.009628 69 0.019434 62
Equity(22102 [CXW]) 35 6 51 24 0.308073 43 0.000801 3 0.018939 37 0.023450 49
Equity(23599 [JBLU]) 3 9 5 13 0.603442 11 0.017914 48 0.022583 22 0.042698 25
Equity(24519 [SWHC]) 4 8 17 3 0.397855 34 0.000601 2 0.038417 7 0.056226 12
Equity(24799 [MTN]) 5 8 18 2 0.456117 28 0.004220 18 0.056094 4 0.091654 4
Equity(28051 [UAL]) 17 7 38 14 0.603687 10 0.036959 65 0.021920 23 0.061094 11
Equity(32608 [OC]) 31 9 6 64 0.222857 53 0.008319 32 0.010776 66 0.017813 64
Equity(33729 [DAL]) 18 7 39 15 0.576525 16 0.047381 75 0.024209 20 0.075044 6
Equity(35359 [DAN]) 38 7 40 39 0.147470 72 0.001196 6 0.013437 52 0.032567 36
Equity(36372 [SNI]) 43 4 77 8 0.714306 3 0.028543 57 0.035048 9 0.043984 24
Equity(36714 [RAX]) 13 8 20 21 0.669574 5 0.006715 25 0.016111 42 0.022212 56
Equity(36930 [DISC_A]) 28 6 53 12 0.661973 7 0.004532 20 0.019405 34 0.027685 42
Equity(38921 [LEA]) 16 8 21 27 0.097985 82 0.001400 8 0.021103 26 0.046074 19
Equity(39047 [PEB]) 29 8 22 47 0.408802 33 0.006543 24 0.012534 57 0.016666 67
Equity(39665 [CPS]) 21 9 7 51 0.181552 64 0.004122 17 0.013191 54 0.021682 57
Equity(40606 [SWFT]) 36 7 41 36 0.393449 36 0.017102 46 0.015696 44 0.032807 35
Equity(41636 [MPC]) 40 7 42 41 0.139736 74 0.035678 63 0.024988 18 0.048644 17

Below is some additional code that might be useful in future

I need to see how much pipeline can handle in terms of years of fundamental data before it falls over or takes so long that it's impractical to run

In [2]:
class GM_GROWTH_8YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.gross_margin ]
    window_length = 8
    def compute(self, today, assets, out, gm):
        out[:] = gmean([gm[-1]+1, gm[-2]+1, gm[-3]+1, gm[-4]+1, gm[-5]+1, gm[-6]+1, gm[-7]+1, gm[-8]+1])-1
        
class GM_STABILITY_8YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.gross_margin ]
    window_length = 9
    def compute(self, today, assets, out, gm):
        out[:] = gm[-8]#-gm[-2]#np.std([gm[-1]-gm[-2],gm[-2]-gm[-3],gm[-3]-gm[-4],gm[-4]-gm[-5],gm[-5]-gm[-6],gm[-6]-gm[-7],gm[-7]-gm[-8]]) #,gm[-8]-gm[-9]
        
class ROA_GROWTH_8YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.roa ]
    window_length = 9
    def compute(self, today, assets, out, roa):
        out[:] = gmean([roa[-1]/100+1, roa[-2]/100+1,roa[-3]/100+1,roa[-4]/100+1,roa[-5]/100+1,roa[-6]/100+1,roa[-7]/100+1,roa[-8]/100+1])-1
        
class ROIC_GROWTH_8YR(CustomFactor):
    inputs = [ morningstar.operation_ratios.roic ]
    window_length = 9
    def compute(self, today, assets, out, roic):
        out[:] = gmean([roic[-1]/100+1, roic[-2]/100+1,roic[-3]/100+1,roic[-4]/100+1,roic[-5]/100+1,roic[-6]/100+1,roic[-7]/100+1,roic[-8]/100+1])-1
In [140]:
def make_pipeline2():
    initial_screen = filter_universe()
    
    ta = morningstar.balance_sheet.total_assets.latest
    cace = morningstar.balance_sheet.cash_and_cash_equivalents.latest
    cd = morningstar.balance_sheet.current_debt.latest # same as short-term debt?
    mi = morningstar.balance_sheet.minority_interest.latest
    ltd = morningstar.balance_sheet.long_term_debt.latest # check same?
    ps = morningstar.balance_sheet.preferred_stock.latest # check same?
    
    #mi = np.where(mi.isnan(), 0, mi) 
    
    np.where(ta.isfinite(),ta,0)
    #np.where(ps.isnan(),0,ps)
    #np.where(mi.isnan(),0,mi)
    #mi[np.isnan(mi)]=0

    sta = STA()
    sta_rank = sta.rank(mask=initial_screen, ascending=False)
    snoa = SNOA()
    snoa_rank = snoa.rank(mask=initial_screen, ascending=False)
    
    combined_forensic = (sta_rank+snoa_rank)/2.0
    combined_forensic_rank = combined_forensic.rank(ascending=True)
    
    # This filter screens out the worst 10% of stocks by forensic rank, i.e. the 10% with the lowest rank
    passes_forensic = combined_forensic_rank.percentile_between(0,90)
    #fails_forensic = combined_forensic_rank.percentile_between(90,100)
    
    value = Value()
    value_pass_rank = value.rank(mask=passes_forensic, ascending=False)
    #value_fail_rank = value.rank(mask=passes_forensic)
    
    passes_value = value_pass_rank.percentile_between(0,10) # take the 10% highest ranked stocks by value
    
    # These are the elements of the f-score, or financial strength measure
    roa=ROA()
    fcfta=FCFTA()
    roa_growth=ROA_GROWTH()
    fcfta_roa=FCFTA_ROA()
    fcfta_growth=FCFTA_GROWTH()
    ltd_growth=LTD_GROWTH()
    cr_growth=CR_GROWTH()
    gm_growth=GM_GROWTH()
    atr_growth=ATR_GROWTH()
    neqiss = NEQISS()
    
    fin_strength = roa+fcfta+roa_growth+fcfta_roa+fcfta_growth+ltd_growth+cr_growth+gm_growth+atr_growth+neqiss
    fin_strength_rank = fin_strength.rank(mask=passes_value)

    
    gm_growth_2yr = GM_GROWTH_2YR()
    gm_growth_2yr_rank = gm_growth_2yr.rank(ascending=False)
    gm_stability_2yr = GM_STABILITY_2YR()
    gm_stability_2yr_rank = gm_stability_2yr.rank(ascending=False)
    roa_growth_2yr = ROA_GROWTH_2YR()
    roa_growth_2yr_rank = roa_growth_2yr.rank(ascending=False)
    roic_growth_2yr = ROIC_GROWTH_2YR()
    roic_growth_2yr_rank = roic_growth_2yr.rank(ascending=False)
    
    dollar_volume = AverageDollarVolume(window_length=30) # factor
    
    #high_dollar_volume = (dollar_volume > 10000000)
    #high_dollar_volume = dollar_volume.percentile_between(90,100) # factor method returning a filter
    
    #is_tradeable = high_dollar_volume & close_price_filter
    #fscore= np.where(ta>0,True,False)
    
    return Pipeline(
    columns={
            #'ta':ta,
            #'cace':cace,
            #'cd':cd,
            #'mi':mi,
            #'ltd':ltd,
            #'ps':ps,
            #'sta' : sta,
            #'sta_rank' : sta_rank,
            #'snoa' : snoa,
            #'snoa_rank' : snoa_rank,
            'combined_forensic' : combined_forensic,
            'comb_forensic_rank' : combined_forensic_rank,
            'roa': roa,
            'value':value,
            'value_rank': value_pass_rank,
            'passes_value':passes_value,
            'passes_forensic':passes_forensic,
            'gm_growth':gm_growth_2yr,
            'gm_growth_rank':gm_growth_2yr_rank,
            'gm_stability':gm_stability_2yr,
            'gm_stability_rank':gm_stability_2yr_rank,
            'roa_growth':roa_growth_2yr,
            'roa_growth_rank':roa_growth_2yr_rank,
            'roic_growth':roic_growth_2yr,
            'roic_growth_rank':roic_growth_2yr_rank
        },
        screen= passes_forensic
    )