Piotroski Score + Aroon Indicator

Just having some fun, thought I would share.
(warning: its one of those "very long, cant fast-forward" marketing videos)

594
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 talib

import pandas as pd

"""
Initialize and Handle Data
"""

def initialize(context):

#: context.days holds the number of days that we've had this algorithm
context.days = 0

#: 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

"""
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,
)
.order_by(fundamentals.valuation.market_cap.desc())
.limit(490)
)

#: 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):

#: 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):

highPrices = history(101, '1d', 'high')
lowPrices  = history(101, '1d', 'low')

"""
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]

#: Stocks to long
for stock in num_long:
Aroon = talib.AROON(highPrices[stock], lowPrices[stock], timeperiod = 100)
AroonDown = Aroon[0][-1]
AroonUp   = Aroon[1][-1]

record(AroonUp=AroonUp, AroonDown=AroonDown)

if stock in data and AroonUp > AroonDown:
log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
order_target_percent(stock, 2.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 or AroonUp < AroonDown) 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)


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

Is there some kind of error here? Line 222

# : 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']


Well spotted! That must have been a typo by the original author. That 1 char change significantly impacted the drawdown and total returns as shown below.

594
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 talib

import pandas as pd

"""
Initialize and Handle Data
"""

def initialize(context):

#: context.days holds the number of days that we've had this algorithm
context.days = 0

#: 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

"""
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,
)
.order_by(fundamentals.valuation.market_cap.desc())
.limit(490)
)

#: 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):

#: 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):

highPrices = history(101, '1d', 'high')
lowPrices  = history(101, '1d', 'low')

"""
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]

#: Stocks to long
for stock in num_long:
Aroon = talib.AROON(highPrices[stock], lowPrices[stock], timeperiod = 100)
AroonDown = Aroon[0][-1]
AroonUp   = Aroon[1][-1]

record(AroonUp=AroonUp, AroonDown=AroonDown)

if stock in data and AroonUp > AroonDown:
log.info("Going long on stock %s with score %s" % (stock.symbol, scores[stock]))
order_target_percent(stock, 2.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 or AroonUp < AroonDown) 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)


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

looks like the earning season is killing this algo every once a while. Is there any way to avoid that?

We have data partners who provide earnings calendar data. One such example is EventVestor.

This data isn't currently usable in an algo, only accessible in Research right now. But you can familiarize yourself with it in preparation for the day that these partner data sets are usable inside algos. (that day is coming!)

The event data is well suited for risk management, as you suggest above.

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.

And looking at the content of the algo, I'd bet that the new Pipeline API would make this type of algo a lot simpler and open you up to a larger universe of stocks to rank. The initial fundamentals query limits you to 490 stocks right now. The Pipeline API lets you build ranking systems over the entirety of the tradeable universe.

The thing is, earnings often move the price significantly - that is just part of any algo that is in the market all the time.

I think what would make this a killer algo is adding shorting for the reverse of its rankings. Right now it is long only - which reflects in the performance of 2015 - i.e. not very good. Making it long / short with zero market exposure would really make it something nice.

Josh is right - using pipeline api would help with better rankings, specially when it comes to identifying the shorts.

Sorry for my misunderstanding. I am new to this field. So what I am looking for is abnormally patterns, and from the back-testing results, I notice the earning season is not favoring our algo. Therefore, is there anyway to limit the earning season results to our algo? For example, don't trade in the earning season. Overall, this is just a thought from a beginner. Thanks!

Well, Im not sure what your proposal would be. You could exit the longs just before earnings announcements, but like Josh said, you would need to know exactly when earnings are - which would require access to an earnings calendar in the algo - which we don't have right now.

Also, Im not sure how you concluded that "the earning season is not favoring our algo"? Did you test this theory?

Any idea what the time delay is on updated Fundamental Data? There could be significant returns lost due to delayed data.

I am new to this area so I might come up with some idea that is not so relevant to your algo. No offense at all. So I did not test this theory. And what I did is simply look at the graph. Noticing that quite a few downturns happened during the earning seasons. This is simply a naive hypothesis.

Is there anyway we could omit earning season effect, which means we do not trade during Mid Jan to Early Feb, etc. Again, it is just a thought. Probably I will do myself when I learn more coding. Thanks anyway!

