1
$\begingroup$

I am looking to implement Piotroski's F-score Value strategy discussed in the paper "Piotroski, J. D. (2000). Value investing: The use of historical financial statement information to separate winners from losers. Journal of Accounting Research, 38, 1-41." which lists 9 criteria for judging a company:

# 1 - Positive Net Income (1 point)                                                                   Cash-flow
# 2 - Positive return on assets (net income / total assets) in the current year (1 pnt)     Cash-flow & Balance Sheet
# 3 - Positive operating cash flow (NI + DeprExp + chgReceive + chgInven) for current year (1 pnt) Cash-flow
# 4 - Cash flow from operations being greater than net Income (quality of earnings) (1 pnt)
# Leverage, Liquidity and Source of Funds Criteria:
# 5 - Lower ratio of long term debt in the current period,                                          Balance Sheet
#     compared to the previous year (decreased leverage) (1 pnt)
# 6 - Higher current ratio this year compared to the previous year (more liquidity) (1 pnt)         Balance Sheet
# 7 - No new shares were issued in the last year (lack of dilution) (1 pnt).                        Key Stats
# Operating Efficiency Criteria:
# 8 - A higher gross margin compared to the previous year (1 pnt)
# 9 - A higher asset turnover ratio compared to the previous year (1 pnt)
# Stocks with 8 or 9 points should be bought, while stocks with 3 points or less should be sold short.

I am working with the IEXfinance library in python to pull data from a company's financial sheets to determine if they meet this criteria (although I might switch to scrapping it from the EDGAR system if that proves to be a bit quicker because the IEXfinance has parts behind a pay-wall. Anyhow, I am having trouble determining if the data returned from IEXfinance is the same as what I am looking for because the terms are slightly different.

Pics of their data: Balance Sheet Balance Sheet Pic

On the Balance Sheet, I can see the data needed for 1,2,5,6.

Cash Flow Cash Flow

Operating cash flow. is defined as Net Income - Depreciation Expense + Change in Receivables + Change in Inventory which I can see on the Cash Flow Report. So 3 is met.

Income Statement Income Statement

My questions are this: -Is the cash flows from operations used in #4, the positive operating cash flow we calculate in 3? -Would share equity remaining the same mean that no new shares were issued? I would guess no because the stock price can change making this not the right number. In key_stats, a data point is named shares_outstanding which is what I am guessing I am looking for, yes? One website argues that the "Weighted Average Shares" number should be checked. - I have Gross profit for number 8 but not total sales. Is comparing Gross Profit across years the equivalent? -#9 stumps me. Here is very rough code:

 # This is an implementation of some of the stock-selection principles outlined here:
