# 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_growth_rate = np.minimum(EPS_growth_6Y_to_10Y/2, 3) # Around long term inflation rate, conservative estimate 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, margin_of_safety # Gradio App and UI with gr.Blocks() as app: with gr.Row(): gr.HTML("