@Haolun, it is not possible to implement what you are suggesting with Quantopian right now - but probably will be in the near future considering how fast the Q team work on extending the platform.

In the meantime, I tried adding the "sell in May and go away" strategy to the system, but it was not fruitful.

If you want to play with it yourself, you could drop something like this into the algo:

    date = data[data.keys()[0]].datetime
month = date.strftime("%B") #get current month as a string
if month in ('May', 'June', 'July', 'August', 'September'):
context.sell_all = True
for stock in context.portfolio.positions:
if (stock in data):
order_target_percent(stock, 0)


Thanks a lot! I will check the results.

Hi guys,

How come this error appears after about 5% achieved of a 2003-2015 backtest ?

Something went wrong. Sorry for the inconvenience. Try using the built-in debugger to analyze your code. If you would like help, send us an email.
Exception: inputs are all NaN
There was a runtime error on line 90.

Try using the debugger and set a conditional breakpoints around the dates the algo is failing. You can step through the parameters and see how they're getting populated.

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.

I played with pipelining but it's super slow and breaks because of memory.

9
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

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
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 AroonUp(CustomFactor):
window_length = 100
inputs = [USEquityPricing.high]

def compute(self, today, assets, out, highs):
out[:] = (np.argmax(highs, 0).astype(float)/float(self.window_length))*100.0

class AroonDown(CustomFactor):
window_length = 100
inputs = [USEquityPricing.low]

def compute(self, today, assets, out, lows):
out[:] = (np.argmin(lows, 0).astype(float)/float(self.window_length))*100.0

def initialize(context):
set_universe(universe.DollarVolumeUniverse(floor_percentile=98, ceiling_percentile=100))
pipe = Pipeline()
pipe = attach_pipeline(pipe, name='piotroski')

piotroski = Piotroski()
aroonup = AroonUp()
aroondown = AroonDown()
aroonspread = aroonup - aroondown

pipe.set_screen((piotroski >= 7) and (aroonspread > 75))
mc = SimpleMovingAverage(inputs=[morningstar.valuation.market_cap], window_length=10)

context.results = pipeline_output('piotroski')
context.long_stocks = context.results.sort('mc', ascending=False).head(400)

context.total_piotroski = context.long_stocks.piotroski.sum()
context.piotroski_weight = context.long_stocks.piotroski/context.long_stocks.piotroski.sum()

record(
update_universe(context.long_stocks.index)

num_valid_stocks = 0
valid_stocks = set(data.keys()).intersection(set(context.long_stocks.index))

for stock in valid_stocks:
order_target_percent(stock, (context.piotroski_weight[stock] + context.aroon_weight[stock])/2.0)

for stock in context.portfolio.positions:
if stock not in valid_stocks:
order_target_percent(stock, 0)

# Will be called on every trade event for the securities you specify.
def handle_data(context, data):
pass


There was a runtime error.

Hi Lucas,

I just remade your algo to optimize it a little bit more memory-wise. I split up all of the subcomponents of the Piotroski score into individual CustomFactors. The logic behind this was that you don't actually need a 100-day window for each of the data fields. By breaking them up into individual components, you don't spend any time/memory getting data you don't need! I then sum them all together in the before_trading_start function into the Piotroski score.

I hope this helps!

85
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

# Put any initialization logic here.  The context object will be passed to
# the other methods in your algorithm.
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)

class AroonUp(CustomFactor):
window_length = 100
inputs = [USEquityPricing.high]

def compute(self, today, assets, out, highs):
out[:] = (np.argmax(highs, 0).astype(float)/float(self.window_length))*100.0

class AroonDown(CustomFactor):
window_length = 100
inputs = [USEquityPricing.low]

def compute(self, today, assets, out, lows):
out[:] = (np.argmin(lows, 0).astype(float)/float(self.window_length))*100.0

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

aroonup = AroonUp()
aroondown = AroonDown()
aroonspread = aroonup - aroondown

pipe.set_screen((piotroski >= 7) and (aroonspread > 75))
mc = SimpleMovingAverage(inputs=[morningstar.valuation.market_cap], window_length=10)

