Back to Community
Any advice on making this algorithm satisfy the risk requirements?

Greetings,
This is my first algorithm on Quantopian. I can't figure out how to make it satisfy the style requirements, and with certain adjustments to only funds it trades outside the Quantopian universe. I would appreciate any advice on how to make it:
- Invest all of the capital given
- Satisfy investment style requirements
- Trade only within the Quantopian Tradable universe
EDIT:
I would like to clarify that QTradableStocksUS() is provided both as a mask for all factors and as part of the screen

Clone Algorithm
14
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
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.experimental import risk_loading_pipeline  
from quantopian.pipeline.factors import Returns, MarketCap
from quantopian.pipeline.filters import QTradableStocksUS
MAX_GROSS_EXPOSURE = 1
MAX_POSITION_CONCENTRATION = 0.03
RETURNS_LOOKBACK_DAYS = 5
def initialize(context):
    algo.schedule_function(
        rebalance,
        algo.date_rules.week_start(days_offset=0),
        algo.time_rules.market_open(hours=1, minutes=30)
    )
    algo.attach_pipeline(make_pipeline(context), 'vbux_algo')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_loading_pipeline')
def make_pipeline(context):
    universe = QTradableStocksUS()
    recent_returns = Returns(
        window_length=RETURNS_LOOKBACK_DAYS, 
        mask=universe
    )
    mcap = MarketCap(
        window_length = RETURNS_LOOKBACK_DAYS,
        mask=universe
    )
    mcap_zscore = mcap.zscore()
    recent_returns_zscore = recent_returns.zscore()
    low_rets = recent_returns_zscore.percentile_between(0,2)
    high_rets = recent_returns_zscore.percentile_between(98,100)
    low_cap = mcap_zscore.percentile_between(0,10)
    high_cap = mcap_zscore.percentile_between(90,100)
    securities_to_trade =  (high_cap & high_rets) | (low_cap | low_rets) & QTradableStocksUS()
    balanscore = (-mcap_zscore-(0.2*recent_returns_zscore))
    pipe = Pipeline(
        columns={
            'recent_returns_zscore': recent_returns_zscore,
            'balanscore': balanscore
        },
        screen=securities_to_trade
    )
    return pipe
def before_trading_start(context, data):
    context.output = algo.pipeline_output('vbux_algo')
    context.recent_returns_zscore = context.output['recent_returns_zscore']
    context.risk_loading_pipeline = algo.pipeline_output('risk_loading_pipeline')
    context.balanco = context.output['balanscore']
def rebalance(context, data):
    objective = opt.MaximizeAlpha(context.balanco)
    max_gross_exposure = opt.MaxGrossExposure(MAX_GROSS_EXPOSURE)
    max_position_concentration = opt.PositionConcentration.with_equal_bounds(
        -MAX_POSITION_CONCENTRATION,
        MAX_POSITION_CONCENTRATION
    )
    dollar_neutral = opt.DollarNeutral()
    constrain_sector_style_risk = opt.experimental.RiskModelExposure(  
        risk_model_loadings=context.risk_loading_pipeline, 
        version=opt.Newest,  
        min_momentum=-0.05,  
        max_momentum=0.05,  
    )
    constraints = [
        dollar_neutral,
        max_gross_exposure,
        max_position_concentration,
        constrain_sector_style_risk,
    ]
    algo.order_optimal_portfolio(objective, constraints)
There was a runtime error.
6 responses

You might want to start with $10 million initial capital instead of $100,000. Also try and trade with a larger top and bottom percentile for your factors.

Hello Malloy,

I've added some constraints (sector neutral & beta neutral). Unfortunately, by construction, your algo is too focused on short terme reversal factor. Try to relax pipeline filters or add another factor to mitigate this.

Good luck !

