How to Build a Macro Dashboard in Python with pandas & Plotly banner image

Implementation

How-To Guides

How to Build a Macro Dashboard in Python with pandas & Plotly

Step-by-step guide to fetching FXMacroData indicators via Python, reshaping time series with pandas, and assembling an interactive multi-panel dashboard with Plotly.

A macro dashboard puts the most important central-bank and economic indicators in one place, so you can scan market conditions in seconds instead of bouncing between data terminals. This guide walks you through building a fully interactive, multi-panel dashboard in Python — fetching live indicator data from the FXMacroData API, reshaping it with pandas, and rendering it with Plotly. Every panel updates from a single API key and a handful of function calls.

What You Will Build

A self-contained Python script that pulls policy rates, inflation, unemployment, and PMI for four major FX currencies (USD, EUR, GBP, AUD), assembles a clean pandas DataFrame for each indicator, and renders a four-panel Plotly dashboard — all in under 150 lines of code.

Prerequisites

You will need the following before starting:

  • Python 3.9+
  • FXMacroData API key — sign up at /subscribe and copy your key from the dashboard
  • Python packages: requests, pandas, plotly

Install the dependencies in one command:

pip install requests pandas plotly

Store your API key as an environment variable — never hard-code credentials in scripts:

export FXMD_API_KEY="YOUR_API_KEY"

Step 1 — Understand the Indicator Endpoint Shape

Every FXMacroData indicator follows the same REST pattern, which makes it trivial to generalise into a single fetch function. A policy-rate request for USD looks like this:

GET https://fxmacrodata.com/api/v1/announcements/usd/policy_rate?api_key=YOUR_API_KEY&start=2020-01-01

The JSON response is a flat object with a data array:

{
  "data": [
    { "date": "2025-03-19", "val": 4.25, "announcement_datetime": "2025-03-19T18:00:00Z" },
    { "date": "2025-01-29", "val": 4.25, "announcement_datetime": "2025-01-29T19:00:00Z" },
    { "date": "2024-12-18", "val": 4.25, "announcement_datetime": "2024-12-18T19:00:00Z" }
  ]
}

Each record carries a date (YYYY-MM-DD), a numeric val, and — where available — a second-level UTC announcement_datetime. The consistent shape across all indicators is what lets one function serve every panel in the dashboard.

Step 2 — Write a Reusable Fetch Function

Create a module called macro_fetch.py. The function below requests any indicator for any currency, converts the response to a pandas Series, and returns it indexed by date — ready to drop straight into a DataFrame.

import os
import requests
import pandas as pd

BASE_URL = "https://fxmacrodata.com/api/v1"
API_KEY = os.environ["FXMD_API_KEY"]


def fetch_indicator(currency: str, indicator: str, start: str = "2020-01-01") -> pd.Series:
    """
    Fetch a single indicator series from FXMacroData and return it as a
    pandas Series with a DatetimeIndex, named '{currency.upper()}_{indicator}'.
    """
    url = f"{BASE_URL}/announcements/{currency}/{indicator}"
    resp = requests.get(url, params={"api_key": API_KEY, "start": start}, timeout=15)
    resp.raise_for_status()

    records = resp.json().get("data", [])
    if not records:
        return pd.Series(name=f"{currency.upper()}_{indicator}", dtype=float)

    series = (
        pd.DataFrame(records)
        .assign(date=lambda df: pd.to_datetime(df["date"]))
        .set_index("date")["val"]
        .sort_index()
        .rename(f"{currency.upper()}_{indicator}")
    )
    return series

Why a Series per indicator?

Indicators from different currencies are released on different dates, so they never share a perfectly aligned index. Storing each as a separate Series and joining them with pd.concat(..., axis=1) lets pandas handle the date alignment automatically, filling gaps with NaN where a currency hasn't yet reported.

Step 3 — Fetch All Indicators for the Dashboard

With the fetch helper in place, pulling four indicators for four currencies is a concise loop. Add a small retry wrapper to handle transient network hiccups:

import time

CURRENCIES = ["usd", "eur", "gbp", "aud"]
INDICATORS = {
    "policy_rate": "Policy Rate (%)",
    "inflation": "CPI Inflation (% YoY)",
    "unemployment": "Unemployment Rate (%)",
    "pmi": "Manufacturing PMI",
}