# -Amor-Tapia, B. & Tascón, M.T. (2016). Separating winners from losers: Composite indicators
# based on fundamentals in the European context *. Finance a Uver,66(1), 70-94.
# -Piotroski, J. D. (2000). Value investing: The use of historical financial statement information
# to separate winners from losers. Journal of Accounting Research, 38, 1-41.
# No guarantees are provided about the performance of these principles as described
# or as implemented here. As with any strategy, you should validate its performance
# with backtesting and forward testing before committing to its use.
#
# List of 9 Selection Criteria
# Profitability Criteria:
# 1 - Positive Net Income (1 point)                                                                   Cash-flow
# 2 - Positive return on assets (net income / total assets) in the current year (1 pnt)     Cash-flow & Balance Sheet
# 3 - Positive operating cash flow (NI + DeprExp + chgReceive + chgInven) for current year (1 pnt) Cash-flow
# 4 - Cash flow from operations being greater than net Income (quality of earnings) (1 pnt)
# Leverage, Liquidity and Source of Funds Criteria:
# 5 - Lower ratio of long term debt in the current period,                                          Balance Sheet
#     compared to the previous year (decreased leverage) (1 pnt)
# 6 - Higher current ratio this year compared to the previous year (more liquidity) (1 pnt)         Balance Sheet
# 7 - No new shares were issued in the last year (lack of dilution) (1 pnt).                        Key Stats
# Operating Efficiency Criteria:
# 8 - A higher gross margin compared to the previous year (1 pnt)
# 9 - A higher asset turnover ratio compared to the previous year (1 pnt)
# Stocks with 8 or 9 points should be bought, while stocks with 3 points or less should be sold short.
#
"""
#################################################################################################################

# Imports to set-up our Alpaca login
import os
import sys
import yaml
import json

# Imports for Alpaca specific libraries
import alpaca_trade_api as tradeapi

# Imports for IEXfinance stock data
from iexfinance.base import _IEXBase
from iexfinance.stocks import Stock
from urllib.parse import quote

#
import pandas as pd


class PiotroskiFScoreValueTrader:

    # --------------------------------------------------------------------------------------------------
    def __init__(self):
        # Connect to the website and make sure we are good to trade.
        self.api = self.login()

        # Get the complete list of stocks we want to check.
        self.tradeable = self.get_tradeable_assets()

        # Get the relevant data we need for each stock.

    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
    def login(self):
        # We retrieve our login info for the Alpaca API from the config.yml file here and then login.
        with open(os.path.join(sys.path[0], 'config.yml'), 'r') as f:
            config = yaml.load(f)

            #  These are the params we need to send to the Alpaca API
            base_url = config['base_url']
            key_id = config['key_id']
            secret = config['secret']

        api = tradeapi.REST(base_url=base_url, key_id=key_id, secret_key=secret)
        if api.get_account().status != 'ACTIVE':
            raise RuntimeError("Error: Account is not active. Account is: ", api.get_account().status)
        return api
    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
    def get_tradeable_assets(self):
        symbols = []
        possible_assets = self.api.list_assets()
        for asset in possible_assets:
            temp_dict = vars(asset)
            if temp_dict['_raw']['status'] == 'active':
                symbols.append(temp_dict['_raw']['symbol'])
        return sorted(symbols)
    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
    def get_asset_data(self):

        # IEX doesn't like batch queries for more than 100 symbols at a time.
        # We need to build our fundamentals info iteratively.
        batch_idx = 0
        batch_size = 99
        fundamentals_dict = {}
        while batch_idx < len(self.tradeable):
            symbol_batch = [s['symbol']
                            for s in self.tradeable[batch_idx:batch_idx + batch_size]]
            stock_batch = Stock(symbol_batch)

            # Pull all the data we'll need from IEX.
            financials_json = stock_batch.get_financials()
            quote_json = stock_batch.get_quote()
            stats_json = stock_batch.get_key_stats()
            earnings_json = stock_batch.get_earnings()

            for symbol in symbol_batch:
                # We'll filter based on earnings first to keep our fundamentals
                # info a bit cleaner.
                if not self.positive_return_on_assets(earnings_json[symbol]):
                    continue

                # Make sure we have all the data we'll need for our filters for
                # this stock.
                if not self.data_quality_good(
                        symbol,
                        financials_json,
                        quote_json,
                        stats_json):
                    continue

                fundamentals_dict[symbol] = self.get_fundamental_data_for_symbol(
                    symbol,
                    financials_json,
                    quote_json,
                    stats_json
                )

            batch_idx += batch_size

        # Transform all our data into a more filterable form - a dataframe - with
        # a bit of pandas magic.
        return pd.DataFrame.from_dict(fundamentals_dict).T

        # Stock.get_company() is one way to get sector.

    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
        # Want to mimic if not eps.good and data quality good to filter out stocks we do not want to
        # bother with. Then we want to get the required data so we can filter our stocks.
    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
    # Return on Assets = Net Income / Total Assets
    def positive_return_on_assets(self, earnings_reports):
        # This method contains logic for filtering based on earnings reports.
        if len(earnings_reports) < 4:
            # The company must be very new. We'll skip it until it's had time to
            # prove itself.
            return False

        # earnings_reports should contain the information about the last four
        # quarterly reports.
        for report in earnings_reports:
            # We want to see consistent positive EPS.
            try:
                if not (report['actualEPS']):
                    return False
                if report['actualEPS'] < 0:
                    return False
            except KeyError:
                # A KeyError here indicates that some data was missing or that a company is
                # less than two years old. We don't mind skipping over new companies until
                # they've had more time in the market.
                return False
        return True
    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
    def data_quality_good(self, symbol, financials_json, quote_json, stats_json):
        # This method makes sure that we're not going to be investing in
        # securities we don't have accurate data for.

        if len(financials_json[symbol]
               ) < 1 or quote_json[symbol]['latestPrice'] is None:
            # No recent data was found. This can sometimes happen in case of recent
            # market suspensions.
            return False

        try:
            if not (
                    quote_json[symbol]['marketCap'] and
                    stats_json[symbol]['priceToBook'] and
                    stats_json[symbol]['sharesOutstanding'] and
                    financials_json[symbol][0]['totalAssets'] and
                    financials_json[symbol][0]['currentAssets'] and
                    quote_json[symbol]['latestPrice']
            ):
                # Ignore companies IEX cannot report all necessary data for, or
                # thinks are untradable.
                return False
        except KeyError:
            # A KeyError here indicates that some data we need to evaluate this
            # stock was missing.
            return False

        return True
    # --------------------------------------------------------------------------------------------------

    # --------------------------------------------------------------------------------------------------
    def get_fundamental_data_for_symbol(self, symbol, financials_json,
                                        quote_json, stats_json):

        fundamentals_dict_for_symbol = {}

        financials = financials_json[symbol][0]

        # Calculate PB ratio.
        fundamentals_dict_for_symbol['pb_ratio'] = stats_json[symbol]['priceToBook']

        # Find the "Current Ratio" - current assets to current debt.
        current_debt = financials['currentDebt'] if financials['currentDebt'] else 1
        fundamentals_dict_for_symbol['current_ratio'] = financials['currentAssets'] / current_debt

        # Find the ratio of long term debt to short-term liquiditable assets.
        total_debt = financials['totalDebt'] if financials['totalDebt'] else 0
        fundamentals_dict_for_symbol['debt_to_liq_ratio'] = total_debt / financials['currentAssets']

        # Store other information for this stock so we can filter on the data
        # later.
        fundamentals_dict_for_symbol['pe_ratio'] = quote_json[symbol]['peRatio']
        fundamentals_dict_for_symbol['market_cap'] = quote_json[symbol]['marketCap']
        fundamentals_dict_for_symbol['dividend_yield'] = stats_json[symbol]['dividendYield']

        return fundamentals_dict_for_symbol
    # --------------------------------------------------------------------------------------------------

# **************************************************************************************************
def main():
    trader = PiotroskiFScoreValueTrader()
    stock_batch = Stock(['AAPL'])

    # Pull all the data we'll need from IEX.
    financials_json = stock_batch.get_financials()
    print("Financials: ", financials_json)
    quote_json = stock_batch.get_quote()
    print("Quote: ", quote_json)
    stats_json = stock_batch.get_key_stats()
    print("Stats: " , stats_json)
    earnings_json = stock_batch.get_earnings()
    print("Earnings: ",earnings_json)

if __name__ == '__main__':
    main()
# **************************************************************************************************

########################################################################################################################

For this code:

trader = PiotroskiFScoreValueTrader()
stock_batch = Stock(['AAPL'])
# Pull all the data we'll need from IEX.
financials_json = stock_batch.get_financials()
print("Financials: \n", json.dumps(financials_json, indent=4))
quote_json = stock_batch.get_quote()
print("Quote: \n", json.dumps(quote_json, indent=4))
stats_json = stock_batch.get_key_stats()
print("Stats: \n", json.dumps(stats_json, indent=4))
earnings_json = stock_batch.get_earnings()
print("Earnings: \n", json.dumps(earnings_json, indent=4))
balance_json = stock_batch.get_balance_sheet()
print("Balance Sheet: \n", json.dumps(balance_json, indent=4))
cash_flow_json = stock_batch.get_cash_flow()
print("Cash Flow: \n", json.dumps(cash_flow_json, indent=4))
income_json = stock_batch.get_income_statement()
print("Income Statement: \n", json.dumps(income_json, indent=4))

I get this output (which mirrors the pictures):

Financials: 
 [
    {
        "reportDate": "2018-12-31",
        "grossProfit": 31719000000,
        "costOfRevenue": 52654000000,
        "operatingRevenue": 84373000000,
        "totalRevenue": 84373000000,
        "operatingIncome": 23034000000,
        "netIncome": 19965000000,
        "researchAndDevelopment": 3902000000,
        "operatingExpense": 61339000000,
        "currentAssets": 140828000000,
        "totalAssets": 373719000000,
        "totalLiabilities": 255827000000,
        "currentCash": 44771000000,
        "currentDebt": 21741000000,
        "shortTermDebt": 21741000000,
        "longTermDebt": 92989000000,
        "totalCash": 86427000000,
        "totalDebt": 114730000000,
        "shareholderEquity": 117892000000,
        "cashChange": 18858000000,
        "cashFlow": 26690000000
    }
]
Quote: 
 {
    "symbol": "AAPL",
    "companyName": "Apple, Inc.",
    "calculationPrice": "tops",
    "open": 196.42,
    "openTime": 1554730200095,
    "close": 197,
    "closeTime": 1554494400394,
    "high": 199.5,
    "low": 196.34,
    "latestPrice": 198.875,
    "latestSource": "IEX real time price",
    "latestTime": "12:17:00 PM",
    "latestUpdate": 1554740220243,
    "latestVolume": 12781502,
    "iexRealtimePrice": 198.875,
    "iexRealtimeSize": 100,
    "iexLastUpdated": 1554740220243,
    "delayedPrice": 199.06,
    "delayedPriceTime": 1554739331755,
    "extendedPrice": 196.5,
    "extendedChange": -2.375,
    "extendedChangePercent": -0.01194,
    "extendedPriceTime": 1554730194696,
    "previousClose": 197,
    "change": 1.875,
    "changePercent": 0.00952,
    "iexMarketPercent": 0.027141489317922103,
    "iexVolume": 346909,
    "avgTotalVolume": 28583873,
    "iexBidPrice": 198.87,
    "iexBidSize": 100,
    "iexAskPrice": 198.89,
    "iexAskSize": 100,
    "marketCap": 937751310000,
    "peRatio": 16.21,
    "week52High": 233.47,
    "week52Low": 142,
    "ytdChange": 0.25698699999999997
}
Stats: 
 {
    "week52change": 0.16997299999999999,
    "week52high": 233.47,
    "week52low": 142,
    "marketcap": 928910160000,
    "employees": 132000,
    "day200MovingAvg": 190.7,
    "day50MovingAvg": 177.71,
    "float": 4708742476,
    "avg10Volume": 27937902.7,
    "avg30Volume": 28583873.43,
    "ttmEPS": 12.27,
    "ttmDividendRate": 2.82,
    "companyName": "Apple, Inc.",
    "sharesOutstanding": 4715280000,
    "maxChangePercent": 194.049505,
    "year5ChangePercent": 1.634394,
    "year2ChangePercent": 0.371293,
    "year1ChangePercent": 0.169973,
    "ytdChangePercent": 0.247467,
    "month6ChangePercent": -0.119632,
    "month3ChangePercent": 0.331711,
    "month1ChangePercent": 0.12881,
    "day30ChangePercent": 0.130689,
    "day5ChangePercent": 0.030119,
    "nextDividendRate": null,
    "dividendYield": 0.01431472081218274,
    "nextEarningsDate": "2019-05-01",
    "exDividendDate": "2019-02-08",
    "peRatio": 16.21
}
Earnings: 
 [
    {
        "actualEPS": 4.18,
        "consensusEPS": 4.66,
        "announceTime": "AMC",
        "numberOfEstimates": 36,
        "EPSSurpriseDollar": -0.48,
        "EPSReportDate": "2019-01-29",
        "fiscalPeriod": "Q4 2018",
        "fiscalEndDate": "2018-12-31",
        "yearAgo": 3.89,
        "yearAgoChangePercent": 0.0746
    }
]
Balance Sheet: 
 {
    "symbol": "AAPL",
    "balancesheet": [
        {
            "reportDate": "2018-12-31",
            "currentCash": 44771000000,
            "shortTermInvestments": 41656000000,
            "receivables": 18077000000,
            "inventory": 4988000000,
            "otherCurrentAssets": 12432000000,
            "currentAssets": 140828000000,
            "longTermInvestments": 158608000000,
            "propertyPlantEquipment": 39597000000,
            "goodwill": 0,
            "intangibleAssets": null,
            "otherAssets": 34686000000,
            "totalAssets": 373719000000,
            "accountsPayable": 44293000000,
            "currentLongTermDebt": 9772000000,
            "otherCurrentLiabilities": 42249000000,
            "totalCurrentLiabilities": 108283000000,
            "longTermDebt": 92989000000,
            "otherLiabilities": 23607000000,
            "minorityInterest": 0,
            "totalLiabilities": 255827000000,
            "commonStock": 40970000000,
            "retainedEarnings": 80510000000,
            "treasuryStock": null,
            "capitalSurplus": null,
            "shareholderEquity": 117892000000,
            "netTangibleAssets": 117892000000
        }
    ]
}
Cash Flow: 
 {
    "symbol": "AAPL",
    "cashflow": [
        {
            "reportDate": "2018-12-31",
            "netIncome": 19965000000,
            "depreciation": 3395000000,
            "changesInReceivables": 5109000000,
            "changesInInventories": -1076000000,
            "cashChange": 18858000000,
            "cashFlow": 26690000000,
            "capitalExpenditures": -3355000000,
            "investments": 9422000000,
            "investingActivityOther": -56000000,
            "totalInvestingCashFlows": 5844000000,
            "dividendsPaid": -3568000000,
            "netBorrowings": 6000000,
            "otherFinancingCashFlows": -1318000000,
            "cashFlowFinancing": -13676000000,
            "exchangeRateEffect": null
        }
    ]
}
Income Statement: 
 {
    "symbol": "AAPL",
    "income": [
        {
            "reportDate": "2018-12-31",
            "totalRevenue": 84373000000,
            "costOfRevenue": 52654000000,
            "grossProfit": 31719000000,
            "researchAndDevelopment": 3902000000,
            "sellingGeneralAndAdmin": 4783000000,
            "operatingExpense": 61339000000,
            "operatingIncome": 23034000000,
            "otherIncomeExpenseNet": 872000000,
            "ebit": 23034000000,
            "interestIncome": 890000000,
            "pretaxIncome": 23906000000,
            "incomeTax": 3941000000,
            "minorityInterest": 0,
            "netIncome": 19965000000,
            "netIncomeBasic": 19965000000
        }
    ]
}
$\endgroup$
2
$\begingroup$

First, this is kind of difficult to follow and generally obscures what you're actually asking. Second, getting your answers from the paper itself seems like your best answer, barring simply running with what makes the most sense to you given your interpretation.

If I'm understanding, this is really an accounting question, so this is probably not the best place to be asking, but notwithstanding:

My questions are this: -Is the cash flows from operations used in #4, the positive operating cash flow we calculate in 3?

(1) That would seem likely.

-Would share equity remaining the same mean that no new shares were issued? I would guess no because the stock price can change making this not the right number.

(2) It's not clear what the units on commonStock are, and given the way this is set up, I'd expect it's actual $ rather than shares (cs + re ~ shareholderEquity; they probably aren't retrieving everything on the balance sheet).

In key_stats, a data point is named shares_outstanding which is what I am guessing I am looking for, yes? One website argues that the "Weighted Average Shares" number should be checked.

(3) I would guess that's what you need for shares instead as well.

  • I have Gross profit for number 8 but not total sales. Is comparing Gross Profit across years the equivalent? -#9 stumps me.

(4) No, as a gross profit of 10 on sales/revenue of 100 is obviously not the same as gross profit of 10 on sales/revenue of 50. Asset turnover is an efficiency measure (usually defined as some version of sales/assets (or average assets)). Like above, the API you're accessing is likely not providing all fields present in the financial statement. You can't calculate AT if you don't have sales/revenues.

Practically, you can only really get 'good enough' with free feeds or APIs for analyses like this.

Edit (in response to comment as it may be useful to others):

@DavidFrick, well, when I reference free (versus not), I'm not talking about a $20 a month subscription...your typical corp data sources have all this and more (eg, Thomson Reuters/Worldscope, etc). For instance, here's a link to the WS data definitions guide I found with a quick search.

Factset and Bloomberg are otherwise usual suspects for stuff like this; both provide ready access but cost ~$30k a year.

Otherwise, depending on how motivated you are, you might check out this page. I haven't gone through most of it, but a lot of typical financial data sources are included (as well as a host of others). Signal to noise might not be great on this though. For instance, I checked out the SimFin product included therein, and while data quality appeared good, coverage was pretty slim (only about ~2400 securities included). Would probably work well for the right project, but wasn't all that useful to me. YMMV.

$\endgroup$
  • $\begingroup$ Yeah, I decided to scrap the SEC website instead but they use a very complex SGML format that no python libraries are designed for. Do you have any suggestions where I can get the data? I do not mind paying as long as it is clean. I would be eternally grateful. @Chris $\endgroup$ – David Frick Apr 11 at 0:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.