Riksbank Exchange Rates
Fetch daily exchange rates from the Swedish Riksbank API. No authentication required. Incremental loading by date.
models/staging/exchange_rates.sqlFetch daily exchange rates from the Swedish Riksbank (Riksbanken) SWEA API. No authentication, no API key. Incremental loading by date with automatic pagination per series.
Quick Start
1. Add the blueprint
Copy lib/riksbank.star into your project.
2. Create models
Raw model — one row per series per date:
-- models/raw/exchange_rates.sql
-- @kind: append
-- @incremental: date
-- @incremental_initial: 2026-04-01
SELECT series, date, value
FROM riksbank('SEKEURPMI,SEKUSDPMI,SEKGBPPMI,SEKNOKPMI,SEKDKKPMI,SEKCHFPMI,SEKJPYPMI')
The SQL arg controls which series to fetch. One API call per series.
Staging model — pivots to one row per date:
-- models/staging/exchange_rates.sql
-- @kind: table
-- @constraint: not_null(date)
SELECT
date::DATE AS date,
MAX(CASE WHEN series = 'SEKEURPMI' THEN value END)::DECIMAL AS eur,
MAX(CASE WHEN series = 'SEKUSDPMI' THEN value END)::DECIMAL AS usd,
MAX(CASE WHEN series = 'SEKGBPPMI' THEN value END)::DECIMAL AS gbp,
MAX(CASE WHEN series = 'SEKNOKPMI' THEN value END)::DECIMAL AS nok,
MAX(CASE WHEN series = 'SEKDKKPMI' THEN value END)::DECIMAL AS dkk,
MAX(CASE WHEN series = 'SEKCHFPMI' THEN value END)::DECIMAL AS chf,
MAX(CASE WHEN series = 'SEKJPYPMI' THEN value END)::DECIMAL AS jpy
FROM raw.exchange_rates
GROUP BY date
ORDER BY date
3. Run
ondatrasql run
[OK] raw.exchange_rates (append, backfill, 91 rows, 1m1s — first run)
[OK] staging.exchange_rates (table, backfill, 13 rows, 269ms — first run)
Raw — one row per observation:
| series | date | value |
|---|---|---|
| SEKCHFPMI | 2026-04-01 | 11.846 |
| SEKDKKPMI | 2026-04-01 | 1.457 |
| SEKEURPMI | 2026-04-01 | 10.888 |
| SEKGBPPMI | 2026-04-01 | 12.499 |
| SEKJPYPMI | 2026-04-01 | 0.059 |
| SEKNOKPMI | 2026-04-01 | 0.971 |
| SEKUSDPMI | 2026-04-01 | 9.382 |
Staging — pivoted, one row per date:
| date | eur | usd | gbp | nok | dkk | chf | jpy |
|---|---|---|---|---|---|---|---|
| 2026-04-01 | 10.888 | 9.382 | 12.499 | 0.971 | 1.457 | 11.846 | 0.059 |
| 2026-04-02 | 10.948 | 9.499 | 12.547 | 0.975 | 1.465 | 11.883 | 0.060 |
| 2026-04-07 | 10.990 | 9.509 | 12.595 | 0.983 | 1.471 | 11.891 | 0.059 |
91 raw rows (7 series × 13 trading days) → 13 pivoted rows.
4. Run again — only new dates
ondatrasql run raw.exchange_rates
[OK] raw.exchange_rates (append, incremental, 0 rows, 214ms — incremental run)
No API calls when data is up to date.
Available Series
| Series ID | Currency |
|---|---|
SEKEURPMI | Euro |
SEKUSDPMI | US Dollar |
SEKGBPPMI | British Pound |
SEKNOKPMI | Norwegian Krone |
SEKDKKPMI | Danish Krone |
SEKCHFPMI | Swiss Franc |
SEKJPYPMI | Japanese Yen |
SEKAUDPMI | Australian Dollar |
SEKCADPMI | Canadian Dollar |
SEKCNYPMI | Chinese Yuan |
Values are SEK per 1 unit of foreign currency. Full list at the Riksbank Series API.
Customization
Fewer currencies
Only need EUR and USD?
SELECT series, date, value
FROM riksbank('SEKEURPMI,SEKUSDPMI')
Two series = two API calls instead of seven.
Different start date
-- @incremental_initial: 2020-01-01
Downstream model — join with revenue
-- models/mart/revenue_sek.sql
-- @kind: table
SELECT
r.date,
r.amount,
r.currency,
r.amount * CASE r.currency
WHEN 'EUR' THEN fx.eur
WHEN 'USD' THEN fx.usd
WHEN 'GBP' THEN fx.gbp
END AS amount_sek
FROM staging.revenue r
JOIN staging.exchange_rates fx ON r.date = fx.date
DuckDB PIVOT syntax
Instead of MAX(CASE WHEN ...), DuckDB supports native PIVOT:
-- models/staging/exchange_rates.sql
-- @kind: table
PIVOT raw.exchange_rates
ON series
USING MAX(value)
GROUP BY date::DATE AS date
ORDER BY date
How It Works
- Blueprint fetches one series at a time from
GET /Observations/{seriesId}/{from}/{to} - Returns raw rows:
(series, date, value)— one per observation - Pagination cursor tracks which series and which date range is next
- Each series fetches up to 365 days per API call, then advances
- Rate limited to 2 requests per second (declared in API dict)
- On incremental runs, only dates after
MAX(date)are fetched - Staging model pivots series into columns using SQL
Blueprint
lib/riksbank.star
API = {
"base_url": "https://api.riksbank.se/swea/v1",
"retry": 3,
"backoff": 2,
"rate_limit": {"requests": 2, "per": "1s"},
"fetch": {
"args": ["series"],
"dynamic_columns": True,
},
}
def fetch(series, page, columns=[], target=""):
yesterday = _yesterday()
if incremental.is_backfill:
start_date = incremental.initial_value
else:
start_date = _next_day(incremental.last_value)
if start_date > yesterday:
abort()
# Fetch one series at a time, then advance to next series
series_list = [s.strip() for s in series.split(",")]
series_idx = 0
date_cursor = start_date
if page.cursor != None and page.cursor != "":
state = json.decode(page.cursor)
series_idx = state["idx"]
date_cursor = state["from"]
if series_idx >= len(series_list):
return {"rows": [], "next": None}
current_series = series_list[series_idx]
end_date = min(_add_days(date_cursor, 365), yesterday)
resp = http.get("/Observations/" + current_series + "/" + date_cursor + "/" + end_date)
if not resp.ok:
fail(current_series + " (" + date_cursor + " to " + end_date + "): " + resp.text)
rows = []
for obs in resp.json:
rows.append({
"series": current_series,
"date": obs["date"],
"value": obs["value"],
})
# Determine next cursor
if end_date < yesterday:
# More dates for this series
next_cursor = json.encode({"idx": series_idx, "from": _next_day(end_date)})
elif series_idx + 1 < len(series_list):
# Next series, reset date
next_cursor = json.encode({"idx": series_idx + 1, "from": start_date})
else:
next_cursor = None
return {"rows": rows, "next": next_cursor}
def _yesterday():
return (time.now() - time.parse_duration("24h")).format("2006-01-02")
def _next_day(d):
return (time.parse_time(d + "T00:00:00Z") + time.parse_duration("24h")).format("2006-01-02")
def _add_days(d, n):
return (time.parse_time(d + "T00:00:00Z") + time.parse_duration(str(n * 24) + "h")).format("2006-01-02")
def min(a, b):
if a < b:
return a
return b
Notes
- No authentication — the Riksbank SWEA API is public
- Rate limiting — 2 requests/second declared in API dict, runtime handles it
- One request per series — the API only accepts one series ID per call (max 25 characters)
- Pagination — JSON cursor tracks series index and date offset. Each call fetches up to 365 days for one series.
- Values — API returns floats (e.g.
10.7685), cast toDECIMALin staging - Weekends/holidays — no data points for non-trading days
- Pivot in SQL — raw stores normalized data (series, date, value). Staging pivots to one row per date using
MAX(CASE WHEN ...)or DuckDB’s nativePIVOT
See API Dict Reference for all configuration options.
OndatraSQL
