mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 20:42:26 +00:00
Add warnings based on Sean Seah book using MarketWatch source data
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user