Back to Community
Modified Piotroski score on ev/ebitda sort

EDIT: The first few backtests contain a leveraging bug. So please ignore. I eventually fixed the bugs in latter programs.

I feel like I'm spamming this board with my backtests, but I think they might be really interesting to someone who is new to finance.

Anyhoo, the piotroski score which can be described as a fundamental momentum screener - has been shown to augment most strategies.

http://www.quant-investing.com/blogs/general/2015/03/12/can-the-piotroski-f-score-also-improve-your-investment-strategy

It is also used by QVAL to filter their portfolio, which means that some very smart and thoughtful people thought it was a good idea.
I was having difficulty with my ev/ebitda screens' performance from 2010-2015 as you might have seen from previous posts, so I thought I'd quickly whip up something that might show its potential when combined with the second best valuation ratio tested: ev/ebitda.

The returns are amazing! I've never had a strategy that returns more than 2000%!

However, one look at the graph shows that it has terrible drawdowns. The 2008 drawdown is 98% (quite the achievement, not even momentum could do this). Even during the 2011 volatility, it dipped 41% within the span of a few months. This is inconsistent with the results on oldschoolvalues.com and other sites such as the association of individual investors. These sites show that piotroski scores outperform in bear markets.

What could possibly explain why my simple Quantopian code does not replicate the findings of others?

PS. I ran the backtest using a modified piotroski score as per: http://www.oldschoolvalue.com/blog/investing-strategy/best-piotroski-screen-combination/
This does not have an effect on the drawdowns, as I saw them before modifying the piotroski.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.ev_to_ebitda,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
                             .limit(200)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    if context.prime == False:
        order_target_percent(symbol('SPY'),1)
        context.prime = True
        
    #: Only run every 25 trading days
    if context.days % 25 == 0:
        
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 25 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 9]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + 2*int(current_ratio) + 2*int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + 2*int(asset_turnover)


  
    
    
There was a runtime error.
13 responses

Here is the same test with pb_ratio. This was the ratio used in the original paper by piotroski. It was also the best performing metric according to the quant investing link above.

125% drawdown.......... Momentum crashes are a thing. Maybe I should coin a term called the Piotroski crash...

I'm 100% sure something is wrong with my method though. Will follow up when I find something.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.pb_ratio,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.valuation_ratios.pb_ratio > 0)
.order_by(fundamentals.valuation_ratios.pb_ratio.asc())
                             .limit(200)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    if context.prime == False:
        order_target_percent(symbol('SPY'),1)
        context.prime = True
        
    #: Only run every 25 trading days
    if context.days % 25 == 0:
        
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 25 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 9]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + 2*int(current_ratio) + 2*int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + 2*int(asset_turnover)


  
    
    
There was a runtime error.

Great work. 2009 crashes are expected as quant strategies suffered at that time, and both of these were used by folks. One problem with Piotroski... you get a few stocks and they might not be liquid enough. Really nice to see that the algo found stocks that I remember having good Piotroski scores back in 2003.

@Johnny Wu,

Thanks for your posts. Please continue to do so because I am learning a lot about fundamentals from you.

Best regards,
Beginner

Kai: Thank you for taking the time to respond and I find your real life experience to be valuable in finding a solution to this problem.
I do have a few counter-arguments though.
The portfolio value is not so high that slippage from poor liquidity should explain these crashes. I ran the test again for market cap over 10 billion, and it still has a 90% drawdown. I decreased the starting capital to 10k and ran it right before a crash. The algo literally had to buy 1k worth of stock of giants such as Astrazenica. These dips were confirmed by checking individual stock performance, and indeed they do have poor returns. It is not an artifact.

You're right in that it could be due to low number of stocks that pass the filter during these times. Perhaps it could be explained by a concentration of risk? I don't really think this explains it either because there are other times when number of stocks are pretty close, but do not experience this crash.

Also, I don't think that enough people use the piotroski scores that it should affect these stocks specifically. Even if a large number of people did use it, Piotroski is a value-based indicator, shouldn't value investors know not to sell during dips if they are confident in their fundamentals?

