Back to Community
Piotroski's F-Score Algorithm

The Piotroski score is a discrete score between 0-9 that reflects nine criteria used to determine the strength of a firm's financial position.

Profitability Criteria:
- Positive Net Income (1 point)
- Positive return on assets in the current year (1 point)
- Positive operating cash flow in the current year (1 point)
- Cash flow from operations being greater than net Income (quality of earnings) (1 point)

Leverage, Liquidity and Source of Funds Criteria:
- Lower ratio of long term debt in the current period, compared to the previous year (decreased leverage) (1 point)
- Higher current ratio this year compared to the previous year (more liquidity) (1 point)
- No new shares were issued in the last year (lack of dilution) (1 point).

Operating Efficiency Criteria:
- A higher gross margin compared to the previous year (1 point)
- A higher asset turnover ratio compared to the previous year (1 point)

The strategy just goes long the top 10 stocks based on Piotroski's score and rebalances the portfolio every month end.
Piotroski's Score

Clone Algorithm
664
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import morningstar

"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following points in Profitablity, Leverage & Operating Effificieny means one point. 
    We are going to select 

    Profitability 
    - Positive ROA
    - Positive Operating Cash Flow
    - Higher ROA in current year versus last year
    - Cash flow from operations > ROA of current year

    Leverage
    - Current ratio of long term debt < last year's ratio of long term debt
    - Current year's current_ratio > last year's current_ratio
    - No new shares issued this year

    Operating Efficiency
    - Higher gross margin compared to previous year
    - Higher asset turnover ratio compared to previous year

"""


class Piotroski(CustomFactor):
    inputs = [
        morningstar.operation_ratios.roa,
        morningstar.cash_flow_statement.operating_cash_flow,
        morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities,
        
        morningstar.operation_ratios.long_term_debt_equity_ratio,
        morningstar.operation_ratios.current_ratio,
        morningstar.valuation.shares_outstanding,
        
        morningstar.operation_ratios.gross_margin,
        morningstar.operation_ratios.assets_turnover,
    ]
    window_length = 100
    
    def compute(self, today, assets, out,
                roa, cash_flow, cash_flow_from_ops,
                long_term_debt_ratio, current_ratio, shares_outstanding,
                gross_margin, assets_turnover):
        profit = (
            (roa[-1] > 0).astype(int) +
            (cash_flow[-1] > 0).astype(int) +
            (roa[-1] > roa[0]).astype(int) +
            (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        )
        
        leverage = (
            (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int) +
            (current_ratio[-1] > current_ratio[0]).astype(int) + 
            (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        )
        
        operating = (
            (gross_margin[-1] > gross_margin[0]).astype(int) +
            (assets_turnover[-1] > assets_turnover[0]).astype(int)
        )
        
        out[:] = profit + leverage + operating

class ROA(CustomFactor):
    window_length = 1
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > 0).astype(int)
        
class ROAChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > roa[0]).astype(int)
        
class CashFlow(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.operating_cash_flow]
    
    def compute(self, today, assets, out, cash_flow):
        out[:] = (cash_flow[-1] > 0).astype(int)
        
class CashFlowFromOps(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities, morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, cash_flow_from_ops, roa):
        out[:] = (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        
class LongTermDebtRatioChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.long_term_debt_equity_ratio]
    
    def compute(self, today, assets, out, long_term_debt_ratio):
        out[:] = (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int)
        
class CurrentDebtRatioChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.current_ratio]
    
    def compute(self, today, assets, out, current_ratio):
        out[:] = (current_ratio[-1] > current_ratio[0]).astype(int)
        
class SharesOutstandingChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.valuation.shares_outstanding]
    
    def compute(self, today, assets, out, shares_outstanding):
        out[:] = (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        
class GrossMarginChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.gross_margin]
    
    def compute(self, today, assets, out, gross_margin):
        out[:] = (gross_margin[-1] > gross_margin[0]).astype(int)
        
class AssetsTurnoverChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.assets_turnover]
    
    def compute(self, today, assets, out, assets_turnover):
        out[:] = (assets_turnover[-1] > assets_turnover[0]).astype(int) 
        
def initialize(context):

    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='piotroski')
    
    profit = ROA() + ROAChange() + CashFlow() + CashFlowFromOps()
    leverage = LongTermDebtRatioChange() + CurrentDebtRatioChange() + SharesOutstandingChange()
    operating = GrossMarginChange() + AssetsTurnoverChange()
    piotroski = profit + leverage + operating
    
    pipe.add(piotroski, 'piotroski')
    pipe.set_screen(piotroski >= 7)
    context.is_month_end = False
    schedule_function(set_month_end, date_rules.month_end(1)) 
    schedule_function(trade, date_rules.month_end(), time_rules.market_close())

def set_month_end(context, data):
    print "---- Set Month End -----"
    context.is_month_end = True
    
def before_trading_start(context, data):
    if context.is_month_end:
        context.results = pipeline_output('piotroski')
        context.long_stocks = context.results.sort_values('piotroski', ascending=False).head(10)
        context.total_piotroski = context.long_stocks.piotroski.sum()
        context.piotroski_weight = context.long_stocks.piotroski/context.long_stocks.piotroski.sum()
        update_universe(context.long_stocks.index)
   

def trade(context, data):
    valid_stocks = set(data.keys()).intersection(set(context.long_stocks.index))
    print data.keys()
    print "-------- New Set ----------"
    print valid_stocks
    
    for stock in valid_stocks:
        order_target_percent(stock, context.piotroski_weight[stock])
    
    for stock in context.portfolio.positions:
        if stock not in valid_stocks:
            order_target_percent(stock, 0)
    context.is_month_end = False

def track_orders(context, data):
    '''
    Show orders when made and filled.
    Info: https://www.quantopian.com/posts/track-orders
    '''
    c = context
    if 'trac' not in c:
        c.t_options = {           # __________    O P T I O N S    __________
            'log_neg_cash': 1,    # Show cash only when negative.
            'log_cash'    : 0,    # Show cash values in logging window or not.
            'log_ids'     : 1,    # Include order id's in logging window or not.
            'log_unfilled': 1,    # When orders are unfilled. (stop & limit excluded).
        }    # Move these to initialize() for better efficiency.
        c.trac = {}
        c.t_dates  = {  # To not overwhelm the log window, start/stop dates can be entered.
            'active': 0,
            'start' : [],   # Start dates, option like ['2007-05-07', '2010-04-26']
            'stop'  : []    # Stop  dates, option like ['2008-02-13', '2010-11-15']
        }
    from pytz import timezone     # Python only does once, makes this portable.
                                  #   Move to top of algo for better efficiency.
    # If 'start' or 'stop' lists have something in them, triggers ...
    if c.t_dates['start'] or c.t_dates['stop']:
        date = str(get_datetime().date())
        if   date in c.t_dates['start']:    # See if there's a match to start
            c.t_dates['active'] = 1
        elif date in c.t_dates['stop']:     #   ... or to stop
            c.t_dates['active'] = 0
    else: c.t_dates['active'] = 1           # Set to active b/c no conditions.
    if c.t_dates['active'] == 0: return     # Skip if not active.
    def _minute():   # To preface each line with the minute of the day.
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((bar_dt.hour * 60) + bar_dt.minute - 570).rjust(3) # (-570 = 9:31a)
    def _trac(to_log):      # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    for oid in c.trac.copy():               # Existing known orders
      o = get_order(oid)
      if o.dt == o.created: continue        # No chance of fill yet.
      cash = ''
      prc  = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
      if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
        cash = 'cash {}'.format(int(c.portfolio.cash))
      if o.status == 2:                      # Canceled
        do = 'Buy' if o.amount > 0 else 'Sell' ; style = ''
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}     Canceled {} {} {}{} at {}   {}  {}'.format(_minute(), do, o.amount,
           o.sid.symbol, style, prc, cash, o.id[-4:] if c.t_options['log_ids'] else ''))
        del c.trac[o.id]
      elif o.filled:                             # Filled at least some.
        filled = '{}'.format(o.amount)
        filled_amt = 0
        if o.status == 1:            # Complete
          if 0 < c.trac[o.id] < o.amount:
            filled   = 'all {}/{}'.format(o.filled - c.trac[o.id], o.amount)
          filled_amt = o.filled
        else:                                    # c.trac[o.id] value is previously filled total
          filled_amt = o.filled - c.trac[o.id]   # filled this time, can be 0
          c.trac[o.id] = o.filled                # save fill value for increments math
          filled = '{}/{}'.format(filled_amt, o.amount)
        if filled_amt:
          now = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
          pnl = ''  # for the trade only
          amt = c.portfolio.positions[o.sid].amount ; style = ''
          if (amt - o.filled) * o.filled < 0:    # Profit-taking scenario including short-buyback
            cb = c.portfolio.positions[o.sid].cost_basis
            if cb:
              pnl  = -filled_amt * (prc - cb)
              sign = '+' if pnl > 0 else '-'
              pnl  = '  ({}{})'.format(sign, '%.0f' % abs(pnl))
          if o.stop:
            style = ' stop {}'.format(o.stop)
            if o.limit: style = ' stop () limit {}'.format(o.stop, o.limit)
          elif o.limit: style = ' limit {}'.format(o.limit)
          if o.filled == o.amount: del c.trac[o.id]
          _trac(' {}      {} {} {}{} at {}{}{}'.format(_minute(),
            'Bot' if o.amount > 0 else 'Sold', filled, o.sid.symbol, now,
            '%.2f' % prc, pnl, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))
      elif c.t_options['log_unfilled'] and not (o.stop or o.limit):
        _trac(' {}         {} {}{} unfilled  {}'.format(_minute(), o.sid.symbol, o.amount,
         ' limit' if o.limit else '', o.id[-4:] if c.t_options['log_ids'] else ''))

    oo = get_open_orders().values()
    if not oo: return                       # Handle new orders
    cash = ''
    if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
      cash = 'cash {}'.format(int(c.portfolio.cash))
    for oo_list in oo:
      for o in oo_list:
        if o.id in c.trac: continue         # Only new orders beyond this point
        prc = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
        c.trac[o.id] = 0 ; style = ''
        now  = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}   {} {} {}{} at {}{}'.format(_minute(), 'Buy' if o.amount > 0 else 'Sell',
          o.amount, o.sid.symbol, now, '%.2f' % prc, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))


            
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    track_orders(context, data)
There was a runtime error.
35 responses

Karl - Thanks for the link. I will go through the paper. I think Piotroski score adds value by acting like filter but do not think it could be a good strategy on its own. I'm looking at other ways to combine and generate alpha.

It would be a good idea to add ten shorts based on the lower tier stocks.
I am sure dollar volume traded should be included in a mask for the pipeline.

Sure will add it tonight.

Thanks awesome stuff.

Doesn't this compare for the previous day:

 def compute(self, today, assets, out, shares_outstanding):  
        out[:] = (shares_outstanding**[-1]** <= shares_outstanding[0]).astype(int)  

I attempted to implement piotroski a few years ago, it took a lot of gymnastics to fix this and get this to run within the limitations of the platform.

The Piotroski score is a surprisingly good strategy long term - I have a backtest of it going back to 2000 here https://investorsedge.net/Strategy/Definition/24430/23672

InvestorsEdge allows you to access the ranking at each rebalance point through an API / JSON - can I link to it from my code here?

Sorry guys not getting time to update the algo. Will do it over the weekend.

@Liam - InvestoresEdge sounds interesting, is it free to use ?

It has a free trial, but after that there's a monthly charge. The API is a recent enhancement that people have been after for a while and I thought it would be useful to test that everything works!

@ Praveen Bhushan
No worries I am looking forward to it.

@Kevin Stevens -1 index means you are selecting the element from right side instead of left side. So -1 is basically the last element. Since I'm rebalancing the portfolio every month end, so the window size should be roughly 22 days. Thanks for pointing it out.

Great stuff

Long/Short Portfolio and extra filtering based on Market Cap and EV to EBITDA Ratio

Clone Algorithm
664
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.filters import QTradableStocksUS

"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following points in Profitablity, Leverage & Operating Effificieny means one point. 
    We are going to select 

    Profitability 
    - Positive ROA
    - Positive Operating Cash Flow
    - Higher ROA in current year versus last year
    - Cash flow from operations > ROA of current year

    Leverage
    - Current ratio of long term debt < last year's ratio of long term debt
    - Current year's current_ratio > last year's current_ratio
    - No new shares issued this year

    Operating Efficiency
    - Higher gross margin compared to previous year
    - Higher asset turnover ratio compared to previous year

"""