Clone Algorithm
12
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 pandas as pd
import quantopian.algorithm as algo
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.experimental import risk_loading_pipeline  
from quantopian.pipeline.factors import Returns, MarketCap, RollingLinearRegressionOfReturns
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.classifiers.fundamentals import Sector
MAX_GROSS_EXPOSURE = 1
MAX_POSITION_CONCENTRATION = 0.03
RETURNS_LOOKBACK_DAYS = 5
def initialize(context):
    algo.schedule_function(
        rebalance,
        algo.date_rules.week_start(days_offset=0),
        algo.time_rules.market_open(hours=1, minutes=30)
    )
    algo.attach_pipeline(make_pipeline(context), 'vbux_algo')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_loading_pipeline')
def make_pipeline(context):
    universe = QTradableStocksUS()
    recent_returns = Returns(
        window_length=RETURNS_LOOKBACK_DAYS, 
        mask=universe
    )
    mcap = MarketCap(
        window_length = RETURNS_LOOKBACK_DAYS,
        mask=universe
    )
    mcap_zscore = mcap.zscore()
    recent_returns_zscore = recent_returns.zscore()
    low_rets = recent_returns_zscore.percentile_between(0,2)
    high_rets = recent_returns_zscore.percentile_between(98,100)
    low_cap = mcap_zscore.percentile_between(0,10)
    high_cap = mcap_zscore.percentile_between(90,100)
    securities_to_trade =  (high_cap & high_rets) | (low_cap | low_rets) & QTradableStocksUS()
    balanscore = (-mcap_zscore-(0.2*recent_returns_zscore))
    beta = 0.66*RollingLinearRegressionOfReturns(
                    target=sid(8554),
                    returns_length=5,
                    regression_length=260,
                    mask=universe & Sector().notnull()
                    ).beta + 0.33*1.0

    pipe = Pipeline(
        columns={
            'recent_returns_zscore': recent_returns_zscore,
            'balanscore': balanscore,
            'sector': Sector(),
            'beta': beta,
        },
        screen=securities_to_trade & Sector().notnull() & beta.notnull()
    )
    return pipe
def before_trading_start(context, data):
    context.output = algo.pipeline_output('vbux_algo')
    context.recent_returns_zscore = context.output['recent_returns_zscore']
    context.risk_loading_pipeline = algo.pipeline_output('risk_loading_pipeline')
    context.balanco = context.output['balanscore']
def rebalance(context, data):
    objective = opt.MaximizeAlpha(context.balanco)
    max_gross_exposure = opt.MaxGrossExposure(MAX_GROSS_EXPOSURE)
    max_position_concentration = opt.PositionConcentration.with_equal_bounds(
        -MAX_POSITION_CONCENTRATION,
        MAX_POSITION_CONCENTRATION
    )
    dollar_neutral = opt.DollarNeutral()
    constrain_sector_style_risk = opt.experimental.RiskModelExposure(  
        risk_model_loadings=context.risk_loading_pipeline, 
        version=opt.Newest,  
    )
    sector_neutral = opt.NetGroupExposure.with_equal_bounds(
        labels=context.output.sector,
        min=-0.05,
        max=0.05,
    )
    
    beta_neutral = opt.FactorExposure(
        pd.DataFrame(context.output.beta),
        min_exposures={'beta': -0.25},
        max_exposures={'beta': 0.25},
    )
    constraints = [
        dollar_neutral,
        max_gross_exposure,
        max_position_concentration,
        constrain_sector_style_risk,
        sector_neutral,
        beta_neutral,
    ]
    algo.order_optimal_portfolio(objective, constraints)
There was a runtime error.

Thank you Mathieu, these changes are very interesting.
I'm a little confused about the style focus. The alpha is weighted towards size, not recent returns.
It still seems to not be dollar neutral. Any ideas?

Hi Maloy,

I changed @Mathieu's version slightly so it meets all contest requirements. I had to relax your Pipeline outputs a bit, as the Optimizer was having difficulty finding a portfolio that meets all requirements. Good luck and be careful not to overfit (this one likely is).

:)