If one is imaginative and does not care for evidence-based explanations, you could dream of a reason like... these high quality, liquid, cheap stocks are cheap for a reason, the market hates them, and when the market needs stock to dump during a crash, these are the first sold due to the bad press surrounding them. Reasonable explanation, but that's not what I'm looking for because it cannot be proven. I'm also pretty sure something is wrong with my code and not the strategy as every other backtester from other sources have had success with piotroski.

And thanks Beginner :)
We're all learning together

Stian Andreassen gave me this great idea on the other thread that I should use a stop loss order. Unfortunately using a simple order_target_percent with target zero percent will sometimes produce pending sells even after another procedure has already sold from the portfolio, effectively shorting the stock.

I hacked together a stop loss procedure for post #1.
Basically, every month I purchase the top ranked stocks according to value and piotroski and instead of holding for the entire month through thick and thin, I exit if the stock dips x% and move it to cash for the remainder of the month.
This backtest uses x=5%

As you can see, it doesn't do very much. Not a worthwhile strategy.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None
    
    context.holdings_df = pd.DataFrame()

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.ev_to_ebitda,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
                             .limit(200)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    
    for stock in context.holdings_df:
        if data[stock].price < 0.95*context.holdings_df.loc['buy price', stock]:
            order_target_percent(stock, 0)
            print("selling", str(stock))
            #take the stock out of holdings
            context.holdings_df = context.holdings_df.drop(stock, 1)
        
    if context.prime == False:
        order_target_percent(symbol('SPY'),1)
        context.prime = True
    
    
    
    #: Only run every 25 trading days
    if context.days % 25 == 0:
        #clear dataframe
        context.holdings_df = pd.DataFrame()
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 25 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 9]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
            #log the buy price of the stock
            context.holdings_df.loc['buy price', stock] = data[stock].price
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + 2*int(current_ratio) + 2*int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + 2*int(asset_turnover)


  
    
    
There was a runtime error.

In biology, we would say that we need a "negative" control to see if the effect on the results were due to the intended treatment and not a confounding factor. So here is the x=15% version.

Certain metrics are better. Sharpe ratio is better for instance.

Time to give up on Piotroski+value? Unlike pure value, it almost always outperforms every year, but the drawdowns are just too much for one's health.

I really like the idea of piotroski, though. I think I will use it to filter out really bad stocks instead of using it as a main criteria.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None
    
    context.holdings_df = pd.DataFrame()

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.ev_to_ebitda,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
                             .limit(200)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    
    for stock in context.holdings_df:
        if data[stock].price < 0.85*context.holdings_df.loc['buy price', stock]:
            order_target_percent(stock, 0)
            print("selling", str(stock))
            #take the stock out of holdings
            context.holdings_df = context.holdings_df.drop(stock, 1)
        
    if context.prime == False:
        order_target_percent(symbol('SPY'),1)
        context.prime = True
    
    
    
    #: Only run every 25 trading days
    if context.days % 25 == 0:
        #clear dataframe
        context.holdings_df = pd.DataFrame()
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 25 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 9]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
            #log the buy price of the stock
            context.holdings_df.loc['buy price', stock] = data[stock].price
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + 2*int(current_ratio) + 2*int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + 2*int(asset_turnover)


  
    
    
There was a runtime error.

Please ignore this entire post, I found out that leverage steadily increased. No wonder the drawdowns were insane.

Great thread! I'm curious to know what the performance looks like if the leverage bug is fixed?

I think that anyone who visits this thread gets an email every time I post. I apologize if I'm spamming your email, but unfortunately I can only post one backtest per post :(

I fixed the bug that caused the leverage. The bug is actually part of the template that was in the tutorial code. This shows how unintuitive the order system is. I hope that there will be either
1. An improvement in the system so we don't have to keep tracking leverage
or
2. An explanation of how the order system works and how it can cause leverage bugs along with common errors

