Have you ever wanted to look up a stock's performance on a specific date, only to find out the market was closed that day because of a weekend or a holiday? In this tutorial, we will build a program that solves this problem using Python, Plotly and Dash.
Instead of only telling us the price for one date, this program starts a local web server and fetches historical prices from Yahoo Finance using the yfinance module. It takes the ticker symbol and date chosen by the user, opens a window of dates around the target date, handles market closures by showing the nearest trading days, and draws an interactive candlestick chart.
Note:
yfinanceis an unofficial library and this project is for learning purposes only. It is not financial advice.
💼 Project features and result
By the end of this guide, you will be able to build a program that does the following:
- ➔ Historical data extraction: It connects to Yahoo Finance through the
yfinancemodule to fetch the Open, High, Low and Close prices. - ➔ Relevant window: It creates a window of dates (10 days before and 5 days after your target date), which is useful for studying trends.
- ➔ Market closure alerts: It checks whether the stock exchange was open on your target date. If the market was closed, it alerts the user and shows the nearest trading days instead.
- ➔ Runs in different environments: It runs from a normal terminal (you open the dashboard in your browser) and also inline inside Jupyter and Google Colab notebooks.
📦 Step 1: All the imports and dependencies
First, install the libraries by running this line in your terminal (in a Jupyter or Colab notebook, put ! in front of it):
pip install dash pandas plotly yfinance
Then create a new file named app.py and add these imports at the top. They bring in the libraries that manage our layout, data and graph.
import dash
from dash import dcc, html
from dash.dependencies import Input, Output, State
import plotly.graph_objects as go
from datetime import datetime, timedelta
import yfinance as yf
import pandas as pd
Why these modules?
-
yfinance➔ pulls the stock prices. -
dash➔ builds the dashboard and its user interface. -
plotly.graph_objects➔ makes the chart. -
datetimeandtimedelta➔ work with dates. -
pandas➔ structures the data in a more manageable form.
🌐 Step 2: Starting the server
Afterwards, type in the following line:
app = dash.Dash(__name__)
What it does: This single line sets up our app. The __name__ part tells Dash where the root folder of the project is, so it can find assets like styles and images.
✍️ Step 3: Designing the interface
Now we choose how we want the dashboard to look. I have provided my own style as an example, but feel free to change it and style the interface to your liking. Below the code block are explanations of what the components do.
app.layout = html.Div(
style={
'fontFamily': '"Segoe UI", Helvetica, Arial, sans-serif',
'padding': '30px',
'maxWidth': '900px',
'margin': 'auto',
'backgroundColor': '#ffffff'
},
children=[
html.H2(
"Historical Stock Analysis Dashboard 📊",
style={'textAlign': 'center', 'color': '#2c3e50', 'marginBottom': '10px'}
),
html.P(
"Enter a stock ticker and target date to analyze historical market performance.",
style={'textAlign': 'center', 'color': '#7f8c8d', 'marginBottom': '30px'}
),
html.Div(
style={
'backgroundColor': '#f8f9fa',
'padding': '25px',
'borderRadius': '8px',
'marginBottom': '25px',
'boxShadow': '0 4px 6px rgba(0,0,0,0.05)'
},
children=[
html.Label(
"Stock Ticker Symbol:",
style={'fontWeight': 'bold', 'display': 'block', 'marginBottom': '8px', 'color': '#34495e'}
),
dcc.Input(
id='ticker-input',
type='text',
value='AAPL',
style={
'width': '100%', 'padding': '10px', 'fontSize': '14px',
'marginBottom': '20px', 'borderRadius': '4px',
'border': '1px solid #ccc', 'boxSizing': 'border-box'
}
),
html.Label(
"Select Date (YYYY-MM-DD):",
style={'fontWeight': 'bold', 'display': 'block', 'marginBottom': '8px', 'color': '#34495e'}
),
dcc.Input(
id='date-input',
type='text',
value='2026-01-15',
style={
'width': '100%', 'padding': '10px', 'fontSize': '14px',
'marginBottom': '25px', 'borderRadius': '4px',
'border': '1px solid #ccc', 'boxSizing': 'border-box'
}
),
html.Button(
"Get Market Summary",
id='submit-btn',
n_clicks=0,
style={
'width': '100%', 'padding': '12px', 'backgroundColor': '#007bff',
'color': 'white', 'border': 'none', 'borderRadius': '4px',
'fontSize': '16px', 'cursor': 'pointer', 'fontWeight': 'bold'
}
)
]
),
html.Div(
id='status-output',
style={'textAlign': 'center', 'fontWeight': 'bold', 'marginBottom': '20px', 'fontSize': '16px'}
),
dcc.Graph(id='price-graph', config={'displayModeBar': False})
]
)
Explaining the UI components
-
html.Div:- Makes a box containing other elements. You choose what is inside the box usingchildren=[...]and change how it looks usingstyle={...}(color, font, size, bold and so on). -
dcc.Input:- Creates a box where the user types data, in this case the ticker symbol and the target date. -
dcc.Graph:- Creates the graph that shows the data. The settingconfig={'displayModeBar': False}removes the extra button tray for a clean, minimalist look.
Feel free to play around and experiment a lot with these components so you get the hang of them.
🔗 Step 4: The reactive callback
To give life to our interface, we use the Dash @app.callback() decorator. This part of the script listens for user input and updates the page.
First, add this small helper function. It builds the style of the status message, so the text stays centered and bold whatever color we choose:
def status_style(color):
return {
'textAlign': 'center',
'fontWeight': 'bold',
'marginBottom': '20px',
'fontSize': '16px',
'color': color
}
Now add the callback and the start of the function it controls:
@app.callback(
[Output('price-graph', 'figure'),
Output('status-output', 'children'),
Output('status-output', 'style')],
[Input('submit-btn', 'n_clicks')],
[State('ticker-input', 'value'),
State('date-input', 'value')]
)
def update_dashboard(n_clicks, ticker, date_str):
# This empty figure is a clean placeholder canvas
empty_fig = go.Figure(layout=go.Layout(
paper_bgcolor='rgba(0,0,0,0)',
plot_bgcolor='rgba(0,0,0,0)',
xaxis={'visible': False},
yaxis={'visible': False}
))
-
Outputpoints to the exact properties on the page that we want to change. Here we target the graph'sfigure, plus thechildren(text) andstyleof our status message. -
Inputtracks interactions. Whenever the button is clicked,n_clickschanges and the function runs. (Dash also runs it once when the page first loads, so you will see the default stock right away.) -
Stategives the function the current values of the input boxes without triggering it. For example, if the user is typingAAPL, the function won't run when they have only typedA. It waits until the button is clicked.
🧮 Step 5: The financial logic
Now we add our data, logic and graph code. This goes inside the update_dashboard function, directly under the empty_fig block, indented once (4 spaces) like the code above.
# 1. Check for empty fields
if not ticker or not date_str:
return empty_fig, "Please input a ticker symbol and target date.", status_style('#e74c3c')
try:
# 2. Turn the input text into a date object (raises ValueError if the format is wrong)
target_date = datetime.strptime(date_str.strip(), "%Y-%m-%d").date()
target_str = target_date.strftime("%Y-%m-%d")
# 3. Build the context window (10 days before and 5 days after the target date)
start_date = target_date - timedelta(days=10)
end_date = target_date + timedelta(days=5)
# 4. Clean and normalize the ticker text
ticker_clean = ticker.strip().upper()
stock = yf.Ticker(ticker_clean)
# 5. Get the historical prices for the window
history = stock.history(
start=start_date.strftime("%Y-%m-%d"),
end=(end_date + timedelta(days=1)).strftime("%Y-%m-%d")
)
if history.empty:
return empty_fig, f"No trading data found for symbol '{ticker_clean}' around {target_str}.", status_style('#e74c3c')
# Turn the dates into plain text so we can look them up easily
history.index = history.index.strftime('%Y-%m-%d')
# 6. Build the candlestick chart
fig = go.Figure(data=[go.Candlestick(
x=history.index,
open=history['Open'],
high=history['High'],
low=history['Low'],
close=history['Close'],
increasing_line_color='#2ecc71', # soft green for gains
decreasing_line_color='#e74c3c' # clean red for losses
)])
fig.update_layout(
title=f"{ticker_clean} Performance around Target Date: {target_str}",
yaxis_title="Stock Value (USD)",
xaxis_title="Trading Session Date",
template="plotly_white",
xaxis_rangeslider_visible=False,
margin=dict(l=40, r=40, t=60, b=40)
)
# 7. Check whether the market was open on the target date
if target_str in history.index:
t_open = history.loc[target_str, 'Open']
t_close = history.loc[target_str, 'Close']
msg = f"➔ Success! Target date metrics found ({target_str}) -> Open: ${t_open:,.2f} | Close: ${t_close:,.2f}"
return fig, msg, status_style('#27ae60')
else:
msg = f"⚠️ Target date ({target_str}) was a market closure day. Showing the nearest open sessions."
return fig, msg, status_style('#f39c12')
except ValueError:
return empty_fig, "Invalid date format. Please use YYYY-MM-DD.", status_style('#e74c3c')
except Exception as e:
return empty_fig, f"Could not fetch data: {str(e)}", status_style('#e74c3c')
Main logic
-
Polishing input: We use
.strip()and.upper()on the ticker so the user's input is accepted in any form. For example, to check Tesla shares (tickerTSLA), the user can writetslaorTslawithout the program rejecting it. -
Pandas: With
history.index.strftime('%Y-%m-%d'), we turn the dates into plainYYYY-MM-DDtext. This lets us check whether the target date exists in the data with a simpleif/else. If it isn't there, the market was closed. -
API boundary: When we request the window, we add one extra day to the end date with
timedelta(days=1). Yahoo Finance treats the end date as exclusive, so without this the last day would be missing. -
Error handling: A wrong date format raises a
ValueError, which we catch to show a friendly message. Any other problem (for example a network error) is caught by the lastexcept.
🌐 Step 6: Running in different environments
To complete our script, add this at the very end of app.py (at the left edge, outside any function):
if __name__ == '__main__':
app.run(
debug=True,
jupyter_mode='inline',
jupyter_height=750
)
This block starts the app. Here is how it behaves in each environment:
-
In a terminal: It starts a local server. Open
http://127.0.0.1:8050in your browser to see the dashboard. -
In Jupyter or Google Colab:
jupyter_mode='inline'shows the dashboard directly below the cell, andjupyter_height=750sets its height.
jupyter_mode needs a recent version of Dash (2.11 or later). If you are not in a notebook, the setting is simply ignored.
🎉 Conclusion and summary of achievements
Congratulations! You have built an interactive, cross-environment financial dashboard using Python, Dash and Plotly. You have learned how to take user input, fetch historical stock data, handle market closure days, and draw a candlestick chart.
To take your dashboard to the next level, try these upgrades:
- ➔ Add a toggle-able dark mode directly in the program.
- ➔ Expand your callback to accept a second ticker, so you can compare two stocks in one window.
At the end of this tutorial, your dashboard should look something like this:

Top comments (0)