Plotly & Dash — Dashboard of stocks from a chosen date of reference
A few days back I had written a note to describe how MultiIndexing can be used to compare quantities of widely different absolute values. In order to demonstrate the concept I used world currencies and plotted them on the same graph for the purpose of comparison. On this note, I want to extend this effort further and use it to compare various stock prices and plot them on a dashboard. The layout will consist of four charts from the chosen date of reference — 1) Line graph — %Return on chosen stocks; 2) Line graph — The closing price of stocks; 3) Histogram — Distribution of %Return; & 4) Coefficient of Variation.
The dashboard so created will be current, i.e will have the latest value of closing prices, and will enable the users to choose multiple stocks on the same layout for the purpose of comparison. The user will be able to use this dashboard on the internet and will be fully compatible with being viewed on the screen of mobile phones. So without wasting any more time let's begin with the process of creating this dashboard.
Stock’s Pricing Data
This the user can pull easily from yahoo finance. The code used for the same is reproduced below.
import yfinance as yf
#Choosing the set of stocks we plan to analyse (user can add more to this dict)
stock_tickers = {"Nokia": "NOK","Apple":"AAPL","Qualcomm": "QCOM","Google": "GOOG","Facebook":"META",
"Netflix": "NFLX","Reliance": "RELIANCE.NS","Nvidia":"NVDA","Amazon": "AMZN",
"Airtel":"BHARTIARTL.BO","MediaTek":"2454.TW","Cisco":"CSCO", "Tesla":"TSLA",
"Microsoft":"MSFT","TSMC":"TSM","Tencent":"0700.HK", "Samsung":"005930.KS", "Broadcom":"AVGO",
"ASML Holding":"ASML", "Oracle":"ORCL", "Alibaba":"BABA", "Salesforce":"CRM", "Texas Inst":"TXN",
"Adobe":"ADBE","SAP":"SAP","AMD":"AMD"}
#Sorting the stocks alphabatically
myKeys = list(stock_tickers.keys())
myKeys.sort()
stock_tickers = {i: stock_tickers[i] for i in myKeys}
#pulling the stock data of chosen stocks and combining them in one dataframe
def download_stocks(tickers):
df_comb=pd.DataFrame()
for ticker in tickers:
try:
stock = yf.Ticker(ticker)
df = stock.history(period="10y")
df["ticker"]=ticker
df_comb=pd.concat([df,df_comb], axis=0)
except:
pass
return df_comb
The output data frame will appear something like this.
Calculating %Returns on Investment
The next step is to calculate the %Return on investments. For this purpose, the following code can be used.
def process_for_df_ret(date_value, tickers, df_comb):
#processing all stock files
# df_comb=pd.read_csv("combined.csv", index_col="Date", parse_dates=True)
df_comb.index=[dt.datetime.strftime(x, "%d-%m-%Y") for x in list(df_comb.index)]
df_comb.index = pd.to_datetime(df_comb.index, dayfirst = True)
returns ={}
for ticker in tickers:
filt = (df_comb["ticker"]==ticker)
temp_df= df_comb[filt]
closest_date_index = temp_df.index.get_loc(date_value, method="nearest")
ref_value_close = temp_df.iloc[closest_date_index,:][3]
temp_df = temp_df.iloc[closest_date_index:,:]
value_df = temp_df["Close"]/ref_value_close
returns.update({ticker: (value_df-1)*100})
df_ret = pd.DataFrame(returns)
df_ret = df_ret.reset_index()
df_ret = df_ret.melt(id_vars=['index'], value_vars=tickers, ignore_index=False)
df_ret.columns =["Date","Ticker","%Returns"]
df_ret = df_ret.dropna()
df_ret = df_ret.set_index("Date")
return df_ret
The concept is similar to what I described in my earlier note on world currencies. For this, we need to identify the data index for the chosen data of reference, and then use that to calculate %Returns. The only point to note here is that the date will be used as an index and needs to be properly formatted so as be palatable to the output “date value” from the bootstrap date picker component. The output data frame will appear something like this.
Calculating Coff of Variations
The coefficient of variation is nothing but the ratio between the standard deviation of the stock from the chosen date of reference and its mean value. The code used for calculating the same is reproduced below.
def process_for_df_cov(date_value, tickers, df_comb):
df_comb.index=[dt.datetime.strftime(x, "%d-%m-%Y") for x in list(df_comb.index)]
df_comb.index = pd.to_datetime(df_comb.index, dayfirst = True)
cov = {}
for ticker in tickers:
filt = (df_comb["ticker"]==ticker)
temp_df= df_comb[filt]
closest_date_index = temp_df.index.get_loc(date_value, method="nearest")
ref_value_close = temp_df.iloc[closest_date_index,:][3]
temp_df = temp_df.iloc[closest_date_index:,:]
value_df = temp_df["Close"]/ref_value_close
sd = temp_df[temp_df["ticker"]==ticker]["Close"].std()
mean = temp_df[temp_df["ticker"]==ticker]["Close"].mean()
cov.update({ticker: round(sd/mean,2)})
df_cov = pd.DataFrame.from_dict(cov, orient='index')
df_cov = df_cov.reset_index()
df_cov.columns = ["Ticker","COV"]
return df_cov
This is simple and uses stock tickers, the combined data frame, and chosen data value as input parameters. The output data frame will appear something like what is shown below.
The Main Program
The main program is a dash application and consists of a “dbc.Container” with four rows. Each of the rows has multiple columns containing the dropdown, datepicker, and the dcc.Graphs. The code is shown below.
#main application
# Customizing the Layout
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
# app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP],
#the meta_tags below is for mobile viewing
meta_tags=[{'name': 'viewport',
'content': 'width=device-width, initial-scale=1.0'}]
)
#The beginning of the main container
app.layout = dbc.Container([
dbc.Row([
dbc.Col(html.H5("Performance of stocks from the date of reference",
className="text-center opacity-100 p-2 m-1 bg-primary text-light fw-bold rounded"),#width=12
xs=12, sm=12, md=12, lg=5, xl=5 #for viewing in different screen size
)
],justify="center"
),
#Dropdown for selecting multiple stocks
dbc.Row([
dbc.Col([
html.P("Choose Stocks of Interest:", style={"textDecoration":"bold"}),
dcc.Dropdown(id='stock-type', clearable=False,
value=["AAPL"],multi=True,
options=[{'label': key, 'value': value} for key, value in
stock_tickers.items()]),
], #width={'size':6},
xs=12, sm=12, md=12, lg=5, xl=5, #for viewing in different screen size
),
#Picking a reference date using the datepicker calendar
dbc.Col([
html.P("Choose a Starting Date: ", style={"textDecoration":"bold"}),
dcc.DatePickerSingle(id='date-picker-single',
min_date_allowed=date(2000,1, 1),
max_date_allowed=date(2023,3, 1),
initial_visible_month= date(2022,1,31),
date=date(2022,1, 31))
], #width ={'size':6}
xs=12, sm=12, md=12, lg=5, xl=5 #for viewing in different screen size
)
], justify="center"
),
#These are the rows hosting the dynamic graphs that will get created based on user selection
dbc.Row([
dbc.Col([dcc.Graph(id="fig1_line", className="shadow-2g",figure={}, style={"pading":10,"border":"ridge"})
],#width ={'size':12}
xs=12, sm=12, md=12, lg=5, xl=5 #for viewing in different screen size
),
dbc.Col([dcc.Graph(id="fig_hist", className="shadow-2g",figure={}, style={"pading":10,"border":"ridge"})
],#width ={'size':12}
xs=12, sm=12, md=12, lg=5, xl=5 #for viewing in different screen size
),
], justify="center"
),
dbc.Row([
dbc.Col([dcc.Graph(id="fig2_line", className="shadow-2g",figure={}, style={"pading":10,"border":"ridge"})
],#width ={'size':12}
xs=12, sm=12, md=12, lg=5, xl=5 #for viewing in different screen size
),
dbc.Col([dcc.Graph(id="fig_bar", className="shadow-2g",figure={}, style={"pading":10,"border":"ridge"})
],#width ={'size':12}
xs=12, sm=12, md=12, lg=5, xl=5 #for viewing in different screen size
),
], justify="center"
),
], fluid=False) #Choose fluid = True for killing the margins on either side of the Dashboard
The main program is fed by a set of callbacks. These are listed below.
# Callback section: connecting the components
# ************************************************************************
# Line chart 1
@app.callback(
Output('fig1_line', 'figure'),
Input(component_id="date-picker-single",component_property="date"),
Input(component_id="stock-type",component_property="value"))
def update_graph(date_value, tickers):
if date_value is not None:
date_object = date.fromisoformat(date_value)
date_string = date_object.strftime('%b %d, %y')
df_comb = download_stocks(tickers)
df_ret = process_for_df_ret(date_value,tickers, df_comb)
fig1_line = px.line(df_ret, x=df_ret.index, y="%Returns", color="Ticker", template='plotly_white',
title="% Returns on Stocks from "+date_string)
fig1_line.update_xaxes(fixedrange=True)
fig1_line.update_yaxes(fixedrange=True)
fig1_line.update_layout(uniformtext_minsize=14, uniformtext_mode='hide',
legend={'x':0,'y':1.05,"orientation":"h"})
fig1_line.update_layout(height=fig_height, width=fig_width,yaxis_title=None, xaxis_title=None)
fig1_line.update_layout(
margin=dict(
l=0,
r=10,
t=50,
b=0,
))
return fig1_line
# Line chart 2
@app.callback(
Output('fig2_line', 'figure'),
Input(component_id="date-picker-single",component_property="date"),
Input(component_id="stock-type",component_property="value"))
def update_graph(date_value, tickers):
if date_value is not None:
date_object = date.fromisoformat(date_value)
date_string = date_object.strftime('%b %d, %y')
df_comb = download_stocks(tickers)
df_stocks = process_for_df_stocks(date_value,tickers, df_comb)
df_close = df_stocks.loc[:,["Close","ticker"]]
df_close = df_close.rename(columns={'ticker': 'Ticker'})
fig2_line = px.line(df_close, x=df_close.index, y="Close", color="Ticker", log_y=True, template='plotly_white',
labels={"Close":"Closing Price","index":"Date"}, title="Closing Price of Stocks from "+date_string)
fig2_line.update_xaxes(fixedrange=True)
fig2_line.update_yaxes(fixedrange=True)
fig2_line.update_layout(uniformtext_minsize=14, uniformtext_mode='hide',legend={'x':0,'y':1.1,"orientation":"h"})
fig2_line.update_layout(height=fig_height, width=fig_width,yaxis_title=None, xaxis_title=None)
fig2_line.update_layout(
margin=dict(
l=0,
r=10,
t=50,
b=0,
))
fig2_line.update_layout(showlegend=False)
return fig2_line
# Histogram
@app.callback(
Output('fig_hist', 'figure'),
Input(component_id="date-picker-single",component_property="date"),
Input(component_id="stock-type",component_property="value"))
def update_graph(date_value, tickers):
if date_value is not None:
date_object = date.fromisoformat(date_value)
date_string = date_object.strftime('%b %d, %y')
df_comb = download_stocks(tickers)
df_ret = process_for_df_ret(date_value,tickers, df_comb)
Tickers = list(set(df_ret["Ticker"]))
fig_hist = make_subplots(rows=len(Tickers), cols=1, shared_xaxes=True)
for i, Ticker in enumerate(Tickers):
filt = df_ret["Ticker"]==Ticker
df_ret_filt = df_ret[filt]
# fig_hist.append_trace(go.Histogram(x=df_ret_filt["%Returns"], texttemplate="%{x}", textfont_size=8), row=i + 1, col=1)
fig_hist.append_trace(go.Histogram(x=df_ret_filt["%Returns"]), row=i + 1, col=1)
fig_hist.update_layout(barmode='group')
# fig_hist.update_yaxes(title_text='Count')
fig_hist.update_layout(height=fig_height, width=fig_width, title_text="Distribution of % Returns from "+date_string, template='plotly_white')
fig_hist.update_layout(showlegend=False)
fig_hist.update_xaxes(fixedrange=True)
fig_hist.update_yaxes(fixedrange=True)
fig_hist.update_traces(textposition="top center", selector=dict(type='scatter'))
fig_hist.update_layout(
margin=dict(
l=0,
r=10,
t=50,
b=0,
))
return fig_hist
# Bar Chart
@app.callback(
Output('fig_bar', 'figure'),
Input(component_id="date-picker-single",component_property="date"),
Input(component_id="stock-type",component_property="value"))
def update_graph(date_value, tickers):
if date_value is not None:
date_object = date.fromisoformat(date_value)
date_string = date_object.strftime('%b %d, %y')
df_comb = download_stocks(tickers)
df_cov = process_for_df_cov(date_value,tickers, df_comb)
fig_bar = px.bar(df_cov, x="Ticker", y="COV", color="Ticker", text_auto=True, template='plotly_white')
fig_bar.update_traces(textfont_size=7, textangle=0, textposition="inside", cliponaxis=False)
fig_bar.update_xaxes(fixedrange=True)
fig_bar.update_yaxes(fixedrange=True)
fig_bar.update_layout(uniformtext_minsize=14, uniformtext_mode='hide',
legend={'x':0,'y':1.05,"orientation":"h"}, title="Coefficient of Var (SD/Mean) from "+date_string),
fig_bar.update_layout(height=fig_height, width=fig_width, yaxis_title=None, xaxis_title=None)
fig_bar.update_layout(showlegend=False)
fig_bar.update_layout(
margin=dict(
l=0,
r=10,
t=50,
b=0,
))
return fig_bar
# Runing the application
if __name__ == '__main__':
app.run_server(debug=True,use_reloader=False)
The Output Dashboard (Desktop Version)
The final output dashboard for the desktop version will appear something like this.
The Output Dashboard (Mobile Version)
The final output dashboard for the mobile version will appear something like this.
Note that the effect of auto adjustment of the dashboard due to the alternation of screen size can be experienced even on the desktop. This the user can do by alternating the size of the web browser manually.
Uploading the Dashboard on the Web
The final step of this process is to upload the dashboard on the web. For that, we need to create a requirement.txt file as described in the following picture.
The content of this file is information to the rendering server as to what set of libraries it needs to load for enabling the application to run in its environment. Then using the drag and drop option, we need to upload the following files in the GitHub repository as shown below.
Then using the HTTPS link of the repository in the GitHub code tab one can render the app on the web for the purpose of general viewing. The link to which I have uploaded this application can be found here. Also, you can visit my GitHub page to get access to the full code. Note while you upload the code on GitHub do not forget to add the following line (server = app.server) just below the main app code line as described below.
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP],
meta_tags=[{'name': 'viewport',
'content': 'width=device-width, initial-scale=1.0'}]
server = app.server
Hope you found this useful. Thanks for reading.
(I am aggregating all the articles on this topic here, for easy discovery and reference.)