Building A Forex Volatility Dashboard using Streamlit and Python
- Nikhil Adithyan
- Aug 11
- 7 min read
A step-by-step guide

Currency volatility is a key aspect of technical analysis that indicates how much prices fluctuate, highlighting the level of risk and potential rewards for traders looking to trade in the forex market. Monitoring FX volatility helps us understand when the market is “risky”, recognise forming trends early, and make smarter decisions that align with their objectives.
Our FX Volatility Dashboard, which we will develop in this article, allows users to track the pair’s volatility, identify times of increased market activity, and rank pairs based on volatility to find trading opportunities. This helps traders respond swiftly to market changes, decide the size of their positions based on the information provided, and apply effective risk management techniques so they will not risk more than their are willing to do.
The Dashboard
In this article, we will showcase a dashboard built with Python and Streamlit. The data will be sourced from the FinancialModelingPrep (FMP) API suite.

The dashboard on the left-hand side will feature a sidebar where the user can do the following:
Select the pairs that want to be included in the list (text with a comma-delimited).
Unchecking the “Use all currencies”, we have added it in case you want to see all the pairs. However, I have to warn you that there are more than 1000 pairs, so the dashboard will become overwhelming
A dropdown menu that will include all the pairs based on your selection above
The timeframe that you wish to display the data for
The indicator that you would like to be plotted in the graph, together with the closing price
And a button to refresh the data when needed.
The main area of the dashboard will include
Some basic current information about the pair, like the current price, the 24h change, together with the daily high and low, as well as the year’s high
The graph shows the closing price together with the indicator selected. I have added the 20-period volatility and the 14-period ATR. However, you will notice later in the code that it will be very easy to add or change other indicators according to your preference.
At the end, you will see the top-ranked pairs based on their volatility.
Let’s Code
First, we will do the necessary imports and set up the page title:
import streamlit as st
import requests
import pandas as pd
import plotly.graph_objects as go
import requests_cache
from datetime import datetime, timedelta
# Set up page configuration
st.set_page_config(
page_title="FX Volatility Dashboard",
page_icon="💱",
layout="wide"
)
# Initialize cache for API requests
requests_cache.install_cache('cache')
# API key
token = 'YOUR FMP TOKEN'
You will notice that we use the requests_cache . This is to make the dashboard run faster during development and testing. In production, you will remove this, since we are already using the cashe_data functionality of streamlit.
Now we will define a function that will retrieve the pairs using FMP’s Forex Historical Data API. The function will also get the values of what we had selected on the sidebar, whether we should filter the pairs and which currencies to include.
@st.cache_data(ttl=3600) # Cache for 1 hour
def get_forex_pairs(included_currencies=None, use_included_currencies=True):
url = 'https://financialmodelingprep.com/api/v3/symbol/available-forex-currency-pairs'
querystring = {"apikey": token}
try:
response = requests.get(url, querystring)
response.raise_for_status() # Raise an exception for HTTP errors
data = response.json()
df = pd.DataFrame(data)
# Filter based on included currencies if the checkbox is checked
if included_currencies and use_included_currencies:
included_list = [curr.strip().upper() for curr in included_currencies.split(',') if curr.strip()]
if included_list:
# Keep only pairs where both currencies are in the included currencies list
filtered_df = pd.DataFrame()
for pair in df['symbol']:
# Extract the two currencies from the pair (e.g., 'EURUSD' -> 'EUR', 'USD')
# Most forex pairs are 6 characters (3 for each currency)
if len(pair) >= 6:
curr1 = pair[:3]
curr2 = pair[3:6]
# Check if both currencies are in the included list
if curr1 in included_list and curr2 in included_list:
filtered_df = pd.concat([filtered_df, df[df['symbol'] == pair]])
df = filtered_df
return df
except Exception as e:
st.error(f"Error fetching forex pairs: {e}")
return pd.DataFrame()
Also, using FMP’s Exchange Rate API for the quotes and the historical data endpoint for the historical chart, we will define two functions. One to actually call the aPI and get the quotes and the other to calculate the historical data for a specific pair and time frame. It is a bit more complex than expected, since, besides calling the API to get the prices, we will be calculating the volatility and the ATR, in a pandas dataframe, the way that is needed for later in the dashboard.
@st.cache_data(ttl=300) # Cache for 5 minutes
def get_forex_quotes():
url = 'https://financialmodelingprep.com/api/v3/quotes/forex'
querystring = {"apikey": token}
try:
response = requests.get(url, querystring)
response.raise_for_status()
data = response.json()
df = pd.DataFrame(data)
return df
except Exception as e:
st.error(f"Error fetching forex quotes: {e}")
return pd.DataFrame()
@st.cache_data(ttl=300) # Cache for 5 minutes
def get_historical_data(pair, timeframe):
# Map timeframe selection to API endpoint
timeframe_map = {
"15min": "15min",
"1hour": "1hour",
"4hours": "4hour",
"1day": "1day",
"1week": "1week"
}
api_timeframe = timeframe_map.get(timeframe, "1hour")
# Calculate date range (last 30 days)
to_date = datetime.now().strftime('%Y-%m-%d')
from_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
url = f'https://financialmodelingprep.com/api/v3/historical-chart/{api_timeframe}/{pair}'
querystring = {"apikey": token, "from": from_date, "to": to_date}
try:
response = requests.get(url, querystring)
response.raise_for_status()
data = response.json()
df = pd.DataFrame(data)
# Convert date string to datetime
df['date'] = pd.to_datetime(df['date'])
# Sort by date (ascending)
df = df.sort_values('date')
# Calculate rolling volatility (standard deviation of returns)
if len(df) > 0:
df['return'] = df['close'].pct_change()
df['volatility_20'] = df['return'].rolling(window=20).std() * 100 # Convert to percentage
# Calculate ATR (Average True Range)
df['high_low'] = df['high'] - df['low']
df['high_close'] = abs(df['high'] - df['close'].shift())
df['low_close'] = abs(df['low'] - df['close'].shift())
df['tr'] = df[['high_low', 'high_close', 'low_close']].max(axis=1)
df['atr_14'] = df['tr'].rolling(window=14).mean()
return df
except Exception as e:
st.error(f"Error fetching historical data: {e} for {pair} with {api_timeframe} timeframe")
return pd.DataFrame()
Before we move to the main app, we will define a function that will calculate all the volatilities using the ranked table at the bottom of the dashboard.
def calculate_all_volatilities(pairs, timeframe):
results = []
for pair in pairs:
try:
df = get_historical_data(pair, timeframe)
if len(df) > 0 and 'volatility_20' in df.columns:
latest_volatility = df['volatility_20'].iloc[-1]
results.append({
'pair': pair,
'volatility': latest_volatility
})
except Exception as e:
st.warning(f"Could not calculate volatility for {pair}: {e}")
return pd.DataFrame(results).sort_values('volatility', ascending=False)
Now let’s move to the main app.
We have already added some comments in the code, it is easier to follow, but let’s point out some parts that more attention should be paid to:
Even though price and indicator are not on the same scale, using plotly, there is the proper scaling of the values as it should be to be meaningful.
How the dashboard is refreshed is a bit tricky all the time with streamlit, so we keep in the session state the last_refresh
You will notice how beautiful the loading of the data is since we are using functions, not leaving the user with a stuck browser while the data is loading or being calculated.

