From c5393e0875d9bb75df16a458f2dd76a18a0d3f4a Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Thu, 27 Jan 2022 11:56:53 -0500 Subject: [PATCH] Bankruptcy Scores (#1271) * Refacotred and expanded analytics * Updated tests * Updated hugo * Updated tests --- .github/workflows/test.yml | 2 +- .pre-commit-config.yaml | 2 +- .../stocks/fundamental_analysis/av_model.py | 86 +++++++++++-------- .../stocks/fundamental_analysis/av_view.py | 21 ++--- .../fundamental_analysis/fa_controller.py | 4 + .../test_av_model/test_get_fraud_ratios.json | 2 +- .../fundamental_analysis/test_av_view.py | 13 +-- .../test_display_fraud[ratios0-nan-nan].txt | 1 + .../test_display_fraud[ratios1-0.1-0.2].txt | 7 ++ .../test_display_fraud[ratios2-0.1-0.2].txt | 8 ++ .../test_display_fraud[ratios3-1-0.2].txt | 7 ++ .../fundamental_analysis/fraud/_index.md | 5 ++ 12 files changed, 101 insertions(+), 57 deletions(-) create mode 100644 tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios0-nan-nan].txt create mode 100644 tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios1-0.1-0.2].txt create mode 100644 tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios2-0.1-0.2].txt create mode 100644 tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios3-1-0.2].txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 13c9be331d3..b85679d4d12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: - run: pip install types-pytz types-requests types-termcolor types-tabulate types-PyYAML types-python-dateutil types-setuptools - run: bandit -x ./tests -r . || true - run: black --check . - - run: codespell --ignore-words-list=zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold --quiet-level=2 --skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml + - run: codespell --ignore-words-list=zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold,ist --quiet-level=2 --skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml - run: flake8 . --count --ignore=E203,W503 --max-line-length=122 --show-source --statistics - run: mypy --ignore-missing-imports --exclude="/setup\.py$" . - run: shopt -s globstar && pyupgrade --py36-plus **/*.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3d1e9101638..d6af7e5b343 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: entry: codespell args: [ - "--ignore-words-list=zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold", + "--ignore-words-list=zlot,ba,buil,coo,ether,hist,hsi,mape,navagation,operatio,pres,ser,yeld,shold,ist", "--quiet-level=2", "--skip=./tests,.git,*.css,*.csv,*.html,*.ini,*.ipynb,*.js,*.json,*.lock,*.scss,*.txt,*.yaml", ] diff --git a/gamestonk_terminal/stocks/fundamental_analysis/av_model.py b/gamestonk_terminal/stocks/fundamental_analysis/av_model.py index f253ff83295..760895ad246 100644 --- a/gamestonk_terminal/stocks/fundamental_analysis/av_model.py +++ b/gamestonk_terminal/stocks/fundamental_analysis/av_model.py @@ -1,12 +1,13 @@ """Alpha Vantage Model""" __docformat__ = "numpy" -from typing import Dict, Tuple +from typing import Dict, Tuple, List import requests from alpha_vantage.fundamentaldata import FundamentalData import pandas as pd +import numpy as np from gamestonk_terminal.stocks.fundamental_analysis.fa_helper import clean_df_index from gamestonk_terminal.helper_funcs import long_number_format from gamestonk_terminal import config_terminal as cfg @@ -301,7 +302,27 @@ def get_earnings(ticker: str, quarterly: bool = False) -> pd.DataFrame: return pd.DataFrame() -def get_fraud_ratios(ticker: str) -> Tuple[Dict[str, float], float]: +def df_values(df: pd.DataFrame, item: str) -> List[int]: + """Clean the values from the df + + Parameters + ---------- + df : pd.DataFrame + The Dataframe to use + item : str + The item to select + + Returns + ------- + values : List[int] + The values for the dataframe + """ + selection = df[item] + values = selection.apply(lambda x: int(x) if x else 0).values + return values.tolist() + + +def get_fraud_ratios(ticker: str) -> Tuple[Dict[str, float], float, float]: """Get fraud ratios based on fundamentals Parameters @@ -326,42 +347,23 @@ def get_fraud_ratios(ticker: str) -> Tuple[Dict[str, float], float]: df_bs = df_bs.set_index("fiscalDateEnding").iloc[:2] df_is = df_is.set_index("fiscalDateEnding").iloc[:2] - ar = df_bs["currentNetReceivables"].apply(lambda x: 0 if x else int(x)).values - sales = df_is["totalRevenue"].apply(lambda x: 0 if x else int(x)).values - cogs = ( - df_is["costofGoodsAndServicesSold"].apply(lambda x: 0 if x else int(x)).values - ) - ni = df_is["netIncome"].apply(lambda x: 0 if x else int(x)).values - ca = df_bs["totalCurrentAssets"].apply(lambda x: 0 if x else int(x)).values - cl = df_bs["totalCurrentLiabilities"].apply(lambda x: 0 if x else int(x)).values - ppe = df_bs["propertyPlantEquipment"].apply(lambda x: 0 if x else int(x)).values - cash = ( - df_bs["cashAndCashEquivalentsAtCarryingValue"] - .apply(lambda x: 0 if x else int(x)) - .values - ) - cash_and_sec = ( - df_bs["cashAndShortTermInvestments"].apply(lambda x: 0 if x else int(x)).values - ) + ar = df_values(df_bs, "currentNetReceivables") + sales = df_values(df_is, "totalRevenue") + cogs = df_values(df_is, "costofGoodsAndServicesSold") + ni = df_values(df_is, "netIncome") + ca = df_values(df_bs, "totalCurrentAssets") + cl = df_values(df_bs, "totalCurrentLiabilities") + ppe = df_values(df_bs, "propertyPlantEquipment") + cash = df_values(df_bs, "cashAndCashEquivalentsAtCarryingValue") + cash_and_sec = df_values(df_bs, "cashAndShortTermInvestments") sec = [y - x for (x, y) in zip(cash, cash_and_sec)] - ta = df_bs["totalAssets"].apply(lambda x: 0 if x else int(x)).values - dep = ( - df_bs["accumulatedDepreciationAmortizationPPE"] - .apply(lambda x: 0 if x else int(x)) - .values - ) - sga = ( - df_is["sellingGeneralAndAdministrative"] - .apply(lambda x: 0 if x else int(x)) - .values - ) - tl = df_bs["totalLiabilities"].apply(lambda x: 0 if x else int(x)).values - icfo = ( - df_is["netIncomeFromContinuingOperations"] - .apply(lambda x: 0 if x else int(x)) - .values - ) - cfo = df_cf["operatingCashflow"].apply(lambda x: 0 if x else int(x)).values + ta = df_values(df_bs, "totalAssets") + dep = df_values(df_bs, "accumulatedDepreciationAmortizationPPE") + sga = df_values(df_is, "sellingGeneralAndAdministrative") + tl = df_values(df_bs, "totalLiabilities") + icfo = df_values(df_is, "netIncomeFromContinuingOperations") + cfo = df_values(df_cf, "operatingCashflow") + ratios: Dict = {} ratios["DSRI"] = (ar[0] / sales[0]) / (ar[1] / sales[1]) ratios["GMI"] = ((sales[1] - cogs[1]) / sales[1]) / ( @@ -392,5 +394,13 @@ def get_fraud_ratios(ticker: str) -> Tuple[Dict[str, float], float]: + (5.679 * (tl[0] / ta[0])) + (0.004 * (ca[0] / cl[0])) ) + v1 = np.log(ta[0] / 1000) + v2 = ni[0] / ta[0] + v3 = cash[0] / cl[0] - return ratios, zscore + x = ((v1 + 0.85) * v2) - 0.85 + y = 1 + v3 + + mckee = x ** 2 / (x ** 2 + y ** 2) + + return ratios, zscore, mckee diff --git a/gamestonk_terminal/stocks/fundamental_analysis/av_view.py b/gamestonk_terminal/stocks/fundamental_analysis/av_view.py index 9d2f714a7dd..356c97c047c 100644 --- a/gamestonk_terminal/stocks/fundamental_analysis/av_view.py +++ b/gamestonk_terminal/stocks/fundamental_analysis/av_view.py @@ -171,19 +171,18 @@ def display_fraud(ticker: str): ticker : str Fundamental analysis ticker symbol """ - ratios, zscore = av_model.get_fraud_ratios(ticker) + ratios, zscore, mckee = av_model.get_fraud_ratios(ticker) if ratios["MSCORE"] > -1.78: - chanceM = "high" + chance_m = "high" elif ratios["MSCORE"] > -2.22: - chanceM = "moderate" + chance_m = "moderate" else: - chanceM = "low" + chance_m = "low" - if zscore < 0.5: - chanceZ = "high" - else: - chanceZ = "low" + chance_z = "high" if zscore < 0.5 else "low" + + chance_mcke = "low" if mckee < 0.5 else "high" if np.isnan(ratios["MSCORE"]) or np.isnan(zscore): console.print("Data incomplete for this ticker. Unable to calculate risk") @@ -196,8 +195,10 @@ def display_fraud(ticker: str): console.print( "\n" + "MSCORE: ", - f"{ratios['MSCORE']:.2f} ({chanceM} chance of fraud)", + f"{ratios['MSCORE']:.2f} ({chance_m} chance of fraud)", ) - console.print("ZSCORE: ", f"{zscore:.2f} ({chanceZ} chance of bankruptcy)", "\n") + console.print("ZSCORE: ", f"{zscore:.2f} ({chance_z} chance of bankruptcy)", "\n") + + console.print("McKee: ", f"{mckee:.2f} ({chance_mcke} chance of bankruptcy)", "\n") return diff --git a/gamestonk_terminal/stocks/fundamental_analysis/fa_controller.py b/gamestonk_terminal/stocks/fundamental_analysis/fa_controller.py index 8196a2b4252..72a26e367a6 100644 --- a/gamestonk_terminal/stocks/fundamental_analysis/fa_controller.py +++ b/gamestonk_terminal/stocks/fundamental_analysis/fa_controller.py @@ -721,6 +721,10 @@ Ticker: [/param] {self.ticker} [cmds] " higher probability of default. One of the criticisms that Zmijewski made was that" " other bankruptcy scoring models oversampled distressed firms and favored situations" " with more complete data.[Source: YCharts]" + "McKee-score:\n------------------------------------------------\n" + "The McKee Score is a bankruptcy model used to predict a firm's bankruptcy in one year" + "It looks at a companie's size, profitability, and liquidity to determine the probability." + "This model is 80% accurate in predicting bankruptcy." ), ) ns_parser = parse_known_args_and_warn( diff --git a/tests/gamestonk_terminal/stocks/fundamental_analysis/json/test_av_model/test_get_fraud_ratios.json b/tests/gamestonk_terminal/stocks/fundamental_analysis/json/test_av_model/test_get_fraud_ratios.json index b0e7471b1b0..322bbf6ce13 100644 --- a/tests/gamestonk_terminal/stocks/fundamental_analysis/json/test_av_model/test_get_fraud_ratios.json +++ b/tests/gamestonk_terminal/stocks/fundamental_analysis/json/test_av_model/test_get_fraud_ratios.json @@ -1 +1 @@ -[{"DSRI": NaN, "GMI": NaN, "AQI": NaN, "SGI": NaN, "DEPI": NaN, "SGAI": NaN, "LVGI": NaN, "TATA": NaN, "MSCORE": NaN}, NaN] \ No newline at end of file +[{"DSRI": 1.1154783925070624, "GMI": 1.0013237094668648, "AQI": 0.7066050530188391, "SGI": 1.2830987061599806, "DEPI": 0.9225617507161853, "SGAI": 0.9263406938399691, "LVGI": 0.713640022276499, "TATA": -0.09743422566541382, "MSCORE": -2.545490667269407}, -1.2961310063630496, 0.05928338445454072] \ No newline at end of file diff --git a/tests/gamestonk_terminal/stocks/fundamental_analysis/test_av_view.py b/tests/gamestonk_terminal/stocks/fundamental_analysis/test_av_view.py index 48a16a2272f..b79ba2f6c12 100644 --- a/tests/gamestonk_terminal/stocks/fundamental_analysis/test_av_view.py +++ b/tests/gamestonk_terminal/stocks/fundamental_analysis/test_av_view.py @@ -110,7 +110,7 @@ def test_check_empty_df(func, kwargs_dict, mocked_func, mocker): @pytest.mark.vcr(record_mode="none") @pytest.mark.record_stdout @pytest.mark.parametrize( - "ratios, zscore", + "ratios, zscore, mckscore", [ ( { @@ -125,15 +125,16 @@ def test_check_empty_df(func, kwargs_dict, mocked_func, mocker): "MSCORE": np.nan, }, np.nan, + np.nan, ), - ({"MSCORE": -1}, 0.1), - ({"AQI": 1, "MSCORE": -2}, 0.1), - ({"MSCORE": -3}, 1), + ({"MSCORE": -1}, 0.1, 0.2), + ({"AQI": 1, "MSCORE": -2}, 0.1, 0.2), + ({"MSCORE": -3}, 1, 0.2), ], ) -def test_display_fraud(mocker, ratios, zscore): +def test_display_fraud(mocker, ratios, zscore, mckscore): mocker.patch( "gamestonk_terminal.stocks.fundamental_analysis.av_view.av_model.get_fraud_ratios", - return_value=(ratios, zscore), + return_value=(ratios, zscore, mckscore), ) av_view.display_fraud(ticker="TSLA") diff --git a/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios0-nan-nan].txt b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios0-nan-nan].txt new file mode 100644 index 00000000000..68f1a8d3142 --- /dev/null +++ b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios0-nan-nan].txt @@ -0,0 +1 @@ +Data incomplete for this ticker. Unable to calculate risk diff --git a/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios1-0.1-0.2].txt b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios1-0.1-0.2].txt new file mode 100644 index 00000000000..77e269d0f3e --- /dev/null +++ b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios1-0.1-0.2].txt @@ -0,0 +1,7 @@ +Mscore Sub Stats: + +MSCORE: -1.00 (high chance of fraud) +ZSCORE: 0.10 (high chance of bankruptcy) + +McKee: 0.20 (low chance of bankruptcy) + diff --git a/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios2-0.1-0.2].txt b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios2-0.1-0.2].txt new file mode 100644 index 00000000000..0defdbf870b --- /dev/null +++ b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios2-0.1-0.2].txt @@ -0,0 +1,8 @@ +Mscore Sub Stats: + AQI : 1.00 + +MSCORE: -2.00 (moderate chance of fraud) +ZSCORE: 0.10 (high chance of bankruptcy) + +McKee: 0.20 (low chance of bankruptcy) + diff --git a/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios3-1-0.2].txt b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios3-1-0.2].txt new file mode 100644 index 00000000000..deb6292399f --- /dev/null +++ b/tests/gamestonk_terminal/stocks/fundamental_analysis/txt/test_av_view/test_display_fraud[ratios3-1-0.2].txt @@ -0,0 +1,7 @@ +Mscore Sub Stats: + +MSCORE: -3.00 (low chance of fraud) +ZSCORE: 1.00 (low chance of bankruptcy) + +McKee: 0.20 (low chance of bankruptcy) + diff --git a/website/content/stocks/fundamental_analysis/fraud/_index.md b/website/content/stocks/fundamental_analysis/fraud/_index.md index d8eaec7ed07..fddd1a339d8 100755 --- a/website/content/stocks/fundamental_analysis/fraud/_index.md +++ b/website/content/stocks/fundamental_analysis/fraud/_index.md @@ -34,6 +34,11 @@ Zscore: ------------------------------------------------ The Zmijewski Score is a bankruptcy model used to predict a firm's bankruptcy in two years. The ratio uses in the Zmijewski score were determined by probit analysis (think of probit as probability unit). In this case, scores less than .5 represent a higher probability of default. One of the criticisms that Zmijewski made was that other bankruptcy scoring models oversampled distressed firms and favored situations with more complete data.[Source: YCharts] +McKee Score: +------------------------------------------------ +The McKee Score is a bankruptcy model used to predict a firm's bankruptcy in one year. It looks at a companie's size, profitability, and liquidity to determine the probability. This model is 80% accurate in predicting bankruptcy. +[Source: McKee and Lensberg](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.619.594&rep=rep1&type=pdf) + ``` optional arguments: -h, --help show this help message