Developing an Interactive Dashboard for Value Investment with Python, Dash and Pandas (Version 1)
By: Lester So
Tags: Investing, Dashboard, Python, Dash, Plotly

利用Python, Dash和Pandas 建立互動式價值投資工具(第一版)

(The full code in .ipynb format has been uploaded to Github)

Link

Version 2: https://medium.com/analytics-vidhya/developing-an-interactive-dashboard-for-value-investment-with-python-dash-and-pandas-version-2-2c2616542b5c

1. Motivation

In a society of capitalism, earning salary without extra income cannot sustain one’s daily life properly for a long time. The monetary need will be increasingly demanding after finishing college study. In order to have a long-term passive income, value investment can be a good choice. Value investment of stocks with fundamental analysis neglects the short- and medium-term fluctuation in the price of a stock. Instead, it focuses on the fundamental aspects of the enterprises and asks a core question: Is the company doing well and will it do well in the future? To answer this question, typically one has to collect a lot of financial figures from the companies’ balance sheets, cashflow statements and income statements. He/she also has to do a series of subsequent calculations to calculate the intrinsic value of the company. Collecting the figures and calculation the intrinsic value one-by-one for each target company can be time-consuming. Therefore, this small project aims at allowing people to do value investment in a convenient way.

2. Goals

The tool to be developed needs to calculate the intrinsic value of a stock and compare it with the current price such that the user can know if it is suitable to buy. Along with this, the tools should be able to display the price trends of the stocks under investigation. Since the user may have several target stocks in mind, the tool should be able to allow he/she to extract and compare financial figures in a convenient and systematic way, like in a table. Moreover, the tool should be interactive to use. The user should be able to change the date range and axis scales of the price trend plot. To better assist the investment decisions, the table should be sortable by the financial figures and the calculated intrinsic value should be able to be optimized by a safety margin.

3. Methods

3.1. Import necessary dependencies

A new environment can be created with

conda create — no-default-packages -n myenv python

if you are using Anaconda, or with

python -m virtualenv venv

in your desired project folder

the following dependencies should be installed with pip or conda

alabaster==0.7.12 astroid==2.3.2 attrs==19.3.0 autopep8==1.4.4 Babel==2.7.0 backcall==0.1.0 beautifulsoup4==4.8.1 bleach==3.1.0 certifi==2019.9.11 cffi==1.13.2 chardet==3.0.4 click==6.7 cloudpickle==1.2.2 colorama==0.4.1 cryptography==2.8 Cython==0.28.2 dash==1.7.0 dash-auth==1.3.2 dash-core-components==1.6.0 dash-html-components==1.0.2 dash-renderer==1.2.2 dash-table==4.5.1 decorator==4.3.0 defusedxml==0.6.0 docutils==0.15.2

At the beginning of the script, the necessary dependencies are imported.

import dash import dash_core_components as dcc import dash_html_components as html import dash_table from dash.dependencies import Input, Output, State import plotly.graph_objs as go import requests import numpy as np import pandas as pd from datetime import datetime

3.2. Get financial figures

As mentioned above, quite a number of financial figures are involved in the fundamental analysis. The most common methods of getting real-time or constantly updated data are web-scraping and API. The former directly gets data from websites by analyzing the content of them in formats like html and json while the latter uses data source provided by other companies, authorities or some good Samaritans. A comparison of web-scraping and API should be done.

Web-scraping is mainly free-of-charge. It is quite attractive for people who want to extract information from the web directly. Another Medium article of value investment uses this method (Value Investing Dashboard with Python Beautiful Soup and Dash Python). I was enlightened by this article and found a book ‘Website Scraping with Python: Using BeautifulSoup and Scrapy’ by Gábor László Hajba. After reading the first three chapters and working with the code inside, I admitted that the learning curve was too steep for someone who wanted to make a tool on-the-fly, like me. Moreover, a typical trick in web-scraping, ‘inspect elements’ in Google Chrome does not always return desirable results. For example, Yahoo Finance does not wrap the data with <table> tags and the data in the cells do not have unique IDs, which both make the content difficult to be scraped.

Yahoo Finance is not using common HTML <table> tags

Wikipedia is using common HTML <table> tags

One more downside of using web-scraping is, this method is vulnerable to any change in the website structure. If one is trying to download a dataset with web-scraping in a one-off way, this is OK. However, it is not a desirable solution for long-term use.

The API approach has other pros and cons. Using it does not require any troublesome scraping. All you need is to find a suitable API service. However, APIs may not be free. Many APIs are using the ‘freemium’ pricing approach. That is, usage below a certain amount of request times is free while the users need to pay for any request exceeding that amount. For example, Yahoo Finance API in RapidAPI has a pricing strategy as follows. Users can have 500 requests per month for free and have 10,000 requests per month at USD 10 per month. Apart from the price issue, APIs do not ensure users with a consistent data format. For example, in the Yahoo Finance API of RapidAPI, the quarterly balance sheets of Microsoft contain the figure or ‘intangible assets’ while those of Apple do not. This can hider the automation of the investment tool.

