Back to Community
Fundamentals Morningstar Ratings

This algo lets Morningstar do the legwork, every month it buys all the stocks trading above their N day (100 here) moving average that have A or B growth and profitability grades from Morningstar. Any remaining capital is put into long term bonds (TLT) to stay 100% invested.

David

Clone Algorithm
121
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""

import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    
    context.num_stocks = 200.
    context.leverage = 1.0
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds, 
                      date_rule=date_rules.month_start(1))
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)  
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.fundamental_df
    moving_avgs = history(100, '1d', 'price').mean()
    for stock in data:
        if stock in fundies.columns and data[stock].price > moving_avgs[stock]:
            order_target_percent(stock, context.leverage / context.num_stocks)
        else:
            order_target(stock, 0)
            

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    
    num_stocks = context.num_stocks
    grades = ['A+', 'A', 'A-',
              'B+', 'B', 'B-']
    
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.growth_grade,
            fundamentals.asset_classification.profitability_grade,
            # fundamentals.asset_classification.financial_health_grade,
        )
        .filter(fundamentals.asset_classification.growth_grade.in_(grades))
        .filter(fundamentals.asset_classification.profitability_grade.in_(grades))
        # .filter(fundamentals.asset_classification.financial_health_grade.in_(grades))
        .limit(num_stocks)
    )
    context.fundamental_df = fundamental_df
    update_universe(context.fundamental_df.columns.values)   
    

def handle_data(context, data):
    record_account_info(context, data)
    
    
def record_account_info(context, data):
    P = context.portfolio
    count = 0
    market_value = 0 
    for stock in data:
        pos = P.positions[stock].amount
        price = data[stock].price
        market_value += price * abs(pos)
        if pos and stock not in context.bonds:
            count += 1
    record(#leverage=market_value / max(1, P.portfolio_value),
           stock_count=count)
    
There was a runtime error.
16 responses

Hi David,

Thanks. The idea looks very interesting. It seems the code trades a lot because of the diversification of stocks. I tested starting with $20K (my trading cash..) and add a commission of $2 per trade, here is the result. But this method should work great if given more starting cash.

Best,
Han

Clone Algorithm
6
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""

import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    
    context.num_stocks = 200.
    context.leverage = 1.0
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds, 
                      date_rule=date_rules.month_start(1))
    set_commission(commission.PerTrade(cost=2.00))
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)  
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.fundamental_df
    moving_avgs = history(100, '1d', 'price').mean()
    for stock in data:
        if stock in fundies.columns and data[stock].price > moving_avgs[stock]:
            order_target_percent(stock, context.leverage / context.num_stocks)
        else:
            order_target(stock, 0)
            

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    
    num_stocks = context.num_stocks
    grades = ['A+', 'A', 'A-',
              'B+', 'B', 'B-']
    
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.growth_grade,
            fundamentals.asset_classification.profitability_grade,
            # fundamentals.asset_classification.financial_health_grade,
        )
        .filter(fundamentals.asset_classification.growth_grade.in_(grades))
        .filter(fundamentals.asset_classification.profitability_grade.in_(grades))
        # .filter(fundamentals.asset_classification.financial_health_grade.in_(grades))
        .limit(num_stocks)
    )
    context.fundamental_df = fundamental_df
    update_universe(context.fundamental_df.columns.values)   
    

def handle_data(context, data):
    record_account_info(context, data)
    
    
def record_account_info(context, data):
    P = context.portfolio
    count = 0
    market_value = 0 
    for stock in data:
        pos = P.positions[stock].amount
        price = data[stock].price
        market_value += price * abs(pos)
        if pos and stock not in context.bonds:
            count += 1
    record(#leverage=market_value / max(1, P.portfolio_value),
           stock_count=count)
    
There was a runtime error.

Nice use of fundamentals Dave. I think I'll use your model for future study (if you don't mind).

