Member-only story
Python & Finance
Creating Financial Dashboards in Python with Solara
In a previous article, I showed how I built a dividend investing dashboard in Streamlit:
I also outlined my motivation for investing in dividend stocks and explained a few metrics that can be used to analyze them.
Recently, I’ve begun using another Python web app framework called Solara. Solara brings more customizability and more effective state management, which is useful for dashboards where various components have their own states and update schedules.
For this reason, I’ve opted to rewrite and improve the dividend dashboard using Solara. One new feature is the use of the OpenAI API to ask questions and gain insights about the analyzed stock.
Here’s the final result:
If you want to run it yourself, you can retrieve all the code on GitHub with the link below:
In the rest of the article, I will explain how the app was built. Let’s get started.
Data
The data is obtained using an API from Financial Modeling Prep (FMP). The API provides a wide variety of financial information useful for fundamental analysis and analyzing dividend stocks.
There is a a free version available that you can use for 250 API calls a day, but also paid versions with fewer limitations and more functionality, such as data for stocks outside of the U.S. If you would like to use one of the paid versions you can use my affiliate link for 15% off:
Here’s a link to the documentation.
Code
I will display the most important parts of the code and use ... for code not shown. Check out the GitHub repository for all code.
Every Solara app starts with the Page()-method (with thesolara.component decorator). Here’s the structure of this method for this dashboard:
@sl.component
def Page():
...
# define states
ticker, set_ticker = sl.use_state(None)
input_ticker, set_input_ticker = sl.use_state("")
...
# data fetched from FMP API
data, set_data = sl.use_state(None)
# method to fetch all data from FMP API
def fetch_data():
# fetch data
data = ...
# update data state
set_data(data)
# fetch in a thread to not block the UI
sl.use_thread(fetch_data, dependencies=[ticker])
# show data
...Firstly, the states are defined. This is done using solara.use_state which returns a tuple with the state and a method to set the state. The two key states are the ticker that is supplied by the user, and the data that is fetched from the FMP API.
The fetching procedure is executed using the solara.use_thread-method. This method spawns a thread that will handle the fetching to avoid blocking the main thread. Anytime any of the arguments specified in the dependencies argument of this method is changed, the thread will be canceled (if it’s still running) and rerun. Thus, whenever the ticker is changed new financial data will be fetched.
The code for fetching the data uses plain HTTP requests to the API endpoints to gather all the necessary data. I use the requests package for this:
import requests
requests.get(f"https://financialmodelingprep.com/api/v3/{endpoint}?{qs}")For all endpoints available in the API, see here. For this dashboard, I use the following endpoints.
General information about the company:
https://financialmodelingprep.com/api/v3/profile/{ticker}
Various financial metrics:
https://financialmodelingprep.com/api/v3/key-metrics/{ticker}
Historical price data, including adjusted closing price:
https://financialmodelingprep.com/api/v3/historical-price-full/{ticker}
Financial ratios:
https://financialmodelingprep.com/api/v3/ratios/{ticker}
Income statement data:
https://financialmodelingprep.com/api/v3/ratios/income-statement/{ticker}
Historical dividends:
https://financialmodelingprep.com/api/v3/historical-price-full/stock_dividend/{ticker}
There’s a lot more data you can access using the API. Feel free to modify the dashboard with info that you would like to use to analyze your stocks.
Interface
After fetching the data it is rendered as components in Solara. In particular, with solara.Columns(...) is frequently used. This component will put the children (rendered inside of the with-statement) side by side with the specified widths.
@sl.component
def Page():
...
def update_ticker():
set_data(None)
set_ticker(input_ticker)
# when ticker is changed but not data => loading
is_loading = data is None and ticker is not None and ticker != ""
# image and title
with sl.Column(align="center"):
# classes is CSS-classes,
# here an extra animation is added during loading
sl.Image("./logo.png", classes=["logo"] + ([] if not is_loading else ["logo-animation"]))
sl.HTML("h1", TITLE)
# sl.Columns takes a list of widths, [1, 1, 1] means 3 columns with equal width
with sl.Columns([1, 1, 1]):
# the first element will have the first width,
# the second the second width, etc.
# sl.HTML("div") is used as a placeholder
sl.HTML("div")
with sl.Column(align="center"):
if not is_loading:
# $ + input + button
with sl.Row(style="align-items: center;"):
sl.Text("$", style="color: gray")
# this input is connected to the state of
# `input_ticker`, which will in turn update
# the `ticker` state after clicking the submit button
sl.InputText(
label="Ticker",
value=input_ticker,
error=error,
continuous_update=True,
on_value=set_input_ticker
)
sl.IconButton(
color="primary",
icon_name='mdi-chevron-right',
on_click=update_ticker,
disabled=input_ticker == "" or input_ticker == ticker
)
else:
FunAnimation()
sl.HTML("div")
if ticker is None or ticker == "":
return
if is_loading:
Loader()
info = None if data is None else data["info"]
# Title
if info is not None:
sl.HTML("h1", f"{info['companyName']} ({info['symbol']})")
# Percentage change across time periods
PercentageChange(data)
# Overview + price side by side
with sl.Columns([1, 4], style="align-items: center;"):
Overview(info)
Price(data)
# Description + AI-analysis side by side
with sl.Columns([2, 3]):
Description(info)
AIAnalysis(ticker, data)
# Charts for various data
Charts(data)Here there are several custom components, such as PercentageChange(), Overview(), Price(data), etc. Creating custom components is a key feature of Solara that enables you to update components independently of one another. It’s also useful to reuse code and reduce clutter. Here’s the component to UI mapping:
PercentageChange
Overview
Price
In this component, there are two states: the time window and the moving average. As these states are only relevant to this component, there is no reason to define them inside the Page() component. Instead, they can encapsulated inside the Price() component. Consequently‚ only Price() will be updated when they are modified and everything else will remain constant.
@sl.component
def Price(data: dict):
moving_average, set_moving_average = sl.use_state(30)
time_window_key, set_time_window_key = sl.use_state("5 years")
...Description
This component uses a nested component called ShowMore, which has a state for whether the entire text or just a small number of characters should be shown:
@sl.component
def ShowMore(text: str, n: int = 400):
show_more, set_show_more = sl.use_state(False)
if show_more or len(text) < n:
sl.Markdown(text)
if len(text) > n:
sl.Button("Show less", on_click=lambda: set_show_more(False))
else:
sl.Markdown(text[:n] + "...")
sl.Button("Show more", style="width: 150px;", on_click=lambda: set_show_more(True))As this component is used in the AI Analysis component as well, it works well as a standalone, reusable component with its own state.
AI Analysis
The AI Analysis component handles the call to the OpenAI API which, just like the fetching of data from the FMP API, is performed in its own thread. If you would like to understand the importance of using solara.use_thread for fetching data you can try to call the fetching method without use_thread and then interact with the UI. You will see that it’s not responsive.
@sl.component
def AIAnalysis(ticker: str | None, data: dict):
...
ai_analysis, set_ai_analysis = sl.use_state(None)
question, set_question = sl.use_state("")
...
def fetch_ai_analysis():
...
client = OpenAI(api_key=envs.openai_api_key)
# reset
set_ai_analysis("")
stream = client.chat.completions.create(
model=envs.openai_model,
messages=...,
stream=True,
temperature=0.5
)
combined = ""
for chunk in stream:
added = chunk.choices[0].delta.content
if added is not None:
combined += added
set_ai_analysis(combined)
...
# fetch in a separate thread to not block UI
fetch_thread = sl.use_thread(fetch_ai_analysis, dependencies=[...])
...
with AppComponent("AI Analysis"):
...
if ai_analysis is not None:
sl.HTML("h3", "Output:")
ShowMore(ai_analysis)Here’s the prompt used. Essentially, the data from FMP is inserted into the prompt and then a question is asked:
DATA_PROMPT = """\
{question}
Company: {company}
Ticker: {ticker}
Earnings:
{earnings}
DIVIDENDS:
{dividends}
P/E:
{pe}
DEBT/EQUITY:
{dte}
CASH/SHARE:
{cps}
FREE CASH FLOW/SHARE:
{fcf}
PAYOUT RATIO:
{payout}
"""
# later
DATA_PROMPT.format(
ticker=ticker,
question=(
"Give me your insights into the following stock:"
if question is None or question == ""
else question
),
company=data["info"]["companyName"],
earnings=data["earnings_per_share"].to_string(),
dividends=data["dividends"].to_string(),
pe=data["historical_PE"].to_string(),
dte=data["debt_to_equity"].to_string(),
cps=data["cash_per_share"].to_string(),
fcf=data["free_cash_flow_per_share"].to_string(),
payout=data["payout_ratio"].to_string()
)Charts
Finally, the graphs show various metrics and how they have evolved over time. Each graph is specified using the following method and then rendered using solara.FigurePlotly(fig):
def plot_data(data, key, title, yaxis_title, show_mean=False, mean_text="", type="line"):
# getattr(px, type) if type = 'line' is px.line
fig = getattr(px, type)(y=data[key], x=data[key].index)
# add a historical mean if specified
if show_mean:
fig.add_hline(data[key].mean(), line_dash="dot", annotation_text=mean_text)
# set title and axis-titles
fig.update_layout(
title=title,
xaxis_title="Date",
yaxis_title=yaxis_title,
title_x = 0.5,
showlegend=False
)
return figRun it
The GitHub repository contains the latest instructions on how to run the app.
Thanks for reading!