With intangible assets

Without intangible assets

Originally, my idea was to create a tool for global stock markets. However, I soon found out this was a too ambitious target for a prototype tool. This is because the inconsistency of data can be more serious if global markets are involved. For example, the Hong Kong and Shanghai markets, which are geological closer to where I live, do not have well-developed APIs for value investment at a fair price. The United States at least has EDGAR data for the stock markets. Considering all the mentioned criteria, I decided to use Financial Modeling Prep service to get the data. This service is free and has a consistent data format for each company. Unfortunately, it only includes stocks of the United States. I guess I can only go with this for now.

Not all the data can be collected from the API. I still need to scrape some figures from the websites. For example, I got the GDP growth rate of countries from Wikipedia. This can be done easily because the websites I used only contain the data inside simple HTML <table> tags.

The financial figure list I used comes from a Udemy course The Complete Value Investing Course (Updated 2020). It is a good introduction to value investment. The figures are as follows.

1. Free Cash Flow

2. Number of Shares Outstanding

3. Long-term Growth Rate

4. Current Share Price

5. Stock Beta

6. Risk-Free Rate

7. Market Risk Premium

8. Business Tax Rate

9. Estimated Interest Rate

10. Market Value of Equity

11. Market Value of Debt

12. Total Liabilities

13. Cash & Cash Equivalents

14. GDP Growth Rate

First, a class ‘stock’ is defined for all company stocks. Single company stock can be initiated as an object using the ticker symbol, the desired start date of the price trend, the desired end date of the price trend and the safety margin of the analysis (explained later).

class stock: def __init__(self,symbol,startDate,endDate,margin): self.symbol=symbol self.startDate=startDate self.endDate=endDate self.safetyMargin=margin

The get_source() method is used to store the API links or web-scraping links to the object itself as data.

def get_source(self): #source of priceHistory self.url_priceHistory ='https://financialmodelingprep.com/api/v3/historical-price-full/{}?serietype=line'.format(self.symbol)

#source of figures 1 self.url_cashflowQuarter='https://financialmodelingprep.com/api/v3/financials/cash-flow-statement/{}?period=quarter'.format(self.symbol) self.url_cashflow='https://financialmodelingprep.com/api/v3/financials/cash-flow-statement/{}'.format(self.symbol) #source of figures 2 self.url_enterpriseValue='https://financialmodelingprep.com/api/v3/enterprise-value/{}'.format(self.symbol) #source of figure 3 self.url_financialGrowth= 'https://financialmodelingprep.com/api/v3/financial-statement- growth/{}'.format(self.symbol) ...

return self

The get_price() method is used to get the historical price of the stock from the corresponding link and store it as data of the object.

def get_price(self): #price history self.response_priceHistory = requests.request("GET",self.url_priceHistory).json()['historical'] self.priceHistory=pd.DataFrame(self.response_priceHistory) self.selectedPriceHistory=self.priceHistory[(self.priceHistory['date']>self.startDate)&(self.priceHistory['date']<self.endDate)] return self

The get_data() method is used to get the required financial figures from the links and to calculate the required numbers and store all of them as data of the object. The calculation mainly follows the discounted cashflow method.

def get_data(self): #epsilon=1e-10 #figure 1: TTM FCF self.response_cashflowQuarter=pd.DataFrame(requests.request('GET',self.url_cashflowQuarter).json()['financials']) self.response_cashflow=pd.DataFrame(requests.request('GET',self.url_cashflow).json()['financials']) #if the most recent annual cashflow statement is the mose recent cashflow statement if self.response_cashflow.loc[0,'date']==self.response_cashflowQuarter.loc[0,'date']: self.ttmFCF=float(self.response_cashflow.loc[0,'Free Cash Flow']) #if there are quarterly cashflow statements released after the latest annual cashflow statement else: #use the free cash flow in the most recent annual cashflow statement and add all those from more recently quarterly cashflow statement then minus those from corrseponding quarterly cashflow from the previous year self.offset=self.response_cashflowQuarter.index[self.response_cashflowQuarter['date']==self.response_cashflow.loc[0,'date']].tolist()[0] self.quarters_added=np.array(self.response_cashflowQuarter.loc[0:self.offset-1,'Free Cash Flow']).astype(np.float).sum() self.quarters_dropped=np.array(self.response_cashflowQuarter.loc[4:self.offset+4-1,'Free Cash Flow']).astype(np.float).sum() self.ttmFCF=float(np.array(self.response_cashflow.loc[0,'Free Cash Flow']).astype(np.float)+self.quarters_added-self.quarters_dropped) #Figure 2: Total number of shares outstanding self.sharesOutstanding=float(requests.request('GET',self.url_enterpriseValue).json()['enterpriseValues'][0]['Number of Shares']) #Figure 3: Long term growth rate self.longTermGrowthRate=float(requests.request('GET',self.url_financialGrowth).json()['growth'][0]['5Y Revenue Growth (per Share)']) #Figure 4: Current share price self.currentSharePrice=float(requests.request('GET',self.url_profile).json()['profile']['price'])

