Plotly Dash TA Stock Market App

Photo by Behnam Norouzi on Unsplash

  • The objective of this post is to deploy a Plotly Dash stock market app in Python using the dashboard of user-defined stock prices with popular technical indicators available in TA.
  • Read more the tutorial on plotting stock price chart along with volume, MACD & stochastic.
  • The entire workflow consists of the following 5 steps:
  • Step 1: Select a stock ticker symbol (NVDA) and retrieve stock data frame (df) from yfinance API at an interval of 1m
  • Step 2: Add Moving Averages (5day and 20day) to df and plot live share price
  • Step 3: Add Volume, MACD, and Stochastic Oscillator from TA
  • Step 4: Save our stock chart in HTML form, which means all the interactive features will be retained in the graph
  • Step 5: Implement the Plotly Dash app containing live NVDA stock prices with MA 5-15-50-200, Volume, RSI, and MACD.

Table of Contents

  1. Step 1: Imports & Input Data
  2. Step 2: Add Moving Averages
  3. Step 3: Add TA Technical Indicators
  4. Step 4: HTML Chart Output
  5. Step 5: Implement Plotly Dash App
  6. Summary
  7. Explore More
  8. Appendix: Stock App Functions

Step 1: Imports & Input Data

Let’s set the working directory YOURPATH

import os
os.chdir('YOURPATH')    
os. getcwd()

and import/install the following Python libraries

# Raw Package
import numpy as np
import pandas as pd
from pandas_datareader import data as pdr

# Market Data 
import yfinance as yf

#Graphing/Visualization
import datetime as dt 
import plotly.graph_objs as go 

# Override Yahoo Finance 
yf.pdr_override()

!pip install ta

from datetime import date
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from ta.trend import MACD
from ta.momentum import RSIIndicator
from dash import html, dcc, Dash
from dash.dependencies import Input, Output, State
# Create input field for our desired stock 
stock=input("Enter a stock ticker symbol: ")

# Retrieve stock data frame (df) from yfinance API at an interval of 1m 
df = yf.download(tickers=stock,period='1d',interval='1m')

# add Moving Averages (5day and 20day) to df 
df['MA5'] = df['Close'].rolling(window=5).mean()
df['MA20'] = df['Close'].rolling(window=20).mean()

print(df)
Enter a stock ticker symbol: NVDA
[*********************100%***********************]  1 of 1 completed
                           Open        High         Low       Close  \
Datetime                                                              
2023-11-09 09:30:00  474.670013  478.200012  474.470001  475.755005   
2023-11-09 09:31:00  475.880005  476.399597  474.350006  474.899994   
2023-11-09 09:32:00  474.935913  475.769989  474.690094  475.160004   
2023-11-09 09:33:00  475.179993  476.529999  474.250000  474.950012   
2023-11-09 09:34:00  474.940002  476.179993  474.049988  475.079498   
...                         ...         ...         ...         ...   
2023-11-09 13:04:00  476.597992  477.899994  476.429993  477.899414   
2023-11-09 13:05:00  477.899994  478.189911  476.109985  476.390106   
2023-11-09 13:06:00  476.512604  476.649994  475.610107  475.709991   
2023-11-09 13:07:00  475.660004  476.119995  475.279999  475.474213   
2023-11-09 13:08:21  475.261200  475.261200  475.261200  475.261200   

                      Adj Close   Volume         MA5        MA20  
Datetime                                                          
2023-11-09 09:30:00  475.755005  3106825         NaN         NaN  
2023-11-09 09:31:00  474.899994   530602         NaN         NaN  
2023-11-09 09:32:00  475.160004   278254         NaN         NaN  
2023-11-09 09:33:00  474.950012   486628         NaN         NaN  
2023-11-09 09:34:00  475.079498   362195  475.168903         NaN  
...                         ...      ...         ...         ...  
2023-11-09 13:04:00  477.899414   184727  478.246539  479.946407  
2023-11-09 13:05:00  476.390106   199381  477.357562  479.808162  
2023-11-09 13:06:00  475.709991   213521  476.721240  479.626161  
2023-11-09 13:07:00  475.474213   210938  476.382745  479.422871  
2023-11-09 13:08:21  475.261200        0  476.146985  479.183136  