class Piotroski(CustomFactor):
    inputs = [
        morningstar.operation_ratios.roa,
        morningstar.cash_flow_statement.operating_cash_flow,
        morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities,
        
        morningstar.operation_ratios.long_term_debt_equity_ratio,
        morningstar.operation_ratios.current_ratio,
        morningstar.valuation.shares_outstanding,
        
        morningstar.operation_ratios.gross_margin,
        morningstar.operation_ratios.assets_turnover,
    ]
    window_length = 22
    
    def compute(self, today, assets, out,
                roa, cash_flow, cash_flow_from_ops,
                long_term_debt_ratio, current_ratio, shares_outstanding,
                gross_margin, assets_turnover):
        profit = (
            (roa[-1] > 0).astype(int) +
            (cash_flow[-1] > 0).astype(int) +
            (roa[-1] > roa[0]).astype(int) +
            (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        )
        
        leverage = (
            (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int) +
            (current_ratio[-1] > current_ratio[0]).astype(int) + 
            (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        )
        
        operating = (
            (gross_margin[-1] > gross_margin[0]).astype(int) +
            (assets_turnover[-1] > assets_turnover[0]).astype(int)
        )
        
        out[:] = profit + leverage + operating

class ROA(CustomFactor):
    window_length = 1
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > 0).astype(int)
        
class ROAChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > roa[0]).astype(int)
        
class CashFlow(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.operating_cash_flow]
    
    def compute(self, today, assets, out, cash_flow):
        out[:] = (cash_flow[-1] > 0).astype(int)
        
class CashFlowFromOps(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities, morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, cash_flow_from_ops, roa):
        out[:] = (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        
class LongTermDebtRatioChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.long_term_debt_equity_ratio]
    
    def compute(self, today, assets, out, long_term_debt_ratio):
        out[:] = (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int)
        
class CurrentDebtRatioChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.current_ratio]
    
    def compute(self, today, assets, out, current_ratio):
        out[:] = (current_ratio[-1] > current_ratio[0]).astype(int)
        
class SharesOutstandingChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.valuation.shares_outstanding]
    
    def compute(self, today, assets, out, shares_outstanding):
        out[:] = (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        
class GrossMarginChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.gross_margin]
    
    def compute(self, today, assets, out, gross_margin):
        out[:] = (gross_margin[-1] > gross_margin[0]).astype(int)
        
class AssetsTurnoverChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.assets_turnover]
    
    def compute(self, today, assets, out, assets_turnover):
        out[:] = (assets_turnover[-1] > assets_turnover[0]).astype(int) 
        
       
def initialize(context):

    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='piotroski')
    
    profit = ROA() + ROAChange() + CashFlow() + CashFlowFromOps()
    leverage = LongTermDebtRatioChange() + CurrentDebtRatioChange() + SharesOutstandingChange()
    operating = GrossMarginChange() + AssetsTurnoverChange()
    piotroski = profit + leverage + operating
    
    ev_ebitda = morningstar.valuation_ratios.ev_to_ebitda.latest > 0  
    market_cap = morningstar.valuation.market_cap > 1e9
    
    pipe.add(piotroski, 'piotroski')
    pipe.set_screen(((piotroski >= 7) | (piotroski <= 3)) & ev_ebitda & market_cap)
    context.is_month_end = False
    schedule_function(set_month_end, date_rules.month_end(1)) 
    schedule_function(trade_long, date_rules.month_end(), time_rules.market_open())
    schedule_function(trade_short, date_rules.month_end(), time_rules.market_open())
    schedule_function(trade, date_rules.month_end(), time_rules.market_close())

def set_month_end(context, data):
    print "---- Set Month End -----"
    context.is_month_end = True
    
def before_trading_start(context, data):
    if context.is_month_end:
        context.results = pipeline_output('piotroski')
        try:
            context.long_stocks = context.results.sort_values('piotroski', ascending=False).head(10)
            context.short_stocks = context.results.sort_values('piotroski', ascending=True).head(10)
            print context.long_stocks
            print context.short_stocks
        except: 
            print ("In exception") 
   
def trade_long(context, data):
    for stock in context.long_stocks.index:
        if data.can_trade(stock):
            order_target_percent(stock, 1.0/20)

def trade_short(context, data):
    for stock in context.short_stocks.index:
        if data.can_trade(stock):
            order_target_percent(stock, -1.0/20)


def trade(context, data):
    print "-------- New Set ----------"
    print context.long_stocks 
    print context.short_stocks
     
    for stock in context.portfolio.positions:
        if stock not in context.long_stocks.index and stock not in context.short_stocks.index:
            order_target_percent(stock, 0)
    context.is_month_end = False

def track_orders(context, data):
    '''
    Show orders when made and filled.
    Info: https://www.quantopian.com/posts/track-orders
    '''
    c = context
    if 'trac' not in c:
        c.t_options = {           # __________    O P T I O N S    __________
            'log_neg_cash': 1,    # Show cash only when negative.
            'log_cash'    : 0,    # Show cash values in logging window or not.
            'log_ids'     : 1,    # Include order id's in logging window or not.
            'log_unfilled': 1,    # When orders are unfilled. (stop & limit excluded).
        }    # Move these to initialize() for better efficiency.
        c.trac = {}
        c.t_dates  = {  # To not overwhelm the log window, start/stop dates can be entered.
            'active': 0,
            'start' : [],   # Start dates, option like ['2007-05-07', '2010-04-26']
            'stop'  : []    # Stop  dates, option like ['2008-02-13', '2010-11-15']
        }
    from pytz import timezone     # Python only does once, makes this portable.
                                  #   Move to top of algo for better efficiency.
    # If 'start' or 'stop' lists have something in them, triggers ...
    if c.t_dates['start'] or c.t_dates['stop']:
        date = str(get_datetime().date())
        if   date in c.t_dates['start']:    # See if there's a match to start
            c.t_dates['active'] = 1
        elif date in c.t_dates['stop']:     #   ... or to stop
            c.t_dates['active'] = 0
    else: c.t_dates['active'] = 1           # Set to active b/c no conditions.
    if c.t_dates['active'] == 0: return     # Skip if not active.
    def _minute():   # To preface each line with the minute of the day.
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((bar_dt.hour * 60) + bar_dt.minute - 570).rjust(3) # (-570 = 9:31a)
    def _trac(to_log):      # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    for oid in c.trac.copy():               # Existing known orders
      o = get_order(oid)
      if o.dt == o.created: continue        # No chance of fill yet.
      cash = ''
      prc  = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
      if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
        cash = 'cash {}'.format(int(c.portfolio.cash))
      if o.status == 2:                      # Canceled
        do = 'Buy' if o.amount > 0 else 'Sell' ; style = ''
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}     Canceled {} {} {}{} at {}   {}  {}'.format(_minute(), do, o.amount,
           o.sid.symbol, style, prc, cash, o.id[-4:] if c.t_options['log_ids'] else ''))
        del c.trac[o.id]
      elif o.filled:                             # Filled at least some.
        filled = '{}'.format(o.amount)
        filled_amt = 0
        if o.status == 1:            # Complete
          if 0 < c.trac[o.id] < o.amount:
            filled   = 'all {}/{}'.format(o.filled - c.trac[o.id], o.amount)
          filled_amt = o.filled
        else:                                    # c.trac[o.id] value is previously filled total
          filled_amt = o.filled - c.trac[o.id]   # filled this time, can be 0
          c.trac[o.id] = o.filled                # save fill value for increments math
          filled = '{}/{}'.format(filled_amt, o.amount)
        if filled_amt:
          now = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
          pnl = ''  # for the trade only
          amt = c.portfolio.positions[o.sid].amount ; style = ''
          if (amt - o.filled) * o.filled < 0:    # Profit-taking scenario including short-buyback
            cb = c.portfolio.positions[o.sid].cost_basis
            if cb:
              pnl  = -filled_amt * (prc - cb)
              sign = '+' if pnl > 0 else '-'
              pnl  = '  ({}{})'.format(sign, '%.0f' % abs(pnl))
          if o.stop:
            style = ' stop {}'.format(o.stop)
            if o.limit: style = ' stop () limit {}'.format(o.stop, o.limit)
          elif o.limit: style = ' limit {}'.format(o.limit)
          if o.filled == o.amount: del c.trac[o.id]
          _trac(' {}      {} {} {}{} at {}{}{}'.format(_minute(),
            'Bot' if o.amount > 0 else 'Sold', filled, o.sid.symbol, now,
            '%.2f' % prc, pnl, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))
      elif c.t_options['log_unfilled'] and not (o.stop or o.limit):
        _trac(' {}         {} {}{} unfilled  {}'.format(_minute(), o.sid.symbol, o.amount,
         ' limit' if o.limit else '', o.id[-4:] if c.t_options['log_ids'] else ''))

    oo = get_open_orders().values()
    if not oo: return                       # Handle new orders
    cash = ''
    if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
      cash = 'cash {}'.format(int(c.portfolio.cash))
    for oo_list in oo:
      for o in oo_list:
        if o.id in c.trac: continue         # Only new orders beyond this point
        prc = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
        c.trac[o.id] = 0 ; style = ''
        now  = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}   {} {} {}{} at {}{}'.format(_minute(), 'Buy' if o.amount > 0 else 'Sell',
          o.amount, o.sid.symbol, now, '%.2f' % prc, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))


            
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    track_orders(context, data)
There was a runtime error.