def main():
st.title("FX Volatility Dashboard")
# Sidebar for inputs
with st.sidebar:
st.header("Settings")
# Currency filter inputs
including_currencies = st.text_input("Including Currencies", value="USD,EUR,GBP,JPY,AUD,CAD,CHF,SGD,NZD,SEK,NOK,DKK",
help="Enter currencies to include, separated by commas")
use_included_currencies = st.checkbox("Use Including Currencies", value=True,
help="When checked, only pairs with the currencies above will be shown. When unchecked, all pairs will be shown.")
# Get forex pairs for dropdown
forex_pairs_df = get_forex_pairs(included_currencies=including_currencies,
use_included_currencies=use_included_currencies)
if not forex_pairs_df.empty:
# Extract symbols for dropdown
forex_symbols = forex_pairs_df['symbol'].tolist()
selected_pair = st.selectbox("Select Forex Pair", forex_symbols, index=0)
else:
st.error("Could not load forex pairs")
selected_pair = "EURUSD" # Default
# Timeframe selection
timeframe_options = ["15min", "1hour", "4hours", "1day", "1week"]
selected_timeframe = st.selectbox("Select Timeframe", timeframe_options, index=1)
# Indicator selection
indicator_options = ["20-period volatility", "14-period ATR"]
selected_indicator = st.selectbox("Select Indicator", indicator_options, index=0)
# Refresh button
refresh = st.button("Refresh")
# Main content area
# Initialize last_refresh in session state if it doesn't exist
if 'last_refresh' not in st.session_state:
st.session_state.last_refresh = datetime.now()
# Update last_refresh and clear cache if refresh button is clicked
if refresh:
st.session_state.last_refresh = datetime.now()
# Clear cache to force data refresh
get_forex_quotes.clear()
get_historical_data.clear()
# Get forex quotes - always execute this regardless of refresh button
quotes_df = get_forex_quotes()
# Display basic information for selected pair
if not quotes_df.empty:
selected_quote = quotes_df[quotes_df['symbol'] == selected_pair]
if not selected_quote.empty:
quote_data = selected_quote.iloc[0]
# Display basic information with large font
st.markdown(f"<h1 style='text-align: center;'>{selected_pair}: {quote_data['price']:.5f}</h1>", unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Change", f"{quote_data['change']:.5f}", f"{quote_data['changesPercentage']:.2f}%")
with col2:
st.metric("Day High", f"{quote_data['dayHigh']:.5f}")
with col3:
st.metric("Day Low", f"{quote_data['dayLow']:.5f}")
with col4:
st.metric("Year High", f"{quote_data['yearHigh']:.5f}")
else:
st.warning(f"No data available for {selected_pair}")
# Get historical data and create chart
historical_df = get_historical_data(selected_pair, selected_timeframe)
if not historical_df.empty:
# Create price chart with selected indicator
fig = go.Figure()
# Add price line
fig.add_trace(go.Scatter(
x=historical_df['date'],
y=historical_df['close'],
mode='lines',
name='Close Price',
line=dict(color='blue')
))
# Add selected indicator on secondary y-axis
if selected_indicator == "20-period volatility":
fig.add_trace(go.Scatter(
x=historical_df['date'],
y=historical_df['volatility_20'],
mode='lines',
name='20-period Volatility (%)',
line=dict(color='red'),
yaxis='y2'
))
secondary_axis_title = "Volatility (%)"
else: # "14-period ATR"
fig.add_trace(go.Scatter(
x=historical_df['date'],
y=historical_df['atr_14'],
mode='lines',
name='14-period ATR',
line=dict(color='green'),
yaxis='y2'
))
secondary_axis_title = "ATR"
# Set up layout with dual y-axes
fig.update_layout(
title=f"{selected_pair} - {selected_timeframe} Chart with {selected_indicator}",
xaxis_title="Date",
yaxis_title="Price",
yaxis2=dict(
title=secondary_axis_title,
overlaying="y",
side="right"
),
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
height=600
)
st.plotly_chart(fig, use_container_width=True)
# Calculate and display volatility table for all pairs
st.subheader("Forex Pairs Ranked by Volatility")
with st.spinner("Calculating volatilities for filtered pairs..."):
# Use the filtered forex_symbols list (respecting the "Including Currencies" filter)
# Still limit to 10 pairs to avoid API rate limits
volatility_df = calculate_all_volatilities(forex_symbols[:10], selected_timeframe)
if not volatility_df.empty:
st.dataframe(volatility_df, use_container_width=True)
else:
st.warning("Could not calculate volatilities")
else:
st.warning(f"No historical data available for {selected_pair} with {selected_timeframe} timeframe")
if __name__ == "__main__":
main()
Some practical examples
Let’s start with something obvious. When the US reached an agreement with the EU on the tariffs, EURUSD had a significant spike as shown below. This might seem obvious, but it indicates a trading opportunity to short EURUSD, while also encouraging you to read the news ;)