[215 rows x 8 columns]

Step 2: Add Moving Averages

# Declare plotly figure (go)
fig=go.Figure()

fig.add_trace(go.Candlestick(x=df.index,
                open=df['Open'],
                high=df['High'],
                low=df['Low'],
                close=df['Close'], name = 'market data'))

fig.update_layout(
    title= str(stock)+' Live Share Price:',
    yaxis_title='Stock Price (USD per Shares)')               

fig.update_xaxes(
    rangeslider_visible=True,
    rangeselector=dict(
        buttons=list([
            dict(count=15, label="15m", step="minute", stepmode="backward"),
            dict(count=45, label="45m", step="minute", stepmode="backward"),
            dict(count=1, label="HTD", step="hour", stepmode="todate"),
            dict(count=3, label="3h", step="hour", stepmode="backward"),
            dict(step="all")
        ])
    )
)
# Add 5-day Moving Average Trace
fig.add_trace(go.Scatter(x=df.index, 
                         y=df['MA5'], 
                         opacity=0.7, 
                         line=dict(color='blue', width=2), 
                         name='MA 5'))
# Add 20-day Moving Average Trace
fig.add_trace(go.Scatter(x=df.index, 
                         y=df['MA20'], 
                         opacity=0.7, 
                         line=dict(color='orange', width=2), 
                         name='MA 20'))

fig.show()
NVDA live share price

Step 3: Add TA Technical Indicators

from ta.trend import MACD 
from ta.momentum import StochasticOscillator 
# MACD 
macd = MACD(close=df['Close'], 
            window_slow=26,
            window_fast=12, 
            window_sign=9)
# Stochastic
stoch = StochasticOscillator(high=df['High'],
                             close=df['Close'],
                             low=df['Low'],
                             window=14, 
                             smooth_window=3)
import plotly
from plotly.subplots import make_subplots

# Declare plotly figure (go)
fig=go.Figure()
# add subplot properties when initializing fig variable ***don't forget to import plotly!!!***
fig = plotly.subplots.make_subplots(rows=4, cols=1, shared_xaxes=True, vertical_spacing=0.01, row_heights=[0.5,0.1,0.2,0.2])


#Full code
import numpy as np
import pandas as pd
from pandas_datareader import data as pdr
from ta.trend import MACD
from ta.momentum import StochasticOscillator

# Market Data 
import yfinance as yf

#Graphing/Visualization
import datetime as dt 
import plotly
import plotly.graph_objs as go 

# Override Yahoo Finance 
yf.pdr_override()

# Create input field for our desired stock 
stock=input("Enter a stock ticker symbol: ")

# Retrieve stock data frame (df) from yfinance API at an interval of 1m 
df = yf.download(tickers=stock,period='1d',interval='1m')

df['MA5'] = df['Close'].rolling(window=5).mean()
df['MA20'] = df['Close'].rolling(window=20).mean()

# MACD
macd = MACD(close=df['Close'], 
            window_slow=26,
            window_fast=12, 
            window_sign=9)

# stochastic
stoch = StochasticOscillator(high=df['High'],
                             close=df['Close'],
                             low=df['Low'],
                             window=14, 
                             smooth_window=3)


# Declare plotly figure (go)
fig=go.Figure()

# add subplot properties when initializing fig variable
fig = plotly.subplots.make_subplots(rows=4, cols=1, shared_xaxes=True,
                    vertical_spacing=0.01, 
                    row_heights=[0.5,0.1,0.2,0.2])

fig.add_trace(go.Candlestick(x=df.index,
                open=df['Open'],
                high=df['High'],
                low=df['Low'],
                close=df['Close'], name = 'market data'))
    
fig.add_trace(go.Scatter(x=df.index, 
                         y=df['MA5'], 
                         opacity=0.7, 
                         line=dict(color='blue', width=2), 
                         name='MA 5'))