Sadly, during 2014, Dave's original strat only made 5.8% (1/1/2014...) I believe this will become a common theme here: excellent theoretical returns over 10+ years (a noble goal), but with paltry returns in 2014 -- which will eliminate such strategies from contention for selection in the Q-Fund. This leads me to believe that the Q-Fund, who's parameters have been established in a rather strong bull market, over a very short period, may have trouble in the near future.

I wonder how those strategies, shown in the Q-Fund webinar video, would perform in such a 10+ year test like the one above? When the regime changes, will you be ready? Will your strategy?

Han,
I had that set to trade up to 200 stocks, $20K is way too little for any strategy trading that many assets. I kept your settings for this one, but dropped it to 20 stocks and have it only take Morningstar A grades.

Anony, I'm not sure which one my 'original stat' is, but that's to be expected, especially if it's a long only/long term strat, let it paper trade, it's not over yet.

David

Clone Algorithm
31
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""

import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    
    context.num_stocks = 20.
    context.leverage = 1.0
    
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds,
                      date_rule=date_rules.month_start(1))
    
    set_commission(commission.PerTrade(cost=2.00))
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)  
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.fundamental_df
    moving_avgs = history(100, '1d', 'price').mean()
    log.info('\n%s'%fundies)
    for stock in data:
        if stock in fundies.columns and data[stock].price > moving_avgs[stock]:
            order_target_percent(stock, context.leverage / context.num_stocks)
        else:
            order_target(stock, 0)
            

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    
    num_stocks = context.num_stocks
    grades = ['A+', 'A', 'A-',
              # 'B+', 'B', 'B-'
             ]
    
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.growth_grade,
            fundamentals.asset_classification.profitability_grade,
            
        )
        .filter(fundamentals.asset_classification.growth_grade.in_(grades))
        .filter(fundamentals.asset_classification.profitability_grade.in_(grades))
        # .order_by(fundamentals.asset_classification.growth_grade.desc())
        .limit(num_stocks)
    )
    context.fundamental_df = fundamental_df
    update_universe(context.fundamental_df.columns.values)   
    

def handle_data(context, data):
    record_account_info(context, data)
    
    
def record_account_info(context, data):
    P = context.portfolio
    count = 0
    market_value = 0 
    for stock in data:
        pos = P.positions[stock].amount
        price = data[stock].price
        market_value += price * abs(pos)
        if pos and stock not in context.bonds:
            count += 1
    record(#leverage=market_value / max(1, P.portfolio_value),
           stock_count=count)
    
There was a runtime error.

I see. The result is very impressive. Thanks.

Notice the holding of value 2008, maybe thanks to bonds, I'm not sure.
This message edited to not waste people's time.
I had expressed some doubts

Gary, you're right, it looks like it shorted VRNT and PBHC, a check for open orders seems to have fixed that. The transactions and positions in the full backtest seem to disagree with your logging sample btw, it looks like it bought and sold a lot of different stocks throughout the test, much more than 34 anyway.

David

Clone Algorithm
121
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""

import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    
    context.num_stocks = 200.
    context.leverage = 1.0
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds, 
                      date_rule=date_rules.month_start(1))
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)  
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.fundamental_df
    moving_avgs = history(100, '1d', 'price').mean()
    for stock in data:
        if get_open_orders(stock):
            continue
        if stock in fundies.columns and data[stock].price > moving_avgs[stock]:
            order_target_percent(stock, context.leverage / context.num_stocks)
        else:
            order_target(stock, 0)
            

