Back to Community
Patrick O'Shaughnessy's "Millennial Money" Value Investing Algorithm #Fundamentals

Hi,

I'm implementing the Algorithm described in the book "Millennial Money" by Patrick O'Shaughnessy.
The Checklist version is ready now and in the next couple of weeks, I'll implement the ranking version.

The threshold values are slightly different in comparison to the book and the the positions are updated quarterly.

  • Stakeholder yield < 5%. Stakeholder yield = Cash from financing 12m / Market Cap Q1
  • ROIC > 20%
    ROIC = operating_income / (invested_capital - cash)
  • CFO > Net Income (Earnings Quality)
  • EV/FCF < 15 (Value)
  • 6M Relative Strength top three-quarters of the market.
    6M Relative Strength = 6M Stock Total Return / 6M Total Return S&P500 (Momentum)
12 responses

Here is the code and the backtest results: From 2002-01-03 to 2014-12-28 6417.8% returns and 49% max drawndown.

Clone Algorithm
1878
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
"""
    Patrick O'Shaughnessy - Millennial Money
    
    1. Stakeholder yield < 5%. Stakeholder yield = Cash from financing 12m / Market Cap Q1
    2. ROIC > 20%
       ROIC = operating_income /  (invested_capital - cash)
    3. CFO > Net Income (Earnings Quality)
    4. EV/FCF < 15 (Value)
    5. 6M Relative Strength top three-quarters of the market. 
       6M Relative Strength = 6M Stock Total Return / 6M Total Return S&P500 (Momentum)
       
    The positions are updated quarterly.
"""

import pandas as pd
import numpy as np

def initialize(context):
    context.max_num_stocks = 50
    context.days = 0
    context.quarter_days = 65
    context.relative_strength_6m = {}
          
def quarter_passed(context): 
    """
    Screener results quarterly updated
    """
    return context.days % context.quarter_days == 0

def before_trading_start(context): 
    context.days += 1
    
    if not quarter_passed(context):
        return
    
    do_screening(context)
    update_universe(context.fundamental_df.columns.values)
    
    
def do_screening(context):
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.morningstar_sector_code,
            fundamentals.company_reference.country_id,
            fundamentals.company_reference.primary_exchange_id,
            fundamentals.share_class_reference.is_depositary_receipt,
            fundamentals.share_class_reference.is_primary_share,
            fundamentals.cash_flow_statement.financing_cash_flow,
            fundamentals.valuation.market_cap,
            
            fundamentals.income_statement.operating_income,
            fundamentals.balance_sheet.invested_capital,
            fundamentals.balance_sheet.cash_and_cash_equivalents,
            
            fundamentals.cash_flow_statement.operating_cash_flow,
            fundamentals.income_statement.net_income,
            fundamentals.valuation.enterprise_value,
            fundamentals.cash_flow_statement.free_cash_flow
        )

        # No Financials (103) and Real Estate (104) Stocks, no ADR or PINK, only USA
        .filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        .filter(fundamentals.company_reference.country_id == "USA")
        .filter(fundamentals.asset_classification.morningstar_sector_code != 104)
        .filter(fundamentals.share_class_reference.is_depositary_receipt == False)
        .filter(fundamentals.share_class_reference.is_primary_share == True)
        .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK")
        
        # Check for data sanity (i,e. avoid division by zero)
        .filter(fundamentals.valuation.market_cap > 0)
        .filter(fundamentals.valuation.shares_outstanding > 0)
        .filter(fundamentals.cash_flow_statement.free_cash_flow > 0)
        .filter(fundamentals.balance_sheet.invested_capital > 0)
        .filter(fundamentals.balance_sheet.cash_and_cash_equivalents > 0)
        .filter(fundamentals.balance_sheet.invested_capital != fundamentals.balance_sheet.cash_and_cash_equivalents)
        
        .filter((fundamentals.cash_flow_statement.financing_cash_flow / fundamentals.valuation.market_cap) < 0.05)
        .filter((fundamentals.income_statement.operating_income / (fundamentals.balance_sheet.invested_capital - fundamentals.balance_sheet.cash_and_cash_equivalents)) > 0.20)
       
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > fundamentals.income_statement.net_income)
        .filter((fundamentals.valuation.enterprise_value / fundamentals.cash_flow_statement.free_cash_flow) < 15)
        
        .limit(context.max_num_stocks)
    )
   
     # Update context
    context.stocks = [stock for stock in fundamental_df]
    context.fundamental_df = fundamental_df
    
    
def rebalance(context, data):
    """
        Exit all positions before starting new ones.
        Apply the Momentum Criteria
        Buy all stocks equally 
    """
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in context.fundamental_df:
            order_target_percent(stock, 0)

    # Filter out stocks without data and apply the momentum criteria
    # -0.6745 is an approximation for the top three-quarters of the market
    context.stocks = [stock for stock in context.stocks
                      if stock in data and context.relative_strength_6m[stock] > -0.6745]
   
    if len(context.stocks) == 0:
        log.info("No Stocks to buy")
        return
    
    weight = 1.0/len(context.stocks)

    log.info("Ordering %0.0f%% for each of %s (%d stocks)" % (weight * 100, ', '.join(stock.symbol for stock in context.stocks), len(context.stocks)))
    
    # buy all stocks equally
    for stock in context.stocks:
        order_target_percent(stock, weight)

    # track how many positions we're holding
    record(num_positions = len(context.fundamental_df))
    
    
def compute_relative_strength(context):
    symbol('SPY')
    prices = history(bar_count=150, frequency='1d', field='price')
    # Price % change in the last 6 months
    pct_change = (prices.ix[-130] - prices.ix[0]) / prices.ix[0]
    
    pct_change_spy = pct_change[symbol('SPY')]
    pct_change = pct_change - pct_change_spy
    if pct_change_spy != 0:
        pct_change = pct_change / abs(pct_change_spy)
    pct_change = pct_change.drop(symbol('SPY'))
    context.relative_strength_6m = pct_change
        
        
def handle_data(context, data):
    if not quarter_passed(context):
        return
    
    compute_relative_strength(context)
    rebalance(context, data)

    
We have migrated this algorithm to work with a new version of the Quantopian API. The code is different than the original version, but the investment rationale of the algorithm has not changed. We've put everything you need to know here on one page.
There was a runtime error.

Some tools and changes to consider. Try to avoid negative cash.