Clone Algorithm
7
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 pandas as pd
import quantopian.algorithm as algo
import quantopian.optimize as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.experimental import risk_loading_pipeline  
from quantopian.pipeline.factors import Returns, MarketCap, RollingLinearRegressionOfReturns
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.classifiers.fundamentals import Sector
MAX_GROSS_EXPOSURE = 1
MAX_POSITION_CONCENTRATION = 0.01
RETURNS_LOOKBACK_DAYS = 5
def initialize(context):
    algo.schedule_function(
        rebalance,
        algo.date_rules.week_start(days_offset=0),
        algo.time_rules.market_open(hours=1, minutes=30)
    )
    algo.attach_pipeline(make_pipeline(context), 'vbux_algo')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk_loading_pipeline')
def make_pipeline(context):
    universe = QTradableStocksUS()
    recent_returns = Returns(
        window_length=RETURNS_LOOKBACK_DAYS, 
        mask=universe
    )
    mcap = MarketCap(
        window_length = RETURNS_LOOKBACK_DAYS,
        mask=universe
    )
    mcap_zscore = mcap.zscore()
    recent_returns_zscore = recent_returns.zscore()
    low_rets = recent_returns_zscore.percentile_between(0,5)
    high_rets = recent_returns_zscore.percentile_between(95,100)
    low_cap = mcap_zscore.percentile_between(0,10)
    high_cap = mcap_zscore.percentile_between(90,100)
    securities_to_trade =  (high_cap & high_rets) | (low_cap | low_rets) & QTradableStocksUS()
    balanscore = (-(recent_returns_zscore))
    # beta = 0.66*RollingLinearRegressionOfReturns(
    #                 target=sid(8554),
    #                 returns_length=5,
    #                 regression_length=260,
    #                 mask=universe & Sector().notnull()
    #                 ).beta + 0.33*1.0

    pipe = Pipeline(
        columns={
            'recent_returns_zscore': recent_returns_zscore,
            'balanscore': balanscore,
            'sector': Sector(),
            # 'beta': beta,
        },
        screen=securities_to_trade & Sector().notnull() #& beta.notnull()
    )
    return pipe
def before_trading_start(context, data):
    context.output = algo.pipeline_output('vbux_algo')
    context.recent_returns_zscore = context.output['recent_returns_zscore']
    context.risk_loading_pipeline = algo.pipeline_output('risk_loading_pipeline')
    context.balanco = context.output['balanscore']
def rebalance(context, data):
    objective = opt.MaximizeAlpha(context.balanco)
    max_gross_exposure = opt.MaxGrossExposure(MAX_GROSS_EXPOSURE)
    max_position_concentration = opt.PositionConcentration.with_equal_bounds(
        -MAX_POSITION_CONCENTRATION,
        MAX_POSITION_CONCENTRATION
    )
    dollar_neutral = opt.DollarNeutral()
    constrain_sector_style_risk = opt.experimental.RiskModelExposure(  
        risk_model_loadings=context.risk_loading_pipeline, 
        version=opt.Newest,  
        min_short_term_reversal=-0.1, 
        max_short_term_reversal=0.1, 
    )
    sector_neutral = opt.NetGroupExposure.with_equal_bounds(
        labels=context.output.sector,
        min=-0.05,
        max=0.05,
    )
    
    # beta_neutral = opt.FactorExposure(
    #     pd.DataFrame(context.output.beta),
    #     min_exposures={'beta': -0.25},
    #     max_exposures={'beta': 0.25},
    # )
    constraints = [
        dollar_neutral,
        max_gross_exposure,
        max_position_concentration,
        constrain_sector_style_risk,
        sector_neutral,
        # beta_neutral,
    ]
    algo.order_optimal_portfolio(objective, constraints)
There was a runtime error.

Welp, should be entertaining if not educational. Kind of outdid myself this time.

Clone Algorithm
12
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
'''
https://www.quantopian.com/posts/any-advice-on-making-this-algorithm-satisfy-the-risk-requirements

See weights before and after optimize (not added)
https://www.quantopian.com/posts/optimization-weights-logging

See trades (not added here)
http://quantopian.com/posts/track-orders

These are present ...

See outputs of pipe or other
https://quantopian.com/posts/overview-of-pipeline-content-easy-to-add-to-your-backtest

Others
norm        https://www.quantopian.com/posts/normalizing-positive-and-negative-values-separately
record_pnl  https://www.quantopian.com/posts/record-pnl-per-stock
log_pnl     https://www.quantopian.com/posts/generate-a-report-at-the-end-of-backtest
'''

