Back to Community
How To Weight By Fundamental Value?

Hi,

I was wondering if it was possible to weight the stocks by their FCF Yield value.

I know it would be something along the lines of:

weight = (this_stocks_value)/(sum_of_all_values)  

However, I'm not entirely sure how to do this, can someone show me how? I'm aiming to assign a higher weight to stocks with a higher FCF Yield.

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
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.data import builtin 
from quantopian.pipeline.filters import QTradableStocksUS
import quantopian.pipeline.factors as Factors
from quantopian.pipeline.data.builtin import USEquityPricing
import pandas as pd
import time
from random import randint
def initialize(context):
    context.leverage = 1.0
    schedule_function(rebalance, date_rules.month_start(), time_rules.market_close(hours=1))
    schedule_function(record_vars, date_rules.month_start(), time_rules.market_close())
    attach_pipeline(make_pipeline(), 'my_pipeline')
def make_pipeline():
    base_universe = QTradableStocksUS()
    USEP = builtin.USEquityPricing
    base = USEP.volume.latest
    Base_Lower_Bound = 70
    Base_Upper_Bound = 100
    Filter_1 = morningstar.valuation_ratios.fcf_yield.latest
    Filter_1_Upper_Bound = 100
    Filter_1_Lower_Bound = 70
    Filter_1 = Filter_1.percentile_between(Filter_1_Lower_Bound, Filter_1_Upper_Bound, mask = base_universe)
    long_mask = Filter_1
    longs = base.percentile_between(Base_Lower_Bound, Base_Upper_Bound, mask=long_mask)
    return Pipeline(
        columns = {
            'LONGS': longs,
        },
        screen = base_universe
    )
    return Pipeline()
def before_trading_start(context, data):
    context.outputs = pipeline_output('my_pipeline')
    context.longs = context.outputs[context.outputs['LONGS']].index
    context.long_weight = assign_weights_longs(context)
def assign_weights_longs(context):
    context.weight = len(context.longs)
    if context.weight > 0:
        long_weight = context.leverage / (len(context.longs)*2)
    if context.weight > 0:
        return long_weight
def record_vars(context, data):
    longs = 0
    for position in context.portfolio.positions.itervalues():
        if position.amount > 0:
            longs += 1
    record(leverage=context.account.leverage, long_count=longs)
def rebalance(context,data):
    
    order_target_percent(sid(23921), 0.25)
    order_target_percent(sid(26807), 0.25)
    
    stocks = [sid(26807), sid(23921)]
    
    for security in context.portfolio.positions:
        if security not in context.longs and data.can_trade(security) and security not in stocks:
            order_target_percent(security, 0)
    for security in context.longs:
        if context.long_weight > 0:
            if data.can_trade(security):
                order_target_percent(security, context.long_weight)
There was a runtime error.
4 responses

Some routes to consider, log values preview, and comments in the code.

Clone Algorithm
5
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
'''
Log preview of values can be set to always show.
Add columns in pipe and they'll show up.
Or define details for specific columns.

With breakpoint on this line, examine the variable - out
    if 'log_pipe_done' not in context:         # show pipe info once
'''

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data import builtin
from quantopian.pipeline.filters import QTradableStocksUS
import quantopian.pipeline.factors as Factors
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.classifiers.fundamentals import Sector
import pandas as pd
def initialize(context):
    context.special_stocks = [sid(26807), sid(23921)]
    context.leverage = 1.0
    schedule_function(trade,   date_rules.month_start(), time_rules.market_close(hours=1))
    schedule_function(records, date_rules.month_start(), time_rules.market_close())
    attach_pipeline(make_pipeline(), 'pipeline')
def make_pipeline():
    fcf = Fundamentals.fcf_yield.latest
    m   = QTradableStocksUS() & fcf.notnull()  # mask
    
    fcfvals = fcf.rank(mask=m).demean()  # ranked low to high, demean moves middle toward 0
    
    # Raw fcf have outliers, you might want to find a way to remove those.
    # What rank() does is tame them.
    
    # original filter but it has nans
    #fcf_filter  = fcf.percentile_between(70, 100, mask=m)
    
    # midrange after demean
    #fcf_filter =  fcfvals.percentile_between(30, 70, mask=m)
    
    # An alternative, ~ (tilde) means not, so this would take 0-30 and 70-100
    fcf_filter = ~fcfvals.percentile_between(30, 70, mask=m)
    
    #fcf_filter = ~fcf.rank(mask=m).demean().percentile_between(30, 70, mask=m) # fcf instead of fcfvals
    
    m &= fcf_filter     # adding to mask with &=
    
    return Pipeline(
        screen  = m & Sector().notnull(),
        columns = {
            'fcf'    : fcf,
            'fcfvals': fcfvals,
            'vol'    : builtin.USEquityPricing.volume.latest,  # unused
        }
    )