Anyhoo, here is the original piotroski (not modified). I reverted to the original piotroski because it's easier to get a grasp of what the score means in the context of out of sample scores from outside Quantopian. Although I won't spam the results, I found that the enhanced version produces better results.
This looks more normal. I like it.
How do I know that Piotroski scores are improving the results? I ran the program with middle scores (4-6) and results were meh. I also ran <= 3, and the results were just atrocious. I suppose I could code a piotroski neutral version as it would be a better negative control, but I have to clean the house before the Mrs. returns.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    set_benchmark(symbol('IWM')) 
    
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None
    
    context.holdings_df = pd.DataFrame()

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    if context.days % 60 != 0:
        return
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.ev_to_ebitda,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.income_statement.ebit > 0)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
 .filter(fundamentals.valuation.enterprise_value > 0)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
                             .limit(100)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    
    '''for stock in context.holdings_df:
        if data[stock].price < 0.85*context.holdings_df.loc['buy price', stock]:
            order_target_percent(stock, 0)
            print("selling", str(stock))
            #take the stock out of holdings
            context.holdings_df = context.holdings_df.drop(stock, 1)'''
    record(leverage=context.account.leverage)

    if context.prime == False:
        order_target_percent(symbol('IWM'),1)
        context.prime = True
    
    
    
    #: Only run every 25 trading days
    if context.days % 60 == 0:
        #clear dataframe
        context.holdings_df = pd.DataFrame()
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 60 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            #log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 7]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    for stock in context.portfolio.positions:
        if stock not in num_long:
            order_target_percent(stock, 0)
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            #log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
            #log the buy price of the stock
            context.holdings_df.loc['buy price', stock] = data[stock].price
    
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    '''for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            #log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)'''
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + int(current_ratio) + int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + int(asset_turnover)


  
    
    
There was a runtime error.

Now this is the reason why I'm so excited: the stop loss
It works really well! For the most part, it is able to avoid large drawdowns, even during the value mini-crash of 2012. Here it is at 10% stoploss.

By the way, the program needs to run for a year before it can start trading. This is the reason why it matches the benchmark for the first year. That is intentional to ensure that they are visually comparable (I called this priming in my code). I also changed the benchmark to IWM (Russell 2000) because I think it's a better representative of my investable universe.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    set_benchmark(symbol('IWM')) 
    
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None
    
    context.holdings_df = pd.DataFrame()

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    if context.days % 60 != 0:
        return
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.ev_to_ebitda,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.income_statement.ebit > 0)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
 .filter(fundamentals.valuation.enterprise_value > 0)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
                             .limit(100)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    
    for stock in context.holdings_df:
        if data[stock].price < 0.90*context.holdings_df.loc['buy price', stock]:
            order_target_percent(stock, 0)
            print("selling", str(stock))
            #take the stock out of holdings
            context.holdings_df = context.holdings_df.drop(stock, 1)
    record(leverage=context.account.leverage)

    if context.prime == False:
        order_target_percent(symbol('IWM'),1)
        context.prime = True
    
    
    
    #: Only run every 25 trading days
    if context.days % 60 == 0:
        #clear dataframe
        context.holdings_df = pd.DataFrame()
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 60 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            #log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 7]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    for stock in context.portfolio.positions:
        if stock not in num_long:
            order_target_percent(stock, 0)
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            #log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
            #log the buy price of the stock
            context.holdings_df.loc['buy price', stock] = data[stock].price
    
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    '''for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            #log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)'''
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + int(current_ratio) + int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + int(asset_turnover)


  
    
    
There was a runtime error.

Here is the 20% stoploss. Less isn't always more apparently. Better results but lower Sharpe. If found to be robust, I'd favour the 10% over the 20% any day.

Clone Algorithm
333
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
"""
    Creating an algorithm based off the Piotroski Score index which is based off of a score (0-9)
    Each of the following marks (-) satisfied means one point. And in the end we'll long the stocks with a score of >= 8

    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


    This algorithm demonstrates how to grasp historical fundamental data by storing it in a Pandas Panel similar to how the Quantopian 'data' is currently structured
"""

import pytz
from datetime import datetime, timedelta

import pandas as pd
    
"""
    Initialize and Handle Data
"""
    
