Back to Community
Piotroski's F-Score Algorithm

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

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

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

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

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

Clone Algorithm
369
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
# Backtest ID: 5b42c0084cea47437f742806
There was a runtime error.
32 responses

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

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

Sure will add it tonight.

Thanks awesome stuff.

Doesn't this compare for the previous day:

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

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

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

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

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

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

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

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

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

Great stuff

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

Clone Algorithm
369
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
# Backtest ID: 5b4d1af1baecb544af85a916
There was a runtime error.

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

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

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

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

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

Clone Algorithm
23
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
# Backtest ID: 5b4df27daff81c445f9ae76b
There was a runtime error.

Here is the backtest analysis for the previous post.

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

Of note:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Clone Algorithm
9
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
# Backtest ID: 5b4e7825aff81c445f9af3cc
There was a runtime error.

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

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

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

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

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

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

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

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

Disclaimer

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

Thanks for the steps Delaney. Going through it now.

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

Hi Praveen Bhushan,

Have you checked the results?

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

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

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

Regards,

Hey guys,

I have some questions about the Piotroski Score:

Can somebody explain to me how these translate:

Profitability Criteria:

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

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

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

Regards,
Ben

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

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

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

Thank you for the answer. Very helpful.

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

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