def before_trading_start(context): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from fetch_fundamentals.
    """
    
    num_stocks = context.num_stocks
    grades = ['A+', 'A', 'A-',
              'B+', 'B', 'B-']
    
    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.growth_grade,
            fundamentals.asset_classification.profitability_grade,
            # fundamentals.asset_classification.financial_health_grade,
        )
        .filter(fundamentals.asset_classification.growth_grade.in_(grades))
        .filter(fundamentals.asset_classification.profitability_grade.in_(grades))
        # .filter(fundamentals.asset_classification.financial_health_grade.in_(grades))
        .limit(num_stocks)
    )
    context.fundamental_df = fundamental_df
    update_universe(context.fundamental_df.columns.values)   
    

def handle_data(context, data):
    record_account_info(context, data)
    
    
def record_account_info(context, data):
    P = context.portfolio
    count = 0
    market_value = 0 
    for stock in data:
        pos = P.positions[stock].amount
        price = data[stock].price
        market_value += price * abs(pos)
        if pos and stock not in context.bonds:
            count += 1
    record(leverage=market_value / max(1, P.portfolio_value),
           stock_count=count)
    
There was a runtime error.

You're right, it trades tons of them. When schedule_function is in place, due to the sequence of functions, there is no opportunity in handle_data to make use of get_open_orders for home-grown accounting, I didn't realize that previously. It can be done with another schedule_function to one's own accounting function. This has 1/10th the initial number of stocks and has an example of some run summary code, updated to do the right thing when schedule_function is present (I think).

Give this a spin ...

Clone Algorithm
29
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
'''
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range,
    put any remaining capital in long term bonds.
    Rebalance monthly

'''

import pandas as pd
import numpy as np

def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))

    context.num_stocks = 20
    context.leverage   = 1.0

    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks,
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds,
                      date_rule=date_rules.month_start(1))

    schedule_function(func=summary,
                      date_rule=date_rules.every_day())

def buy_bonds(context, data):
    P = context.portfolio
    market_value = sum(data[i].price * abs(P.positions[i].amount) for i in data)
    leverage     = market_value / max(1, P.portfolio_value)
    gap          = max(context.leverage - leverage, 0.0)
    weight       = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        order_target_percent(bnd, weight)

def buy_stocks(context, data):
    fundies = context.fundamental_df
    moving_avgs = history(100, '1d', 'price').mean()
    for stock in data:
        if get_open_orders(stock):
            continue
        if stock in fundies.columns and data[stock].price > moving_avgs[stock]:
            order_target_percent(stock, context.leverage / context.num_stocks)
        else:
            if context.portfolio.positions[stock].amount:
                order_target(stock, 0)

def before_trading_start(context):
    '''
      Called before the start of each trading day.
      It updates the universe with the
      securities and values found from get_fundamentals.
    '''

    num_stocks = context.num_stocks
    grades = ['A+', 'A', 'A-',
              'B+', 'B', 'B-']

    fundamental_df = get_fundamentals(
        query(
            fundamentals.asset_classification.growth_grade,
            fundamentals.asset_classification.profitability_grade,
            # fundamentals.asset_classification.financial_health_grade,
        )
        .filter(fundamentals.asset_classification.growth_grade.in_(grades))
        .filter(fundamentals.asset_classification.profitability_grade.in_(grades))
        # .filter(fundamentals.asset_classification.financial_health_grade.in_(grades))
        .limit(num_stocks)
    )
    context.fundamental_df = fundamental_df
    update_universe(context.fundamental_df.columns.values)

def handle_data(context, data):
    record_account_info(context, data)

def record_account_info(context, data):
    P = context.portfolio
    count = 0
    market_value = 0
    for stock in data:
        pos   = P.positions[stock].amount
        price = data[stock].price
        market_value += price * abs(pos)
        if pos and stock not in context.bonds:
            count += 1
    record(leverage=market_value / max(1, P.portfolio_value),
           stock_count=count)


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.
            'arena'         : None,    # To become 'backtest' or 'live'.
            'mode'          : None,    # To become 'daily' or 'minute'.
            'prep_prnt'     : '',
            'orders'        : {        # Keep orders for accounting.
                'open' : {},           # Orders not completely filled yet.
                'syms' : {},           # Dict of symbols with open orders,
            },                         #   can be used to not order over another.
            'first_trading_date' : str(get_datetime().date()),
        }

        b = context.books

        # First/last dates and
        #   Arena: backtest or live.  Mode: daily or minute.
        try:
            # Prep some environment info
            b['last_trading_date'] = re.findall('period_end=(.*?) ', str(sid))[0]
            arena     = re.findall('Live|Simulation', str(sid))[0]
            b['mode'] = re.findall('data_frequency=(.*?),', str(sid))[0]

            if arena == 'Live':
                b['arena'] = 'live'
            elif arena == 'Simulation':
                b['arena'] = 'backtest'
        except:
            log.info('Error in str(sid), summary will not print.')
            b['last_trading_date'] = 'unknown'
            b['arena'] = 'Arena unknown'
            b['mode']  = 'Mode unknown'

        # Show environment at the beginning of the run
        msg  = ' {0:s}\n  {1:s}  {2:s} to {3:s}  {4:s}  {5:s}\n'
        b['prep_prnt'] = msg.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
        # Scenarios with price missing ...
        price = data[sec].price if 'price' in data[sec] else 0
        if sym in b:
            continue
        if sec not in b['sids_seen']:
            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:
            # Convenience option to be able in handle_data to
            #   avoid ordering if an order already exists.
            b['orders']['syms'][sym] = 1

            # If an order not seen before, add for tracking
            if order_obj.id not in b['orders']['open']:
                b['orders']['open'][order_obj.id] = order_obj.filled

    # Take a look at current orders
    for id in b['orders']['open']:
        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
        b[sym]['price'] = data[sec].price if 'price' in data[sec] else b[sym]['price']
        commission      = accounting[id]['commission']
        filled          = accounting[id]['filled']  # Number filled, sell neg.
        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
        if sym in b['orders']['syms']:    # There's a scenario in multiple buys
            del b['orders']['syms'][sym]  #   where this key could be gone.
        del b['orders']['open'][id]

        # Overall keep track of lowest cash point
        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']))
                )

        # And per symbol
        if b[sym]['balance'] < b[sym]['cash_low']:
            b[sym]['cash_low'] = b[sym]['balance']

    '''
        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 (+=)
        #   although the print any time is deprecated, couldn't find a way
        #   to make work with schedule_function.
        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[sym]['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'])
        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']):
            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.

Would it be possible to get a Q2 version of this to play with?

Haas anyone had success over the week on getting this to work with Q2?

Just giving this a final bump to see if there is any interest.

thats awesome, i was wondering how their grading performs. I'm always surprised that grades, forecasts, indicators, none of it does very well. maybe i code it wrong.

Hi all,
I took some time to port this over to Q2. Now, all the stock data and filtering is generated through Pipeline, using the same Morningstar fundamental data.

The algorithm seems to hit the cap of 200 stock picks a day, leading me to believe that the pipeline is capable of generating more accurate data from Morningstar. As a result, the amount of capital held in bonds is much lower throughout the algorithm.

Also - instead of arbitrarily picking 200 stocks out of our selection, I pick the 200 stocks with the lowest difference between moving average and close price. The way the 200 stocks are picked is something you all should play around with.

Let me know if you guys have any questions!

Matt

Clone Algorithm
16
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.factors import SimpleMovingAverage
import math
import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    context.num_stocks = 200.
    context.leverage = 1.0
    
    fundamental_pipe = make_fundamental_pipeline()
    attach_pipeline(fundamental_pipe, 'fundamental_pipe')
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds, 
                      date_rule=date_rules.month_start(1))
    schedule_function(func=record_leverage,
                      date_rule=date_rules.every_day())
    
def record_leverage(context, data):
    record(leverage=context.account.leverage)
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data.current(i, 'price') * abs(P.positions[i].amount) for i in context.portfolio.positions)
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        if not math.isnan(weight):
            order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.stocks['price'].index.tolist()
    count = 0    
    for stock in fundies:
        if data.can_trade(stock):
            order_target_percent(stock, context.leverage / context.num_stocks)
            count = count + 1
            if count >= 200:
                break

    # Exit Securities
    for stock in context.portfolio.positions:
        if stock not in fundies and \
           stock not in context.bonds and\
           data.can_trade(stock):
            order_target_percent(stock, 0)


def before_trading_start(context, data): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from our fundamental pipeline.
    """
    pipe_out = pipeline_output('fundamental_pipe').sort('moving_avg_difference')
    context.stocks = pipe_out.iloc[0:200]