def initialize(context):
    set_benchmark(symbol('IWM')) 
    
    context.prime = False
    #: context.days holds the number of days that we've had this algorithm
    context.days = 99
    
    #: context.fundamental_dict holds the date:dictionary reference that we need
    context.fundamental_dict = {}
    
    #: context.fundamental_data holds the pandas Panel that's derived from the fundamental_dict
    context.fundamental_data = None
    
    context.holdings_df = pd.DataFrame()

def before_trading_start(context): 
    """
        Called before the start of each trading day (handle_data) and updates our universe with the securities and values found from fetch_fundamentals
    """
    if context.days % 60 != 0:
        return
    #: Reference fundamentals in a shorter variable
    f = fundamentals
    
    #: Query for the data that we need from fundamentals
    fundamental_df = get_fundamentals(query(
                                    f.valuation.market_cap,
                                    f.operation_ratios.roa,
                                    f.cash_flow_statement.operating_cash_flow,
                                    f.cash_flow_statement.cash_flow_from_continuing_operating_activities,
                                    f.operation_ratios.long_term_debt_equity_ratio,
                                    f.operation_ratios.current_ratio,
                                    f.valuation.shares_outstanding,
                                    f.operation_ratios.gross_margin,
                                    f.operation_ratios.assets_turnover,
            f.valuation_ratios.ev_to_ebitda,
                            )

.filter(fundamentals.valuation.market_cap > 1.5e9)
.filter(fundamentals.income_statement.ebit > 0)
.filter(fundamentals.valuation_ratios.ev_to_ebitda > 0)
 .filter(fundamentals.valuation.enterprise_value > 0)
.order_by(fundamentals.valuation_ratios.ev_to_ebitda.asc())
                             .limit(100)
                             )
    
    #: Set our fundamentals into a context variable
    context.fundamental_df = fundamental_df
    
    #: Update our universe with the values
    update_universe(fundamental_df.columns.values)  
    
def handle_data(context, data):
    
    for stock in context.holdings_df:
        if data[stock].price < 0.80*context.holdings_df.loc['buy price', stock]:
            order_target_percent(stock, 0)
            print("selling", str(stock))
            #take the stock out of holdings
            context.holdings_df = context.holdings_df.drop(stock, 1)
    record(leverage=context.account.leverage)

    if context.prime == False:
        order_target_percent(symbol('IWM'),1)
        context.prime = True
    
    
    
    #: Only run every 25 trading days
    if context.days % 60 == 0:
        #clear dataframe
        context.holdings_df = pd.DataFrame()
        #: Insert a new dataframe into our dictionary 
        context.fundamental_dict[get_datetime()] = context.fundamental_df
        
        #: If it's greater than the first trading day
        if context.days > 0:
            context.fundamental_data = pd.Panel(context.fundamental_dict)
            scores = get_piotroski_scores(context.fundamental_data, get_datetime())
            
            #: Only rebalance when we have enough data
            if scores != None:
                rebalance(context, data, scores)
    
    #: Log our current positions
    if (context.days - 1) % 60 == 0:
        
        #: Portfolio position string
        portfolio_string = "Current positions: "
        
        #: Don't log if we have no positions
        if len(context.portfolio.positions) != 0:
            
            #: Add our current positions to a string
            for pos in context.portfolio.positions:
                portfolio_string += "Symbol: %s and Amount: %s, " % (pos.symbol, context.portfolio.positions[pos].amount)
        
            #: Log all our portfolios
            #log.info(portfolio_string)
    
    
    context.days += 1
            
"""
    Defining our rebalance method
"""