import pandas as pd
import quantopian.algorithm as algo
import quantopian.optimize  as opt
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.experimental import risk_loading_pipeline
from quantopian.pipeline.factors import Returns, MarketCap, RollingLinearRegressionOfReturns
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.classifiers.fundamentals import Sector

RETURNS_LOOKBACK_DAYS = 21

def trade(context, data):  # moved up b/c working with it a lot
    c = context            # brief, for readability, efficiency/speed in editing.

    conc  = 1.0 / len(c.output)  #0.03             # MAX_POS_CONCENTRATION
    conc  = conc  # Just neutralizing in case not used below ... Local variable 'conc' assigned to, never used
    alpha = c.balanco  # + c.rtrns
    #alpha /= alpha[alpha > 0].sum()     # some normalization, opt happier, less work for leverage etc

    try:  # This try: except: is here b/c in experimenting there are times with all inf et al. To view values.
        algo.order_optimal_portfolio(
            # MaximizeAlpha invests as much as it is allowed to, in the strongest signals
            #  (mainly based on PositionConcentration) and ignores the rest when it runs out of room.
            #objective   = opt.MaximizeAlpha(alpha),

            # TargetWeights can wind up with more stocks, every one of them in the game
            #   so long as they are normalized for a leverage constraint (MaxGrossExposure)
            #   or other constraints also allow for all of them (?)
            objective   = opt.TargetWeights(alpha),

            # -alpha multiplies all values by -1, flipping long, short, a test that can be insightful.
            # In an algo that is consistently downward, great, easily solved, just add a minus sign.
            #objective   = opt.TargetWeights(  -alpha  ),

            # TargetWeights with most original settings, before balanscore.demean()
            #   1 of 2 Structural Constraints Met, 5 of 7 Risk Constraints Met
            #   Returns were a disaster.

            # Note that TargetWeights by itself with no constraints,
            #   yet normalized properly for leverage, is a good test to do.
            #   Especially useful when working with pipeline filters etc.

            constraints = [  # turn each of them on one at a time
                #opt.DollarNeutral(),
                opt.MaxGrossExposure(1.0),
                #opt.PositionConcentration.with_equal_bounds(-conc, conc),
                #opt.experimental.RiskModelExposure(         # sector_style_risk
                #    risk_model_loadings = c.risk_pipe,
                #    version = opt.Newest,
                #),
                ##opt.NetGroupExposure.with_equal_bounds(     # sector_neutral
                #    labels = c.output.sector,
                #    min = -0.05,
                #    max =  0.05,
                #),
                #opt.FactorExposure(                         # beta_neutral
                #    pd.DataFrame(c.output.beta),
                #    min_exposures = {'beta': -0.25},
                #    max_exposures = {'beta':  0.25},
                #),
            ]
        )
    except Exception as e:
        print('.\n\n\n    Exception: {}    \n\n.'.format(e))  # making this more visible
        log_data(c, alpha   , 4)  # log just alpha
        assert(0)  # opt crash, see Logs tab at bottom. (Q: Why is Exception sometimes None?)

