Riksbank SWEA
Fetch financial data from the Swedish Riksbank SWEA API — exchange rates, interest rates, bonds, STIBOR. No authentication required. Incremental loading by date.
models/raw/exchange_rates.sqlFetch financial data from the Swedish Riksbank SWEA API. Exchange rates, interest rates, government bonds, STIBOR — any series available at api.riksbank.se. No authentication, no API key. Incremental loading by date.
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, 105 rows, 1m1s — first run)
[OK] staging.exchange_rates (table, backfill, 15 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 |
105 raw rows (7 series × 15 trading days) → 15 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
The blueprint works with any series from the Riksbank SWEA API. Pass series IDs as a comma-separated string in the SQL arg.
Exchange rates (SEK per unit)
| 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 |
SEKBRLPMI | Brazilian Real |
SEKCZKPMI | Czech Koruna |
SEKHKDPMI | Hong Kong Dollar |
SEKHUFPMI | Hungarian Forint |
SEKIDRPMI | Indonesian Rupiah |
SEKILSPMI | Israeli Shekel |
SEKINRPMI | Indian Rupee |
SEKISKPMI | Icelandic Krona |
SEKKRWPMI | South Korean Won |
SEKMXNPMI | Mexican Peso |
SEKMYRPMI | Malaysian Ringgit |
SEKNZDPMI | New Zealand Dollar |
SEKPHPPMI | Philippine Peso |
SEKPLNPMI | Polish Zloty |
SEKRONPMI | Romanian Leu |
SEKSGDPMI | Singapore Dollar |
SEKTHBPMI | Thai Baht |
SEKTRYPMI | Turkish Lira |
SEKZARPMI | South African Rand |
Interest rates
-- Riksbank policy rate
FROM riksbank('SECBREPOEFF')
-- STIBOR rates
FROM riksbank('SEDPT_NSTIBORDELAYC,SEDP3MSTIBORDELAYC,SEDP6MSTIBORDELAYC')
-- Reference and discount rates
FROM riksbank('SECBREFEFF,SECBDISCEFF')
Government bonds
-- Swedish government bonds (2, 5, 10 year)
FROM riksbank('SEGVB2YC,SEGVB5YC,SEGVB10YC')
-- Swedish treasury bills
FROM riksbank('SETB1MBENCHC,SETB3MBENCH,SETB6MBENCH')
-- International (US, Germany, Japan 10-year)
FROM riksbank('USGVB10Y,DEGVB10Y,JPGVB10Y')
Full list at api.riksbank.se/swea/v1/Series.
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 splits the series arg into individual series IDs
- 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, date range, and end date (
until) is next untilis pinned at pipeline start — stable if the run spans midnight- Each series fetches up to 365 days per API call, then advances
- On incremental runs, only dates after
MAX(date)are fetched - Staging model pivots series into columns using SQL
Blueprint
lib/riksbank.star
RESERVED_COLUMNS = ()
API = {
"base_url": "https://api.riksbank.se/swea/v1",
"retry": 3,
"backoff": 2,
"rate_limit": {"requests": 1, "per": "1s"},
"fetch": {
"args": ["series"],
"supported_kinds": ["append"],
},
}
def fetch(series, page, is_backfill=True, last_value="", initial_value=""):
if is_backfill:
start_date = _to_iso_date(initial_value)
else:
start_date = _next_day(_to_iso_date(last_value))
series_list = [s.strip() for s in series.split(",")]
series_idx = 0
date_cursor = start_date
# Pin yesterday at pipeline start — stable across midnight
yesterday = _yesterday()
if page.cursor != None:
series_idx = page.cursor["idx"]
date_cursor = page.cursor["from"]
yesterday = page.cursor["until"]
if date_cursor > yesterday:
abort()
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)
rows = []
if not resp.ok:
fail(current_series + " (" + date_cursor + " to " + end_date + "): " + resp.text)
# 200 = data, 204 = no observations in range (weekends, holidays, future)
if resp.status_code == 200:
for obs in resp.json:
rows.append({
"series": current_series,
"date": obs["date"],
"value": obs["value"],
})
# Advance: more dates for this series, or next series
if end_date < yesterday:
next_cursor = {"idx": series_idx, "from": _next_day(end_date), "until": yesterday}
elif series_idx + 1 < len(series_list):
next_cursor = {"idx": series_idx + 1, "from": start_date, "until": yesterday}
else:
next_cursor = None
return {"rows": rows, "next": next_cursor}
def _to_iso_date(d):
"""Normalize to YYYY-MM-DD. Handles YYYYMMDD, YYYY-MM-DD, and YYYY-MM-DDTHH:MM:SSZ."""
if len(d) >= 8 and d[4] != "-":
return d[:4] + "-" + d[4:6] + "-" + d[6:8]
return d[:10]
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")
Notes
- No authentication — the Riksbank SWEA API is public
- Rate limiting — 1 request/second declared in API dict, runtime handles it. The API returns HTTP 429 with a cooldown period if exceeded.
- One request per series — the API only accepts one series ID per call (max 25 characters)
- Pagination — dict cursor tracks series index, date offset, and pinned end date. Each call fetches up to 365 days for one series.
- 204 No Content — returned for ranges with no data (weekends, holidays, future dates). Blueprint handles this gracefully — no rows, no error.
- 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 - Runtime kwargs filtering — The blueprint only declares the parameters it uses. The runtime automatically filters kwargs to match.
See API Dict Reference for all configuration options.
OndatraSQL
