In this post I will show a simple stock dashboard with two different data sources. In my previous posts about stock data collection and analysis I wrote about how I sync my stocks to a local parquet file-based database PyStore (Swedish stock market data – pt. 1) or to a PostgreSQL TimeScaleDB database running wherever you want (Swedish stock market data – pt. 2).

This stock dashboard will mainly be a stepping stone for something more advanced. It will mainly be a plot of the candlestick chart for a selected stock, together with a few technical indicators (Bollinger bands and MACD). I made a short gif animation showing the basic operation of the dashboard that gets the data directly from Investing.com through the investpy Python module. I also set up a deployment on Heroku so you can test it before download: https://simple-stock-dash.herokuapp.com/

Gif animation showing operation of stock and crypto dashboard plotting candlestick charts.
Plotly dash dashboard for plotting candlestick graphs for any stock or crypto found on Investing.com. As you can see it is a bit sluggish in performance.

The dashboard is powered by Plotly‘s Dash module, and hence most of the code will be various Dash calls. First we start with the imports, and here I will go through the dashboard that does not require any locally run database populated with data. There is a simple introduction and tutorial on Dash here, I would recommend that before diving into this walk-through. The full script for the standalone dashboard (using investpy to get quotes) is here: https://github.com/vilhelmp/stock_dash_investpy

Imports

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output

import plotly.graph_objects as go
import datetime as dt
import pandas as pd
import numpy as np
import investpy

Nothing strange, the first block is just a bunch of imports from the Dash library. The second part is plotly for our graph needs, and then various libraries needed for data wrangling. Lastly we import investpy.

Getting stock and crypto data

First we define functions to get the stock data and then we define similar functions to get the crypto data. The difference is that crypto does not really belong in a specific country or market, like stocks.

Stock data

Since it is nice to have a drop down list with available countries we need to first get a list of all countries where investpy can get stock data. This is a simple call.

countries_list = investpy.get_stock_countries()
countries_list.sort()

We then define two functions, one to get a list of stocks given a country, and another to get historic data of a specific stock (as a Pandas DataFrame). To get the list of stocks in a given country, we use the investpy function investpy.get_stocks, and to get the historical data the investpy.get_stock_historical_data function. Do check out the links to the documentation for more info about each function.

def get_tickers(country):
    # function to get a dictionary of all tickers/stocks available
    # in a specific country
    try:
        tickers_df = investpy.get_stocks(country=country.lower())
    except:
        tickers_df = pd.DataFrame(data=dict(name=['No results'], symbol=['No results']) )
    # extract names/labels and symbols
    tickers_df.loc[:, 'name'] = tickers_df.loc[:, 'name'].apply(lambda x: x.strip('u200b') )
    labels= [', '.join(i) for i in tickers_df.loc[:, ['name','symbol']].values]
    values = tickers_df.loc[:, ['symbol']].values.T[0]
    tickers_dict = [{'label':i, 'value':j} for i,j in zip(labels, values)]
    return tickers_dict

def get_df_ticker(ticker, country, age='2Y'):
    # function to get ticker OLHC prices from name and country
    # by default 2 years back from today
    if 'Y' in age:
        age = float(age.strip('Y'))
        date_since = dt.date.today() - dt.timedelta(days=age*365.25)
    elif 'M' in age:
        date_since = dt.date.today() - dt.timedelta(days=age*31)
    date_since = date_since.strftime('%d/%m/%Y')
    date_to = dt.date.today().strftime('%d/%m/%Y')
    try:
        df = investpy.get_stock_historical_data(stock=ticker, 
                                            country=country, 
                                            from_date=date_since,
                                            to_date=date_to, 
                                            as_json=False, 
                                            order='ascending')
        df.reset_index(inplace=True)
        df.columns = [i.lower() for i in df.columns]
    except:
        # couldn't retrieve data!!
        raise Exception(f"Couldn\'t get data for {ticker} in {country}")
        df = pd.DataFrame([])
    return df

Crypto data