Clone Algorithm
534
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
"""
    Patrick O'Shaughnessy - Millennial Money
    
    1. Stakeholder yield < 5%. Stakeholder yield = Cash from financing 12m / Market Cap Q1
    2. ROIC > 20%
       ROIC = operating_income /  (invested_capital - cash)
    3. CFO > Net Income (Earnings Quality)
    4. EV/FCF < 15 (Value)
    5. 6M Relative Strength top three-quarters of the market. 
       6M Relative Strength = 6M Stock Total Return / 6M Total Return S&P500 (Momentum)
       
    The positions are updated quarterly.
"""

import pandas as pd
import numpy as np

def initialize(context):
    set_long_only()
    context.max_num_stocks = 50
    context.days = 0
    context.quarter_days = 65
    context.relative_strength_6m = {}
    
    schedule_function(func = track_orders, date_rule = date_rules.every_day())
    schedule_function(func = summary, date_rule = date_rules.every_day())
          
def quarter_passed(context): 
    """
    Screener results quarterly updated
    """
    return context.days % context.quarter_days == 0

def before_trading_start(context): 
    context.days += 1
    
    if not quarter_passed(context):
        return
    
    do_screening(context)
    update_universe(context.fundamental_df.columns.values)
    
def do_screening(context):
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.morningstar_sector_code,
            
            # no need for the other contents here
        )

        # No Financials (103) and Real Estate (104) Stocks, no ADR or PINK, only USA
        .filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        .filter(fundamentals.company_reference.country_id == "USA")
        .filter(fundamentals.asset_classification.morningstar_sector_code != 104)
        .filter(fundamentals.share_class_reference.is_depositary_receipt == False)
        .filter(fundamentals.share_class_reference.is_primary_share == True)
        .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK")
        
        # Check for data sanity (i,e. avoid division by zero)
        .filter(fundamentals.valuation.market_cap > 0)
        .filter(fundamentals.valuation.shares_outstanding > 0)
        .filter(fundamentals.cash_flow_statement.free_cash_flow > 0)
        .filter(fundamentals.balance_sheet.invested_capital > 0)
        .filter(fundamentals.balance_sheet.cash_and_cash_equivalents > 0)
        .filter(fundamentals.balance_sheet.invested_capital != fundamentals.balance_sheet.cash_and_cash_equivalents)
        
        .filter((fundamentals.cash_flow_statement.financing_cash_flow / fundamentals.valuation.market_cap) < 0.05)
        .filter((fundamentals.income_statement.operating_income / (fundamentals.balance_sheet.invested_capital - fundamentals.balance_sheet.cash_and_cash_equivalents)) > 0.20)
       
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > fundamentals.income_statement.net_income)
        .filter((fundamentals.valuation.enterprise_value / fundamentals.cash_flow_statement.free_cash_flow) < 15)
        
        .limit(context.max_num_stocks)
    )
   
    # Update context
    context.stocks = [stock for stock in fundamental_df]
    context.fundamental_df = fundamental_df
    
def rebalance(context, data):
    """
        Exit all positions before starting new ones.
        Apply the Momentum Criteria
        Buy all stocks equally 
    """
    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if context.portfolio.positions[stock].amount > 0:
            order_target_percent(stock, 0)

    # Filter out stocks without data and apply the momentum criteria
    # -0.6745 is an approximation for the top three-quarters of the market
    context.stocks = [stock for stock in context.stocks
                      if stock in data and context.relative_strength_6m[stock] > -0.6745]
   
    to_order = []
    for s in context.stocks:    
        if get_open_orders(s):
            continue
        to_order.append(s)
        
    if len(to_order) == 0:
        log.info("No Stocks to buy")
        return
    
    weight = 1.0 / len(to_order)

    log.info("Ordering %0.0f%% for each of %s (%d stocks)" % (
            weight * 100, ', '.join(stock.symbol for stock in to_order), len(to_order)
        ))
    
    # buy all stocks equally
    for stock in to_order:
        order_target_percent(stock, weight)
        
    track_orders(context, data)

    # chart how many positions rebalanced
    record(num_positions = len(to_order))
    
def compute_relative_strength(context):
    symbol('SPY')
    prices = history(bar_count=150, frequency='1d', field='price')
    # Price % change in the last 6 months
    pct_change = (prices.ix[-130] - prices.ix[0]) / prices.ix[0]
    
    pct_change_spy = pct_change[symbol('SPY')]
    pct_change     = pct_change - pct_change_spy
    if pct_change_spy != 0:
        pct_change = pct_change / abs(pct_change_spy)
    pct_change = pct_change.drop(symbol('SPY'))
    context.relative_strength_6m = pct_change
        
def handle_data(context, data):
    current = []
    for s in context.portfolio.positions:
        if context.portfolio.positions[s].amount > 0:
            current.append(s.symbol)
    if current:
        print str(current)
        
    if not quarter_passed(context):
        return
    
    if get_open_orders():
        return
    
    compute_relative_strength(context)
    rebalance(context, data)
    
def track_orders(context, data):
    '''
        Log orders after they are filled.
    '''
    try:
        context.orders
    except:
        context.orders = {}    # Keys are order id's, values are orders.
        
    orders = []          # Any current order id's that are visible, for later.
    msg1 = []; msg2 = [] # For logging.

    # Open orders
    for sec, oo_for_sid in get_open_orders().iteritems():
        sym = sec.symbol
        for o in oo_for_sid:    # Orders per security
            orders.append(o.id)
            if o.id in context.orders:
                if o.filled > context.orders[o.id]['filled']:
                    # Filled at least some
                    trade = 'Bot' if o.amount > 0 else 'Sold'
                    diff  = o.filled - context.orders[o.id]['filled']
                    msg2.append(
                        '   {} {} {} at {}\n'.format(trade, diff, sym, data[sec].price)
                    )
                if o.filled == o.amount: # Complete so drop tracking
                    del context.orders[o.id]
            else:   # New order
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:  # Limit order
                    msg2.append(
                        '{} {} {} now {} target {}\n'.format(
                            trade, o.amount, sym, data[sec].price, o.limit
                        )
                    )
                else:        # Market order
                    msg2.append(
                        '{} {} {} at {}\n'.format(
                            trade, o.amount, sym, data[sec].price
                        )
                    )
                context.orders[o.id] = o

    # Stored orders
    to_delete = []
    for id in context.orders:
        if id not in orders:
            # Completed order
            o     = context.orders[id]
            sym   = o.sid.symbol
            trade = 'Bot' if o.amount > 0 else 'Sold'
            diff  = o.amount - o.filled
            msg1.append(
                '   {} {} {} at {}'.format(trade, diff, sym, data[o.sid].price)
            )
            to_delete.append(id)
    for d in to_delete: # (can't delete while iterating above, hence this)
        del context.orders[d]

    # Logging
    for m in msg2:
        msg1.append(m)
    for m in msg1:
        log.info(m)