... self.df_figures=pd.DataFrame(data={'Symbol':[self.symbol],'Intrinsic Value per Share':[self.intrinsicValuePerShare],'Intrinsic Value per Share with Safety Margin':[self.intrinsicValuePerShareSafe],'1. TTM Free Cash Flow':[self.ttmFCF],'2. Shares Outstanding':[self.sharesOutstanding],'3. Long Term Growth Rate':[self.longTermGrowthRate],'4. Current Share Price':[self.currentSharePrice],'5. Stock Beta':[self.stockBeta],'6. Risk Free Rate':[self.riskFreeRate],'7. Market Risk Premium':[self.riskPremium],'8. Business Tax Rate':[self.taxRate],'9. Estimate Interest Rate':[self.longTermIntRate],'10. Market Value of Equity':[self.marketCap],'11. Market Value of Debt':[self.mvDebt],'12. Total Liabilities':[self.totalLiab],'13. Cash & Cash Equivalents':[self.cce],'14. GDP Growth Rate':[self.gdpGrowthRate]}) return self

3.3. Make a list of dictionaries of stock symbols and company names

This tool involves the United States stock markets Nasdaq and NYSE. The pandas dataframes containing the ticker symbols and company names of the two market are combined together to form a list to be chosen from in the tool.

Combined DataFrame

#Read stock symbols and names #US Nasdaq stocks nsdq = pd.read_csv('data/NASDAQcompanylist.csv') nsdq['Country']='United States' #US NYSE stocks nyse=pd.read_csv('data/NYSEcompanylist.csv') nyse['Country']='United States' #combining dataframes allStocks=pd.concat([nsdq[['Symbol','Name','Country']],nyse[['Symbol','Name','Country']]],axis=0) #allStocks.set_index('Symbol',inplace=True,drop=False) #put the symbols and names into a list of dictionaries options = [] for tic in allStocks['Symbol']: options.append({'label':'{} {}'.format(tic,allStocks[allStocks['Symbol']==tic]['Name'].values[0]), 'value':tic})

3.4. Create the layout of the tool

After taking the Udemy course Interactive Python Dashboards with Plotly and Dash, I found that Dash suits my need for an interactive tool for fundamental analysis. An additional library called ‘dashtable’ is needed for building the interactive table.

Dashboard layout

At the top left corner, there is the title of the dashboard ‘Stock Ticker Dashboard’. Under the title, there is a dropdown component for users to select the stock they would like to investigate. The selection can be multiple. Also, the user can type on the dropdown component to search for the company symbols and names they want. The options included in this dropdown component are defined by the list of dictionaries mentioned in section 3.3. The selected stock symbols are used for the price trend display and the fundamental analysis in the table below. At the right of the dropdown component, there is date range picker for users to determine the date range of the price displayed below. The ‘Update Price’ button next to it is for refreshing the graph below.

In the price trend graph, stock closing prices are displayed in different colours with legends shown at the right side. This graph is interactive. Users can enlarge a certain area, change the axis scales and toggle the display of certain trends etc.

Below the graph, there is the core of the dashboard tool, the fundamental analysis part. Before doing the analysis, users can enter a safety margin in terms of percentage. This safety margin is for deducting the calculated intrinsic value of the stock with a certain percentage such that the analysis has tolerance for some analysis error. The ‘Fundamental Analysis’ button next to the safety margin is for refreshing the interactive table below to display the important figures and analysis results. Users need to compare ‘Intrinsic Value per Share with Safety Margin’ with ‘4. Current Share Price’. If the intrinsic value with a safety margin is higher than the current price, users may consider buying the stock and vice versa.

