# 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("

Bohmian's Stock Intrinsic Value Calculator

") 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("

Calculated Intrinsic Value

") 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("

Intrinsic Values with Different Discount Rates (Each Row) Long Term Growth Rates (Each Column)

") 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("

Metrics Obtained (and Estimated) from FinViz Website

") with gr.Row(): gr.HTML("

https://finviz.com/

") 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("

Actual Historical Financial Statements Data from Financial Modelling Prep API

") with gr.Row(): gr.HTML("

https://site.financialmodelingprep.com/developer

") with gr.Row(): gr.HTML("

IMPORTANT NOTE: DCF model works best only if the free cash flows are POSITIVE, STABLE and STEADILY INCREASING. Check if this is the case.

") 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("

Forecasted Cash Flows for Next 10 Years

") 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()