def fetch_all(start: str = "2020-01-01", retries: int = 3) -> dict[str, pd.DataFrame]:
    """
    Return a dict mapping each indicator slug to a wide DataFrame where
    each column is one currency (e.g. USD_policy_rate, EUR_policy_rate).
    """
    frames: dict[str, list[pd.Series]] = {ind: [] for ind in INDICATORS}

    for currency in CURRENCIES:
        for indicator in INDICATORS:
            for attempt in range(retries):
                try:
                    s = fetch_indicator(currency, indicator, start=start)
                    frames[indicator].append(s)
                    break
                except requests.HTTPError as exc:
                    if attempt == retries - 1:
                        print(f"Warning: could not fetch {currency}/{indicator}: {exc}")
                    else:
                        time.sleep(1.5 ** attempt)

    return {ind: pd.concat(series, axis=1) for ind, series in frames.items() if series}

You can inspect any DataFrame interactively to verify the data looks right before rendering:

data = fetch_all(start="2021-01-01")
print(data["policy_rate"].tail())
# Output (example):
#             USD_policy_rate  EUR_policy_rate  GBP_policy_rate  AUD_policy_rate
# date
# 2025-01-29             4.25              NaN              NaN              NaN
# 2025-02-06              NaN              NaN             4.50              NaN
# 2025-02-18              NaN              NaN              NaN             4.10
# 2025-03-06              NaN             2.50              NaN              NaN
# 2025-03-19             4.25              NaN              NaN              NaN

Step 4 — Shape the Data for Charting

Plotly works best with forward-filled series for line charts, so the most recently known value carries forward until the next release. Add a forward-fill step before charting:

def prepare_for_chart(df: pd.DataFrame) -> pd.DataFrame:
    """
    Forward-fill each column so lines in the chart step at each release date
    rather than showing gaps between announcements.
    Resample to a monthly frequency for a cleaner visual.
    """
    return (
        df
        .ffill()
        .resample("ME")      # month-end
        .last()
        .dropna(how="all")
    )

For PMI, which is a monthly indicator, forward-fill is less relevant — but the function handles it gracefully by simply passing through the already-monthly data unchanged.

Step 5 — Build the Plotly Dashboard

Plotly's make_subplots utility lets you arrange multiple charts in a single figure object, which you can display in a browser, export as HTML, or embed in a Jupyter notebook.

import plotly.graph_objects as go
from plotly.subplots import make_subplots

# Palette aligned with FXMacroData brand colours
COLORS = {
    "usd": "#3B82F6",   # finance blue
    "eur": "#D97706",   # gold
    "gbp": "#16A34A",   # green
    "aud": "#7C3AED",   # purple
}

SUBPLOT_TITLES = list(INDICATORS.values())


def build_dashboard(data: dict[str, pd.DataFrame]) -> go.Figure:
    fig = make_subplots(
        rows=2, cols=2,
        subplot_titles=SUBPLOT_TITLES,
        shared_xaxes=False,
        vertical_spacing=0.12,
        horizontal_spacing=0.08,
    )

    positions = [(1, 1), (1, 2), (2, 1), (2, 2)]

    for (indicator, label), (row, col) in zip(INDICATORS.items(), positions):
        df = prepare_for_chart(data[indicator])
        for col_name in df.columns:
            currency = col_name.split("_")[0].lower()
            fig.add_trace(
                go.Scatter(
                    x=df.index,
                    y=df[col_name],
                    mode="lines",
                    name=currency.upper(),
                    line=dict(color=COLORS[currency], width=2),
                    legendgroup=currency,
                    showlegend=(indicator == "policy_rate"),  # one legend entry per currency
                    hovertemplate=f"%{{x|%b %Y}}: %{{y:.2f}}{currency.upper()}",
                ),
                row=row, col=col,
            )

    fig.update_layout(
        title=dict(
            text="G4 Central Bank Macro Dashboard",
            font=dict(size=22, color="#1e3a5f"),
            x=0.5,
        ),
        paper_bgcolor="#f8fafc",
        plot_bgcolor="#f1f5f9",
        font=dict(family="Inter, system-ui, sans-serif", size=12, color="#334155"),
        legend=dict(
            orientation="h",
            yanchor="bottom",
            y=1.04,
            xanchor="center",
            x=0.5,
        ),
        height=700,
        margin=dict(t=100, b=50, l=60, r=40),
    )

    fig.update_xaxes(showgrid=True, gridcolor="#e2e8f0", zeroline=False)
    fig.update_yaxes(showgrid=True, gridcolor="#e2e8f0", zeroline=False)

    return fig