def make_pipeline(context):
    '''
        Short variable names & vertical alignment for readability, to some.
        Always using mask progressively. Doesn't always matter, but when it does, it's big.
        'm &=' is shorthand for m = m & <something>, adding to the mask, often easy to comment out for a test.
    '''
    m          = QTradableStocksUS()
    m         &= Sector().notnull()    # early so nulls don't take part in calculations
    m         &= USEquityPricing.volume.latest.percentile_between(60, 98, mask=m)
    mcap       = MarketCap(window_length=RETURNS_LOOKBACK_DAYS, mask=m)
    rtrns      = Returns  (window_length=RETURNS_LOOKBACK_DAYS, mask=m)
    mcap       = mcap .zscore(mask=m)  # try commenting out these zscore, shows how important they are.
    low_cap    = mcap .percentile_between( 0, 10, mask=m)
    high_cap   = mcap .percentile_between(90,100, mask=m)
    rtrns      = rtrns.zscore(mask=m)
    low_rets   = rtrns.percentile_between( 0,  2, mask=m)
    high_rets  = rtrns.percentile_between(98,100, mask=m)
    balanscore = -mcap - (    0.1     * rtrns)
    #balanscore = mcap   # checks that verify the above line is valid
    #balanscore = rtrns
    #balanscore = balanscore.zscore(mask=m)
    # Because the following line makes such a huge difference (too sensitive), maybe some overfitting around here
    m         &= balanscore.percentile_between(3, 97, mask=m)   # trimming edges
    m         &= ( (high_cap & high_rets) | (low_cap | low_rets) )
    #                                                ^____ & | | (?)  not & | & (?)
    beta       = 0.66 * RollingLinearRegressionOfReturns(
                    target = sid(8554),
                    returns_length = 2,
                    regression_length = 126,  # 126 might be what Q uses
                    mask=m
                    ).beta + 0.33 * 1.0   # by the way, what happens with .alpha instead of .beta?
    m &= beta.notnull()
    # These are two lines in one, with the semi-colon, to easily remove/comment_out, or adjust value.
    # When using this, notice the first logging window line change, saying, like, 102 in pipe (number of stocks)
    #betalimit = 4.5   ;   m &= ( (beta < betalimit) & (beta > -betalimit) )

    return Pipeline(
        columns = {
            'rtrns'     : rtrns,
            'sector'    : Sector(),
            'beta'      : beta,
            'balanscore': balanscore  #.demean(),
        },
        screen = m
    )

def before_trading_start(context, data):
    c = context
    c.risk_pipe = algo.pipeline_output('risk')
    c.output    = algo.pipeline_output('pipe')
    c.rtrns     = c.output['rtrns']
    c.balanco   = norm(c,  c.output['balanscore']  )  # normalizing positive & negative values separately
    #c.balanco   = c.output['balanscore']
    # original:
    #                      min          mean          max
    # balanscore     -2.272618      0.429645     1.797102    <== balanced?

    overview_line(context, data)

    # show pipe info
    if c.do_log_preview:
        log_data(c, c.output , 4)
        log_data(c, c.rtrns  , 4)
        #log_data(c, c.risk_pipe, 4)
    c.do_log_preview = 0            # for just once
    #assert(0)  # deliberate halt to take a look at logging from the previous lines

    if context.record_pnls:
        return  # would chart some pnl's in record_pnl() so has to skip the others

    longs = 0 ; shrts = 0
    for pos in c.portfolio.positions.values():
        if pos.amount > 0: longs += 1
        if pos.amount < 0: shrts += 1

    record(longs = longs)
    record(shrts = shrts)
    record(pos   = len(c.portfolio.positions))
    record(cash  = c.portfolio.cash)
    record(lv    = c.account.leverage)

def initialize(context):
    context.do_log_preview = 1    # can set to 0, a way to toggle this off when it becomes annoying

    # try to force full fills as a temporary test, gathering information
    #set_slippage(slippage.FixedSlippage(spread = 0))

    #schedule_function(trade, date_rules.every_day(),  time_rules.market_open(hours=1, minutes=30))
    #schedule_function(trade, date_rules.month_start(0),time_rules.market_open(hours=1, minutes=30))
    schedule_function(trade, date_rules.week_start(0), time_rules.market_open(hours=1, minutes=30))

    algo.attach_pipeline(make_pipeline(context),  'pipe')
    algo.attach_pipeline(risk_loading_pipeline(), 'risk')

    # for record_pnl()
    context.record_pnls = 0  # will replace records with some stocks and their pnl
    if context.record_pnls:
        schedule_function(record_pnl, date_rules.every_day(), time_rules.market_close())
    context.day_count = 0
    context.pnl_sids  = [ ]
    context.pnl_sids_exclude  = [ ]

    # For log_pnl()
    context.log_pnl_mode = 0  #'daily'  # 'daily' or anything else for just at the end of the run
    context.last_day = get_environment('end').date()
    schedule_function(log_pnl, date_rules.every_day(), time_rules.market_close())