def summary(context, data):
    '''
        Summary processing

        https://www.quantopian.com/posts/run-summary
    '''
    # Need a couple of imports, you might need to comment these out if already imported.
    #   That's pretty much the only change that might be necessary.
    from pytz import timezone
    import re

    # Yes try/except is narly yet makes to work with set_universe etc.
    # Is there a better way?  An -| if 'books' in context: |- didn't work.
    try:
        context['books']    # See if this key exists yet.
        b = context.books   # For brevity.
    except:
        '''
            Preparation. Initialize one time.
        '''
        cash = context.portfolio.starting_cash
        context.books = {   # Starting cash value from GUI or live restart...
            'cash'          : cash,
            'init_cash'     : cash,
            'cash_low'      : cash,
            'shares'        : 0,
            'shares_value'  : 0,
            'count_buy'     : 0,       # Overall buy count, number of shares.
            'count_sell'    : 0,       # Overall sell count.
            'cnt_buy_evnts' : 0,       # Overall buy events count.
            'cnt_sel_evnts' : 0,
            'summary_print' : 0,
            'costs_total'   : 0,       # Commissions.
            'sids_seen'     : [],      # For set_universe since dynamic.
            'prep_prnt'     : '',
            'orders'        : {},      # Keep orders for accounting,
        }                              #   orders not completely filled yet.

        b = context.books

        # Environment   First/last dates and
        #   Arena: backtest or live.  Mode: daily or minute.
        env = get_environment('*')
        b['first_trading_date'] = str(env['start'].date())
        b['last_trading_date']  = str(env['end']  .date())
        b['arena'] = env['arena']
        b['mode']  = env['data_frequency']

        if b['arena'] == 'live':
            b['arena'] = 'paper'
        elif b['arena'] != 'backtest': # ie like 'IB'
            b['arena'] = 'live'

        # Show environment at the beginning of the run
        b['prep_prnt'] = ' {}\n  {}  {} to {}  {}  {}\n'.format(
            b['arena'],
            b['mode'],
            b['first_trading_date'],
            b['last_trading_date'],
            '   $' + "%.0f" % b['cash'],
            '  First bar stocks ({}) ...'.format(len(data)),
        )

        # Show current universe once
        for sec in data:
            if isinstance(sec, basestring):
                continue   # Skip any injected fetcher string keys.
            b['prep_prnt'] += (sec.symbol + ' ')
        log.info(b['prep_prnt'])

    '''
        Prepare individual securities dictionaries
          with dynamic set_universe, fetcher, IPO's appearing etc.
    '''
    for sec in data:
        if isinstance(sec, basestring):
            continue   # Skip any injected fetcher string keys.
        sym = sec.symbol
        if sym in b:
            continue
        if sec not in b['sids_seen']:
            # Scenarios with price missing ...
            price = data[sec].price if 'price' in data[sec] else 0
            b['sids_seen'].append(sec)
            b[sym] = {
                'init_price'    : price,  # Save for summary.
                'price'         : price,  # Most recent price.
                'cash_low'      : 0,      # Lowest level of cash.
                'balance'       : 0,      # For individual 'x' return.
                'shares'        : 0,
                'count_buy'     : 0,      # Individual buy number of shares.
                'count_sell'    : 0,
                'cnt_buy_evnts' : 0,      # Individual buy events count.
                'cnt_sel_evnts' : 0,
            }

    '''
        Accounting. Update the numbers, manage orders if any.
    '''
    accounting = {} # Locally, any orders ready to be counted.
    
    # Read open orders
    for security, oo_for_sid in get_open_orders().iteritems():
        sym = security.symbol
        for order_obj in oo_for_sid:
            # If an order not seen before, add for tracking
            if order_obj.id not in b['orders']:
                b['orders'][order_obj.id] = order_obj.filled

    # Take a look at current orders
    for id in b['orders']:
        o = get_order(id)         # Current order, might be updated.

        # If filled is not zero, account for it
        if o.filled != 0:
            accounting[id] = o    # Set to account for filled.

            # Bugbug: The only way I could make sense of things so far ...
            # If filled is not amount (shares), that's a partial fill,
            #   cancelling remainder to simplify life.
            # ToDo: Not sure of official actual fill prices.
            if o.filled != o.amount:
                cancel_order(id)  # You might want to change/remove this.

    # Do any accounting, into books{}
    for id in accounting:
        sec             = accounting[id]['sid']
        sym             = sec.symbol
        commission      = accounting[id]['commission']
        filled          = accounting[id]['filled']  # Number filled, sell neg.
        if sec in data and 'price' in data[sec]:    # Update if available.
            b[sym]['price'] = data[sec].price
        lkp             = b[sym]['price']           # Last known price.
        transaction     = filled * lkp

        b[sym]['shares']  += filled      # The transaction on sell is negative
        b[sym]['balance'] -= transaction #   so this line adds to balance then.
        b[sym]['balance'] -= commission
        b['costs_total']  += commission

        if filled > 0:                          # Buy
            b[sym]['cnt_buy_evnts'] += 1
            b[sym]['count_buy']     += filled
        elif filled < 0:                        # Sell
            b[sym]['cnt_sel_evnts'] += 1
            b[sym]['count_sell']    += abs(filled)

        # Remove from the list, accounting done
        del b['orders'][id]

        # Keep track of lowest cash per symbol
        if b[sym]['balance'] < b[sym]['cash_low']:
            b[sym]['cash_low'] = b[sym]['balance']

        # And overall
        cash_now = context.portfolio.cash
        if cash_now < b['cash_low']:
            b['cash_low'] = cash_now

            # An alert for negative cash unless you like "leverage"
            if b['cash_low'] < 0:   # Lowest cash points reached ...
                log.info(str(sym).ljust(5) \
                    + ' order for ' + (('$' + "%.0f" % transaction) \
                    + ',').ljust(8) + ' cash low: ' + str(int(b['cash_low']))
                )
    '''
        Show summary if this is the last bar
    '''
    last_bar_now = 0

    if not b['summary_print']:
        if context.books['arena'] == 'live':
            # When paper/live print summary every day end of day
            last_bar_now = 1
        elif context.books['arena'] == 'backtest':
            # Flag for summary output if last bar now
            bar = get_datetime()
            if b['last_trading_date'] == str(bar.date()):
                if b['mode'] == 'daily':
                    last_bar_now = 1
                elif b['mode'] == 'minute':
                    last_bar_now = 1

    if last_bar_now or b['summary_print']:
        '''
            Summary output to the logging window
        '''
        # Independent copy of context.books using dict() in case summary print
        #   is set to happen more than once in a run, due to concats below (+=)
        b    = dict(context.books)
        done = {}   # Protect against any listed twice.

        # Some overall values by adding individual values
        for sec in b['sids_seen']:
            if sec in done:
                continue

            # There's a problem with a dynamic run where a security
            #   can have dropped out of the picture, and its price
            #   is no longer accessible. Bad. Need help from Q.
            if sec in data and 'price' in data[sec]:
                b[sec.symbol]['price'] = data[sec].price

            sym    = sec.symbol
            shares = b[sym]['shares']
            b['count_buy']     += b[sym]['count_buy']
            b['count_sell']    += b[sym]['count_sell']
            b['cnt_buy_evnts'] += b[sym]['cnt_buy_evnts']
            b['cnt_sel_evnts'] += b[sym]['cnt_sel_evnts']
            b['shares']        += shares
            b['shares_value']  += (shares * b[sym]['price'])
            done[sec] = 1

        q__portfolio  = str(int(context.portfolio.portfolio_value))
        cash_end      = context.portfolio.cash
        init_cash     = b['init_cash']
        avg_init_cash = init_cash / len(b['sids_seen'])
        cash_low      = b['cash_low']
        my_portfolio  = cash_end + b['shares_value']
        cash_profit   = cash_end - b['init_cash']
        xval          = 'x0'
        max_spent     = init_cash - cash_low
        drawdown      = max(init_cash, init_cash - cash_low)
        cnt_b_evts    = ('  (' + str(b['cnt_buy_evnts']) + ' trades)').rjust(17)
        cnt_s_evts    = ('  (' + str(b['cnt_sel_evnts']) + ' trades)').rjust(17)
        untouchd      = '' if int(cash_low) <= 0 else \
                        '  (' + str(int(cash_low)) + ' unused)'
        neg_cash      = '' if int(cash_low) >= 0 else '                       ' \
                            + "%.0f" % cash_low + ' max negative cash'
        if drawdown  != 0:               # Pure profit over input used.
            xval      = 'x'  + "%.3f" % ((my_portfolio - init_cash) / drawdown)

        w1 = 16; w2 = 8  # Widths of columns
        outs = [
            '  QPortfolio: '.rjust(w1)+('$'+str(q__portfolio)) .rjust(w2),
            '   Buy Count: '.rjust(w1)+str(b['count_buy'])     .rjust(w2)+cnt_b_evts,
            '  Sell Count: '.rjust(w1)+str(b['count_sell'])    .rjust(w2)+cnt_s_evts,
            '  Shares Now: '.rjust(w1) + str(b['shares'])      .rjust(w2),
            'Shares Value: '.rjust(w1) + str(int(b['shares_value'])).rjust(w2),
            '    Cash Now: '.rjust(w1) + str(int(cash_end))         .rjust(w2),
            ' Cash Profit: '.rjust(w1) + str(int(cash_profit))      .rjust(w2),
            ' Commissions: '.rjust(w1) + str(int(b['costs_total'])) .rjust(w2),
            '   Max Spent: '.rjust(w1) + str(int(max_spent))        .rjust(w2)+neg_cash,
            'Initial Cash: '.rjust(w1) + str(int(init_cash))        .rjust(w2)+untouchd,
            '   Portfolio: '.rjust(w1)+('$'+str(int(my_portfolio))) .rjust(w2),
        ]
        out  = '_\r\n'
        for o in outs:
            out += (o + '\r\n')
        out += '        Return:  ' + xval + '   Profit/Drawdown\r\n'

        # -------------------------------
        # Individual securities detail
        # -------------------------------
        out_content_collections = []
        count_sids  = len(b['sids_seen'])
        
        
        out_content = '.\r\n\r\n\t0 buys filtered out\r\n\r\n.'        
        
        
        
        sec_word    = ' security' if count_sids == 1 else ' securities'
        out_content += '_      ' + "%.0f" % int(b['init_cash'] / count_sids) \
                + ' average initial cash, ' + str(count_sids) + sec_word + '\r\n'
        lines_out   = 11    # Log in clumps to stay under logging limits.
        count_lines = 0
        col_widths  = {1: 8, 2: 7, 3: 7, 4: 12, 5: 8, 6: 8, 7: 9, 8: 9, 9: 8, 10: 9}
        header1 = [
        '',     'Return','Buy|','By|Sl','By|Sl', 'Price',  'Max', 'Cash','Shrs','Shrs'
        ]
        header2 = [
        'Symbol','Ratio','Hold','Count','Evnts','Strt|Now','Spent','Now','Now', 'Value'
        ]

        cc = 1  # Column count
        for h in header1:
            out_content += h.center(col_widths[cc])
            cc += 1
        out_content += '~\r\n' # Tilde at the end of line for replace-all in an editor
        # later after copy/paste, since new lines are gone at least on Windows.
        # Unfortunate to not be able to copy and paste results easily.

        count_lines += 1
        cc = 1
        for h in header2:
            out_content += h.center(col_widths[cc])
            cc += 1
        out_content += '~\r\n'
        count_lines += 1

        for sym in sorted(s.symbol for s in b['sids_seen']):
            
            
            
            if not b[sym]['cnt_buy_evnts']:
                continue
            
            
            
            balance      = b[sym]['balance']
            init_price   = b[sym]['init_price']
            shares       = b[sym]['shares']
            shares_value = shares * b[sym]['price']
            buy_hold     = 0.0
            xval         = 'x0'
            max_spent    = abs(b[sym]['cash_low'])
            drawdown     = min( avg_init_cash, abs(b[sym]['cash_low']) )
            if drawdown != 0:
                portf = balance + shares_value
                xval  = 'x' + "%.1f" % ((portf - drawdown) / drawdown)
                if xval == 'x-0.0' or xval == 'x0.0':  # Mainly clearing -0.0
                    xval = 'x0'    # -0.0 would have been something like -0.02
            if init_price:
                buy_hold = "%.1f" % ((b[sym]['price'] - init_price) / init_price)
                if buy_hold == '-0.0' or buy_hold == '0.0':
                    buy_hold = '0'
            content = [
                sym,
                xval,
                buy_hold,
                str(b[sym]['count_buy']) + '|' \
                    + str(b[sym]['count_sell']),
                str(b[sym]['cnt_buy_evnts']) + '|' \
                    + str(b[sym]['cnt_sel_evnts']),
                "%.0f" % init_price + '|' + "%.0f" % b[sym]['price'],
                "%.0f" % max_spent,
                "%.0f" % balance,
                shares,
                int(shares_value)
            ]
            cc = 1
            for c in content:
                out_content += str(c).center(col_widths[cc])
                cc += 1
            out_content += '~\r\n'
            count_lines += 1

            # Decide when to tuck a group away for later and
            #    start a new group, using modulus (remainder).
            if count_lines % lines_out == 0:
                out_content_collections.append(out_content)
                out_content = '_\r\n'       # Restart a group

        if count_lines % lines_out != 0:    # A few remaining lines.
            out_content_collections.append(out_content)

        log.info(out)        # The top, general overall output first.

        # Show the stored groups
        for occ in out_content_collections:
            log.info(occ)

        # Add any other content you want ---------------------------
        out_content  = '_\n' # Underscore to a new line for left alignment,
                             #   '\n' by itself would be ignored/dropped.
        # Some variables or whatever you might want to add...
        out_content += ''

        log.info(out_content)




    
    