context.results = pipeline_output('piotroski')
context.long_stocks = context.results.sort('mc', ascending=False).head(400)

context.total_piotroski = context.long_stocks.piotroski.sum()
context.piotroski_weight = context.long_stocks.piotroski/context.long_stocks.piotroski.sum()

record(

update_universe(context.long_stocks.index)

num_valid_stocks = 0
valid_stocks = set(data.keys()).intersection(set(context.long_stocks.index))

for stock in valid_stocks:
order_target_percent(stock, (context.piotroski_weight[stock] + context.aroon_weight[stock])/2.0)

for stock in context.portfolio.positions:
if stock not in valid_stocks:
order_target_percent(stock, 0)

# Will be called on every trade event for the securities you specify.
def handle_data(context, data):
pass


There was a runtime error.
Disclaimer

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

thank you jamie i am working at the same thing and this is my first attempt, i have to rank a make the scores now, mabye i will continue with my code just as an exercise but your example here is very usefull to me thanks!

37
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
"""
from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline
from quantopian.pipeline import CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data import morningstar

class Piotroski(CustomFactor):
def compute(self, today, assets, out, values):
if self.window_length >1:
out[:] = values[-1] - values[0]
else:
out[:] = values[-1]
def piotrosky_scores(x):
x['Roa'] > 0
def initialize(context):
last_year_length = 200
pipe = Pipeline()
attach_pipeline(pipe, name='my_pipeline')
ROA_current_vs_last = Piotroski(inputs= [morningstar.operation_ratios.roa],window_length = last_year_length )
ROA = Piotroski(inputs= [morningstar.operation_ratios.roa],window_length = 1 )
cash_flow = Piotroski(inputs = [morningstar.cash_flow_statement.operating_cash_flow],window_length = 1 )
ratio_long_term_debt = Piotroski(inputs= [morningstar.operation_ratios.long_term_debt_equity_ratio],window_length = 1 )
ratio_long_term_debt_current_vs_last = Piotroski(inputs= [morningstar.operation_ratios.long_term_debt_equity_ratio],window_length = last_year_length )
current_ratio = Piotroski(inputs= [morningstar.operation_ratios.current_ratio],window_length = 1 )
current_ratio_current_vs_last = Piotroski(inputs= [morningstar.operation_ratios.current_ratio],window_length = last_year_length )
shares_outstanding_current_vs_last = Piotroski(inputs= [morningstar.valuation.shares_outstanding],window_length = last_year_length )
gross_margin_current_vs_last = Piotroski(inputs= [morningstar.operation_ratios.gross_margin],window_length = last_year_length )
asset_turnover_current_vs_last = Piotroski(inputs= [morningstar.operation_ratios.assets_turnover],window_length = last_year_length )
pipe.add(ROA_current_vs_last, name= 'ROA var')
pipe.add(ROA, name = 'ROA')
pipe.add(cash_flow , name= 'cash flow')
pipe.add(ratio_long_term_debt, name= 'debt ratio')
pipe.add(ratio_long_term_debt_current_vs_last, name= 'debt ratio var')
pipe.add(current_ratio_current_vs_last, name= 'Cur ratio var')
pipe.add(shares_outstanding_current_vs_last, name= 'New shares')
pipe.add(gross_margin_current_vs_last, name= 'Gross var')
pipe.add(asset_turnover_current_vs_last, name= 'Turnover var')

def handle_data(context, data):
order(sid(24), 50)

There was a runtime error.

how do you utilize minute data with this?

@Daniel: Unfortunately, pipeline is only designed to operate on daily data for performance reasons. In order to run this on minutely data, you would have to implement the algorithm without pipeline. As a result, you would be bound by the 500 security limit to filter from. You would have to filter only a specific list of stocks.

Ah, I got ya. Ok. Thanks for letting me know.

@Mohammad how to eliminate this 1 year wherein.. there's no activity.. its so annoying.... tnxs...

Im not sure you can. I think it needs 1 year of data to get the scores. If you look at line 173 it specifically makes sure that a year has passed before it calculates the scores.

The pipelined version Jamie, Giuseppe and I were using gets rid of it, as you can see in the backtests.