app.layout = html.Div([ html.H1('Stock Ticker Dashboard'), html.Div([ html.H3('Select stock symbols:', style={'paddingRight':'30px'}), dcc.Dropdown( id='my_ticker_symbol', options=options, value=['AAPL'], multi=True ) ], style={'display':'inline-block', 'verticalAlign':'top', 'width':'30%'}),

...

html.Div([ dash_table.DataTable( id='table', columns=[ {'name':'Symbol','id':'Symbol'}, {'name':'Intrinsic Value per Share with Safety Margin','id':'Intrinsic Value per Share with Safety Margin'}, {'name':'Intrinsic Value per Share','id':'Intrinsic Value per Share'}, {'name':'1. TTM Free Cash Flow','id':'1. TTM Free Cash Flow', "deletable": True, "selectable": True}, {'name':'2. Shares Outstanding','id':'2. Shares Outstanding', "deletable": True, "selectable": True}, {'name':'3. Long Term Growth Rate','id':'3. Long Term Growth Rate', "deletable": True, "selectable": True}, {'name':'4. Current Share Price','id':'4. Current Share Price', "deletable": True, "selectable": True}, {'name':'5. Stock Beta','id':'5. Stock Beta', "deletable": True, "selectable": True}, {'name':'6. Risk Free Rate','id':'6. Risk Free Rate', "deletable": True, "selectable": True}, {'name':'7. Market Risk Premium','id':'7. Market Risk Premium', "deletable": True, "selectable": True}, {'name':'8. Business Tax Rate','id':'8. Business Tax Rate', "deletable": True, "selectable": True}, {'name':'9. Estimate Interest Rate','id':'9. Estimate Interest Rate', "deletable": True, "selectable": True}, {'name':'10. Market Value of Equity','id':'10. Market Value of Equity', "deletable": True, "selectable": True}, {'name':'11. Market Value of Debt','id':'11. Market Value of Debt', "deletable": True, "selectable": True}, {'name':'12. Total Liabilities','id':'12. Total Liabilities', "deletable": True, "selectable": True}, {'name':'13. Cash & Cash Equivalents','id':'13. Cash & Cash Equivalents', "deletable": True, "selectable": True}, {'name':'14. GDP Growth Rate','id':'14. GDP Growth Rate', "deletable": True, "selectable": True} ], data=[], filter_action="native", sort_action="native", row_deletable=True, ) ]) ])

CAUTION: Since the API used has some missing information for some of the stocks, the layout shown above is not an accurate illustration.

3.5. Create callback functions

After deciding the layout of the dashboard, callback functions have to be defined for realizing the features described above. These functions are set up for the dashboard to react to the changes made by users.

The update_graph() function can be triggered by the clicking of ‘Update Price’ button. After being triggered, it passes the stock symbols selected, the start and end dates selected and the safety margin to create new objects for the ‘stock’ class. Then the get_source() and get_price() functions are called to acquire the price trends. The returned figure format is used to replace the original figure in the layout.

@app.callback( Output('my_graph', 'figure'), [Input('price-button', 'n_clicks')], [State('my_ticker_symbol', 'value'), State('my_date_picker', 'start_date'), State('my_date_picker', 'end_date'), State('safety_margin','value')]) def update_graph(n_clicks, stock_ticker, start_date, end_date, margin): #when the 'price-button' is clicked, display the close price data in the graph traces = [] for tic in stock_ticker: equity=stock(tic,start_date[:10],end_date[:10],margin) equity.get_source() equity.get_price() traces.append({'x':equity.selectedPriceHistory['date'],'y': equity.selectedPriceHistory['close'], 'name':tic}) fig = { 'data': traces, 'layout': {'title':', '.join(stock_ticker)+' Closing Prices'} } return fig

The update_table() function can be triggered by the clicking of ‘Fundamental Analysis’ button. After being triggered, it passes the stock symbols selected, the start and end dates selected and the safety margin to create new objects for the ‘stock’ class. Then the get_source() and get_data() functions are called to acquire the financial figures and to calculate the intrinsic values.

@app.callback( Output('table', 'data'), [Input('analysis-button', 'n_clicks')], [State('my_ticker_symbol', 'value'), State('my_date_picker', 'start_date'), State('my_date_picker', 'end_date'), State('safety_margin','value')]) def update_table(n_clicks, stock_ticker, start_date, end_date, margin): #when the 'analysis-button' is pressed, display the fundamental analysis data in the table figure_rows=pd.DataFrame() for tic in stock_ticker: equity=stock(tic,start_date[:10],end_date[:10],margin) equity.get_source() equity.get_data() figure_rows=figure_rows.append(equity.df_figures) return figure_rows.to_dict('records')

4. Future versions

As mentioned above, the API used in this prototype is still not perfect. Some important data is missing or wrong. This can be extremely misleading for the final analysis result. Therefore, to make the best of both automation and manual inputs, the interactive table should be editable. In the future version, the numbers in the table can be changed and the final results can be calculated on-the-fly like what an Excel spreadsheet does. In this way, users can manually adjust the financial figures without leaving the dashboard.

The original ambition of this tool was to involve the global stock markets but it was hardly possible to achieve due to the difficulty of acquiring consistent data worldwide. The workaround can be allowing users to add a fully empty row to input the figures they find online. Although this method is not as attractive as having everything automated, it can still let users get the intrinsic value without doing the calculation.