fig.add_trace(go.Scatter(x=df.index, 
                         y=df['MA20'], 
                         opacity=0.7, 
                         line=dict(color='orange', width=2), 
                         name='MA 20'))

# Plot volume trace on 2nd row
colors = ['green' if row['Open'] - row['Close'] >= 0 
          else 'red' for index, row in df.iterrows()]
fig.add_trace(go.Bar(x=df.index, 
                     y=df['Volume'],
                     marker_color=colors
                    ), row=2, col=1)

# Plot MACD trace on 3rd row
colorsM = ['green' if val >= 0 
          else 'red' for val in macd.macd_diff()]
fig.add_trace(go.Bar(x=df.index, 
                     y=macd.macd_diff(),
                     marker_color=colorsM
                    ), row=3, col=1)
fig.add_trace(go.Scatter(x=df.index,
                         y=macd.macd(),
                         line=dict(color='black', width=2)
                        ), row=3, col=1)
fig.add_trace(go.Scatter(x=df.index,
                         y=macd.macd_signal(),
                         line=dict(color='blue', width=1)
                        ), row=3, col=1)

# Plot stochastics trace on 4th row
fig.add_trace(go.Scatter(x=df.index,
                         y=stoch.stoch(),
                         line=dict(color='black', width=2)
                        ), row=4, col=1)
fig.add_trace(go.Scatter(x=df.index,
                         y=stoch.stoch_signal(),
                         line=dict(color='blue', width=1)
                        ), row=4, col=1)

# update layout by changing the plot size, hiding legends & rangeslider, and removing gaps between dates
fig.update_layout(height=900, width=1200, 
                  showlegend=False, 
                  xaxis_rangeslider_visible=False)
                  

# Make the title dynamic to reflect whichever stock we are analyzing
fig.update_layout(
    title= str(stock)+' Live Share Price:',
    yaxis_title='Stock Price (USD per Shares)') 

# update y-axis label
fig.update_yaxes(title_text="Price", row=1, col=1)
fig.update_yaxes(title_text="Volume", row=2, col=1)
fig.update_yaxes(title_text="MACD", showgrid=False, row=3, col=1)
fig.update_yaxes(title_text="Stoch", row=4, col=1)           

fig.update_xaxes(
    rangeslider_visible=False,
    rangeselector_visible=False,
    rangeselector=dict(
        buttons=list([
            dict(count=15, label="15m", step="minute", stepmode="backward"),
            dict(count=45, label="45m", step="minute", stepmode="backward"),
            dict(count=1, label="HTD", step="hour", stepmode="todate"),
            dict(count=3, label="3h", step="hour", stepmode="backward"),
            dict(step="all")
        ])
    )
)


fig.show()
Enter a stock ticker symbol: NVDA
[*********************100%***********************]  1 of 1 completed

Step 4: HTML Chart Output

NVDA live share price, Volume, MACD, and Stochastic Oscillator.
fig.write_html(r'filename.html')

Step 5: Implement Plotly Dash App

Let’s invoke the Plotly Dash functions in Appendix and run the app

if __name__ == '__main__':
    application.run(debug = False, port = 8080)
* Running on http://127.0.0.1:8080
Press CTRL+C to quit
NVDA price action, MA 5-15-50-200, Volume, RSI, and MACD.

Summary

  • In this post, we have used Plotly to generate interactive visualizations of NVIDIA stock prices and basic technical indicators from TA
  • The Plotly Dash framework has been employed for building web app with the added benefit that no JavaScript is needed.
  • Plotly Dash provides increased interactivity and the ability to manipulate data with modern UI elements like dropdowns, sliders and real-time graphs.
  • Plotly figures are interactive when viewed in a web browser: we can hover over data points, pan and zoom axes, and show and hide traces by clicking or double-clicking on the legend. We have exported them to HTML files which can be opened in a browser.
  • The most significant benefits include our preference for the dashboard layout of all of the charts instead of separate cells of the Jupyter notebook. 
  • By default, Dash apps run on localhost. To share a Dash app, we need to deploy it to a server using Dash Enterprise.

Explore More

Appendix: Stock App Functions

