Building an Interactive Dashboard to Visualize FTSE MIB (FTSEMIB) Performance with Vega-Lite and FastAPI
You need to ship a small, interactive dashboard that charts FTSE MIB performance and updates during the trading week. By the end of this guide, you’ll have a FastAPI backend that proxies Indices-API requests for FTSEMIB and a Vega-Lite frontend that renders a live chart, with sensible caching and guardrails for non-trading days.
What we’re building and which endpoints we’ll use
We’ll display a real-time snapshot and a daily history line chart for the FTSE MIB (symbol: FTSEMIB). We’ll combine:
- Latest endpoint: to show “right now” status and update the chart on refresh.
- Time-series endpoint: to backfill a historical series over a date range for the line chart.
- Optionally, OHLC endpoint: to switch the chart into a candlestick view per day when needed.
You can confirm the exact symbol code for your index on the Indices-API Supported Symbols page. We’ll use FTSEMIB here.
If you’re new to the API, start at the homepage indices-api.com and the full Indices-API Documentation. You can get a free key here: Register.
Data model essentials you must handle in the UI
- Base currency: API responses are relative to USD by default (base = USD). You’ll see rates keyed by symbols.
- How quotes are expressed: rates are “per index,” meaning with base=USD, a rate is the price of one index unit in USD terms. For charting the index level itself, invert the rate: level = 1 / rates["FTSEMIB"].
- Timestamps: timestamp is a UNIX epoch (seconds). date is an ISO date string.
- Trading days: Indices don’t print new values on weekends/holidays; your time-series will show gaps. Don’t expect updates on non-trading days.
- Update cadence: depending on plan, latest may update every 60 minutes, 10 minutes, or similar intervals. Cache accordingly to avoid excessive polling.
- end_date cannot be “today”: time-series and certain historical queries typically require past dates. Set end_date to yesterday to avoid empty data for markets still open or for late settlements.
Project layout and runtime flow
We’ll separate concerns:
- FastAPI service: two endpoints:
- /api/ftsemib/latest: returns the latest FTSEMIB reading and derived level.
- /api/ftsemib/timeseries?start=YYYY-MM-DD&end=YYYY-MM-DD: returns daily levels for charting.
- Vega-Lite frontend: fetches JSON from FastAPI, then renders a line or candlestick chart.
FastAPI will also:
- Add simple in-memory caching (e.g., 60s–300s) to stay within update frequencies.
- Validate dates and adjust end to yesterday when the user inputs today.
One-minute test: call the latest endpoint for FTSEMIB
Copy-paste the curl request below, replacing YOUR_API_KEY with your key. This fetches the latest FTSEMIB quote with base=USD. If you have not created an API key yet, get one here: Indices-API Register.
curl -s "https://indices-api.com/latest?access_key=YOUR_API_KEY&symbols=FTSEMIB&base=USD"
Sample response (use these exact values to test parsing):
{"data":{"success":true,"timestamp":1791769200,"date":"2026-10-12","base":"USD","rates":{"FTSEMIB":1.7921534193832e-5,"USD":1,"USDFTSEMIB":55798.79429876974}}}
What you’ll use from this payload:
- data.success: a quick check before parsing.
- data.timestamp and data.date: to display “as of” and for cache keys.
- data.base: ensure the base is USD if you expect to invert rates for an index level.
- data.rates.FTSEMIB: USD per 1 index unit. To get the index level you usually chart, compute level = 1 / data.rates.FTSEMIB.
FastAPI backend: proxy, normalize, and cache
The backend will expose two routes: latest and time-series. It will:
- Forward requests to Indices-API with your access_key as a query parameter.
- Normalize output to a consistent format your Vega-Lite spec expects (e.g., an array of {date, level}).
- Cache successful responses for N seconds to align with your plan’s update frequency and reduce bandwidth.
Python (FastAPI) example: latest FTSEMIB endpoint
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
import httpx
import os
import time
from datetime import date, timedelta
API_BASE = "https://indices-api.com"
ACCESS_KEY = os.getenv("INDICES_API_KEY", "YOUR_API_KEY")
CACHE_TTL = 120 # seconds; adjust to your plan’s update cadence
app = FastAPI(title="FTSEMIB Dashboard API")
_cache = {"latest": {"ts": 0, "payload": None}}
def invert_rate_to_level(rate: float) -> float:
# Convert USD-per-index-unit to an index level number.
# Protect against divide-by-zero if rate is absent or zero.
return (1.0 / rate) if rate else None
@app.get("/api/ftsemib/latest")
async def latest_ftsemib():
now = time.time()
if (_cache["latest"]["payload"] is not None) and (now - _cache["latest"]["ts"] < CACHE_TTL):
return JSONResponse(_cache["latest"]["payload"])
url = f"{API_BASE}/latest"
params = {"access_key": ACCESS_KEY, "symbols": "FTSEMIB", "base": "USD"}
async with httpx.AsyncClient(timeout=10) as client:
r = await client.get(url, params=params)
if r.status_code != 200:
raise HTTPException(status_code=502, detail="Upstream error")
raw = r.json()
# Expect the structure shown in the sample JSON with a 'data' wrapper.
data = raw.get("data", raw)
if not data.get("success"):
raise HTTPException(status_code=502, detail="API returned success=false")
rates = data.get("rates", {})
ftsemib_rate = rates.get("FTSEMIB")
level = invert_rate_to_level(ftsemib_rate)
payload = {
"as_of_date": data.get("date"),
"as_of_timestamp": data.get("timestamp"),
"base": data.get("base"),
"ftsemib_rate_usd_per_index": ftsemib_rate,
"ftsemib_level": level
}
_cache["latest"] = {"ts": now, "payload": payload}
return JSONResponse(payload)
Notes:
- We accept either a root-level or data-wrapped structure and then standardize to a consistent payload.
- We invert the rate so the frontend can directly plot “index level.”
- We cache for 120 seconds (tune this to your plan’s refresh interval; see Documentation).
Python (FastAPI) example: time-series FTSEMIB route
The time-series route returns an array of daily levels to drive a Vega-Lite line chart. Remember to use end_date no later than yesterday, as endpoints generally don’t return partial “today” series.
from typing import List, Dict
from datetime import datetime
@app.get("/api/ftsemib/timeseries")
async def timeseries_ftsemib(start: str, end: str):
# Validate and adjust dates
try:
start_dt = datetime.strptime(start, "%Y-%m-%d").date()
end_dt = datetime.strptime(end, "%Y-%m-%d").date()
except ValueError:
raise HTTPException(status_code=400, detail="Dates must be YYYY-MM-DD")
today = date.today()
if end_dt >= today:
end_dt = today - timedelta(days=1)
if start_dt > end_dt:
raise HTTPException(status_code=400, detail="start must be on/before end")
# Fetch time-series
url = f"{API_BASE}/timeseries"
params = {
"access_key": ACCESS_KEY,
"base": "USD",
"symbols": "FTSEMIB",
"start_date": start_dt.isoformat(),
"end_date": end_dt.isoformat()
}
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(url, params=params)
if r.status_code != 200:
raise HTTPException(status_code=502, detail="Upstream error")
raw = r.json()
data = raw.get("data", raw)
if not data.get("success"):
raise HTTPException(status_code=502, detail="API returned success=false")
rates_by_date: Dict[str, Dict[str, float]] = data.get("rates", {})
rows: List[Dict] = []
for d, symbols in sorted(rates_by_date.items()):
rate = symbols.get("FTSEMIB")
level = invert_rate_to_level(rate)
if level:
rows.append({"date": d, "level": level})
return {"series": rows, "start_date": data.get("start_date", start_dt.isoformat()),
"end_date": data.get("end_date", end_dt.isoformat()), "base": data.get("base", "USD")}
Notes:
- We normalize to a flat array of {date, level}.
- We adjust end to yesterday if the caller passes today.
- For weekends, you’ll see gaps—Vega-Lite can handle missing dates or you can densify on the client if desired.
Vega-Lite: line chart of FTSEMIB levels
The frontend fetches the /api/ftsemib/latest for the top “as of” badge and /api/ftsemib/timeseries for the chart’s data. Then a small Vega-Lite spec renders a line chart of the index level by date.
<script type="module">
import embed from "https://cdn.jsdelivr.net/npm/vega-embed@6?module";
async function loadSeries() {
const params = new URLSearchParams({
start: "2026-08-01",
end: "2026-10-11" // avoid 'today'
});
const r = await fetch(`/api/ftsemib/timeseries?${params.toString()}`);
if (!r.ok) throw new Error("Failed to load series");
return r.json();
}
async function loadLatest() {
const r = await fetch("/api/ftsemib/latest");
if (!r.ok) throw new Error("Failed to load latest");
return r.json();
}
(async () => {
const [{ series }, latest] = await Promise.all([loadSeries(), loadLatest()]);
document.querySelector("#asof").textContent =
`As of ${latest.as_of_date} (timestamp ${latest.as_of_timestamp}), FTSEMIB level ≈ ${latest.ftsemib_level.toFixed(2)}`;
const spec = {
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
width: "container",
height: 320,
data: { values: series },
mark: { type: "line", interpolate: "monotone" },
encoding: {
x: { field: "date", type: "temporal", title: "Date" },
y: { field: "level", type: "quantitative", title: "FTSE MIB Level" },
tooltip: [
{ field: "date", type: "temporal" },
{ field: "level", type: "quantitative", format: ".2f" }
]
}
};
await embed("#chart", spec, { actions: false });
})();
</script>
<div id="asof">Loading...</div>
<div id="chart"></div>
Vega-Lite tips:
- Use monotone interpolation for a smooth daily line (market closes create step-wise patterns otherwise).
- If you switch to candles, wire a separate endpoint using the OHLC API and a Vega-Lite layered spec (rule + bars).
Comparing the endpoints you’ll wire up
| Endpoint | Purpose | Key request params | Response fields to use | Notes |
|---|---|---|---|---|
| latest | Show current FTSEMIB reading | access_key, base=USD, symbols=FTSEMIB | success, timestamp, date, base, rates.FTSEMIB | Cache ~60–600s. Invert rate to get level. |
| timeseries | Backfill historical daily series | access_key, base=USD, symbols=FTSEMIB, start_date, end_date | success, start_date, end_date, base, rates[date].FTSEMIB | end_date cannot be today. Expect gaps on weekends/holidays. |
| open-high-low-close (OHLC) | Candlestick chart per day | access_key, base=USD, date | success, timestamp, base, date, rates.FTSE...{open,high,low,close} | Switch chart mode based on a UI toggle. |
Handling base currency, units, and conversion math
All examples here assume base=USD. With base=USD, FTSEMIB in rates is USD per index unit. If you want the “index level” as commonly displayed on terminals, invert the rate:
- level = 1 / rates["FTSEMIB"]
Keep the “unit” semantics consistent across your app. If you later request a different base, revisit your math and labeling. Surface base and units in the UI (e.g., a subtitle that reads “Base USD; level derived from USD-per-index conversion”).
Practical guardrails that save time
- Cache thoughtfully: latest updates at plan-specific intervals; caching for 60–300 seconds will avoid unnecessary calls.
- Don’t ask for today in end_date: many feeds don’t finalize until market close. Use yesterday or handle empty-day responses.
- Weekend/holiday gaps: let Vega-Lite render time on the x-axis as temporal so gaps are natural. Alternatively, forward-fill in your backend if your users expect a continuous curve.
- Resilience: if success=false or rates.FTSEMIB is missing/zero, show a small banner in the UI and fall back to the last cached series.
- Symbols: rely on the authoritative Symbols list when you expand to other indices. Don’t hardcode guesses.
Optional: Add an OHLC mode
Once your time-series line is stable, add a toggle to request OHLC by date and render a candlestick. You’ll read:
- rates.FTSE...open, high, low, close for the specified date
For implementation details, see the Documentation and build a layered Vega-Lite spec (rule for high-low, bars for open-close).
Deployment and production notes
- Secrets: store ACCESS_KEY in environment variables. Don’t leak it to the browser; always call Indices-API server-side from FastAPI.
- CORS: serve the Vega-Lite page from the same origin as FastAPI or configure CORS explicitly.
- Compression: enable gzip on FastAPI responses to speed up large time-series transfers.
- Monitoring: log status_code, success, and response latency from upstream. Set alerts for repeated success=false.
- Docs and support: keep the API reference handy at indices-api.com and the detailed Documentation.
FAQ
Q: My time-series is empty when I set end_date to today. Why?
A: Use yesterday for end_date. Intraday or unsettled data may not be returned for “today.” Adjust in your backend when users pass today.
Q: Do I need to convert the FTSEMIB rate to get an index level?
A: With base=USD, the rate is USD per index unit. For a conventional index level chart, invert it: level = 1 / rates["FTSEMIB"]. Label clearly in the UI.
Q: How often can I poll the latest endpoint?
A: The latest refresh frequency depends on your plan (e.g., every 60 or 10 minutes). Cache accordingly and avoid polling more frequently than updates occur.
Q: How should I handle weekends and holidays?
A: Expect no new data. Let gaps appear naturally on the temporal x-axis, or forward-fill on the server if your audience prefers a continuous line.
Q: Where can I confirm the FTSE MIB symbol code?
A: Use the authoritative Symbols page. Do not guess codes.
Build your FTSE MIB dashboard today: get your API key from the Register page, explore the Documentation, and check MCP for platform integration patterns you can reuse.