bohmian's picture
edited script
a6b8551
raw
history blame
17.1 kB
# Importing required modules
import pandas as pd
import numpy as np
import numpy as np
import plotly.express as px
# To extract and parse fundamental data like beta and growth estimates from finviz website
import requests
from bs4 import BeautifulSoup as bs
# For parsing financial statements data from financialmodelingprep api
from urllib.request import urlopen
import json
# For Gradio App
import gradio as gr
import os
# uncomment and set API Key in the environment variable below
# or you can choose to set it using any other method you know
#os.environ['FMP_API_KEY'] = "your_api_key"
# read the environment variable to use in API requests later
apiKey = os.environ['FMP_API_KEY']
############################################################################################################
###### GET DATA FROM FINANCIAL MODELING PREP
############################################################################################################
# Financialmodelingprep api url
base_url = "https://financialmodelingprep.com/api/v3/"
def get_jsonparsed_data(url):
response = urlopen(url)
data = response.read().decode("utf-8")
return json.loads(data)
# 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 + '?' + '&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 = []
# 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')
# 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)
# Yay finally
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
# 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)
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) + '%'
# 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")
# 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 above)")
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])
app.launch()