There was a runtime error.

Hey, Python/SQL newbie here, so this might be a silly question regarding your code, but when I'm looking at the do_screening method

.filter(fundamentals.cash_flow_statement.operating_cash_flow > fundamentals.income_statement.net_income)
        .filter((fundamentals.valuation.enterprise_value / fundamentals.cash_flow_statement.free_cash_flow) < 15)  
        .limit(context.max_num_stocks)  
    )  

the .limit query modifier appears to be for the purpose of limiting the amount of stocks in your portfolio to the maximum defined in the initalize method.

However, I don't understand what the .limit behavior is in the absence of an order_by modifier. IE, I assume there is an inherent ordering off of which the max_num_stocks cuts off of, but it's unclear to me whether that's based on the data order in Morningstar's database, or an order shift based on how the queries are run. Can you tell me how this ends up working int he context of the algorithm or where to look in the documentation?

Thanks.

Hi Don,

you're right, .order_by is essential if you use the .limit clause. The code and backtest above are quite old. I've completely redesigned my implementation of the Millennial Money Algorithm und maybe I'll publish is in the next time.

Please be aware that this strategy is almost 2000% laverage.

I just took a crack at making a simple hedged portfolio inspired by this algo. (I used the algo shared above by @garyha as the foundation). I simply short an equivalent amount of SPY (e.g. dollar-neutralize the portfolio) at each quarterly rebalance period. It does quite a good job of bringing the beta down to nearly 0 over the whole backtest as well as reducing the Max Drawdown from 47% to 25% (Yes, at the expense of overall total return).