def log_data(context, z, num, fields=None):
    ''' Log info about pipeline output or, z can be any DataFrame or Series
    https://quantopian.com/posts/overview-of-pipeline-content-easy-to-add-to-your-backtest
    '''
    if not len(z):
        log.info('Empty pipe')
        return

    # Options
    log_nan_only = 0          # Only log if nans are present.
    show_sectors = 1          # If sectors, see them or not.
    show_sorted_details = 1   # [num] high & low securities sorted, each column.
    padmax = 6                # num characters for each field, starting point.

    # Change index to just symbols for readability, meanwhile, right-aligned
    z = z.rename(index=dict(zip(z.index.tolist(), [i.symbol.rjust(6) for i in z.index.tolist()])))

    # Series ......
    if 'Series' in str(type(z)):    # is Series, not DataFrame
        nan_count = len(z[z != z])
        nan_count = 'NaNs {}/{}'.format(nan_count, len(z)) if nan_count else ''
        if (log_nan_only and nan_count) or not log_nan_only:
            pad = max( padmax, len('%.5f' % z.max()) )
            log.info('{}{}{}   Series  len {}'.format('min'.rjust(pad+5),
                'mean'.rjust(pad+5), 'max'.rjust(pad+5), len(z)))
            log.info('{}{}{} {}'.format(
                ('%.5f' % z.round(6). min()).rjust(pad+5),
                ('%.5f' % z.round(6).mean()).rjust(pad+5),
                ('%.5f' % z.round(6). max()).rjust(pad+5),
                nan_count
            ))
            log.info('High\n{}'.format(z.sort_values(ascending=False).head(num)))
            log.info('Low\n{}' .format(z.sort_values(ascending=False).tail(num)))
        return

    # DataFrame ......
    content_min_max = [ ['','min','mean','max',''] ] ; content = ''
    for col in z.columns:
        try: z[col].max()
        except: continue   # skip non-numeric
        if col == 'sector' and not show_sectors: continue
        nan_count = len(z[col][z[col] != z[col]])
        nan_count = 'NaNs {}/{}'.format(nan_count, len(z)) if nan_count else ''
        padmax    = max( padmax, len(str(z[col].max())) ) ; mean_ = ''
        if len(str(z[col].max())) > 8 and 'float' in str(z[col].dtype):
            z[col] = z[col].round(6)   # Reduce number of decimal places for floating point values
        if 'float' in str(z[col].dtype): mean_ = str(round(z[col].mean(), 6))
        elif 'int' in str(z[col].dtype): mean_ = str(round(z[col].mean(), 1))
        content_min_max.append([col, str(z[col] .min()), mean_, str(z[col] .max()), nan_count])
    if log_nan_only and nan_count or not log_nan_only:
        content = 'Rows: {}  Columns: {}'.format(z.shape[0], z.shape[1])
        if len(z.columns) == 1: content = 'Rows: {}'.format(z.shape[0])

        paddings = [6 for i in range(4)]
        for lst in content_min_max:    # set max lengths
            i = 0
            for val in lst[:4]:    # value in each sub-list
                paddings[i] = max(paddings[i], len(str(val)))
                i += 1
        headr = content_min_max[0]
        content += ('\n{}{}{}{}{}'.format(
             headr[0] .rjust(paddings[0]),
            (headr[1]).rjust(paddings[1]+5),
            (headr[2]).rjust(paddings[2]+5),
            (headr[3]).rjust(paddings[3]+5),
            ''
        ))
        for lst in content_min_max[1:]:    # populate content using max lengths
            content += ('\n{}{}{}{}     {}'.format(
                lst[0].rjust(paddings[0]),
                lst[1].rjust(paddings[1]+5),
                lst[2].rjust(paddings[2]+5),
                lst[3].rjust(paddings[3]+5),
                lst[4],
            ))
        log.info(content)

    if not show_sorted_details: return
    if len(z.columns) == 1:     return     # skip detail if only 1 column
    if fields == None: details = z.columns
    for detail in details:
        if detail == 'sector' and not show_sectors: continue
        hi = z[details].sort_values(by=detail, ascending=False).head(num)
        lo = z[details].sort_values(by=detail, ascending=False).tail(num)
        content  = ''
        content += ('_ _ _   {}   _ _ _'  .format(detail))
        content += ('\n\t... {} highs\n{}'.format(detail, str(hi)))
        content += ('\n\t... {} lows \n{}'.format(detail, str(lo)))
        if log_nan_only and not len(lo[lo[detail] != lo[detail]]):
            continue  # skip if no nans
        log.info(content)