def make_fundamental_pipeline():
    """
        This pipeline aggregates all stocks which match 3 filters
            1. Growth grade is in the A or B range
            2. Profitability grade is in the A or B range
            3. The latest open is greater than the 100 day moving avg
    """
    
    grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-'] 
    
    # Factors we are tracking
    moving_avg =  SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=100)
    latest_close = USEquityPricing.close.latest
    growth_grade = morningstar.asset_classification.growth_grade.latest
    prof_grade = morningstar.asset_classification.profitability_grade.latest
    moving_avg_difference = latest_close - moving_avg

    
    # Filters for our universe
    moving_avg_filter = latest_close > moving_avg
    growth_filter = growth_grade.element_of(grades)
    prof_filter = prof_grade.element_of(grades)
   
    mask = growth_filter & prof_filter & moving_avg_filter
   
    return Pipeline(columns={'price':latest_close, 'moving_avg_difference':moving_avg_difference}, screen=mask)
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.

Matthew, thank you for your port! Last night before I went to sleep, I also managed to get something working without any deprecation warning, but it does not look or perform anywhere close to as nice as your code does. If anyone is interested, I can post it so that you guys can compare the differences, if anyone is interested. Just be forewarned, it's not pretty.

Changing the stock count from 200 to 20 drives this algo to insane amounts of leverage. I tried changing various things around, and I can't figure out why. Is it possibly the way the buy/sell logic is implemented? I've attached a backtest with only to 200 to 20 change.