As well, I also added a code snippet to clean up all the delisted/'dead' positions that seem to accumulate in the portfolio over time, which really affected how orders were placed/$ allocated in the latter years of the backtest (I applied the methodology discussed in this thread: https://www.quantopian.com/posts/why-does-the-number-of-positions-and-leverage-creep-up-for-this-algorithm ).

Just wanted to share it here, as well as its accompanying tearsheet.

Clone Algorithm
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
"""
    Patrick O'Shaughnessy - Millennial Money
    
    1. Stakeholder yield < 5%. Stakeholder yield = Cash from financing 12m / Market Cap Q1
    2. ROIC > 20%
       ROIC = operating_income /  (invested_capital - cash)
    3. CFO > Net Income (Earnings Quality)
    4. EV/FCF < 15 (Value)
    5. 6M Relative Strength top three-quarters of the market. 
       6M Relative Strength = 6M Stock Total Return / 6M Total Return S&P500 (Momentum)
       
    The positions are updated quarterly.
"""

import pandas as pd
import numpy as np

def initialize(context):
    #set_long_only()
    context.max_num_stocks = 50
    context.days = 0
    context.quarter_days = 65
    context.relative_strength_6m = {}
    
    schedule_function(func = track_orders, date_rule = date_rules.every_day())
    schedule_function(func = summary, date_rule = date_rules.every_day())
          
def quarter_passed(context): 
    """
    Screener results quarterly updated
    """
    return context.days % context.quarter_days == 0

def before_trading_start(context): 
    context.days += 1
    
    if not quarter_passed(context):
        return
    
    do_screening(context)
    update_universe(context.fundamental_df.columns.values)
    
def do_screening(context):
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.morningstar_sector_code,
            
            # no need for the other contents here
        )

        # No Financials (103) and Real Estate (104) Stocks, no ADR or PINK, only USA
        .filter(fundamentals.asset_classification.morningstar_sector_code != 103)
        .filter(fundamentals.company_reference.country_id == "USA")
        .filter(fundamentals.asset_classification.morningstar_sector_code != 104)
        .filter(fundamentals.share_class_reference.is_depositary_receipt == False)
        .filter(fundamentals.share_class_reference.is_primary_share == True)
        .filter(fundamentals.company_reference.primary_exchange_id != "OTCPK")
        
        # Check for data sanity (i,e. avoid division by zero)
        .filter(fundamentals.valuation.market_cap > 0)
        .filter(fundamentals.valuation.shares_outstanding > 0)
        .filter(fundamentals.cash_flow_statement.free_cash_flow > 0)
        .filter(fundamentals.balance_sheet.invested_capital > 0)
        .filter(fundamentals.balance_sheet.cash_and_cash_equivalents > 0)
        .filter(fundamentals.balance_sheet.invested_capital != fundamentals.balance_sheet.cash_and_cash_equivalents)
        
        .filter((fundamentals.cash_flow_statement.financing_cash_flow / fundamentals.valuation.market_cap) < 0.05)
        .filter((fundamentals.income_statement.operating_income / (fundamentals.balance_sheet.invested_capital - fundamentals.balance_sheet.cash_and_cash_equivalents)) > 0.20)
       
        .filter(fundamentals.cash_flow_statement.operating_cash_flow > fundamentals.income_statement.net_income)
        .filter((fundamentals.valuation.enterprise_value / fundamentals.cash_flow_statement.free_cash_flow) < 15)
        
        .limit(context.max_num_stocks)
    )
   
    # Update context
    context.stocks = [stock for stock in fundamental_df]
    context.fundamental_df = fundamental_df
    