'''
017-01-03 05:45 log_data:210 INFO Rows: 225  Columns: 4
                     min          mean          max
balanscore     -2.272618      0.429645     1.797102
      beta      0.198086      1.591189      3.29539
   rtrns     -7.944727     -0.599605     6.397996
2017-01-03 05:45 log_data:225 INFO _ _ _   balanscore   _ _ _
    ... balanscore highs
        balanscore      beta   rtrns  sector
  ELGX    1.797102  1.436419 -7.348112     206
   OPK    1.789925  1.357251 -7.944727     206
  SGEN    1.183371  1.572524 -5.218633     206
  GSAT    1.171254  1.492234 -4.390041     308
    ... balanscore lows
        balanscore      beta   rtrns  sector
   FSM   -0.562338  0.602982  4.401680     101
  SAND   -0.680211  0.512596  5.022209     101
   RIC   -0.950408  0.198086  6.397996     101
   AGN   -2.272618  0.808363  2.494167     206
2017-01-03 05:45 log_data:225 INFO _ _ _   beta   _ _ _
    ... beta highs
        balanscore      beta   rtrns  sector
  TMST    0.505691  3.295390 -0.919452     101
  BCRX    0.974728  3.293104 -3.235420     206
    TK    0.377003  3.230135 -0.276441     309
  CENX    0.794795  3.182309 -2.373135     101
    ... beta lows
        balanscore      beta   rtrns  sector
  SAND   -0.680211  0.512596  5.022209     101
    SA   -0.333804  0.512147  3.310630     101
   NMZ    0.212995  0.368561  0.537187     103
   RIC   -0.950408  0.198086  6.397996     101
2017-01-03 05:45 log_data:225 INFO _ _ _   rtrns   _ _ _
    ... rtrns highs
        balanscore      beta   rtrns  sector
   RIC   -0.950408  0.198086  6.397996     101
  SAND   -0.680211  0.512596  5.022209     101
   FSM   -0.562338  0.602982  4.401680     101
   EXK   -0.388377  0.791564  3.582733     101
    ... rtrns lows
        balanscore      beta   rtrns  sector
  GSAT    1.171254  1.492234 -4.390041     308
  SGEN    1.183371  1.572524 -5.218633     206
  ELGX    1.797102  1.436419 -7.348112     206
   OPK    1.789925  1.357251 -7.944727     206
'''

def overview_line(context, data):
    log.info('portfolio ${:,}   positions {}   {} in pipe'.format(
        int(context.portfolio.portfolio_value),
        len(context.portfolio.positions),
        len(context.output),
    ))

def norm(c, d):    # d data, it's a series, normalize it pos, neg separately
    # https://www.quantopian.com/posts/normalizing-positive-and-negative-values-separately
    # Normalizing positive and negative values separately, recombining for input to optimize.
    if not len(d): return d           # In case None
    d = d[ d == d ]                   # insure no nans
    if d.min() >= 0 or d.max() <= 0:  # If all are pos or neg, balance around 0
        d -= d.mean()
    pos  = d[ d > 0 ]
    neg  = d[ d < 0 ]

    # same number of stocks for positive & negative
    num  = min(len(pos), len(neg))
    pos  = pos.sort_values(ascending=False).head(num)
    neg  = neg.sort_values(ascending=False).tail(num)
    pos /=   pos.sum()
    neg  = -(neg / neg.sum())
    return pos.append(neg)