def rebalance(context, data, scores):
    """
        This method takes in the scores found by get_piotroski_scores and orders our portfolio accordingly
    """
    #: Find which stocks we need to long and which ones we need to short
    num_long = [stock for stock in scores if scores[stock] >= 7]
    num_short = [stock for stock in scores if scores[stock] <= 2]
    
    for stock in context.portfolio.positions:
        if stock not in num_long:
            order_target_percent(stock, 0)
    #: Stocks to long
    for stock in num_long:
        if stock in data:
            #log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
            order_target_percent(stock, 1.0/len(num_long))
            #log the buy price of the stock
            context.holdings_df.loc['buy price', stock] = data[stock].price
    
    
    # #: Stocks to short
    # for stock in num_short:
    #     if stock in data:
    #         log.info("Going short on stock %s with score %s" % (stock.symbol, scores[stock]))
    #         order_target_percent(stock, -1.0/len(num_short))
    
    #: Exit any positions we might have
    '''for stock in context.portfolio.positions:
        if stock in data and stock not in num_long and stock not in num_short:
            #log.info("Exiting our positions on %s" % (stock.symbol))
            order_target_percent(stock, 0)'''
    
    record(number_long=len(num_long))
    # record(number_short=len(num_short))
    
"""
    Defining our methods for the piotroski score
"""

def get_piotroski_scores(fundamental_data, current_date):
    """
        This method finds the dataframe that contains the data for the time period we want
        and finds the total Piotroski score for those dates
    """
    all_scores = {}
    all_dates = fundamental_data.items
    
    utc = pytz.UTC
    last_year = utc.localize(datetime(year=current_date.year - 1, month = current_date.month, day = current_date.day))
    
    #: If one year hasn't passed just return None
    if last_year < min(all_dates):
        return None
    
    #: Figure out which date to use
    for i, date in enumerate(all_dates):
        if i == len(all_dates) - 1:
            continue
        if last_year > date and last_year < all_dates[i + 1]:
            break
        elif last_year == date:
            break
        
    #: This is pretty robust so just set last_year to whatever date currently is when you broke
    #: or ended the for loop
    last_year = date
    old_data = fundamental_data[last_year]
    current_data = fundamental_data[current_date]
    
    #: Find the score for each security
    for stock in current_data:
        profit = profit_logic(current_data, old_data, stock)
        leverage = leverage_logic(current_data, old_data, stock)
        operating = operating_logic(current_data, old_data, stock)
        total_score = profit + leverage + operating
        all_scores[stock] = total_score
        
    return all_scores

def profit_logic(current_data, old_data, sid):
    """
        Define our profitability logic here
    """
    
    #: Positive ROA
    positive_roa = current_data[sid]['roa'] > 0
    #: Positive Operating Cash Flow
    positive_ocf = current_data[sid]['operating_cash_flow'] > 0
    #: Current ROA > Last Year ROA
    current_last_roa = current_data[sid]['roa'] > old_data[sid]['roa']
    #: Cash flow from operations > ROA
    cash_flow_roa = current_data[sid]['cash_flow_from_continuing_operating_activities'] > current_data[sid]['roa']
    
    return int(positive_roa) + int(positive_ocf) + int(current_last_roa)+ int(cash_flow_roa)
    
def leverage_logic(current_data, old_data, sid):
    """
        Define our leverage logic here 
    """
    
    #: Current ratio of long-term debt < last year's ratio of long-term debt
    long_term_debt = current_data[sid]['long_term_debt_equity_ratio'] > old_data[sid]['long_term_debt_equity_ratio']
    #: Current year's current_ratio > last year's current_ratio
    current_ratio = current_data[sid]['current_ratio'] > old_data[sid]['current_ratio']
    #: No new shares
    new_shares = current_data[sid]['shares_outstanding'] <= old_data[sid]['shares_outstanding']
    
    return int(long_term_debt) + int(current_ratio) + int(new_shares)
    
def operating_logic(current_data, old_data, sid):
    """
        Define our operating efficiency logic here
    """
    
    #: Higher gross margin compared to previous year
    gross_margin = current_data[sid]['gross_margin'] > old_data[sid]['gross_margin']
    #: Higher asset turnover ratio compared to previous year
    asset_turnover = current_data[sid]['assets_turnover'] > old_data[sid]['assets_turnover']
    
    return int(gross_margin) + int(asset_turnover)


  
    
    
There was a runtime error.

There is an error in the code. Current_data long term debt to equity ratio should be lower than old_data long term debt to equity ratio.

Johnny, I am new to algos and am looking to adapt your source code. I understand that this is a few years old. When I clone and go to run this as is, it breaks with the fundamental_df. Does that have to do with some changes quantopian has made? Thanks