For risk management, we will showcase EURGBP using ATR with 1-day prices. For example, you will notice ATR spikes around July 6th and July 27th, indicating periods where price movement becomes more intense.

What else?
Within a single post, it is challenging to cover extensive functionality and more complex dashboards. The aim is to guide you in understanding how to use Python, Streamlit, and FMP APIs to customise your dashboard. To achieve this, you can also implement the ideas below.
Add more indicators: Incorporate popular trading tools like RSI, MACD, Bollinger Bands, or custom user-defined indicators.
Alerting system: Enable push notifications or email alerts for specified volatility thresholds or when sudden changes occur in selected pairs.
News integration: Display relevant news headlines or economic events directly in the dashboard contextually alongside market spikes.
When putting a trade, log what you see in the dashboard to your trade journal for later analysis of your trade
Conclusions
The FX Volatility Dashboard is a tool that can help you with real-time insights into the FX market.
By monitoring volatility and key indicators like ATR using various timeframes, you can identify opportunities, manage your risks, and make decisions based on data rather than guesswork.
The dashboard’s layout is customizable and allows filtering, ensuring you quickly access the information you need. Especially if you enhance the dashboard with the ideas above and adapt it to your own style, it will turn unnecessary risks into opportunities.
With that being said, you’ve reached the end of the article. Hope you learned something new and useful today.