def makeCandlestick(fig, stockDF):
    #sets parameters for subplots
    fig = make_subplots(rows = 4, cols = 1, shared_xaxes = True,
                    vertical_spacing = 0.01,
                    row_heights = [0.6, 0.1, 0.15, 0.15])


    #plots candlestick values using stockDF
    fig.add_trace(go.Candlestick(x = stockDF.index,
                                 open = stockDF['Open'],
                                 high = stockDF['High'],
                                 low = stockDF['Low'],
                                 close = stockDF['Close'],
                                 name = 'Open/Close'))
    
    return fig
def makeMA(fig, stockDF):
    #create moving average values
    stockDF["MA5"] = stockDF["Close"].rolling(window = 5).mean()
    stockDF["MA15"] = stockDF["Close"].rolling(window = 15).mean()
    stockDF["MA50"] = stockDF["Close"].rolling(window = 50).mean()
    stockDF["MA200"] = stockDF["Close"].rolling(window = 200).mean()


    #plots moving average values; the 50-day and 200-day averages
    #are visible by default, and the 5-day and 15-day are accessed via legend
    fig.add_trace(go.Scatter(x = stockDF.index, y = stockDF['MA5'], opacity = 0.4, visible="legendonly",
                        line = dict(color = 'blue', width = 2), name = 'MA 5'))
            
    fig.add_trace(go.Scatter(x = stockDF.index, y = stockDF['MA15'], opacity = 0.7, visible = "legendonly",
                        line = dict(color = 'orangered', width = 2), name = 'MA 15'))

    fig.add_trace(go.Scatter(x = stockDF.index, y = stockDF['MA50'], opacity = 0.7,
                        line = dict(color = 'purple', width = 2), name = 'MA 50'))

    fig.add_trace(go.Scatter(x = stockDF.index, y = stockDF['MA200'], opacity = 0.7,
                        line = dict(color = 'black', width = 2), name = 'MA 200'))

    return fig

def makeVolume(fig, stockDF):
    #sets colors of volume bars
    colors = ['green' if row['Open'] - row['Close'] >= 0
          else 'red' for index, row in stockDF.iterrows()]


    #Plot volume trace
    fig.add_trace(go.Bar(x = stockDF.index,
                         y = stockDF['Volume'],
                         marker_color = colors,
                         showlegend = False,
                         name = "Volume"
                         ), row = 2, col = 1)

    return fig
def makeMACD(fig, stockDF):
    #Create MACD values
    macd = MACD(close = stockDF["Close"],
                window_slow = 26,
                window_fast = 12,
                window_sign = 9)

            
    #Sets color for MACD
    colors = ['green' if val >= 0 
                      else 'red' for val in macd.macd_diff()]


    #Plots MACD values
    fig.add_trace(go.Bar(x = stockDF.index,
                         y = macd.macd_diff(),
                         marker_color = colors,
                         showlegend = False,
                         name = "Histogram"
                         ), row = 4, col = 1)


    fig.add_trace(go.Scatter(x = stockDF.index,
                             y = macd.macd(),
                             line = dict(color = 'red', width = 1),
                             showlegend = False,
                             name = "MACD"
                             ), row = 4, col = 1)


    fig.add_trace(go.Scatter(x = stockDF.index,
                             y = macd.macd_signal(),
                             line = dict(color = 'blue', width = 2),
                             showlegend = False,
                             name = "Signal"
                             ), row = 4, col = 1)


    return fig

def makeRSI(fig, stockDF):
    #Create RSI values
    rsi = RSIIndicator(close = stockDF["Close"],
                       window = 14)


    #Plots RSI values
    fig.add_trace(go.Scatter(x = stockDF.index,
                             y = rsi.rsi(),
                             line = dict(color = 'black', width = 2),
                             showlegend = False,
                             name = "RSI"
                             ), row = 3, col = 1)


    fig.add_trace(go.Scatter(x = stockDF.index,
                             y = [30 for val in range(len(stockDF))],
                             line = dict(color = 'red', width = 1),
                             showlegend = False,
                             name = "Oversold"
                             ), row = 3, col = 1)


    fig.add_trace(go.Scatter(x = stockDF.index,
                             y = [70 for val in range(len(stockDF))],
                             line = dict(color = 'green', width = 1),
                             showlegend = False,
                             name = "Overbought"
                             ), row = 3, col = 1)


    return fig
