Spaces:
Runtime error
Runtime error
# Importing required modules | |
import pandas as pd # for manipulating financial statements in dataframes | |
import numpy as np | |
import plotly.express as px # for visualizing results in interactive plots | |
# To extract and parse fundamental data like beta and growth estimates from finviz website's HTML | |
import requests | |
from bs4 import BeautifulSoup as bs | |
# For parsing financial statements data from financialmodelingprep api | |
from urllib.request import urlopen | |
import json | |
def get_jsonparsed_data(url): | |
response = urlopen(url) | |
data = response.read().decode("utf-8") | |
return json.loads(data) | |
# For Gradio App | |
import gradio as gr | |
# To read the environment variable to use in API requests later | |
import os | |
apiKey = os.environ['FMP_API_KEY'] # the environment variable is set in HuggingFace Spaces directly | |
############################################################################################################ | |
###### GET DATA FROM FINANCIAL MODELING PREP | |
############################################################################################################ | |
# Financialmodelingprep api url | |
base_url = "https://financialmodelingprep.com/api/v3/" | |
# get financial statements using financial modelling prep API | |
def get_financial_statements(ticker): | |
# quarterly cash flow statements for calculating latest trailing twelve months (TTM) free cash flow | |
columns_drop = ['acceptedDate', 'period', 'symbol', 'reportedCurrency', 'cik', 'fillingDate', 'depreciationAndAmortization', 'link', 'finalLink'] | |
q_cash_flow_statement = pd.DataFrame(get_jsonparsed_data(base_url+'cash-flow-statement/' + ticker + '?period=quarter' + '&apikey=' + apiKey)) | |
q_cash_flow_statement = q_cash_flow_statement.set_index('date').drop(columns_drop, axis=1).iloc[:4] # extract for last 4 quarters | |
latest_year = int(q_cash_flow_statement.iloc[0]['calendarYear']) | |
# annual cash flow statements | |
cash_flow_statement = pd.DataFrame(get_jsonparsed_data(base_url+'cash-flow-statement/' + ticker + '?apikey=' + apiKey)) | |
cash_flow_statement = cash_flow_statement.set_index('date').drop(columns_drop, axis=1) | |
# combine annual and latest TTM cash flow statements | |
ttm_cash_flow_statement = q_cash_flow_statement.sum() # sum up last 4 quarters to get TTM cash flow | |
cash_flow_statement = cash_flow_statement[::-1].append(ttm_cash_flow_statement.rename('TTM')).drop(['netIncome'], axis=1) | |
final_cash_flow_statement = cash_flow_statement[::-1] # reverse list to show most recent ones first | |
# quarterly balance sheet statements | |
columns_drop = ['acceptedDate', 'calendarYear', 'period', 'symbol', 'reportedCurrency', 'cik', 'fillingDate', 'link', 'finalLink'] | |
q_balance_statement = pd.DataFrame(get_jsonparsed_data(base_url+'balance-sheet-statement/' + ticker + '?period=quarter' + '&apikey=' + apiKey)) | |
q_balance_statement = q_balance_statement.set_index('date').drop(columns_drop, axis=1) | |
q_balance_statement = q_balance_statement.apply(pd.to_numeric, errors='coerce') | |
return q_cash_flow_statement, cash_flow_statement, final_cash_flow_statement, q_balance_statement, latest_year | |
# check stability of cash flows | |
def plot_cash_flow(ticker, cash_flow_statement): | |
# DCF model works best only if the free cash flows are POSITIVE, STABLE and STEADILY INCREASING. | |
# So let's plot the graph and verify if this is the case. | |
fig_cash_flow = px.bar(cash_flow_statement , y='freeCashFlow', title=ticker + ' Free Cash Flows') | |
fig_cash_flow.update_xaxes(type='category', tickangle=270, title='Date') | |
fig_cash_flow.update_yaxes(title='Free Cash Flows') | |
#fig_cash_flow.show() | |
return fig_cash_flow | |
# get ttm cash flow, most recent total debt and cash & short term investment data from statements | |
def get_statements_data(final_cash_flow_statement, q_balance_statement): | |
cash_flow = final_cash_flow_statement.iloc[0]['freeCashFlow'] # ttm cash flow | |
total_debt = q_balance_statement.iloc[0]['totalDebt'] | |
cash_and_ST_investments = q_balance_statement.iloc[0]['cashAndShortTermInvestments'] | |
return cash_flow, total_debt, cash_and_ST_investments | |
############################################################################################################ | |
###### GET DATA FROM FINVIZ WEBSITE | |
############################################################################################################ | |
# Price, EPS next Y/5Y, Beta, Number of Shares Outstanding | |
# Extract (using requests.get) and Parse (using Beautiful Soup) data from Finviz table in the Finviz website (see screenshot above), needed to calculate intrinsic value of stock. | |
# List of data we want to extract from Finviz Table | |
# Price is the current stock price | |
# EPS next Y is the estimated earnings growth for next year | |
# EPS next 5Y is the estimated earnings growth for next 5 years (if this is not present on finviz, we will use EPS next Y instead) | |
# Beta captures the volatility of the stock, used for estimating discount rate later | |
# Shs Outstand is the number of shares present in the market | |
metric = ['Price', 'EPS next Y', 'EPS next 5Y', 'Beta', 'Shs Outstand'] | |
def fundamental_metric(soup, metric): | |
# the table which stores the data in Finviz has html table attribute class of 'snapshot-td2' | |
return soup.find_all(text = metric)[-1].find_next(class_='snapshot-td2').text | |
# get above metrics from finviz and store as a dict | |
def get_finviz_data(ticker): | |
try: | |
url = ("http://finviz.com/quote.ashx?t=" + ticker.lower()) | |
soup = bs(requests.get(url,headers={'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:20.0) Gecko/20100101 Firefox/20.0'}).content) | |
dict_finviz = {} | |
for m in metric: | |
dict_finviz[m] = fundamental_metric(soup,m) | |
for key, value in dict_finviz.items(): | |
# replace percentages | |
if (value[-1]=='%'): | |
dict_finviz[key] = value[:-1] | |
dict_finviz[key] = float(dict_finviz[key]) | |
# billion | |
if (value[-1]=='B'): | |
dict_finviz[key] = value[:-1] | |
dict_finviz[key] = float(dict_finviz[key])*1000000000 | |
# million | |
if (value[-1]=='M'): | |
dict_finviz[key] = value[:-1] | |
dict_finviz[key] = float(dict_finviz[key])*1000000 | |
try: | |
dict_finviz[key] = float(dict_finviz[key]) | |
except: | |
pass | |
except Exception as e: | |
print (e) | |
print ('Not successful parsing ' + ticker + ' data.') | |
return dict_finviz | |
def parse_finviz_dict(finviz_dict): | |
EPS_growth_5Y = finviz_dict['EPS next 5Y'] | |
# sometimes EPS next 5Y is empty and shows as a '-' string, in this case use EPS next Y | |
if isinstance(EPS_growth_5Y, str): | |
if not EPS_growth_5Y.isdigit(): | |
EPS_growth_5Y = finviz_dict['EPS next Y'] | |
EPS_growth_6Y_to_10Y = EPS_growth_5Y/2 # Half the previous growth rate, conservative estimate | |
# Long term = previous growth rate or around long term inflation rate, whichever is lower to be conservative estimate | |
long_term_growth_rate = np.minimum(EPS_growth_6Y_to_10Y, 3) | |
shares_outstanding = finviz_dict['Shs Outstand'] | |
beta = finviz_dict['Beta'] | |
current_price = finviz_dict['Price'] | |
return EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, beta, shares_outstanding, current_price | |
## Estimate Discount Rate from Beta | |
def estimate_discount_rate(beta): | |
# Beta shows the volatility of the stock, | |
# the higher the beta, we want to be more conservative by increasing the discount rate also. | |
discount_rate = 7 | |
if(beta<0.80): | |
discount_rate = 5 | |
elif(beta>=0.80 and beta<1): | |
discount_rate = 6 | |
elif(beta>=1 and beta<1.1): | |
discount_rate = 6.5 | |
elif(beta>=1.1 and beta<1.2): | |
discount_rate = 7 | |
elif(beta>=1.2 and beta<1.3): | |
discount_rate = 7.5 | |
elif(beta>=1.3 and beta<1.4): | |
discount_rate = 8 | |
elif(beta>=1.4 and beta<1.6): | |
discount_rate = 8.5 | |
elif(beta>=1.61): | |
discount_rate = 9 | |
return discount_rate | |
############################################################################################################ | |
## Calculate Intrinsic Value | |
############################################################################################################ | |
# 1. First Project Cash Flows from Year 1 to Year 10 using Present (TTM) Free Cash Flow | |
# 2. Discount the Cash Flows to Present Value | |
# 3. Calculate the Terminal Value after Year 10 (Discounted to Present Value) Assuming the Company will Grow at a Constant Steady Rate Forever (https://corporatefinanceinstitute.com/resources/financial-modeling/dcf-terminal-value-formula/) | |
# 4. Add the Cash Flows and the Terminal Value Up | |
# 5. Then Account for the Cash + Short Term Investments and Subtract Total Debt | |
# 6. Divide by Total Number of Shares Outstanding | |
def calculate_intrinsic_value(latest_year, cash_flow, total_debt, cash_and_ST_investments, | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, | |
shares_outstanding, discount_rate, current_price): | |
# Convert all percentages to decmials | |
EPS_growth_5Y_d = EPS_growth_5Y/100 | |
EPS_growth_6Y_to_10Y_d = EPS_growth_6Y_to_10Y/100 | |
long_term_growth_rate_d = long_term_growth_rate/100 | |
discount_rate_d = discount_rate/100 | |
# print("Discounted Cash Flows\n") | |
# Lists of projected cash flows from year 1 to year 20 | |
cash_flow_list = [] | |
cash_flow_discounted_list = [] | |
year_list = [] | |
# 1. First Project Cash Flows from Year 1 to Year 10 using Present (TTM) Free Cash Flow | |
# 2. Discount Each of the Cash Flows to Present Value | |
# Years 1 to 5 | |
for year in range(1, 6): | |
year_list.append(year + latest_year) | |
cash_flow*=(1 + EPS_growth_5Y_d) | |
cash_flow_list.append(cash_flow) | |
cash_flow_discounted = cash_flow/((1 + discount_rate_d)**year) | |
cash_flow_discounted_list.append(cash_flow_discounted) | |
# print("Year " + str(year + latest_year) + ": $" + str(cash_flow_discounted)) ## Print out the projected discounted cash flows | |
# Years 6 to 10 | |
for year in range(6, 11): | |
year_list.append(year + latest_year) | |
cash_flow*=(1 + EPS_growth_6Y_to_10Y_d) | |
cash_flow_list.append(cash_flow) | |
cash_flow_discounted = cash_flow/((1 + discount_rate_d)**year) | |
cash_flow_discounted_list.append(cash_flow_discounted) | |
# print("Year " + str(year + latest_year) + ": $" + str(cash_flow_discounted)) ## Print out the projected discounted cash flows | |
# Store all forecasted cash flows in dataframe | |
forecast_cash_flows_df = pd.DataFrame.from_dict({'Year': year_list, 'Cash Flow': cash_flow_list, 'Discounted Cash Flow': cash_flow_discounted_list}) | |
forecast_cash_flows_df = forecast_cash_flows_df.set_index('Year') | |
# 3. Calculate the Terminal Value after Year 10 (Discounted to Present Value) | |
# These are All Future Cash Flows Summed Up | |
# Assuming the Company will Grow at a Constant Steady Rate Forever (https://corporatefinanceinstitute.com/resources/financial-modeling/dcf-terminal-value-formula/) | |
# Growth in Perpuity Approach | |
cashflow_10Y = cash_flow_discounted_list[-1] | |
# Formula to Calculate: https://corporatefinanceinstitute.com/resources/financial-modeling/dcf-terminal-value-formula/ | |
terminal_value = cashflow_10Y*(1+long_term_growth_rate_d)/(discount_rate_d-long_term_growth_rate_d) | |
# 4. Add the Cash Flows and the Terminal Value Up | |
# 5. Then Account for the Cash + Short Term Investments and Subtract Total Debt | |
# 6. Divide by Total Number of Shares Outstanding | |
intrinsic_value = (sum(cash_flow_discounted_list) + terminal_value - total_debt + cash_and_ST_investments)/shares_outstanding | |
margin_of_safety = (1-current_price/intrinsic_value)*100 | |
return forecast_cash_flows_df, terminal_value, intrinsic_value, margin_of_safety | |
# Discount rate and long term growth rate can change intrinsic value significantly | |
# So we estimate the intrinsic values for the different discount rates and long term growth rates below and store them in a DataFrame | |
def calculate_multiple_intrinsic_values(latest_year, cash_flow, total_debt, cash_and_ST_investments, | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, | |
shares_outstanding, discount_rate, current_price): | |
discount_rates = [discount_rate-2, discount_rate-1.5, | |
discount_rate-1, discount_rate-0.5, | |
discount_rate, | |
discount_rate+0.5, discount_rate+1, | |
discount_rate+1.5, discount_rate+2] | |
long_term_growth_rates = [long_term_growth_rate-1, long_term_growth_rate-0.5, | |
long_term_growth_rate, | |
long_term_growth_rate+0.5, long_term_growth_rate+1] | |
intrinsic_values = {} | |
for dr in discount_rates: | |
intrinsic_values[dr] = {} | |
for lr in long_term_growth_rates: | |
_, _, intrinsic_value, _ = calculate_intrinsic_value(latest_year, cash_flow, total_debt, cash_and_ST_investments, | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, lr, | |
shares_outstanding, dr, current_price) | |
intrinsic_values[dr][lr] = intrinsic_value | |
df_intrinsic_values = pd.DataFrame(intrinsic_values).T | |
df_intrinsic_values.index.name = 'Discount Rates' | |
df_intrinsic_values = df_intrinsic_values.reset_index() | |
return df_intrinsic_values | |
# Plot forecasted cash flows from years 1 to 10, as well as the discounted cash flows | |
def plot_forecasted_cash_flows(ticker, forecast_cash_flows_df): | |
fig_cash_forecast = px.bar(forecast_cash_flows_df, barmode='group', title=ticker + ' Projected Free Cash Flows') | |
fig_cash_forecast.update_xaxes(type='category', tickangle=270) | |
fig_cash_forecast.update_xaxes(tickangle=270, title='Forecasted Year') | |
fig_cash_forecast.update_yaxes(title='Free Cash Flows') | |
# fig_cash_forecast.show() | |
return fig_cash_forecast | |
# chain all the steps from the functions above together | |
def run_all_steps(ticker): | |
ticker = ticker.upper() # make sure ticker is caps | |
q_cash_flow_statement, cash_flow_statement, final_cash_flow_statement, q_balance_statement, latest_year = get_financial_statements(ticker) | |
fig_cash_flow = plot_cash_flow(ticker, cash_flow_statement) | |
cash_flow, total_debt, cash_and_ST_investments = get_statements_data(final_cash_flow_statement, q_balance_statement) | |
finviz_dict = get_finviz_data(ticker) | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, beta, shares_outstanding, current_price = parse_finviz_dict(finviz_dict) | |
discount_rate = estimate_discount_rate(beta) | |
forecast_cash_flows_df, terminal_value, intrinsic_value, margin_of_safety = calculate_intrinsic_value(latest_year, cash_flow, total_debt, cash_and_ST_investments, | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, | |
shares_outstanding, discount_rate, current_price) | |
fig_cash_forecast = plot_forecasted_cash_flows(ticker, forecast_cash_flows_df) | |
df_intrinsic_values = calculate_multiple_intrinsic_values(latest_year, cash_flow, total_debt, cash_and_ST_investments, | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, | |
shares_outstanding, discount_rate, current_price) | |
return q_cash_flow_statement.reset_index(), final_cash_flow_statement.reset_index(), q_balance_statement.reset_index(), fig_cash_flow, \ | |
str(EPS_growth_5Y) + '%', str(EPS_growth_6Y_to_10Y) + '%', str(long_term_growth_rate) + '%', \ | |
beta, shares_outstanding, current_price, \ | |
str(discount_rate) + '%', forecast_cash_flows_df.reset_index(), terminal_value, intrinsic_value, fig_cash_forecast, str(margin_of_safety) + '%', df_intrinsic_values | |
# Gradio App and UI | |
with gr.Blocks() as app: | |
with gr.Row(): | |
gr.HTML("<h1>Bohmian's Stock Intrinsic Value Calculator</h1>") | |
with gr.Row(): | |
ticker = gr.Textbox("AAPL", label='Enter stock ticker to calculate its intrinsic value e.g. "AAPL"') | |
btn = gr.Button("Calculate Intrinsic Value") | |
# Show intrinsic value calculation results | |
with gr.Row(): | |
gr.HTML("<h2>Calculated Intrinsic Value</h2>") | |
with gr.Row(): | |
intrinsic_value = gr.Text(label="Intrinsic Value (if this value is negative, it means current cash flow may be negative and this model WOULD NOT WORK, scroll down to check)") | |
current_price = gr.Text(label="Actual Stock Price") | |
margin_of_safety = gr.Text(label="Margin of Safety") | |
# Discount rate and long term growth rate can change intrinsic value significantly, so we show all of the estimates here | |
with gr.Row(): | |
gr.HTML("<h2>Intrinsic Values with Different Discount Rates (Each Row) Long Term Growth Rates (Each Column)</h2>") | |
with gr.Row(): | |
df_intrinsic_values = gr.DataFrame(label="Intrinsic Values with Different Discount Rates/Long Term Growth Rates") | |
# Show metrics obtained and estimated from FinViz website that were essential for calculations | |
with gr.Row(): | |
gr.HTML("<h2>Metrics Obtained (and Estimated) from FinViz Website</h2>") | |
with gr.Row(): | |
gr.HTML("<h3>https://finviz.com/</h3>") | |
with gr.Row(): | |
EPS_growth_5Y = gr.Text(label="EPS Next 5Y (estimated EPS growth for next 5 years)") | |
EPS_growth_6Y_to_10Y = gr.Text(label="EPS growth for 6th to 10th year (estimated as half of 5Y rate)") | |
long_term_growth_rate = gr.Text(label="Long Term Growth Rate (estimated as the above or 3%, whichever is lower)") | |
with gr.Row(): | |
beta = gr.Text(label="Beta (measures volatility of stock)") | |
discount_rate = gr.Text(label="Discount Rate (estimated from beta)") | |
shares_outstanding = gr.Text(label="Shares Outstanding") | |
# Show detailed actual historical financial statements | |
with gr.Row(): | |
gr.HTML("<h2>Actual Historical Financial Statements Data from Financial Modelling Prep API</h2>") | |
with gr.Row(): | |
gr.HTML("<h3>https://site.financialmodelingprep.com/developer</h3>") | |
with gr.Row(): | |
gr.HTML("<h3>IMPORTANT NOTE: DCF model works best only if the free cash flows are POSITIVE, STABLE and STEADILY INCREASING. Check if this is the case.</h3>") | |
with gr.Row(): | |
fig_cash_flow = gr.Plot(label="Historical Cash Flows") | |
with gr.Row(): | |
q_cash_flow_statement = gr.DataFrame(label="Last 4 Quarterly Cash Flow Statements") | |
with gr.Row(): | |
final_cash_flow_statement = gr.DataFrame(label="TTM + Annual Cash Flow Statements") | |
with gr.Row(): | |
q_balance_statement = gr.DataFrame(label="Quarterly Balance Statements") | |
# Show forecasted cash flows and terminal value | |
with gr.Row(): | |
gr.HTML("<h2>Forecasted Cash Flows for Next 10 Years</h2>") | |
with gr.Row(): | |
fig_cash_forecast = gr.Plot(label="Forecasted Cash Flows") | |
with gr.Row(): | |
forecast_cash_flows_df = gr.DataFrame(label="Forecasted Cash Flows") | |
with gr.Row(): | |
terminal_value = gr.Text(label="Terminal Value (after 10th year)") | |
btn.click(fn=run_all_steps, inputs=[ticker], | |
outputs=[q_cash_flow_statement, final_cash_flow_statement, q_balance_statement, fig_cash_flow, \ | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, beta, shares_outstanding, current_price, \ | |
discount_rate, forecast_cash_flows_df, terminal_value, intrinsic_value, fig_cash_forecast, margin_of_safety, df_intrinsic_values]) | |
ticker.submit(fn=run_all_steps, inputs=[ticker], | |
outputs=[q_cash_flow_statement, final_cash_flow_statement, q_balance_statement, fig_cash_flow, \ | |
EPS_growth_5Y, EPS_growth_6Y_to_10Y, long_term_growth_rate, beta, shares_outstanding, current_price, \ | |
discount_rate, forecast_cash_flows_df, terminal_value, intrinsic_value, fig_cash_forecast, margin_of_safety, df_intrinsic_values]) | |
app.launch() |