Step 6 — Run the Dashboard

Wire everything together in a dashboard.py entry-point script. Calling fig.show() opens the dashboard in your default browser. Calling fig.write_html() saves a self-contained HTML file you can share or embed anywhere.

if __name__ == "__main__":
    print("Fetching macro data …")
    data = fetch_all(start="2021-01-01")

    print("Building dashboard …")
    fig = build_dashboard(data)

    # Option A: open in browser
    fig.show()

    # Option B: save as portable HTML file
    fig.write_html("macro_dashboard.html", include_plotlyjs="cdn")
    print("Saved macro_dashboard.html")

Run it from the terminal:

python dashboard.py

Plotly will open a browser window showing a two-row, two-column dashboard with four live panels — one for each indicator — colour-coded by currency. Hovering over any line reveals the exact date and value for that release.

Add more indicators in seconds

The dashboard is designed to scale. Add any entry to the INDICATORS dict — for example "core_inflation": "Core CPI (% YoY)" or "gdp_quarterly": "GDP Growth (% QoQ)" — and the fetch, shape, and chart steps all pick it up automatically. The full indicator catalogue is available in the API documentation.

Step 7 — Add Release-Timing Annotations (Optional)

One of the most useful dashboard enhancements is overlaying release markers — vertical lines or dots that show exactly when each announcement was made. FXMacroData carries second-level announcement_datetime timestamps, so you can add them without any guesswork:

def fetch_release_datetimes(currency: str, indicator: str, start: str) -> pd.Series:
    """Return a Series of UTC announcement datetimes for a given indicator."""
    url = f"{BASE_URL}/announcements/{currency}/{indicator}"
    resp = requests.get(url, params={"api_key": API_KEY, "start": start}, timeout=15)
    resp.raise_for_status()
    records = resp.json().get("data", [])
    if not records:
        return pd.Series(dtype="datetime64[ns, UTC]")
    df = pd.DataFrame(records)
    if "announcement_datetime" not in df.columns:
        return pd.Series(dtype="datetime64[ns, UTC]")
    return pd.to_datetime(df["announcement_datetime"], utc=True)


def add_release_markers(fig: go.Figure, currency: str, indicator: str,
                         start: str, row: int, col: int) -> None:
    """Overlay vertical dashed lines on a subplot at each release datetime."""
    datetimes = fetch_release_datetimes(currency, indicator, start)
    for dt in datetimes:
        fig.add_vline(
            x=dt.timestamp() * 1000,  # Plotly uses ms since epoch for datetime axes
            line_width=1,
            line_dash="dot",
            line_color=COLORS[currency],
            opacity=0.35,
            row=row, col=col,
        )

Call add_release_markers after build_dashboard and before fig.show() to annotate whichever panels are most relevant to your analysis. Release markers are especially useful on the policy-rate panel, where decision dates are infrequent but high-impact.

Step 8 — Export Panels as Individual Images

If you want to include individual charts in a report or slide deck, Plotly can export each subplot as a PNG via Kaleido:

pip install kaleido
fig.write_image("macro_dashboard.png", width=1400, height=700, scale=2)

For per-panel exports, build each indicator as a standalone go.Figure using the same go.Scatter traces and call write_image individually. The fetch and shape functions developed in earlier steps work unchanged for single-panel figures.

Complete Script Reference

Below is the full, self-contained script combining all steps above. Copy it into a file called dashboard.py, set FXMD_API_KEY, and run it.

"""
macro_dashboard.py — G4 Central Bank Macro Dashboard
Requires: requests, pandas, plotly
Usage:    FXMD_API_KEY=your_key python macro_dashboard.py
"""
import os
import time
import requests
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

BASE_URL = "https://fxmacrodata.com/api/v1"
API_KEY = os.environ["FXMD_API_KEY"]

CURRENCIES = ["usd", "eur", "gbp", "aud"]
INDICATORS = {
    "policy_rate": "Policy Rate (%)",
    "inflation": "CPI Inflation (% YoY)",
    "unemployment": "Unemployment Rate (%)",
    "pmi": "Manufacturing PMI",
}
COLORS = {"usd": "#3B82F6", "eur": "#D97706", "gbp": "#16A34A", "aud": "#7C3AED"}


