Quiero poner en práctica la estrategia F-score Value de Piotroski que se expone en el documento "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." que enumera 9 criterios para juzgar a una empresa:
# 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.
Estoy trabajando con la biblioteca IEXfinance en python para extraer datos de las hojas financieras de una empresa para determinar si cumplen con este criterio (aunque podría cambiar a desechar desde el sistema EDGAR si eso resulta ser un poco más rápido porque el IEXfinance tiene partes detrás de un muro de pago. De todos modos, estoy teniendo problemas para determinar si los datos devueltos desde IEXfinance son los mismos que estoy buscando porque los términos son ligeramente diferentes.
En el Balance, puedo ver los datos necesarios para 1,2,5,6.
El flujo de caja operativo. se define como Ingreso Neto - Gasto de Depreciación + Cambio en las Cuentas por Cobrar + Cambio en el Inventario que puedo ver en el Informe de Flujo de Caja. Así que el 3 se cumple.
Mis preguntas son las siguientes -¿Es el flujo de caja de operaciones utilizado en el #4, el flujo de caja de operaciones positivo que calculamos en el 3? -¿El hecho de que el capital social siga siendo el mismo significa que no se han emitido nuevas acciones? Supongo que no porque el precio de las acciones puede cambiar haciendo que este no sea el número correcto. En key_stats, hay un punto de datos llamado shares_outstanding que es lo que supongo que estoy buscando, ¿sí? Un sitio web argumenta que el número de "Acciones promedio ponderadas" debe ser revisado. - Tengo el beneficio bruto para el número 8 pero no las ventas totales. ¿Es equivalente comparar el beneficio bruto entre años? -#El número 9 me deja perplejo. Aquí hay un código muy aproximado:
# 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()
# **************************************************************************************************
########################################################################################################################
Para este código:
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))
Obtengo esta salida (que refleja las imágenes):
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
}
]
}