From the visuals it appears to be not statistically different than SPY returns hence my best guess is that you're just too dependent on the SPY returns. Try running some cointegration tests (ADF) or maybe just a linear regression SPY~Algo to check if you can reject the null of beta=1.

Do you mean to check for co-integration between SPY and each stock pick ?

There are a few problems with this trading strategy. All the principles involved appear reasonable. However, this might be a case where theory and practice might be slightly different.

The only changes I brought was to add more time and raise the stakes to $10M.

See the attached algo. I'll post the backtest analysis next.

Clone Algorithm
56
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.filters import QTradableStocksUS

"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following points in Profitablity, Leverage & Operating Effificieny means one point. 
    We are going to select 

    Profitability 
    - Positive ROA
    - Positive Operating Cash Flow
    - Higher ROA in current year versus last year
    - Cash flow from operations > ROA of current year

    Leverage
    - Current ratio of long term debt < last year's ratio of long term debt
    - Current year's current_ratio > last year's current_ratio
    - No new shares issued this year

    Operating Efficiency
    - Higher gross margin compared to previous year
    - Higher asset turnover ratio compared to previous year

"""


class Piotroski(CustomFactor):
    inputs = [
        morningstar.operation_ratios.roa,
        morningstar.cash_flow_statement.operating_cash_flow,
        morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities,
        
        morningstar.operation_ratios.long_term_debt_equity_ratio,
        morningstar.operation_ratios.current_ratio,
        morningstar.valuation.shares_outstanding,
        
        morningstar.operation_ratios.gross_margin,
        morningstar.operation_ratios.assets_turnover,
    ]
    window_length = 22
    
    def compute(self, today, assets, out,
                roa, cash_flow, cash_flow_from_ops,
                long_term_debt_ratio, current_ratio, shares_outstanding,
                gross_margin, assets_turnover):
        profit = (
            (roa[-1] > 0).astype(int) +
            (cash_flow[-1] > 0).astype(int) +
            (roa[-1] > roa[0]).astype(int) +
            (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        )
        
        leverage = (
            (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int) +
            (current_ratio[-1] > current_ratio[0]).astype(int) + 
            (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        )
        
        operating = (
            (gross_margin[-1] > gross_margin[0]).astype(int) +
            (assets_turnover[-1] > assets_turnover[0]).astype(int)
        )
        
        out[:] = profit + leverage + operating

class ROA(CustomFactor):
    window_length = 1
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > 0).astype(int)
        
class ROAChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > roa[0]).astype(int)
        
class CashFlow(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.operating_cash_flow]
    
    def compute(self, today, assets, out, cash_flow):
        out[:] = (cash_flow[-1] > 0).astype(int)
        
class CashFlowFromOps(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities, morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, cash_flow_from_ops, roa):
        out[:] = (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        
class LongTermDebtRatioChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.long_term_debt_equity_ratio]
    
    def compute(self, today, assets, out, long_term_debt_ratio):
        out[:] = (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int)
        
class CurrentDebtRatioChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.current_ratio]
    
    def compute(self, today, assets, out, current_ratio):
        out[:] = (current_ratio[-1] > current_ratio[0]).astype(int)
        
class SharesOutstandingChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.valuation.shares_outstanding]
    
    def compute(self, today, assets, out, shares_outstanding):
        out[:] = (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        
class GrossMarginChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.gross_margin]
    
    def compute(self, today, assets, out, gross_margin):
        out[:] = (gross_margin[-1] > gross_margin[0]).astype(int)
        
class AssetsTurnoverChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.assets_turnover]
    
    def compute(self, today, assets, out, assets_turnover):
        out[:] = (assets_turnover[-1] > assets_turnover[0]).astype(int) 
        
       
def initialize(context):

    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='piotroski')
    
    profit = ROA() + ROAChange() + CashFlow() + CashFlowFromOps()
    leverage = LongTermDebtRatioChange() + CurrentDebtRatioChange() + SharesOutstandingChange()
    operating = GrossMarginChange() + AssetsTurnoverChange()
    piotroski = profit + leverage + operating
    
    ev_ebitda = morningstar.valuation_ratios.ev_to_ebitda.latest > 0  
    market_cap = morningstar.valuation.market_cap > 1e9
    
    pipe.add(piotroski, 'piotroski')
    pipe.set_screen(((piotroski >= 7) | (piotroski <= 3)) & ev_ebitda & market_cap)
    context.is_month_end = False
    schedule_function(set_month_end, date_rules.month_end(1)) 
    schedule_function(trade_long, date_rules.month_end(), time_rules.market_open())
    schedule_function(trade_short, date_rules.month_end(), time_rules.market_open())
    schedule_function(trade, date_rules.month_end(), time_rules.market_close())

def set_month_end(context, data):
    print "---- Set Month End -----"
    context.is_month_end = True
    
def before_trading_start(context, data):
    if context.is_month_end:
        context.results = pipeline_output('piotroski')
        try:
            context.long_stocks = context.results.sort_values('piotroski', ascending=False).head(10)
            context.short_stocks = context.results.sort_values('piotroski', ascending=True).head(10)
            print context.long_stocks
            print context.short_stocks
        except: 
            print ("In exception") 
   
def trade_long(context, data):
    for stock in context.long_stocks.index:
        if data.can_trade(stock):
            order_target_percent(stock, 1.0/20)

def trade_short(context, data):
    for stock in context.short_stocks.index:
        if data.can_trade(stock):
            order_target_percent(stock, -1.0/20)


def trade(context, data):
    print "-------- New Set ----------"
    print context.long_stocks 
    print context.short_stocks
     
    for stock in context.portfolio.positions:
        if stock not in context.long_stocks.index and stock not in context.short_stocks.index:
            order_target_percent(stock, 0)
    context.is_month_end = False

def track_orders(context, data):
    '''
    Show orders when made and filled.
    Info: https://www.quantopian.com/posts/track-orders
    '''
    c = context
    if 'trac' not in c:
        c.t_options = {           # __________    O P T I O N S    __________
            'log_neg_cash': 1,    # Show cash only when negative.
            'log_cash'    : 0,    # Show cash values in logging window or not.
            'log_ids'     : 1,    # Include order id's in logging window or not.
            'log_unfilled': 1,    # When orders are unfilled. (stop & limit excluded).
        }    # Move these to initialize() for better efficiency.
        c.trac = {}
        c.t_dates  = {  # To not overwhelm the log window, start/stop dates can be entered.
            'active': 0,
            'start' : [],   # Start dates, option like ['2007-05-07', '2010-04-26']
            'stop'  : []    # Stop  dates, option like ['2008-02-13', '2010-11-15']
        }
    from pytz import timezone     # Python only does once, makes this portable.
                                  #   Move to top of algo for better efficiency.
    # If 'start' or 'stop' lists have something in them, triggers ...
    if c.t_dates['start'] or c.t_dates['stop']:
        date = str(get_datetime().date())
        if   date in c.t_dates['start']:    # See if there's a match to start
            c.t_dates['active'] = 1
        elif date in c.t_dates['stop']:     #   ... or to stop
            c.t_dates['active'] = 0
    else: c.t_dates['active'] = 1           # Set to active b/c no conditions.
    if c.t_dates['active'] == 0: return     # Skip if not active.
    def _minute():   # To preface each line with the minute of the day.
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((bar_dt.hour * 60) + bar_dt.minute - 570).rjust(3) # (-570 = 9:31a)
    def _trac(to_log):      # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    for oid in c.trac.copy():               # Existing known orders
      o = get_order(oid)
      if o.dt == o.created: continue        # No chance of fill yet.
      cash = ''
      prc  = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
      if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
        cash = 'cash {}'.format(int(c.portfolio.cash))
      if o.status == 2:                      # Canceled
        do = 'Buy' if o.amount > 0 else 'Sell' ; style = ''
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}     Canceled {} {} {}{} at {}   {}  {}'.format(_minute(), do, o.amount,
           o.sid.symbol, style, prc, cash, o.id[-4:] if c.t_options['log_ids'] else ''))
        del c.trac[o.id]
      elif o.filled:                             # Filled at least some.
        filled = '{}'.format(o.amount)
        filled_amt = 0
        if o.status == 1:            # Complete
          if 0 < c.trac[o.id] < o.amount:
            filled   = 'all {}/{}'.format(o.filled - c.trac[o.id], o.amount)
          filled_amt = o.filled
        else:                                    # c.trac[o.id] value is previously filled total
          filled_amt = o.filled - c.trac[o.id]   # filled this time, can be 0
          c.trac[o.id] = o.filled                # save fill value for increments math
          filled = '{}/{}'.format(filled_amt, o.amount)
        if filled_amt:
          now = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
          pnl = ''  # for the trade only
          amt = c.portfolio.positions[o.sid].amount ; style = ''
          if (amt - o.filled) * o.filled < 0:    # Profit-taking scenario including short-buyback
            cb = c.portfolio.positions[o.sid].cost_basis
            if cb:
              pnl  = -filled_amt * (prc - cb)
              sign = '+' if pnl > 0 else '-'
              pnl  = '  ({}{})'.format(sign, '%.0f' % abs(pnl))
          if o.stop:
            style = ' stop {}'.format(o.stop)
            if o.limit: style = ' stop () limit {}'.format(o.stop, o.limit)
          elif o.limit: style = ' limit {}'.format(o.limit)
          if o.filled == o.amount: del c.trac[o.id]
          _trac(' {}      {} {} {}{} at {}{}{}'.format(_minute(),
            'Bot' if o.amount > 0 else 'Sold', filled, o.sid.symbol, now,
            '%.2f' % prc, pnl, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))
      elif c.t_options['log_unfilled'] and not (o.stop or o.limit):
        _trac(' {}         {} {}{} unfilled  {}'.format(_minute(), o.sid.symbol, o.amount,
         ' limit' if o.limit else '', o.id[-4:] if c.t_options['log_ids'] else ''))

    oo = get_open_orders().values()
    if not oo: return                       # Handle new orders
    cash = ''
    if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
      cash = 'cash {}'.format(int(c.portfolio.cash))
    for oo_list in oo:
      for o in oo_list:
        if o.id in c.trac: continue         # Only new orders beyond this point
        prc = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
        c.trac[o.id] = 0 ; style = ''
        now  = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}   {} {} {}{} at {}{}'.format(_minute(), 'Buy' if o.amount > 0 else 'Sell',
          o.amount, o.sid.symbol, now, '%.2f' % prc, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))


            
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    track_orders(context, data)
There was a runtime error.

Here is the backtest analysis for the previous post.

First, note the extended time interval and the added reserves which are both done outside the strategy itself. Nothing else was changed.

Of note:

It appears as if the strategy did pretty well over those 12 years. Except during the financial crisis where it had a -78% drawdown and during its last year with a -32% drawdown. At other times, it was acceptable. So something to correct its inclination for these drawdowns would be a good start to improving the overall strategy.

Next, the strategy does go overboard in its use of leverage going to as high as 5.15, on average, during its last year. This leverage has also been increasing in time from the start of the simulation (see the exposure chart). I would have to conclude that part of the excess return has for origin this higher leveraging.

The average holding period is increasing over time. This is not a bad thing since it is where it is making most of its profits. Being in and holding while the general market is going up... It is also why its shorts do not perform so well.

See the Long and short holding chart where the shorts are taking dominance of the trading strategy. This might not be as bad as it looks since 2018 has currently gone nowhere. However, its past handling of shorts is poor: taking some 40% of the profits away from the total long profits.

Most of the strategy's return is due to common factors. Nonetheless, it still did better than its benchmark.

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

@Guy - How can you justify the increase in capital as the key factor for strategy returns? That does not sound right to me. Is it that the profitable trades resulted in better returns than the bad deals (negative impact).

Thanks for the tearsheet. Exploring it now to analyze the strategy.

@Praveen, you do a simulation, it is to find out how your trading strategy would have behaved in good and bad times since the future will present you with both. So, you need your strategy to be prepared accordingly. Ignoring the bad times is an invitation for a portfolio disaster.

As for avoiding the 2007-08 crash, the simulation presented does not show very good returns over that period. On the contrary, it did show quite a drawdown, some -78%. That is not very positive. Not many people or fund would stick around for such an adventure into the dark side of inverse-profitability.

The strategy has an average holding time of over 2 years. In an up market, it is all good for longs but not so good for shorts. And yet, in recent years, the number of shorts exceed the number of longs. And that phenomenon is shown to be increasing. So there should be no surprise to see the strategy as if breaking down.

The leveraging is gradually increased up to an average 5.5 as per the tearsheet. Meaning that you would be borrowing 4.5 times the equity where no interest fees are being charged, or even considered.

If you had F(0)∙(1+r)^t to represent portfolio growth without leveraging, then with leverage you would have: 5.5∙ F(0)∙(1+(r - θ∙L)^t. And if you removed the leveraging, or paid the interests on the 4.5 times equity, you might see the total profits disappear. Those fees are compounding too. Note that removing the leveraging would also remove the fees (θ∙L). Is r > |θ∙L| so that r - θ∙L > 0 and by how much? That is the question. From my point of view, I see the overall CAGR presented as insufficient to compensate for the financial burden brought on by this 5.5 leveraging. For now, I will leave that problem for someone else to solve.

@Guy - Thanks for the explanation. It makes more sense now. Here are the results for long only portfolio( without market cap and ev to ebitda filtering).

Clone Algorithm
14
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import morningstar

"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following points in Profitablity, Leverage & Operating Effificieny means one point. 
    We are going to select 

    Profitability 
    - Positive ROA
    - Positive Operating Cash Flow
    - Higher ROA in current year versus last year
    - Cash flow from operations > ROA of current year

    Leverage
    - Current ratio of long term debt < last year's ratio of long term debt
    - Current year's current_ratio > last year's current_ratio
    - No new shares issued this year

    Operating Efficiency
    - Higher gross margin compared to previous year
    - Higher asset turnover ratio compared to previous year

"""


