diff --git a/fundamental_analysis/market_watch_bs.py b/fundamental_analysis/market_watch_bs.py index 8b1b8e41e70..cfbbed8866f 100644 --- a/fundamental_analysis/market_watch_bs.py +++ b/fundamental_analysis/market_watch_bs.py @@ -2,6 +2,7 @@ import argparse import pandas as pd from bs4 import BeautifulSoup import requests +import numpy as np from stock_market_helper_funcs import * # ---------------------------------------------------- INCOME ---------------------------------------------------- @@ -408,3 +409,140 @@ def sec_fillings(l_args, s_ticker): except: print("ERROR!\n") return + + +# ---------------------------------------------------- Sean Seah Warnings ---------------------------------------------------- +def sean_seah_warnings(l_args, s_ticker): + parser = argparse.ArgumentParser(prog='warnings', + description="""Sean Seah warnings. Check: Consistent historical earnings per share; + Consistently high return on equity; Consistently high return on assets; 5x Net + Income > Long-Term Debt; and Interest coverage ratio more than 3. [Source: Market Watch BS]""") + + parser.add_argument('-d', "--debug", action="store_true", default=False, dest="b_debug", help='Show data used to calculate warnings') + + try: + (ns_parser, l_unknown_args) = parser.parse_known_args(l_args) + + if l_unknown_args: + print(f"The following args couldn't be interpreted: {l_unknown_args}") + + # From INCOME STATEMENT, get: 'EPS (Basic)', 'Net Income', 'Interest Expense', 'EBITDA' + url_financials = f"https://www.marketwatch.com/investing/stock/{s_ticker}/financials/income" + text_soup_financials = BeautifulSoup(requests.get(url_financials).text,"lxml") + + # Define financials columns + a_financials_header = list() + for financials_header in text_soup_financials.findAll('th', {'class': 'overflow__heading'}): + a_financials_header.append(financials_header.text.strip('\n').split('\n')[0]) + df_financials = pd.DataFrame(columns=a_financials_header[0:-1]) + + # Add financials values + soup_financials = text_soup_financials.findAll('tr', {'class': 'table__row '}) + soup_financials += text_soup_financials.findAll('tr', {'class': 'table__row is-highlighted'}) + for financials_info in soup_financials: + a_financials_info = financials_info.text.split('\n') + l_financials = [a_financials_info[2]] + l_financials.extend(a_financials_info[5:-2]) + # Append data values to financials + df_financials.loc[len(df_financials.index)] = l_financials + + # Set item name as index + df_financials = df_financials.set_index('Item') + + df_sean_seah = df_financials.loc[['EPS (Basic)', 'Net Income', 'Interest Expense', 'EBITDA']] + + # From BALANCE SHEET, get: 'Liabilities & Shareholders\' Equity', 'Long-Term Debt' + url_financials = f"https://www.marketwatch.com/investing/stock/{s_ticker}/financials/balance-sheet" + text_soup_financials = BeautifulSoup(requests.get(url_financials).text,"lxml") + + # Define financials columns + a_financials_header = list() + for financials_header in text_soup_financials.findAll('th', {'class': 'overflow__heading'}): + a_financials_header.append(financials_header.text.strip('\n').split('\n')[0]) + + s_header_end_trend = "5-year trend" + df_financials = pd.DataFrame(columns=a_financials_header[0:a_financials_header.index(s_header_end_trend)]) + + # Add financials values + soup_financials = text_soup_financials.findAll('tr', {'class': 'table__row '}) + soup_financials += text_soup_financials.findAll('tr', {'class': 'table__row is-highlighted'}) + for financials_info in soup_financials: + a_financials_info = financials_info.text.split('\n') + l_financials = [a_financials_info[2]] + l_financials.extend(a_financials_info[5:-2]) + # Append data values to financials + df_financials.loc[len(df_financials.index)] = l_financials + + # Set item name as index + df_financials = df_financials.set_index('Item') + + # Create dataframe to compute meaningful metrics from sean seah book + df_sean_seah = df_sean_seah.append(df_financials.loc[['Total Shareholders\' Equity', 'Liabilities & Shareholders\' Equity', 'Long-Term Debt']]) + + # Clean these metrics by parsing their values to float + df_sean_seah = df_sean_seah.applymap(lambda x: clean_data_values_to_float(x)) + + # Add additional necessary metrics + series = df_sean_seah.loc['Net Income']/df_sean_seah.loc['Total Shareholders\' Equity'] + series.name = "ROE" + df_sean_seah = df_sean_seah.append(series) + + series = df_sean_seah.loc['EBITDA']/df_sean_seah.loc['Interest Expense'] + series.name = "Interest Coverage Ratio" + df_sean_seah = df_sean_seah.append(series) + + series = df_sean_seah.loc['Net Income']/df_sean_seah.loc['Liabilities & Shareholders\' Equity'] + series.name = "ROA" + df_sean_seah = df_sean_seah.append(series) + + print(df_sean_seah.applymap(lambda x: int_or_round_float(x)).to_string()) + + n_warnings = 0 + print("\nWARNINGS:") + + if np.any(df_sean_seah.loc['EPS (Basic)'].diff().dropna().values < 0): + print("NO consistent historical earnings per share") + n_warnings += 1 + if ns_parser.b_debug: + sa_eps = np.array2string(df_sean_seah.loc['EPS (Basic)'].values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" EPS: {sa_eps}") + sa_growth = np.array2string(df_sean_seah.loc['EPS (Basic)'].diff().dropna().values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" Growth: {sa_growth} < 0") + + if np.any(df_sean_seah.loc['ROE'].values < 0.15): + print("NOT consistently high return on equity") + n_warnings += 1 + if ns_parser.b_debug: + sa_roe = np.array2string(df_sean_seah.loc['ROE'].values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" ROE: {sa_roe} < 0.15") + + if np.any(df_sean_seah.loc['ROA'].values < 0.07): + print("NOT consistently high return on assets") + n_warnings += 1 + if ns_parser.b_debug: + sa_roa = np.array2string(df_sean_seah.loc['ROA'].values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" ROA: {sa_roa} < 0.07") + + if np.any(df_sean_seah.loc['Long-Term Debt'].values > 5*df_sean_seah.loc['Net Income'].values): + print("5x Net Income < Long-Term Debt") + n_warnings += 1 + if ns_parser.b_debug: + sa_net_income = np.array2string(df_sean_seah.loc['Net Income'].values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" NET Income: {sa_net_income}") + sa_5_long_term_debt = np.array2string(5*df_sean_seah.loc['Long-Term Debt'].values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" lower than 5x Long-Term Debt: {sa_5_long_term_debt}") + + if np.any(df_sean_seah.loc['Interest Coverage Ratio'].values < 3): + print("Interest coverage ratio less than 3") + n_warnings += 1 + if ns_parser.b_debug: + sa_interest_coverage_ratio = np.array2string(100*df_sean_seah.loc['Interest Coverage Ratio'].values, formatter={'float_kind': lambda x: int_or_round_float(x)}) + print(f" Interest Coverage Ratio: {sa_interest_coverage_ratio} < 3") + + if n_warnings == 0: + print("None. Good stonk") + + print("") + except: + print("ERROR!\n") + return diff --git a/fundamental_analysis/menu.py b/fundamental_analysis/menu.py index 8dc7ca77860..77b906e8639 100644 --- a/fundamental_analysis/menu.py +++ b/fundamental_analysis/menu.py @@ -29,6 +29,7 @@ def print_fundamental_analysis(s_ticker, s_start, s_interval): print("\nFundamental Analysis:") # https://github.com/JerBouma/FundamentalAnalysis print(" info provides information on main key metrics of company") + print(" warnings company warnings according to Sean Seah based on Market Watch data") print(" help show this fundamental analysis menu again") print(" q quit this menu, and shows back to main menu") print(" quit quit to abandon program") @@ -113,6 +114,9 @@ def fa_menu(fa_parser, s_ticker, s_start, s_interval): if ns_known_args.fa == 'info': info(l_args, s_ticker) + if ns_known_args.fa == 'warnings': + mw_bs.sean_seah_warnings(l_args, s_ticker) + elif ns_known_args.fa == 'help': print_fundamental_analysis(s_ticker, s_start, s_interval) diff --git a/stock_market_cli.py b/stock_market_cli.py index 43f368a641f..67bd0f8fe5b 100644 --- a/stock_market_cli.py +++ b/stock_market_cli.py @@ -61,7 +61,7 @@ def main(): # Add list of arguments that the fundamental analysis parser accepts fa_parser = argparse.ArgumentParser(prog='fundamental_analysis', add_help=False) - fa_parser.add_argument('fa', choices=['info', 'help', 'q', 'quit', + fa_parser.add_argument('fa', choices=['info', 'warnings', 'help', 'q', 'quit', 'overview', 'key', 'income', 'balance', 'cash', 'earnings', # AV 'profile', 'rating', 'quote', 'enterprise', 'dcf', # FMP 'inc', 'bal', 'cashf', 'metrics', 'ratios', 'growth', # FMP diff --git a/stock_market_helper_funcs.py b/stock_market_helper_funcs.py index 527ac46d506..b97800c35db 100644 --- a/stock_market_helper_funcs.py +++ b/stock_market_helper_funcs.py @@ -2,6 +2,7 @@ from pytz import timezone from holidays import US as holidaysUS from datetime import datetime, time as Time +import sys import matplotlib import matplotlib.pyplot as plt @@ -152,3 +153,40 @@ def long_number_format(num): else: return '%.3f%s' % (num, ['', ' K', ' M', ' B', ' T', ' P'][magnitude]) return num + + +# ----------------------------------------------------------------------------------------------------------------------- +def clean_data_values_to_float(val): + # Remove parenthesis if they exist + if val.startswith('('): + val = val[1:] + if val.endswith(')'): + val = val[:-1] + + if val == '-': + val = '0' + + # Convert percentage to decimal + if val.endswith('%'): + val = float(val[:-1])/100.0 + # Convert from billions + elif val.endswith('B'): + val = float(val[:-1])*1_000_000_000 + # Convert from millions + elif val.endswith('M'): + val = float(val[:-1])*1_000_000 + # Convert from thousands + elif val.endswith('K'): + val = float(val[:-1])*1_000 + else: + val = float(val) + + return val + + +# ----------------------------------------------------------------------------------------------------------------------- +def int_or_round_float(x): + if (x - int(x) < -sys.float_info.epsilon) or (x - int(x) > sys.float_info.epsilon): + return ' ' + str(round(x, 2)) + else: + return ' ' + str(int(x))