def before_trading_start(context, data):
    out   = pipeline_output('pipeline')
    ''' norm() is one normalization route, should make positives total 1 and negatives total -1
    debugger output ...
        context.weight.sum()
        float: 1.7763568394e-15                     <== this is essentially zero
        context.weight[context.weight > 0].sum()
        float: 1.0
        context.weight[context.weight < 0].sum()
        float: -1.0
    '''
    context.weight = norm(context, out['fcf'])     
    #context.weight = out['fcf']     # dataframe to series
    #context.weight = assign_weights_longs(context)
    out['wght'] = context.weight  # want these made visible in pipe preview
    
    #if 1 or 'log_pipe_done' not in context:    # show pipe info always
    if 'log_pipe_done' not in context:         # show pipe info once
        log_pipe(context, data, out, 4)
        #log_pipe(context, data, out, 4, details=['alpha', 'beta', ... etc])
def assign_weights_longs(context):
    context.weight = len(context.weight)
    if context.weight > 0:
        long_weight = context.leverage / (len(context.weight)*2)
    if context.weight > 0:
        return long_weight
def records(context, data):
    longs = shrts = 0
    for position in context.portfolio.positions.itervalues():
        if   position.amount > 0: longs += 1
        elif position.amount < 0: shrts += 1
    record(
        leverage=context.account.leverage, 
        longs=longs,
        shrts=shrts,
    )
def norm(c, d):    # d data, it's a series, normalize it pos, neg separately
    # dont return to the same df or it would create nans
    d = d[ d == d ]    # no nans
    if d.min() >= 0 or d.max() <= 0:
        d -= d.mean()
    pos  = d[ d > 0 ]
    neg  = d[ d < 0 ]
    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 log_pipe(context, data, z, num, details=None):
    ''' Log info about pipeline output or, z can be any DataFrame or Series
    https://www.quantopian.com/posts/overview-of-pipeline-content-easy-to-add-to-your-backtest
    '''
    # Options
    log_nan_only = 0          # Only log if nans are present
    show_sectors = 0          # If sectors, do you want to see them or not
    show_sorted_details = 1   # [num] high & low securities sorted, each column

    if 'log_init_done' not in context:
        log.info('${}    {} to {}'.format('%.0e' % (context.portfolio.starting_cash), 
                get_environment('start').date(), get_environment('end').date()))
    context.log_init_done = 1

    if not len(z):
        log.info('Empty')
        return

    # Series ......
    context.log_pipe_done = 1 ; padmax = 6
    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(6, len(str(z.max())))
            log.info('{}{}{}   Series {}  len {}'.format('min' .rjust(pad+5),
                'mean'.rjust(pad+5), 'max' .rjust(pad+5),  z.name, len(z)))
            log.info('{}{}{} {}'.format(str(z.min()) .rjust(pad+5),
                str(z.mean()).rjust(pad+5), str(z.max()) .rjust(pad+5), nan_count
            ))
        return

    # DataFrame ......
    content_min_max = [ ['','min','mean','max',''] ] ; content = ''
    for col in z.columns:
        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, 6, len(str(z[col].max())) )
        content_min_max.append([col, str(z[col] .min()), str(z[col].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 details == None: details = z.columns
    for detail in details:
        if detail == 'sector': 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)
def trade(context,data):
    for s in context.special_stocks:
        #break
        order_target_percent(s, 0.25)

    idx = context.weight.index
    
    for s in context.portfolio.positions:
        if s in idx: continue
        if s in context.special_stocks: continue
        if not data.can_trade(s):       continue
        order_target(s, 0)
        
    for s in idx:
        if not data.can_trade(s): continue
        #if context.weight[s] > 0:
        if 1:  # including short here for illustration
            order_target_percent(s, context.weight[s] * .5)  # multiplier with leverage in mind
            # minus sign to flip short, long, sometimes changes negative to positive returns
            #order_target_percent(s, -context.weight[s])

There was a runtime error.

I would use this:

def to_weights(factor):  
    demeaned_vals = factor - factor.mean()  
    return demeaned_vals / demeaned_vals.abs().sum()  

Ah, I forgot. The pipeline factors have the .zscore() method too.

Thank you very much for your help