class Piotroski(CustomFactor):
    inputs = [
        morningstar.operation_ratios.roa,
        morningstar.cash_flow_statement.operating_cash_flow,
        morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities,
        
        morningstar.operation_ratios.long_term_debt_equity_ratio,
        morningstar.operation_ratios.current_ratio,
        morningstar.valuation.shares_outstanding,
        
        morningstar.operation_ratios.gross_margin,
        morningstar.operation_ratios.assets_turnover,
    ]
    window_length = 100
    
    def compute(self, today, assets, out,
                roa, cash_flow, cash_flow_from_ops,
                long_term_debt_ratio, current_ratio, shares_outstanding,
                gross_margin, assets_turnover):
        profit = (
            (roa[-1] > 0).astype(int) +
            (cash_flow[-1] > 0).astype(int) +
            (roa[-1] > roa[0]).astype(int) +
            (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        )
        
        leverage = (
            (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int) +
            (current_ratio[-1] > current_ratio[0]).astype(int) + 
            (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        )
        
        operating = (
            (gross_margin[-1] > gross_margin[0]).astype(int) +
            (assets_turnover[-1] > assets_turnover[0]).astype(int)
        )
        
        out[:] = profit + leverage + operating

class ROA(CustomFactor):
    window_length = 1
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > 0).astype(int)
        
class ROAChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > roa[0]).astype(int)
        