Clone Algorithm
1
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.factors import SimpleMovingAverage
import math
import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    context.num_stocks = 20
    context.leverage = 1.0
    
    fundamental_pipe = make_fundamental_pipeline()
    attach_pipeline(fundamental_pipe, 'fundamental_pipe')
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds, 
                      date_rule=date_rules.month_start(1))
    schedule_function(func=record_leverage,
                      date_rule=date_rules.every_day())
    
def record_leverage(context, data):
    record(leverage=context.account.leverage)
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data.current(i, 'price') * abs(P.positions[i].amount) for i in context.portfolio.positions)
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        if not math.isnan(weight):
            order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.stocks['price'].index.tolist()
    count = 0    
    for stock in fundies:
        if data.can_trade(stock):
            order_target_percent(stock, context.leverage / context.num_stocks)
            count = count + 1
            if count >= 200:
                break

    # Exit Securities
    for stock in context.portfolio.positions:
        if stock not in fundies and \
           stock not in context.bonds and\
           data.can_trade(stock):
            order_target_percent(stock, 0)


def before_trading_start(context, data): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from our fundamental pipeline.
    """
    pipe_out = pipeline_output('fundamental_pipe').sort('moving_avg_difference')
    context.stocks = pipe_out.iloc[0:200]

def make_fundamental_pipeline():
    """
        This pipeline aggregates all stocks which match 3 filters
            1. Growth grade is in the A or B range
            2. Profitability grade is in the A or B range
            3. The latest open is greater than the 100 day moving avg
    """
    
    grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-'] 
    
    # Factors we are tracking
    moving_avg =  SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=100)
    latest_close = USEquityPricing.close.latest
    growth_grade = morningstar.asset_classification.growth_grade.latest
    prof_grade = morningstar.asset_classification.profitability_grade.latest
    moving_avg_difference = latest_close - moving_avg

    
    # Filters for our universe
    moving_avg_filter = latest_close > moving_avg
    growth_filter = growth_grade.element_of(grades)
    prof_filter = prof_grade.element_of(grades)
   
    mask = growth_filter & prof_filter & moving_avg_filter
   
    return Pipeline(columns={'price':latest_close, 'moving_avg_difference':moving_avg_difference}, screen=mask)
There was a runtime error.

That was because the number 200 was hard coded into some part of the algorithm. I've updated it to instead use num_stocks. The performance with 20 stocks still isn't great - but the leverage is reliable now.

Clone Algorithm
36
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
"""
Trading Strategy: Let Morningstar do the legwork

    Buy all the companies trading above an N day moving average
    with growth and profitability grades from Morningstar in the A-B range, 
    put any remaining capital in long term bonds. 
    Rebalance monthly
    