We do the same thing for cryptos, using the the function investpy.get_cryptos and investpy.get_crypto_historical_data. This could very likely be done in a better and programmatically more correct way, e.g. define the repeat processing steps in separate functions. However I developed this in haste and just wanted something quickly up and running.

def get_cryptos():
    # function to get dictionary of crypto available at Investing 
    try:
        tickers_df = investpy.get_cryptos()
    except:
        tickers_df = pd.DataFrame(data=dict(name=['No results'], symbol=['No results']) )
    # extract names and symbols
    tickers_df.loc[:, 'name'] = tickers_df.loc[:, 'name'].apply(lambda x: x.strip('u200b') )
    
    labels= [', '.join(i) for i in tickers_df.loc[:, ['name','symbol']].values]
    values = tickers_df.loc[:, ['symbol']].values.T[0]
    tickers_dict = [{'label':i, 'value':j.split(',')[0]} for i,j in zip(labels, labels)]
    return tickers_dict

def get_df_crypto(crypto, age='2Y'):
    # function to get crypto OLHC prices from name
    # by default 2 years back in time
    if 'Y' in age:
        age = float(age.strip('Y'))
        date_since = dt.date.today() - dt.timedelta(days=age*365.25)
    elif 'M' in age:
        date_since = dt.date.today() - dt.timedelta(days=age*31)
    date_since = date_since.strftime('%d/%m/%Y')
    date_to = dt.date.today().strftime('%d/%m/%Y')
    try:
        df = investpy.get_crypto_historical_data(crypto=crypto, 
                                            from_date=date_since,
                                            to_date=date_to, 
                                            as_json=False, 
                                            order='ascending')
        df.reset_index(inplace=True)
        df.columns = [i.lower() for i in df.columns]
    except:
        # couldn't retrieve data!!
        raise Exception(f"Couldn\'t get data for {crypto}")
        df = pd.DataFrame([])
    
    return df

Technical analysis indicators

Now lets define some technical analysis indicators to plot into our dashboard. Specifically we are going to plot the moving average (MA), and moving average convergence/divergence (MACD) and exponential moving average (EMA), and finally the Bollinger Bands (which is basically the +/-3-5 sigma deviation from the mean). Some of these were defined correct in the script this is based on (find it here). I had to change/fix some of it, for example the MACD calculation, for everything to work properly.

# MA
def movingaverage(interval, window_size=10):
    window = np.ones(int(window_size))/float(window_size)
    return np.convolve(interval, window, 'same')

# MACD/EMA
def moving_average_convergence(price, nslow=26, nfast=12):
    emaslow = price.ewm(span=nslow, min_periods=1).mean()
    emafast = price.ewm(span=nfast, min_periods=1).mean()
    result = pd.DataFrame({'MACD': emafast-emaslow, 'emaSlw': emaslow, 'emaFst': emafast})
    return result

# BB (bollinger bands)
def bbands(price, window_size=10, num_of_std=5):
    rolling_mean = price.rolling(window=window_size).mean()
    rolling_std  = price.rolling(window=window_size).std()
    upper_band = rolling_mean + (rolling_std*num_of_std)
    lower_band = rolling_mean - (rolling_std*num_of_std)
    return rolling_mean, upper_band, lower_band

Define the dashboard layout

As mentioned this layout is based on this script, at least the base layout is heavily based on it. I built more things into it and have a different backend for reading in data, and also I have a tab with cryptos. This is just a hobby project for me, as I want to learn more about interactive visualizations with real data.

General graph elements

Anyway, the layout is pretty straight forward. First we define the elements of the graph, with basically 2 main subplots. If you look closely, yaxis2 and yaxis3 are behind yaxis. This is because all subplots are shown in the rangeselector that is defined. Thus the whole graph got very messy if I did not do it this way. After this we will define the specific layout of our dashboard.

INCREASING_COLOR = '#17BECF'
DECREASING_COLOR = '#7F7F7F'


layout=dict()

fig = dict(layout=layout )


fig['layout'] = dict()
fig['layout']['plot_bgcolor'] = 'rgb(250, 250, 250)'
fig['layout']['xaxis'] = dict( rangeselector = dict( visible = True ) )