def rebalance(context, data):
    """
        Exit all positions before starting new ones.
        Apply the Momentum Criteria
        Buy all stocks equally 
    """
    
    # Filter out stocks without data and apply the momentum criteria
    # -0.6745 is an approximation for the top three-quarters of the market
    context.stocks = [stock for stock in context.stocks
                      if stock in data and context.relative_strength_6m[stock] > -0.6745]
    
    # make sure to get out of delisted stocks so they don't sit stagnant in portfolio
    # see this thread for discussion of this approach: 
    # https://www.quantopian.com/posts/why-does-the-number-of-positions-and-leverage-creep-up-for-this-algorithm
    context.stocks = [stock for stock in context.stocks
                      if (stock.end_date - get_datetime()).days > 100]
   
    
    to_order = []
    for s in context.stocks:    
        if get_open_orders(s):
            continue
        to_order.append(s)
        
    if len(to_order) == 0:
        log.info("No Stocks to buy, and closing SPY hedge")
        order_target_percent(symbol('SPY'), 0.0)
        return
    
    if len(to_order) > 0:
        weight = 1.0 / float(len(to_order))

        log.info("Ordering %0.0f%% for each of %s (%d stocks)" % (
            weight * 100, ', '.join(stock.symbol for stock in to_order), len(to_order)
                ))
    
        # buy all stocks equally
        for stock in to_order:
            order_target_percent(stock, 0.5 * weight)
            #order_target_percent(symbol('SPY'), -1.0*weight)
        order_target_percent(symbol('SPY'), -0.5 * weight * len(to_order))
        track_orders(context, data)

    # Exit all positions before starting new ones
    for stock in context.portfolio.positions:
        if stock not in to_order and stock is not symbol('SPY'):
            order_target_percent(stock, 0.0)
            
    # chart how many positions rebalanced
    record(num_positions = len(to_order))
    
def compute_relative_strength(context):
    symbol('SPY')
    prices = history(bar_count=150, frequency='1d', field='price')
    # Price % change in the last 6 months
    pct_change = (prices.ix[-130] - prices.ix[0]) / prices.ix[0]
    
    pct_change_spy = pct_change[symbol('SPY')]
    pct_change     = pct_change - pct_change_spy
    if pct_change_spy != 0:
        pct_change = pct_change / abs(pct_change_spy)
    pct_change = pct_change.drop(symbol('SPY'))
    context.relative_strength_6m = pct_change
        
def handle_data(context, data):
    current = []
    for s in context.portfolio.positions:
        current.append(s.symbol)
    
    if current:
        print "Current holdings: " + str(current)
        
    if not quarter_passed(context):
        return
    
    if get_open_orders():
        return
    
    compute_relative_strength(context)
    rebalance(context, data)
    
def track_orders(context, data):
    '''
        Log orders after they are filled.
    '''
    try:
        context.orders
    except:
        context.orders = {}    # Keys are order id's, values are orders.
        
    orders = []          # Any current order id's that are visible, for later.
    msg1 = []; msg2 = [] # For logging.

    # Open orders
    for sec, oo_for_sid in get_open_orders().iteritems():
        sym = sec.symbol
        for o in oo_for_sid:    # Orders per security
            orders.append(o.id)
            if o.id in context.orders:
                if o.filled > context.orders[o.id]['filled']:
                    # Filled at least some
                    trade = 'Bot' if o.amount > 0 else 'Sold'
                    diff  = o.filled - context.orders[o.id]['filled']
                    msg2.append(
                        '   {} {} {} at {}\n'.format(trade, diff, sym, data[sec].price)
                    )
                if o.filled == o.amount: # Complete so drop tracking
                    del context.orders[o.id]
            else:   # New order
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:  # Limit order
                    msg2.append(
                        '{} {} {} now {} target {}\n'.format(
                            trade, o.amount, sym, data[sec].price, o.limit
                        )
                    )
                else:        # Market order
                    msg2.append(
                        '{} {} {} at {}\n'.format(
                            trade, o.amount, sym, data[sec].price
                        )
                    )
                context.orders[o.id] = o

    # Stored orders
    to_delete = []
    for id in context.orders:
        if id not in orders:
            # Completed order
            o     = context.orders[id]
            sym   = o.sid.symbol
            trade = 'Bot' if o.amount > 0 else 'Sold'
            diff  = o.amount - o.filled
            msg1.append(
                '   {} {} {} at {}'.format(trade, diff, sym, data[o.sid].price)
            )
            to_delete.append(id)
    for d in to_delete: # (can't delete while iterating above, hence this)
        del context.orders[d]

    # Logging
    for m in msg2:
        msg1.append(m)
    for m in msg1:
        log.info(m)