def fetch_indicator(currency: str, indicator: str, start: str = "2020-01-01") -> pd.Series:
    url = f"{BASE_URL}/announcements/{currency}/{indicator}"
    resp = requests.get(url, params={"api_key": API_KEY, "start": start}, timeout=15)
    resp.raise_for_status()
    records = resp.json().get("data", [])
    if not records:
        return pd.Series(name=f"{currency.upper()}_{indicator}", dtype=float)
    return (
        pd.DataFrame(records)
        .assign(date=lambda df: pd.to_datetime(df["date"]))
        .set_index("date")["val"]
        .sort_index()
        .rename(f"{currency.upper()}_{indicator}")
    )


def fetch_all(start: str = "2020-01-01", retries: int = 3) -> dict[str, pd.DataFrame]:
    frames: dict[str, list[pd.Series]] = {ind: [] for ind in INDICATORS}
    for currency in CURRENCIES:
        for indicator in INDICATORS:
            for attempt in range(retries):
                try:
                    frames[indicator].append(fetch_indicator(currency, indicator, start))
                    break
                except requests.HTTPError as exc:
                    if attempt == retries - 1:
                        print(f"Warning: {currency}/{indicator}: {exc}")
                    else:
                        time.sleep(1.5 ** attempt)
    return {ind: pd.concat(series, axis=1) for ind, series in frames.items() if series}


def prepare_for_chart(df: pd.DataFrame) -> pd.DataFrame:
    return df.ffill().resample("ME").last().dropna(how="all")


def build_dashboard(data: dict[str, pd.DataFrame]) -> go.Figure:
    fig = make_subplots(
        rows=2, cols=2,
        subplot_titles=list(INDICATORS.values()),
        vertical_spacing=0.12,
        horizontal_spacing=0.08,
    )
    for (indicator, _), (row, col) in zip(INDICATORS.items(), [(1,1),(1,2),(2,1),(2,2)]):
        df = prepare_for_chart(data[indicator])
        for col_name in df.columns:
            currency = col_name.split("_")[0].lower()
            fig.add_trace(
                go.Scatter(
                    x=df.index, y=df[col_name], mode="lines",
                    name=currency.upper(),
                    line=dict(color=COLORS[currency], width=2),
                    legendgroup=currency,
                    showlegend=(indicator == "policy_rate"),
                    hovertemplate=f"%{{x|%b %Y}}: %{{y:.2f}}{currency.upper()}",
                ),
                row=row, col=col,
            )
    fig.update_layout(
        title=dict(text="G4 Central Bank Macro Dashboard", font=dict(size=22, color="#1e3a5f"), x=0.5),
        paper_bgcolor="#f8fafc", plot_bgcolor="#f1f5f9",
        font=dict(family="Inter, system-ui, sans-serif", size=12),
        legend=dict(orientation="h", yanchor="bottom", y=1.04, xanchor="center", x=0.5),
        height=700, margin=dict(t=100, b=50, l=60, r=40),
    )
    fig.update_xaxes(showgrid=True, gridcolor="#e2e8f0", zeroline=False)
    fig.update_yaxes(showgrid=True, gridcolor="#e2e8f0", zeroline=False)
    return fig


if __name__ == "__main__":
    print("Fetching macro data …")
    data = fetch_all(start="2021-01-01")
    print("Building dashboard …")
    fig = build_dashboard(data)
    fig.show()
    fig.write_html("macro_dashboard.html", include_plotlyjs="cdn")
    print("Saved macro_dashboard.html")

Summary

You now have a working macro dashboard that:

  • Fetches policy rates, inflation, unemployment, and PMI for USD, EUR, GBP, and AUD from the FXMacroData announcements endpoint
  • Reshapes the data into aligned, forward-filled pandas DataFrames
  • Renders a four-panel interactive Plotly dashboard with colour-coded currency lines
  • Exports a self-contained HTML file you can share or embed

From here you can extend the dashboard in several directions: add more currencies, overlay unemployment against PMI on dual axes, or wire the data into a scheduled job that refreshes the HTML export every morning. The full indicator catalogue — including trade balance, credit growth, and COT positioning — is available at api-data-docs.