class CashFlow(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.operating_cash_flow]
    
    def compute(self, today, assets, out, cash_flow):
        out[:] = (cash_flow[-1] > 0).astype(int)
        
class CashFlowFromOps(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities, morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, cash_flow_from_ops, roa):
        out[:] = (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        
class LongTermDebtRatioChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.long_term_debt_equity_ratio]
    
    def compute(self, today, assets, out, long_term_debt_ratio):
        out[:] = (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int)
        
class CurrentDebtRatioChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.current_ratio]
    
    def compute(self, today, assets, out, current_ratio):
        out[:] = (current_ratio[-1] > current_ratio[0]).astype(int)
        
class SharesOutstandingChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.valuation.shares_outstanding]
    
    def compute(self, today, assets, out, shares_outstanding):
        out[:] = (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        
class GrossMarginChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.gross_margin]
    
    def compute(self, today, assets, out, gross_margin):
        out[:] = (gross_margin[-1] > gross_margin[0]).astype(int)
        
class AssetsTurnoverChange(CustomFactor):
    window_length = 100
    inputs = [morningstar.operation_ratios.assets_turnover]
    
    def compute(self, today, assets, out, assets_turnover):
        out[:] = (assets_turnover[-1] > assets_turnover[0]).astype(int) 
        
def initialize(context):

    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='piotroski')
    
    profit = ROA() + ROAChange() + CashFlow() + CashFlowFromOps()
    leverage = LongTermDebtRatioChange() + CurrentDebtRatioChange() + SharesOutstandingChange()
    operating = GrossMarginChange() + AssetsTurnoverChange()
    piotroski = profit + leverage + operating
    
    pipe.add(piotroski, 'piotroski')
    pipe.set_screen(piotroski >= 7)
    context.is_month_end = False
    schedule_function(set_month_end, date_rules.month_end(1)) 
    schedule_function(trade, date_rules.month_end(), time_rules.market_close())

def set_month_end(context, data):
    print "---- Set Month End -----"
    context.is_month_end = True
    
def before_trading_start(context, data):
    if context.is_month_end:
        context.results = pipeline_output('piotroski')
        context.long_stocks = context.results.sort_values('piotroski', ascending=False).head(10)
        context.total_piotroski = context.long_stocks.piotroski.sum()
        context.piotroski_weight = context.long_stocks.piotroski/context.long_stocks.piotroski.sum()
        update_universe(context.long_stocks.index)
   

def trade(context, data):
    valid_stocks = set(data.keys()).intersection(set(context.long_stocks.index))
    print data.keys()
    print "-------- New Set ----------"
    print valid_stocks
    
    for stock in valid_stocks:
        order_target_percent(stock, context.piotroski_weight[stock])
    
    for stock in context.portfolio.positions:
        if stock not in valid_stocks:
            order_target_percent(stock, 0)
    context.is_month_end = False