# bottom to top
fig['layout']['yaxis'] = dict( domain = [0.01, 0.19], showticklabels = False ) 
fig['layout']['yaxis2'] = dict( domain = [0, 0.2], showticklabels = False)
fig['layout']['yaxis3'] = dict( domain = [0, 0.2], showticklabels = False, anchor="x", overlaying="y2", side="right")
fig['layout']['yaxis4'] = dict( domain = [0.2, 0.8] )

fig['layout']['legend'] = dict( orientation = 'h', y=0.9, x=0.3, yanchor='bottom' )
fig['layout']['margin'] = dict( t=40, b=40, r=40, l=40 )


rangeselector=dict(
    visible = True,
    x = 0, y = 0.9,
    bgcolor = 'rgba(150, 200, 250, 0.4)',
    font = dict( size = 13 ),
    buttons=list([
        dict(count=1,
             label='reset',
             step='all'),
        dict(count=1,
             label='1yr',
             step='year',
             stepmode='backward'),
        dict(count=3,
            label='3 mo',
            step='month',
            stepmode='backward'),
        dict(count=1,
            label='1 mo',
            step='month',
            stepmode='backward'),
        dict(step='all')
    ]),)
    
fig['layout']['xaxis']['rangeselector'] = rangeselector

Dashboard layout