"""
from quantopian.pipeline import Pipeline
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.factors import SimpleMovingAverage
import math
import pandas as pd
import numpy as np


def initialize(context):
    set_slippage(slippage.FixedSlippage(spread=0.10))
    set_commission(commission.PerShare(cost=0.01, min_trade_cost=1.0))
    context.num_stocks = 20
    context.leverage = 1.0
    
    fundamental_pipe = make_fundamental_pipeline()
    attach_pipeline(fundamental_pipe, 'fundamental_pipe')
    
    context.bonds = symbols('TLT')
    schedule_function(func=buy_stocks, 
                      date_rule=date_rules.month_start())
    schedule_function(func=buy_bonds, 
                      date_rule=date_rules.month_start(1))
    schedule_function(func=record_leverage,
                      date_rule=date_rules.every_day())
    
def record_leverage(context, data):
    record(leverage=context.account.leverage)
    
    
def buy_bonds(context, data):
    P = context.portfolio    
    market_value = sum(data.current(i, 'price') * abs(P.positions[i].amount) for i in context.portfolio.positions)
    leverage = market_value / max(1, P.portfolio_value)
    gap = max(context.leverage - leverage, 0.0)
    weight = gap / len(context.bonds)
    record(bond_pct=gap*100)
    for bnd in context.bonds:
        if not math.isnan(weight):
            order_target_percent(bnd, weight)
        
    
def buy_stocks(context, data):
    fundies = context.stocks['price'].index.tolist()
    count = 0    
    for stock in fundies:
        if data.can_trade(stock):
            order_target_percent(stock, context.leverage / context.num_stocks)
            count = count + 1
            if count >= context.num_stocks:
                break

    # Exit Securities
    for stock in context.portfolio.positions:
        if stock not in fundies and \
           stock not in context.bonds and\
           data.can_trade(stock):
            order_target_percent(stock, 0)


def before_trading_start(context, data): 
    """
      Called before the start of each trading day. 
      It updates our universe with the
      securities and values found from our fundamental pipeline.
    """
    pipe_out = pipeline_output('fundamental_pipe').sort('moving_avg_difference')
    context.stocks = pipe_out.iloc[0:context.num_stocks]

def make_fundamental_pipeline():
    """
        This pipeline aggregates all stocks which match 3 filters
            1. Growth grade is in the A or B range
            2. Profitability grade is in the A or B range
            3. The latest open is greater than the 100 day moving avg
    """
    
    grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-'] 
    
    # Factors we are tracking
    moving_avg =  SimpleMovingAverage(inputs=[USEquityPricing.close], window_length=100)
    latest_close = USEquityPricing.close.latest
    growth_grade = morningstar.asset_classification.growth_grade.latest
    prof_grade = morningstar.asset_classification.profitability_grade.latest
    moving_avg_difference = latest_close - moving_avg

    
    # Filters for our universe
    moving_avg_filter = latest_close > moving_avg
    growth_filter = growth_grade.element_of(grades)
    prof_filter = prof_grade.element_of(grades)
   
    mask = growth_filter & prof_filter & moving_avg_filter
   
    return Pipeline(columns={'price':latest_close, 'moving_avg_difference':moving_avg_difference}, screen=mask)
There was a runtime error.

I messed around with it a little bit, and tried porting the RSI code from another algorithm over, but its looking like the Morningstar ratings might be a bust.