def track_orders(context, data):
    '''
    Show orders when made and filled.
    Info: https://www.quantopian.com/posts/track-orders
    '''
    c = context
    if 'trac' not in c:
        c.t_options = {           # __________    O P T I O N S    __________
            'log_neg_cash': 1,    # Show cash only when negative.
            'log_cash'    : 0,    # Show cash values in logging window or not.
            'log_ids'     : 1,    # Include order id's in logging window or not.
            'log_unfilled': 1,    # When orders are unfilled. (stop & limit excluded).
        }    # Move these to initialize() for better efficiency.
        c.trac = {}
        c.t_dates  = {  # To not overwhelm the log window, start/stop dates can be entered.
            'active': 0,
            'start' : [],   # Start dates, option like ['2007-05-07', '2010-04-26']
            'stop'  : []    # Stop  dates, option like ['2008-02-13', '2010-11-15']
        }
    from pytz import timezone     # Python only does once, makes this portable.
                                  #   Move to top of algo for better efficiency.
    # If 'start' or 'stop' lists have something in them, triggers ...
    if c.t_dates['start'] or c.t_dates['stop']:
        date = str(get_datetime().date())
        if   date in c.t_dates['start']:    # See if there's a match to start
            c.t_dates['active'] = 1
        elif date in c.t_dates['stop']:     #   ... or to stop
            c.t_dates['active'] = 0
    else: c.t_dates['active'] = 1           # Set to active b/c no conditions.
    if c.t_dates['active'] == 0: return     # Skip if not active.
    def _minute():   # To preface each line with the minute of the day.
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        return str((bar_dt.hour * 60) + bar_dt.minute - 570).rjust(3) # (-570 = 9:31a)
    def _trac(to_log):      # So all logging comes from the same line number,
        log.info(to_log)    #   for vertical alignment in the logging window.

    for oid in c.trac.copy():               # Existing known orders
      o = get_order(oid)
      if o.dt == o.created: continue        # No chance of fill yet.
      cash = ''
      prc  = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
      if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
        cash = 'cash {}'.format(int(c.portfolio.cash))
      if o.status == 2:                      # Canceled
        do = 'Buy' if o.amount > 0 else 'Sell' ; style = ''
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}     Canceled {} {} {}{} at {}   {}  {}'.format(_minute(), do, o.amount,
           o.sid.symbol, style, prc, cash, o.id[-4:] if c.t_options['log_ids'] else ''))
        del c.trac[o.id]
      elif o.filled:                             # Filled at least some.
        filled = '{}'.format(o.amount)
        filled_amt = 0
        if o.status == 1:            # Complete
          if 0 < c.trac[o.id] < o.amount:
            filled   = 'all {}/{}'.format(o.filled - c.trac[o.id], o.amount)
          filled_amt = o.filled
        else:                                    # c.trac[o.id] value is previously filled total
          filled_amt = o.filled - c.trac[o.id]   # filled this time, can be 0
          c.trac[o.id] = o.filled                # save fill value for increments math
          filled = '{}/{}'.format(filled_amt, o.amount)
        if filled_amt:
          now = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
          pnl = ''  # for the trade only
          amt = c.portfolio.positions[o.sid].amount ; style = ''
          if (amt - o.filled) * o.filled < 0:    # Profit-taking scenario including short-buyback
            cb = c.portfolio.positions[o.sid].cost_basis
            if cb:
              pnl  = -filled_amt * (prc - cb)
              sign = '+' if pnl > 0 else '-'
              pnl  = '  ({}{})'.format(sign, '%.0f' % abs(pnl))
          if o.stop:
            style = ' stop {}'.format(o.stop)
            if o.limit: style = ' stop () limit {}'.format(o.stop, o.limit)
          elif o.limit: style = ' limit {}'.format(o.limit)
          if o.filled == o.amount: del c.trac[o.id]
          _trac(' {}      {} {} {}{} at {}{}{}'.format(_minute(),
            'Bot' if o.amount > 0 else 'Sold', filled, o.sid.symbol, now,
            '%.2f' % prc, pnl, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))
      elif c.t_options['log_unfilled'] and not (o.stop or o.limit):
        _trac(' {}         {} {}{} unfilled  {}'.format(_minute(), o.sid.symbol, o.amount,
         ' limit' if o.limit else '', o.id[-4:] if c.t_options['log_ids'] else ''))

    oo = get_open_orders().values()
    if not oo: return                       # Handle new orders
    cash = ''
    if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']:
      cash = 'cash {}'.format(int(c.portfolio.cash))
    for oo_list in oo:
      for o in oo_list:
        if o.id in c.trac: continue         # Only new orders beyond this point
        prc = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price
        c.trac[o.id] = 0 ; style = ''
        now  = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _'
        if o.stop:
          style = ' stop {}'.format(o.stop)
          if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit)
        elif o.limit: style = ' limit {}'.format(o.limit)
        _trac(' {}   {} {} {}{} at {}{}'.format(_minute(), 'Buy' if o.amount > 0 else 'Sell',
          o.amount, o.sid.symbol, now, '%.2f' % prc, style).ljust(52) + '  {}  {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else ''))


            
# Will be called on every trade event for the securities you specify. 
def handle_data(context, data):
    track_orders(context, data)
There was a runtime error.

@Guy - As you mentioned about leverage, so does that mean Quantopian platform does not put any charges for naked short orders & its all without any charges.

@Praveen, true. None of it is considered. So you have to estimate this cost yourself.

From the equation given, if r - |θ∙L| < 0, the CAGR will become negative which is not the route to success. If leveraging fees are 4% and the strategy returned r = 12.5% as in this case, then you should expect to get: 5.5∙F(0)∙(1 + 0.125 – 4.5∙0.04)^12. If you do the calculation, it will not turn out to be positive. A strategy can be very sensitive to leveraging fees and their impact is compounding. Nonetheless, many strategies operating at a higher CAGR rate could support these fees and benefit from using leverage.

Note that this is just an estimate and that this particular strategy did not start with a 5.5 leverage. However, based on its last year and looking at the exposure and leverage chart we can observe that its use of leverage is increasing. As an added note, it should be considered that part of the return obtained was due to the use of leverage, and this could reduce r should leverage be eliminated in that strategy.

I think this could be a great chance to step back and try to statistically evaluate the quality of this signal. In general something I've been trying to encourage everybody to do is think more in terms of models, and think of an algorithm as a mechanical wrapper around a set of models.

In order to evaluate the predictive capacity of a model like this, you probably want to find some way to encapsulate it in a pipeline factor (our implementation of a cross sectional model), so that the output is a score corresponding to how well you think a stock is gonna do. Once you have that you can run it through Alphalens to determine the predictive capacity of your model, then you can run it through pyfolio to see a risk breakdown.

https://www.quantopian.com/tutorials/getting-started#lesson4

Again
1. Think about it as a model that's making forecasts.
2. Statistically evaluate quality of forecasts with Alphalens. https://www.youtube.com/watch?v=ZFsar43SCJQ
3. Evaluate the risk exposure with Pyfolio.

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.

Thanks for the steps Delaney. Going through it now.

Some input by experts were to do the re-balancing quarterly or 6 months as monthly is too early for Piotroski's algo. Also to take the top/bottom decile of the universe instead of 10 stocks. I will incorporate those changes over the weekend.

Hi Praveen Bhushan,

Have you checked the results?

I have been doing some tests and for example, if you use morningstar.operation_ratios.roa, it will deliver a quarter ROA result instead of an annual ROA.

I think the Pietroski consist of an annual data, so the algo may be resulting worst than you expected, taking into account it could be using annual data for some metrics and quarter data for others.

Does anybody knows how to turn it into an annual data, I'm trying but I couldn't figure it out

Regards,

Hey guys,

I have some questions about the Piotroski Score:

Can somebody explain to me how these translate:

Profitability Criteria:

  • Positive Net Income (1 point)
    what was used: (roa[-1] > 0).astype(int)
    what I would have used: (net_income[-1] > 0).astype(int) with Fundamentals.net_income_income_statement

  • Positive operating cash flow in the current year (1 point)
    what was used: (roa[-1] > roa[0]).astype(int)
    what I would have used: (ocf[-1] > 0).astype(int) with Fundamentals.operating_cash_flow

  • Cash flow from operations being greater than net Income (quality of earnings) (1 point)
    what was used: (cash_flow_from_ops[-1] > roa[-1]).astype(int)
    what I would have used: (ocf[-1] > net_income[-1]).astype(int) with Fundamentals.operating_cash_flow and Fundamentals.net_income_income_statement

Regards,
Ben

What is the function update_universe() supposed to do?
The also does not seem to work as it is not defined...

Doesnt seem to me that it is working. What about you?

update_universe() is deprecated
https://www.quantopian.com/quantopian2/migration

Thank you for the answer. Very helpful.

And if I may not abuse of your generosity, how would you adapt the following code then (from the algo at the very beginning of this blog page):

def before_trading_start(context, data):
if context.is_month_end:
context.results = pipeline_output('piotroski')
context.long_stocks = context.results.sort_values('piotroski', ascending=False).head(10)
context.total_piotroski = context.long_stocks.piotroski.sum()
context.piotroski_weight = context.long_stocks.piotroski/context.long_stocks.piotroski.sum()
update_universe(context.long_stocks.index)

Here is my implementation of the long-only Piotroskis' score,adapt from Guy Fleury version to adjust it to long_only option. the return is quite disappointing tbh.

Clone Algorithm
7
Loading...
Backtest from to with initial capital
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import numpy as np
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline import CustomFactor, Pipeline
from quantopian.pipeline.factors import SimpleMovingAverage
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline.data import morningstar
from quantopian.pipeline.filters import QTradableStocksUS

"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following points in Profitablity, Leverage & Operating Effificieny means one point. 
    We are going to select 

    Profitability 
    - Positive ROA
    - Positive Operating Cash Flow
    - Higher ROA in current year versus last year
    - Cash flow from operations > ROA of current year

    Leverage
    - Current ratio of long term debt < last year's ratio of long term debt
    - Current year's current_ratio > last year's current_ratio
    - No new shares issued this year

    Operating Efficiency
    - Higher gross margin compared to previous year
    - Higher asset turnover ratio compared to previous year

"""
MAX_GROSS_LEVERAGE = 1.0
MAX_LONG_POSITION_SIZE = 0.04

class Piotroski(CustomFactor):
    inputs = [
        morningstar.operation_ratios.roa,
        morningstar.cash_flow_statement.operating_cash_flow,
        morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities,
        
        morningstar.operation_ratios.long_term_debt_equity_ratio,
        morningstar.operation_ratios.current_ratio,
        morningstar.valuation.shares_outstanding,
        
        morningstar.operation_ratios.gross_margin,
        morningstar.operation_ratios.assets_turnover,
    ]
    window_length = 22
    
    def compute(self, today, assets, out,
                roa, cash_flow, cash_flow_from_ops,
                long_term_debt_ratio, current_ratio, shares_outstanding,
                gross_margin, assets_turnover):
        profit = (
            (roa[-1] > 0).astype(int) +
            (cash_flow[-1] > 0).astype(int) +
            (roa[-1] > roa[0]).astype(int) +
            (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        )
        
        leverage = (
            (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int) +
            (current_ratio[-1] > current_ratio[0]).astype(int) + 
            (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        )
        
        operating = (
            (gross_margin[-1] > gross_margin[0]).astype(int) +
            (assets_turnover[-1] > assets_turnover[0]).astype(int)
        )
        
        out[:] = profit + leverage + operating

class ROA(CustomFactor):
    window_length = 1
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > 0).astype(int)
        
class ROAChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, roa):
        out[:] = (roa[-1] > roa[0]).astype(int)
        
class CashFlow(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.operating_cash_flow]
    
    def compute(self, today, assets, out, cash_flow):
        out[:] = (cash_flow[-1] > 0).astype(int)
        
class CashFlowFromOps(CustomFactor):
    window_length = 1
    inputs = [morningstar.cash_flow_statement.cash_flow_from_continuing_operating_activities, morningstar.operation_ratios.roa]
    
    def compute(self, today, assets, out, cash_flow_from_ops, roa):
        out[:] = (cash_flow_from_ops[-1] > roa[-1]).astype(int)
        
class LongTermDebtRatioChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.long_term_debt_equity_ratio]
    
    def compute(self, today, assets, out, long_term_debt_ratio):
        out[:] = (long_term_debt_ratio[-1] < long_term_debt_ratio[0]).astype(int)
        
class CurrentDebtRatioChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.current_ratio]
    
    def compute(self, today, assets, out, current_ratio):
        out[:] = (current_ratio[-1] > current_ratio[0]).astype(int)
        
class SharesOutstandingChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.valuation.shares_outstanding]
    
    def compute(self, today, assets, out, shares_outstanding):
        out[:] = (shares_outstanding[-1] <= shares_outstanding[0]).astype(int)
        
class GrossMarginChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.gross_margin]
    
    def compute(self, today, assets, out, gross_margin):
        out[:] = (gross_margin[-1] > gross_margin[0]).astype(int)
        
class AssetsTurnoverChange(CustomFactor):
    window_length = 22
    inputs = [morningstar.operation_ratios.assets_turnover]
    
    def compute(self, today, assets, out, assets_turnover):
        out[:] = (assets_turnover[-1] > assets_turnover[0]).astype(int) 
        
       
def initialize(context):

    pipe = Pipeline()
    pipe = attach_pipeline(pipe, name='piotroski')
    
    profit = ROA() + ROAChange() + CashFlow() + CashFlowFromOps()
    leverage = LongTermDebtRatioChange() + CurrentDebtRatioChange() + SharesOutstandingChange()
    operating = GrossMarginChange() + AssetsTurnoverChange()
    piotroski = profit + leverage + operating
    
    ev_ebitda = morningstar.valuation_ratios.ev_to_ebitda.latest > 0  
    market_cap = morningstar.valuation.market_cap > 5e7 # more  than 50 million dollar
    
    pipe.add(piotroski, 'piotroski')
    pipe.set_screen(((piotroski >= 7) | (piotroski <= 3)) & ev_ebitda & market_cap)
    context.is_month_end = False
    
    # set the slippage and commision for the portfolio
    set_slippage_and_commisions()
    
    set_long_only()
    schedule_function(set_month_end, date_rules.month_end(1)) 
    schedule_function(trade_long, date_rules.month_end(), time_rules.market_open())
    #schedule_function(trade, date_rules.month_end(), time_rules.market_close())
    
    # Schedule my plotting function everyday
    schedule_function(func=record_vars,
                      date_rule=date_rules.every_day(),
                      time_rule=time_rules.market_close())

def set_month_end(context, data):
    print "---- Set Month End -----"
    context.is_month_end = True
    
def before_trading_start(context, data):
    if context.is_month_end:
        context.results = pipeline_output('piotroski')
        try:
            context.long_stocks = context.results.sort_values('piotroski', ascending=False).iloc[:25].dropna()
        except: 
            print ("In exception") 
   
def trade_long(context, data):
    if len(context.long_stocks) != 0:
        long_weight = MAX_GROSS_LEVERAGE/ float(len(context.long_stocks))
    else:
        long_weight = 0
        
    #maximum weight per single stock
    if long_weight > MAX_LONG_POSITION_SIZE :
        long_weight = 0.04
    # buy the stock if it is in the long list   
    for stock in context.long_stocks.index:
        if data.can_trade(stock):
            order_target_percent(stock,long_weight)
    
    # sell the stock if the stock is in the portfolio but not in the long list
    for stock in context.portfolio.positions:
        if stock not in context.long_stocks.index:
            order_target_percent(stock, 0)
    context.is_month_end = False
      
"""
counting number of positions
"""
def position_count(context, data):
    num_of_position = 0
    for stock,position in context.portfolio.positions.items():
        if position.amount > 0:
            num_of_position +=1
    return num_of_position
"""
    Plot variables at the end of each day.
"""            
def record_vars(context, data):  
    
     # Record and plot the leverage and number of positions of our portfolio over time. 
    record(leverage = context.account.leverage,
          exposure = context.account.net_leverage,
          number_of_position=position_count(context, data))
"""    
    Define slippage and commission. Fixed slippage at 5 basis point and volume_limit is 0.1%
    Commission is set at Interactive Broker Rate: $0.05 per share with minimum trading cost of $1 per transaction
"""
def set_slippage_and_commisions(): 

    
    set_slippage(
        us_equities=slippage.FixedBasisPointsSlippage(basis_points=5, volume_limit=0.1))
    
    set_commission(commission.PerShare(cost = 0.05, min_trade_cost = 1))
There was a runtime error.

Try default slippage and commissions. Here, 120% overall yet very close to SPY.

Plug any of those fundamentals into https://www.quantopian.com/posts/fundamentals-updating-daily-vs-monthly-or-quarterly#5b612ed04ff15300452476a7, it is easy to do and then takes a couple seconds to run, and decide from there how much faith to place in them.

To use that, since this algo has roa for example, simply replace peg_ratio on line 84 with roa and run it.
(Consider updating the start and end dates).
shares_outstanding has ~600 companies reportedly changing that value 9 times in the last year, does that sound ok? It may.
Might be interesting to look into the 1 company whose assets_turnover changed 6 times.

And in this algo, FactSet instead of morningstar might be interesting if anyone would like to make that translation.

Using shares_outstanding doesn't account for stock splits; a company splitting their stock would have more shares outstanding, and this should be perceived as a positive signal (opposed to raising more equity).

Is there a better way to do this? net_common_stock_issuance results in a bunch of NaNs...whether that's because those stocks don't have accurate info or because the value is zero, I'm not sure.