def makeCurrentPrice(fig, stockDF):
    #Plots the last closing price of stock 
    fig.add_trace(go.Scatter(x = stockDF.index,
              y = [stockDF['Close'].iat[-1] for price in range(len(stockDF))],
              opacity = 0.7, line = dict(color = 'red', width = 2, dash = 'dot'),
              name = "Current Price: " + str(round(stockDF['Close'].iat[-1], 2))))
    

    return fig


def supportLevel(stockDF, index):
    #Finds and returns support levels using fractals;
    #if there are two higher lows on each side of the current stockDF['Low'] value, 
    #return this value
    support = stockDF['Low'][index] < stockDF['Low'][index - 1] and \
              stockDF['Low'][index] < stockDF['Low'][index + 1] and \
              stockDF['Low'][index + 1] < stockDF['Low'][index + 2] and \
              stockDF['Low'][index - 1] < stockDF['Low'][index - 2]

    return support


def resistanceLevel(stockDF, index):
    #Finds and returns resistance levels using fractals;
    #If there are two lower highs on each side of the current stock['High'] value,
    #return this value
    resistance = stockDF['High'][index] > stockDF['High'][index - 1] and \
              stockDF['High'][index] > stockDF['High'][index + 1] and \
              stockDF['High'][index + 1] > stockDF['High'][index + 2] and \
              stockDF['High'][index - 1] > stockDF['High'][index - 2]

    return resistance
def isFarFromLevel(stockDF, level, levels):
    #If a level is found near another level, it returns false;

    ##.88 for longer term .97 for short term
    s = np.mean(stockDF['High'] - (stockDF['Low'] * .89))

    return np.sum([abs(level - x) < s for x in levels]) == 0


def makeLevels(fig, stockDF):
    #Traverses through stockDF and finds key support/resistance levels
    levels = []
    for index in range(2, stockDF.shape[0] - 2):
        if supportLevel(stockDF, index):
            support = stockDF['Low'][index]
            if isFarFromLevel(stockDF, support, levels):
                levels.append((support))
            
        elif resistanceLevel(stockDF, index):
            resistance = stockDF['High'][index]
            if isFarFromLevel(stockDF, resistance, levels):
                levels.append((resistance))

    levels.sort()
    

    #Plots the key levels within levels 
    for i in range(len(levels)):
        fig.add_trace(go.Scatter(x = stockDF.index,
                             y = [levels[i] for val in range(len(stockDF))],
                             line = dict(color = "black"),
                             name = "Sup/Res: " + str(round(levels[i], 2)),
                             hoverinfo = "skip",
                             opacity = 0.3))

    return fig
def findAbsMax(stockDF):
    absMax = 0
    for i in range(len(stockDF)):
        if stockDF["Close"][i] > absMax:
            absMax = stockDF["Close"][i]
        
    return absMax


def findAbsLow(stockDF):
    absLow = 50
    for i in range(len(stockDF)):
        if stockDF["Close"][i] < absLow:
            absLow = stockDF["Close"][i]

    return absLow


def makeFibLevels(fig, stockDF):
    fibRatios = [.236, .382, .5, .618, .786, 1]
    fibLevels = []
    absMax = findAbsMax(stockDF)
    absLow = findAbsLow(stockDF)
    dif = absMax - absLow

    for i in range(len(fibRatios)):
        fibLevels.append(dif * fibRatios[i])
#for prices that are above the last resistance/support line within fibLevels,
    #look to see if there can be any levels drawn using fractals that are also not 
    #too close to the current last support/resistance;
    #We really are just looking for the last resistance level;
    fractal = fibLevels[-1] + (fibLevels[-1] * .17)
    if (fibLevels[-1] < fractal) and (fractal < absMax):
         fibLevels.append(fractal)  
    

    for i in range(len(fibLevels)):
        fig.add_trace(go.Scatter(x = stockDF.index,
                             y = [fibLevels[i] for val in range(len(stockDF))],
                             line = dict(color = "black"),
                             name = "Sup/Res: " + str(round(fibLevels[i], 2)),
                             hoverinfo = "skip",
                             opacity = 0.3))
    
    return fig