def summary(context, data):
    '''
        Summary processing

        https://www.quantopian.com/posts/run-summary
    '''
    # Need a couple of imports, you might need to comment these out if already imported.
    #   That's pretty much the only change that might be necessary.
    from pytz import timezone
    import re

    # Yes try/except is narly yet makes to work with set_universe etc.
    # Is there a better way?  An -| if 'books' in context: |- didn't work.
    try:
        context['books']    # See if this key exists yet.
        b = context.books   # For brevity.
    except:
        '''
            Preparation. Initialize one time.
        '''
        cash = context.portfolio.starting_cash
        context.books = {   # Starting cash value from GUI or live restart...
            'cash'          : cash,
            'init_cash'     : cash,
            'cash_low'      : cash,
            'shares'        : 0,
            'shares_value'  : 0,
            'count_buy'     : 0,       # Overall buy count, number of shares.
            'count_sell'    : 0,       # Overall sell count.
            'cnt_buy_evnts' : 0,       # Overall buy events count.
            'cnt_sel_evnts' : 0,
            'summary_print' : 0,
            'costs_total'   : 0,       # Commissions.
            'sids_seen'     : [],      # For set_universe since dynamic.
            'prep_prnt'     : '',
            'orders'        : {},      # Keep orders for accounting,
        }                              #   orders not completely filled yet.

        b = context.books

        # Environment   First/last dates and
        #   Arena: backtest or live.  Mode: daily or minute.
        env = get_environment('*')
        b['first_trading_date'] = str(env['start'].date())
        b['last_trading_date']  = str(env['end']  .date())
        b['arena'] = env['arena']
        b['mode']  = env['data_frequency']

        if b['arena'] == 'live':
            b['arena'] = 'paper'
        elif b['arena'] != 'backtest': # ie like 'IB'
            b['arena'] = 'live'

        # Show environment at the beginning of the run
        b['prep_prnt'] = ' {}\n  {}  {} to {}  {}  {}\n'.format(
            b['arena'],
            b['mode'],
            b['first_trading_date'],
            b['last_trading_date'],
            '   $' + "%.0f" % b['cash'],
            '  First bar stocks ({}) ...'.format(len(data)),
        )

        # Show current universe once
        for sec in data:
            if isinstance(sec, basestring):
                continue   # Skip any injected fetcher string keys.
            b['prep_prnt'] += (sec.symbol + ' ')
        log.info(b['prep_prnt'])

    '''
        Prepare individual securities dictionaries
          with dynamic set_universe, fetcher, IPO's appearing etc.
    '''
    for sec in data:
        if isinstance(sec, basestring):
            continue   # Skip any injected fetcher string keys.
        sym = sec.symbol
        if sym in b:
            continue
        if sec not in b['sids_seen']:
            # Scenarios with price missing ...
            price = data[sec].price if 'price' in data[sec] else 0
            b['sids_seen'].append(sec)
            b[sym] = {
                'init_price'    : price,  # Save for summary.
                'price'         : price,  # Most recent price.
                'cash_low'      : 0,      # Lowest level of cash.
                'balance'       : 0,      # For individual 'x' return.
                'shares'        : 0,
                'count_buy'     : 0,      # Individual buy number of shares.
                'count_sell'    : 0,
                'cnt_buy_evnts' : 0,      # Individual buy events count.
                'cnt_sel_evnts' : 0,
            }

    '''
        Accounting. Update the numbers, manage orders if any.
    '''
    accounting = {} # Locally, any orders ready to be counted.
    
    # Read open orders
    for security, oo_for_sid in get_open_orders().iteritems():
        sym = security.symbol
        for order_obj in oo_for_sid:
            # If an order not seen before, add for tracking
            if order_obj.id not in b['orders']:
                b['orders'][order_obj.id] = order_obj.filled

    # Take a look at current orders
    for id in b['orders']:
        o = get_order(id)         # Current order, might be updated.

        # If filled is not zero, account for it
        if o.filled != 0:
            accounting[id] = o    # Set to account for filled.

            # Bugbug: The only way I could make sense of things so far ...
            # If filled is not amount (shares), that's a partial fill,
            #   cancelling remainder to simplify life.
            # ToDo: Not sure of official actual fill prices.
            if o.filled != o.amount:
                cancel_order(id)  # You might want to change/remove this.

    # Do any accounting, into books{}
    for id in accounting:
        sec             = accounting[id]['sid']
        sym             = sec.symbol
        commission      = accounting[id]['commission']
        filled          = accounting[id]['filled']  # Number filled, sell neg.
        if sec in data and 'price' in data[sec]:    # Update if available.
            b[sym]['price'] = data[sec].price
        lkp             = b[sym]['price']           # Last known price.
        transaction     = filled * lkp

        b[sym]['shares']  += filled      # The transaction on sell is negative
        b[sym]['balance'] -= transaction #   so this line adds to balance then.
        b[sym]['balance'] -= commission
        b['costs_total']  += commission

        if filled > 0:                          # Buy
            b[sym]['cnt_buy_evnts'] += 1
            b[sym]['count_buy']     += filled
        elif filled < 0:                        # Sell
            b[sym]['cnt_sel_evnts'] += 1
            b[sym]['count_sell']    += abs(filled)

        # Remove from the list, accounting done
        del b['orders'][id]

        # Keep track of lowest cash per symbol
        if b[sym]['balance'] < b[sym]['cash_low']:
            b[sym]['cash_low'] = b[sym]['balance']

        # And overall
        cash_now = context.portfolio.cash
        if cash_now < b['cash_low']:
            b['cash_low'] = cash_now

            # An alert for negative cash unless you like "leverage"
            if b['cash_low'] < 0:   # Lowest cash points reached ...
                log.info(str(sym).ljust(5) \
                    + ' order for ' + (('$' + "%.0f" % transaction) \
                    + ',').ljust(8) + ' cash low: ' + str(int(b['cash_low']))
                )
    '''
        Show summary if this is the last bar
    '''
    last_bar_now = 0

    if not b['summary_print']:
        if context.books['arena'] == 'live':
            # When paper/live print summary every day end of day
            last_bar_now = 1
        elif context.books['arena'] == 'backtest':
            # Flag for summary output if last bar now
            bar = get_datetime()
            if b['last_trading_date'] == str(bar.date()):
                if b['mode'] == 'daily':
                    last_bar_now = 1
                elif b['mode'] == 'minute':
                    last_bar_now = 1

    if last_bar_now or b['summary_print']:
        '''
            Summary output to the logging window
        '''
        # Independent copy of context.books using dict() in case summary print
        #   is set to happen more than once in a run, due to concats below (+=)
        b    = dict(context.books)
        done = {}   # Protect against any listed twice.

        # Some overall values by adding individual values
        for sec in b['sids_seen']:
            if sec in done:
                continue

            # There's a problem with a dynamic run where a security
            #   can have dropped out of the picture, and its price
            #   is no longer accessible. Bad. Need help from Q.
            if sec in data and 'price' in data[sec]:
                b[sec.symbol]['price'] = data[sec].price

            sym    = sec.symbol
            shares = b[sym]['shares']
            b['count_buy']     += b[sym]['count_buy']
            b['count_sell']    += b[sym]['count_sell']
            b['cnt_buy_evnts'] += b[sym]['cnt_buy_evnts']
            b['cnt_sel_evnts'] += b[sym]['cnt_sel_evnts']
            b['shares']        += shares
            b['shares_value']  += (shares * b[sym]['price'])
            done[sec] = 1

        q__portfolio  = str(int(context.portfolio.portfolio_value))
        cash_end      = context.portfolio.cash
        init_cash     = b['init_cash']
        avg_init_cash = init_cash / len(b['sids_seen'])
        cash_low      = b['cash_low']
        my_portfolio  = cash_end + b['shares_value']
        cash_profit   = cash_end - b['init_cash']
        xval          = 'x0'
        max_spent     = init_cash - cash_low
        drawdown      = max(init_cash, init_cash - cash_low)
        cnt_b_evts    = ('  (' + str(b['cnt_buy_evnts']) + ' trades)').rjust(17)
        cnt_s_evts    = ('  (' + str(b['cnt_sel_evnts']) + ' trades)').rjust(17)
        untouchd      = '' if int(cash_low) <= 0 else \
                        '  (' + str(int(cash_low)) + ' unused)'
        neg_cash      = '' if int(cash_low) >= 0 else '                       ' \
                            + "%.0f" % cash_low + ' max negative cash'
        if drawdown  != 0:               # Pure profit over input used.
            xval      = 'x'  + "%.3f" % ((my_portfolio - init_cash) / drawdown)

        w1 = 16; w2 = 8  # Widths of columns
        outs = [
            '  QPortfolio: '.rjust(w1)+('$'+str(q__portfolio)) .rjust(w2),
            '   Buy Count: '.rjust(w1)+str(b['count_buy'])     .rjust(w2)+cnt_b_evts,
            '  Sell Count: '.rjust(w1)+str(b['count_sell'])    .rjust(w2)+cnt_s_evts,
            '  Shares Now: '.rjust(w1) + str(b['shares'])      .rjust(w2),
            'Shares Value: '.rjust(w1) + str(int(b['shares_value'])).rjust(w2),
            '    Cash Now: '.rjust(w1) + str(int(cash_end))         .rjust(w2),
            ' Cash Profit: '.rjust(w1) + str(int(cash_profit))      .rjust(w2),
            ' Commissions: '.rjust(w1) + str(int(b['costs_total'])) .rjust(w2),
            '   Max Spent: '.rjust(w1) + str(int(max_spent))        .rjust(w2)+neg_cash,
            'Initial Cash: '.rjust(w1) + str(int(init_cash))        .rjust(w2)+untouchd,
            '   Portfolio: '.rjust(w1)+('$'+str(int(my_portfolio))) .rjust(w2),
        ]
        out  = '_\r\n'
        for o in outs:
            out += (o + '\r\n')
        out += '        Return:  ' + xval + '   Profit/Drawdown\r\n'

        # -------------------------------
        # Individual securities detail
        # -------------------------------
        out_content_collections = []
        count_sids  = len(b['sids_seen'])
        
        
        out_content = '.\r\n\r\n\t0 buys filtered out\r\n\r\n.'        
        
        
        
        sec_word    = ' security' if count_sids == 1 else ' securities'
        out_content += '_      ' + "%.0f" % int(b['init_cash'] / count_sids) \
                + ' average initial cash, ' + str(count_sids) + sec_word + '\r\n'
        lines_out   = 11    # Log in clumps to stay under logging limits.
        count_lines = 0
        col_widths  = {1: 8, 2: 7, 3: 7, 4: 12, 5: 8, 6: 8, 7: 9, 8: 9, 9: 8, 10: 9}
        header1 = [
        '',     'Return','Buy|','By|Sl','By|Sl', 'Price',  'Max', 'Cash','Shrs','Shrs'
        ]
        header2 = [
        'Symbol','Ratio','Hold','Count','Evnts','Strt|Now','Spent','Now','Now', 'Value'
        ]

        cc = 1  # Column count
        for h in header1:
            out_content += h.center(col_widths[cc])
            cc += 1
        out_content += '~\r\n' # Tilde at the end of line for replace-all in an editor
        # later after copy/paste, since new lines are gone at least on Windows.
        # Unfortunate to not be able to copy and paste results easily.

        count_lines += 1
        cc = 1
        for h in header2:
            out_content += h.center(col_widths[cc])
            cc += 1
        out_content += '~\r\n'
        count_lines += 1

        for sym in sorted(s.symbol for s in b['sids_seen']):
            
            
            
            if not b[sym]['cnt_buy_evnts']:
                continue
            
            
            
            balance      = b[sym]['balance']
            init_price   = b[sym]['init_price']
            shares       = b[sym]['shares']
            shares_value = shares * b[sym]['price']
            buy_hold     = 0.0
            xval         = 'x0'
            max_spent    = abs(b[sym]['cash_low'])
            drawdown     = min( avg_init_cash, abs(b[sym]['cash_low']) )
            if drawdown != 0:
                portf = balance + shares_value
                xval  = 'x' + "%.1f" % ((portf - drawdown) / drawdown)
                if xval == 'x-0.0' or xval == 'x0.0':  # Mainly clearing -0.0
                    xval = 'x0'    # -0.0 would have been something like -0.02
            if init_price:
                buy_hold = "%.1f" % ((b[sym]['price'] - init_price) / init_price)
                if buy_hold == '-0.0' or buy_hold == '0.0':
                    buy_hold = '0'
            content = [
                sym,
                xval,
                buy_hold,
                str(b[sym]['count_buy']) + '|' \
                    + str(b[sym]['count_sell']),
                str(b[sym]['cnt_buy_evnts']) + '|' \
                    + str(b[sym]['cnt_sel_evnts']),
                "%.0f" % init_price + '|' + "%.0f" % b[sym]['price'],
                "%.0f" % max_spent,
                "%.0f" % balance,
                shares,
                int(shares_value)
            ]
            cc = 1
            for c in content:
                out_content += str(c).center(col_widths[cc])
                cc += 1
            out_content += '~\r\n'
            count_lines += 1

            # Decide when to tuck a group away for later and
            #    start a new group, using modulus (remainder).
            if count_lines % lines_out == 0:
                out_content_collections.append(out_content)
                out_content = '_\r\n'       # Restart a group

        if count_lines % lines_out != 0:    # A few remaining lines.
            out_content_collections.append(out_content)

        log.info(out)        # The top, general overall output first.

        # Show the stored groups
        for occ in out_content_collections:
            log.info(occ)

        # Add any other content you want ---------------------------
        out_content  = '_\n' # Underscore to a new line for left alignment,
                             #   '\n' by itself would be ignored/dropped.
        # Some variables or whatever you might want to add...
        out_content += ''

        log.info(out_content)




    
    
There was a runtime error.
Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

The tearsheet in the Q Research Environment

Loading notebook preview...
Notebook previews are currently unavailable.

Hi can someone please convert this to the quantopian v2 syntax? I am not advanced enough to make the necessary changes yet.

Hi.

How did you calculate -0.6745? You said that it is an approximation for the top three-quarters of the market, but that doesnt make much sense to me.

Thanks

Does anyone have an update on this for 2017? Has performance still been consistent with the original?

This algo isn't exactly as the original because of the lack of historical annual fundamental data on Quantopian. It also uses a lot of leverage and this is bad. It was one of my first algo on Q. I'm going to reimplement it correctly not yet Q allows historical fundamentals with the Pipeline API.

It does