Here we will define the dashboard layout. Firstly, we load a common external stylesheet definition (from https://codepen.io/chriddyp/pen/bWLwgP), and the default app-creation. Secondly, we want two tabs, one for stocks and one for crypto, were each tab has its own layout. To get each layout we define callbacks, this is one way to achieve tabs, as per this docs page. When we click a tab the callback is called and the layout drawn.

Starting with the stocks tab, we want two dropdown selectors, one for country, and one for stock (available options changes given a specific country). Here their ids are country-dropdown and tickers-dropdown. I set the stock ticker selector to be able to select multiple stocks, but never implemented anything for plotting the additional selections (basically it takes the first selection for plotting).

For the crypto tab, it is similar, but only one multi-selection dropdown, cryptos-dropdown.

In both layouts, we define a graph with dcc.Graph(id='candlestick-graphic', style={"height": "600px"}), this is basically a handle which is referenced in the callback for plotting.

# Import some nice stylesheet
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = dash.Dash(__name__, external_stylesheets=external_stylesheets,
               )
server = app.server

# When you have tabs, you need this setting to not get a constant 
# exception raised
app.config['suppress_callback_exceptions'] = True

# DEFINE LAYOUT 

app.layout = html.Div([
    dcc.Tabs(id='tabs', value='tab-stocks', children=[
        dcc.Tab(label='Stocks', value='tab-stocks'),
        dcc.Tab(label='Cryptos', value='tab-cryptos'),
    ]),
    html.Div(id='tabs-content')
])

@app.callback(Output('tabs-content', 'children'),
              Input('tabs', 'value'))
def render_content(tab):
    ##############################################################################
    # Tab 1 : Stocks
    if tab == 'tab-stocks':
        return html.Div([
        html.Label('Choose which country for stocks.'),
        dcc.Dropdown(
            options=[{'label': k.capitalize(), 'value': k} for k in countries_list],
            id='country-dropdown',
            #value='Sweden',
            multi=False
        ),

        html.Label('Choose stocks to plot'),
        dcc.Dropdown(
            options=get_tickers('sweden'),
            id='tickers-dropdown',
            #value=[''],
            multi=True
        ),

        html.Hr(),

        html.Div(id='display-selected-values'),

        html.Label('Plot of selected stock(s)'),
        dcc.Graph(id='candlestick-graphic', style={"height": "600px"}),
    ])

    ##############################################################################
    # Tab 2 : Cryptocurrencies
    elif tab == 'tab-cryptos':
        return html.Div([
        html.Label('Choose crypto to plot'),
        dcc.Dropdown(
            options=get_cryptos(),
            id='cryptos-dropdown',
            #value=[''],
            multi=True
        ),

        html.Hr(),

        html.Label('Plot of selected crypto(s)'),
        dcc.Graph(id='candlestick-crypto-graphic'),
        ])
    

Callbacks

I admit that one could probably spend some more time on this and reuse some of the callbacks, and/or define some functions outside the callbacks to make them clearer.

Callbacks: stocks

Now we define the callbacks for the stocks tab. This part is pretty self explanatory, first the callbacks for the drop down selectors, then the graph. In the graph callback make sure to keep track of the various axis (yaxis, yaxis2, yaxis3, and yaxis4). Plotly has a graph type called candlestick, see here for reference. We then add the technical analysis indicators to the correct yaxis. The comments are pretty explanatory.

@app.callback(
    [Output('tickers-dropdown', 'options'),
     Output('tickers-dropdown', 'value')],
    [Input('country-dropdown', 'value')])
def set_ticks_options(selected_country):
    return [get_tickers(selected_country),'']

@app.callback(
    Output('display-selected-values', 'children'),
    [Input('country-dropdown', 'value'),
     Input('tickers-dropdown', 'value')])
def set_display_children(selected_country, selected_tick):    
    if selected_tick:
        # split the selected_tick label and get the symbol
        selected_tick = selected_tick[0]
        return f"{selected_tick} is a stock in {selected_country}",
    else:
        return 'No stocks selected.'

@app.callback(
    Output('candlestick-graphic', 'figure'),
    [Input('country-dropdown', 'value'),
     Input('tickers-dropdown', 'value'),
    ])
def update_graph(selected_country, selected_tick):
    #dff = df[df['Year'] == year_value]
    if not selected_tick:
        return {}
    #ticker = ticker_name[0]
    # split the selected_tick label and get the symbol
    selected_tick = selected_tick[0]
    df = get_df_ticker(selected_tick, selected_country)
    
    #define data
    data = [ dict(
        type = 'candlestick',
        open = df.open,
        high = df.high,
        low = df.low,
        close = df.close,
        x = df.date,
        yaxis = 'y4',
        name = str(selected_tick),
        increasing = dict( line = dict( color = INCREASING_COLOR ) ),
        decreasing = dict( line = dict( color = DECREASING_COLOR ) ),
    ) ]
    # overwrite data, whatever it is
    fig['data'] = data
    
    # Calculate and plot moving averages
    # calculate MA
    mv_y = movingaverage(df.close)
    mv_x = list(df.date)

    # Clip the ends
    mv_x = mv_x[5:-5]
    mv_y = mv_y[5:-5]

    fig['data'].append( dict( x=mv_x, y=mv_y, type='scatter', mode='lines', 
                             line = dict( width = 1 ),
                             marker = dict( color = '#E377C2' ),
                             yaxis = 'y4', name='Moving Average' ) )

    # Define OLHC candlestick colors
    colors = []

    for i in range(len(df.close)):
        if i != 0:
            if df.close[i] > df.close[i-1]:
                colors.append(INCREASING_COLOR)
            else:
                colors.append(DECREASING_COLOR)
        else:
            colors.append(DECREASING_COLOR)


    # Plot the volume
    fig['data'].append( dict( x=df.date, y=df.volume,                         
                             marker=dict( color=colors ),
                             type='bar', yaxis='y', name='Volume' ) )

    # Calculate and plot MACD graph
    macd_y = moving_average_convergence(df.close)
    macd_x = list(df.date)

    # Clip the ends
    macd_x = macd_x[5:-5]
    macd_y = macd_y[5:-5]

    fig['data'].append( dict( x=macd_x, y=macd_y.MACD,                         
                             marker=dict( color=colors ),
                             type='bar', yaxis='y2', name='MACD' ) )

    
    fig['data'].append( dict( x=macd_x, y=macd_y.emaSlw, type='scatter', yaxis='y3', 
                             line = dict( width = 2 ),
                             marker=dict(color='indianred'), hoverinfo='none', 
                             legendgroup='MACD', name='MACD slow') )

    fig['data'].append( dict( x=macd_x, y=macd_y.emaFst, type='scatter', yaxis='y3', 
                             line = dict( width = 2 ),
                             marker=dict(color='cornflowerblue'), hoverinfo='none', 
                             legendgroup='MACD', name='MACD fast') )
    
    # Calculate and plot the bollinger bands
    bb_avg, bb_upper, bb_lower = bbands(df.close)

    fig['data'].append( dict( x=df.date, y=bb_upper, type='scatter', yaxis='y4', 
                             line = dict( width = 1 ),
                             marker=dict(color='#ccc'), hoverinfo='none', 
                             legendgroup='Bollinger Bands', name='Bollinger Bands') )

    fig['data'].append( dict( x=df.date, y=bb_lower, type='scatter', yaxis='y4',
                             line = dict( width = 1 ),
                             marker=dict(color='#ccc'), hoverinfo='none',
                             legendgroup='Bollinger Bands', showlegend=False ) )

    return go.Figure(fig)

Callbacks: crypto

We then have the callbacks for crypto, which is a bit simpler. It only has one drop down selector, and only moving average, volume and bollinger bands.

@app.callback(
    Output('candlestick-crypto-graphic', 'figure'),
    [Input('cryptos-dropdown', 'value'),
    ])
def update_graph_crypto(selected_crypto):
    #dff = df[df['Year'] == year_value]
    if not selected_crypto:
        return {}
    #ticker = ticker_name[0]
    # split the selected_tick label and get the symbol
    selected_crypto = selected_crypto[0]
    df = get_df_crypto(selected_crypto)
    
    #define data
    data = [ dict(
        type = 'candlestick',
        open = df.open,
        high = df.high,
        low = df.low,
        close = df.close,
        x = df.date,
        yaxis = 'y4',
        name = str(selected_crypto),
        increasing = dict( line = dict( color = INCREASING_COLOR ) ),
        decreasing = dict( line = dict( color = DECREASING_COLOR ) ),
    ) ]
    # overwrite data, whatever it is
    
    fig['data'] = data
    
    # calculate MA
    mv_y = movingaverage(df.close)
    mv_x = list(df.date)

    # Clip the ends
    mv_x = mv_x[5:-5]
    mv_y = mv_y[5:-5]

    fig['data'].append( dict( x=mv_x, y=mv_y, type='scatter', mode='lines', 
                             line = dict( width = 1 ),
                             marker = dict( color = '#E377C2' ),
                             yaxis = 'y4', name='Moving Average' ) )


    colors = []

    for i in range(len(df.close)):
        if i != 0:
            if df.close[i] > df.close[i-1]:
                colors.append(INCREASING_COLOR)
            else:
                colors.append(DECREASING_COLOR)
        else:
            colors.append(DECREASING_COLOR)



    fig['data'].append( dict( x=df.date, y=df.volume,                         
                             marker=dict( color=colors ),
                             type='bar', yaxis='y2', name='Volume' ) )


    bb_avg, bb_upper, bb_lower = bbands(df.close)

    fig['data'].append( dict( x=df.date, y=bb_upper, type='scatter', yaxis='y4', 
                             line = dict( width = 1 ),
                             marker=dict(color='#ccc'), hoverinfo='none', 
                             legendgroup='Bollinger Bands', name='Bollinger Bands') )

    fig['data'].append( dict( x=df.date, y=bb_lower, type='scatter', yaxis='y4',
                             line = dict( width = 1 ),
                             marker=dict(color='#ccc'), hoverinfo='none',
                             legendgroup='Bollinger Bands', showlegend=False ) )


    return go.Figure(fig)

Finishing up

Lastly, we just need to wrap the app up, if you want to develop it further you change to debug=True so you get debug messages.

if __name__ == '__main__':
    app.run_server(debug=False,
                  )

We can now easily launch the app locally with python app.py, where app.py is the name of this script file. Of course you need dash, plotly, numpy, pandas and investpy installed in your python environment where you run this. We can then view the stock and crypto dashboard from the browser.