Author: FXMacroData Team
Published: June 6, 2026
This guide shows how to build the live public macro monitor pattern with Plotly Dash. By the end, you will have a Dash app that pulls FXMacroData time series, renders a macro heatmap, opens drill-down charts from each cell, and keeps protected API-key handling separate from public code.
The goal is practical: create a dashboard that a trader, analyst, or data team can actually extend. We will use the release calendar for event context, USD policy rate as a starter macro series, and USD/JPY as the pair reference when you add market context.
- A small FXMacroData client for announcement time series.
- A normalized macro snapshot table that Dash callbacks can reuse.
- A Plotly heatmap with explicit currency and indicator axes.
- A click-to-drill-down chart for the underlying historical series.
- A deployment pattern that does not expose API keys in source control.
Prerequisites
- Python 3.11 or newer.
- A free FXMacroData account, plus an API key if you want protected coverage beyond public examples.
- Basic familiarity with Dash callbacks, pandas, and REST APIs.
- A deploy target such as Render, Hugging Face Spaces, Fly.io, or your own container host.
Install the dependencies used in this pattern:
pip install dash dash-bootstrap-components pandas plotly requests gunicorn python-dotenv
| Package | Role in the app |
|---|---|
dash |
Web app shell, components, callbacks, and graph containers. |
plotly |
Heatmap and drill-down chart rendering. |
pandas |
Normalizes API rows into reusable tables. |
requests |
Calls FXMacroData REST endpoints. |
Step 1: Define the dashboard contract
Before writing UI code, decide which behavior is public and which behavior requires a key. This keeps the app safe to publish and makes later deployment decisions easier.
- Show USD starter coverage without requiring a user login.
- Unlock additional currencies only when an API key is present.
- Cache repeated API calls briefly on the server.
- Keep API keys in environment variables or password fields, not source files.
- Return an empty chart state when data is unavailable, rather than inventing values.
That last point matters. Dash apps are often shared with non-technical users, so the app should make missing data obvious and recoverable instead of silently showing stale or fabricated series.
Step 2: Build a small FXMacroData client
FXMacroData public examples should use query-parameter authentication. Keep that pattern inside one function so the rest of the app never needs to know how request URLs are assembled.
from datetime import date, timedelta
import requests
API_BASE = "https://api.fxmacrodata.com/v1"
def fetch_indicator(currency: str, indicator: str, api_key: str | None = None):
end_date = date.today()
params = {"start_date": (end_date - timedelta(days=730)).isoformat()}
params["end_date"] = end_date.isoformat()
if api_key:
params["api_key"] = api_key
url = f"{API_BASE}/announcements/{currency.lower()}/{indicator}"
response = requests.get(url, params=params, timeout=15)
response.raise_for_status()
return response.json()
You can test the same endpoint from a terminal. Use your own key for protected coverage:
curl "https://api.fxmacrodata.com/v1/announcements/usd/policy_rate?api_key=YOUR_API_KEY"
For macro context, start with USD inflation, USD policy rate, and USD GDP. When you add non-USD rows, use the same endpoint pattern with the target currency code.
Step 3: Normalize the API response
Dash callbacks become much easier when each endpoint response is converted into a consistent row shape. The helper below accepts common date/value fields and ignores malformed rows instead of breaking the whole page.
def normalize_rows(payload: dict) -> list[dict]:
rows = []
for row in payload.get("data", []):
ds = row.get("date") or row.get("announcement_datetime")
value = row.get("val") if "val" in row else row.get("value")
if not ds or value is None:
continue
try:
rows.append({"date": str(ds)[:10], "value": float(value)})
except (TypeError, ValueError):
continue
return sorted(rows, key=lambda item: item["date"])
This is also the right place to decide how your app handles missing values. For a public dashboard, empty rows should produce an empty visual state, not a misleading zero.
Step 4: Assemble the Dash app shell
A good public monitor should be simple at first load: a title, a key input, a currency selector, a heatmap, and a drill-down panel.
from dash import Dash, dcc, html
import dash_bootstrap_components as dbc
app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
server = app.server
app.layout = dbc.Container(
[
html.H1("FXMacroData Public Macro Monitor"),
dbc.Input(id="api-key-input", type="password",
placeholder="Optional FXMacroData API key"),
dcc.Dropdown(id="currency-select", multi=True,
value=["USD"], options=["USD", "EUR", "GBP", "AUD"]),
dcc.Graph(id="heatmap-graph"),
html.Div(id="drilldown-panel"),
],
fluid=True,
)
Keep the first version plain. Once the data flow is correct, you can add cards, tabs, filters, and branded styling without changing the core callbacks.
Step 5: Build a heatmap-ready snapshot
The heatmap needs one latest value per currency and indicator. Store both the display label and the API key for each indicator so click events can map back to the correct endpoint later.
INDICATORS = [
("policy_rate", "Policy Rate"),
("inflation", "Inflation"),
("gdp", "GDP"),
]
def build_macro_snapshot(currencies, api_key=None):
rows = []
for currency in currencies:
key = None if currency == "USD" else api_key
for indicator, label in INDICATORS:
series = normalize_rows(fetch_indicator(currency, indicator, key))
latest = series[-1]["value"] if series else None
rows.append({"currency": currency, "indicator": indicator,
"label": label, "latest": latest})
return rows
This table can power a heatmap, a ranking panel, and a small status card without repeating API requests in every callback.
Step 6: Render the Plotly heatmap
The key detail is to pass explicit x and y labels into go.Heatmap. That makes the visual readable and gives the click callback enough information to fetch the underlying series.
from dash import Input, Output
import plotly.graph_objects as go
@app.callback(Output("heatmap-graph", "figure"),
Input("currency-select", "value"),
Input("api-key-input", "value"))
def update_heatmap(currencies, api_key):
currencies = currencies or ["USD"]
snapshot = build_macro_snapshot(currencies, api_key)
labels = [label for _, label in INDICATORS]
lookup = {(r["currency"], r["label"]): r["latest"] for r in snapshot}
z = [[lookup.get((ccy, label)) for label in labels] for ccy in currencies]
fig = go.Figure(go.Heatmap(x=labels, y=currencies, z=z,
colorscale="RdBu", hoverongaps=False))
fig.update_layout(template="plotly_white", height=460,
xaxis_title="Indicator", yaxis_title="Currency")
return fig
At this point, the app is already useful: users can see which currencies and indicators are available, where values are missing, and which macro fields deserve a deeper look.
Step 7: Add click-to-drill-down charts
Plotly heatmap clicks return the display label, not your API slug. Use a small mapping to translate the clicked indicator back to the endpoint key.
from dash import State
LABEL_TO_INDICATOR = {label: key for key, label in INDICATORS}
@app.callback(Output("drilldown-panel", "children"),
Input("heatmap-graph", "clickData"),
State("api-key-input", "value"))
def show_drilldown(click_data, api_key):
if not click_data:
return html.Div("Click a heatmap cell to inspect the series.")
point = click_data["points"][0]
currency = point["y"]
indicator = LABEL_TO_INDICATOR[point["x"]]
rows = normalize_rows(fetch_indicator(currency, indicator, api_key))
fig = go.Figure(go.Scatter(x=[r["date"] for r in rows],
y=[r["value"] for r in rows],
mode="lines+markers"))
fig.update_layout(template="plotly_white", height=360)
return dcc.Graph(figure=fig)
This callback turns the heatmap from a static overview into a research workflow. A user can spot an outlier, click it, then immediately inspect the time series behind that cell.
Step 8: Cache repeated requests
Dashboard users change filters quickly. Add a short cache around the API request layer so a repeated click does not create a new network request every time.
from functools import lru_cache
@lru_cache(maxsize=128)
def fetch_indicator_cached(currency: str, indicator: str, api_key_marker: str):
api_key = None if api_key_marker == "public" else api_key_marker
return fetch_indicator(currency, indicator, api_key)
In a shared public app, use a server cache such as Redis or Flask-Caching instead of relying only on in-process memory. For a small internal monitor or demo deployment, this lightweight cache is usually enough to remove accidental duplicate calls.
Step 9: Deploy without leaking secrets
For Render, the minimal production process is:
pip freeze > requirements.txt
gunicorn app:server
For Hugging Face Spaces, put the key in the platform secret manager rather than the repository. For a private company dashboard, prefer an environment variable such as FXMACRODATA_API_KEY and never commit a real key into source control.
| Deployment mode | Recommended key handling | Best for |
|---|---|---|
| Public demo | No committed key; optional user-entered key field. | Gallery apps, examples, sales demos. |
| Internal dashboard | Host secret or environment variable. | Trading desks and research teams. |
| Client-specific app | Per-client secret storage and access control. | Managed analytics portals. |
After deployment, compare your version with the live app at /app-gallery/dash/public-macro-monitor. The gallery version adds richer tabs, pair overlays, strategy views, and macro panels, but it uses the same basic pattern: fetch, normalize, visualize, and drill down.
Summary
You now have the core building blocks for a public Plotly Dash macro monitor: a small FXMacroData client, a normalized snapshot table, an explicit-axis heatmap, a click-driven drill-down chart, and a safer secret-handling model.
The most useful next extension is a second dashboard layer that compares pairs like EUR/USD and USD/JPY against macro regime scores. After that, add release timing from the calendar, then link each surfaced indicator to the matching API docs page so users can move from the app into deeper research.