def graphLayout(fig, choice):
    #Sets the layout of the graph and legend
    fig.update_layout(title_text = choice + ' Price Action', 
                  title_x = 0.5, 
                  legend_title_text = "Legend Items",
                  dragmode = "pan", 
                  xaxis_rangeslider_visible = False, 
                  hovermode = "x", 
                  legend = dict(bgcolor="#E2E2E2",
                           bordercolor="Black",
                           borderwidth=2)
                               
                 )

    subplotLabels(fig)

    return fig
def subplotLabels(fig):
    #Sets subplot labels
    fig.update_yaxes(title_text = "Price", row = 1, col = 1)
    fig.update_yaxes(title_text = "Volume", row = 2, col = 1)
    fig.update_yaxes(title_text = "RSI", row = 3, col = 1)
    fig.update_yaxes(title_text = "MACD", showgrid = False, row = 4, col = 1)

    return fig


def xAxes(fig):
    #Remove none trading days from dataset and sets behavior for x-axis mouse-hovering
    fig.update_xaxes(rangebreaks = [dict(bounds = ["sat", "mon"])], 
                 autorange = True, 
                 showspikes = True, 
                 spikedash = "dot",
                 spikethickness = 1, 
                 spikemode = "across", 
                 spikecolor = "black")
    
    return fig



fig = go.Figure()
config = dict({'scrollZoom': True})

stockApp = Dash(__name__, meta_tags=[{'name': 'viewport', 
                       'content':'width=device-width, initial-scale=1.0'}])

application = stockApp.server

stockApp.layout = html.Div([
    dcc.Graph(figure = fig, config = config,

              style = {'width': '99vw', 'height': '93vh'},
              id = "stockGraph"
             ),

             html.Div([
                dcc.Input(
                    id = "userInput",
                    type = "text",
                    placeholder = "Ticker Symbol"
                         ),
            
            html.Button("Submit", id = "btnSubmit")]),
                      ],
            )
 @stockApp.callback(
    Output("stockGraph", "figure"),
    Input("btnSubmit", "n_clicks"),
    State("userInput", "value"))

def update_figure(n, tickerChoice):
    #set choice to something if !isPostBack
    if tickerChoice == None:
        tickerChoice = 'AAPL'


    #make stockDF    
    today = date.today()
    stockDF = yf.download(tickerChoice, start = '2020-01-01', end = today )

    #make go Figure object as fig
    fig = go.Figure()

    #make and plot candlestick chart
    fig = makeCandlestick(fig, stockDF)

    #update layout properties
    fig = graphLayout(fig, tickerChoice.upper())

    #updates x-axis parameters
    fig = xAxes(fig)

    #make and plot subplots charts and moving averages
    fig = makeMA(fig, stockDF)
    fig = makeVolume(fig, stockDF)
    fig = makeMACD(fig, stockDF)
    fig = makeRSI(fig, stockDF)

    #make and plot stock's last closing price
    fig = makeCurrentPrice(fig, stockDF)

    #make and plot stock's resistance/support values using fibonacci retracement
    fig = makeFibLevels(fig, stockDF)

    
    return fig        

Go back

Your message has been sent

Warning

One-Time
Monthly
Yearly

Make a one-time donation

Make a monthly donation

Make a yearly donation

Choose an amount

€5.00
€15.00
€100.00
€5.00
€15.00
€100.00
€5.00
€15.00
€100.00

Or enter a custom amount


Your contribution is appreciated.

Your contribution is appreciated.

Your contribution is appreciated.

DonateDonate monthlyDonate yearly

Discover more from Our Blogs

Subscribe to get the latest posts sent to your email.

Leave a comment

Discover more from Our Blogs

Subscribe now to keep reading and get access to the full archive.

Continue reading