def record_pnl(context, data):
    # https://www.quantopian.com/posts/record-pnl-per-stock

    def _pnl_value(sec, context, data):
        pos = context.portfolio.positions[sec]
        return pos.amount * (data.current(sec, 'price') - pos.cost_basis)

    context.day_count += 1

    for s in context.portfolio.positions:
        if not data.can_trade(s): continue
        if s in context.pnl_sids_exclude: continue

        # periodically log all. will only hit if schedule is every_day unless one wants to change this ...
        if context.day_count % 126 == 0:
            log.info('{} {}'.format(s.symbol, int(_pnl_value(s, context, data))))

        # add up to 5 securities for record
        if len(context.pnl_sids) < 5 and s not in context.pnl_sids:
            context.pnl_sids.append(s)
        if s not in context.pnl_sids: continue     # limit to only them

        # record their profit and loss
        who  = s.symbol
        what = _pnl_value(s, context, data)
        record( **{ who: what } )

def log_pnl(context, data):
    '''  https://www.quantopian.com/posts/generate-a-report-at-the-end-of-backtest
        Logging pnl, not charting
        This is a bit rough so far and will show 0 for those not currently held.
        
        Looks like ... see below ...
    '''

    # log_data() may have to be off or 'Logging limit exceeded; some messages discarded'

    # Check when to run ...
    if context.log_pnl_mode != 'daily':
        # Restrict to just the end of the run
        if get_datetime().date() != context.last_day:
            return

    if not len(context.output): return
    df = context.output.copy()
    df['pnl']           = pd.Series(0, index=df.index)   # init
    df['pnl_per_rtrns'] = pd.Series(0, index=df.index)
    pos = context.portfolio.positions

    for s in df.index:
        pnl = int( pos[s].amount * (data.current(s, 'price') - pos[s].cost_basis) )
        df.loc[s, 'pnl']           = pnl
        df.loc[s, 'pnl_per_rtrns'] = int( pnl / abs(df.loc[s, 'rtrns']) )

    log_data(context, df, 8)
    '''
    Excerpt of output short term. You can see how well pnl and rtrns from pipe line up.

        2017-02-06 12:59 log_data:264 INFO Rows: 61  Columns: 6
                                min          mean          max
           balanscore      0.028573      0.484317      0.61683
                 beta      0.517965      1.644825     2.865878
                rtrns     -1.492958     -0.115033      4.42217
               sector           101         216.1          311
                  pnl         -8071         296.0        41403
        pnl_per_rtrns         -9150        -252.0        11165

        2017-02-06 12:59 log_data:279 INFO _ _ _   rtrns   _ _ _
            ... rtrns highs
                balanscore      beta     rtrns  sector    pnl  pnl_per_rtrns
            SN    0.028573  2.006688  4.422170     309  41403           9362
          ZIOP    0.261251  2.230888  2.097199     206    652            310
          CSTM    0.284729  1.654925  1.886384     101   2278           1207
          IMMU    0.302834  1.784848  1.780039     206  -7635          -4289
           DHT    0.307484  1.354312  1.735309     310   5586           3219
          DPLO    0.309326  1.872144  1.588312     206    501            315
            TK    0.328170  2.096822  1.433889     309    214            149
           MTW    0.334475  1.927583  1.357151     310  -3711          -2734

        2017-02-06 12:59 log_data:279 INFO _ _ _   pnl_per_rtrns   _ _ _
            ... pnl_per_rtrns highs
                balanscore      beta     rtrns  sector    pnl  pnl_per_rtrns
           GTE    0.616830  1.701344 -1.492958     309  16670          11165
            SN    0.028573  2.006688  4.422170     309  41403           9362
          YRCW    0.384841  2.211242  0.950713     310   4070           4280
           DHT    0.307484  1.354312  1.735309     310   5586           3219
           FSM    0.542477  1.661863 -0.722189     101   1576           2182
           DYN    0.394600  2.225754  0.710298     207   1158           1630
          CSTM    0.284729  1.654925  1.886384     101   2278           1207
          CLDX    0.585274  2.342050 -1.017063     206   1195           1174
    '''
There was a runtime error.

@Blue Seahawk
Just to address the question in the code:
I originally had (&) | (&). However, I had a hunch that either low cap or low rets would work, which turned out to be right. Thus, (&) | (|)