diff --git a/modules/README.md b/modules/README.md index 28afd3bc..5881eb78 100644 --- a/modules/README.md +++ b/modules/README.md @@ -12,6 +12,7 @@ - [filesystem](#filesystem) - [formats](#formats) - [fun](#fun) + - [fxmacrodata](#fxmacrodata) - [github](#github) - [gitlab](#gitlab) - [jc](#jc) @@ -110,6 +111,12 @@ Examples of input/output formatters: - [wordle](./fun/wordle.nu) - A Terminal Wordle game. The code is based on this [gist](https://gist.github.com/huytd/6a1a6a7b34a0d0abcac00b47e3d01513), but slightly personalized. +## [fxmacrodata](./fxmacrodata/) + +Economic indicator releases, release calendars and FX rates from the FXMacroData API as Nushell tables +(`fxmacrodata catalogue`, `fxmacrodata announcements`, `fxmacrodata calendar`, `fxmacrodata forex`). +see [README](./fxmacrodata/) + ## github - [branch-protections](../sourced/github/branch-protections/) - Do you have hundreds or thousands of GitHub repositories in your organization? Are you tired of manually managing their branch protection rules? Don't! Let nushell do it for you! see [README](../sourced/github/branch-protections/README.md) diff --git a/modules/fxmacrodata/README.md b/modules/fxmacrodata/README.md new file mode 100644 index 00000000..df5fde64 --- /dev/null +++ b/modules/fxmacrodata/README.md @@ -0,0 +1,51 @@ +# fxmacrodata + +Commands for the [FXMacroData](https://fxmacrodata.com/?utm_source=github&utm_medium=referral&utm_campaign=nu_scripts&utm_content=readme) REST API: economic indicator releases (CPI, payrolls, GDP, policy rates, ...), release calendars and FX spot rates, returned as Nushell tables with `datetime` columns. + +```nushell +use modules/fxmacrodata +``` + +| Command | Description | +|---------|-------------| +| `fxmacrodata catalogue [currency]` | indicators available for a currency (default `usd`) | +| `fxmacrodata announcements ` | release history for one indicator, most recent first. `--start`, `--end` (YYYY-MM-DD), `--limit` | +| `fxmacrodata calendar [currency]` | upcoming scheduled releases. `--indicator` to filter | +| `fxmacrodata forex ` | daily FX spot rates. `--start`, `--end`, `--limit` (needs an API key) | + +Every command accepts `--raw` to return the unmodified JSON response instead of a table. + +## API key + +USD indicators and the USD calendar work without a key. Keyless history covers roughly the last 90 days and new releases show up after a short delay; when either of those changes what you get back, a notice is printed to stderr. Other currencies and `forex` need a key: + +```nushell +$env.FXMACRODATA_API_KEY = "..." +``` + +The key is only ever sent in the `X-API-Key` request header. + +## Examples + +```nushell +> fxmacrodata announcements usd inflation --limit 5 | update date { format date "%Y-%m-%d" } +fxmacrodata: Anonymous access returns the most recent 90 days. Supply an API key for the full history. +╭───┬────────────┬───────┬──────────┬──────────────┬────────╮ +│ # │ date │ value │ previous │ released │ source │ +├───┼────────────┼───────┼──────────┼──────────────┼────────┤ +│ 0 │ 2026-08-31 │ 3.40 │ 3.40 │ 3 weeks ago │ BLS │ +│ 1 │ 2026-07-31 │ 3.40 │ │ 2 months ago │ BLS │ +╰───┴────────────┴───────┴──────────┴──────────────┴────────╯ + +> fxmacrodata calendar usd --indicator inflation +╭───┬──────────────┬───────────┬─────────────────┬────────────┬───────────╮ +│ # │ release_time │ indicator │ name │ importance │ confirmed │ +├───┼──────────────┼───────────┼─────────────────┼────────────┼───────────┤ +│ 0 │ in a week │ inflation │ Inflation (CPI) │ high │ true │ +│ 1 │ in a month │ inflation │ Inflation (CPI) │ high │ true │ +│ 2 │ in 2 months │ inflation │ Inflation (CPI) │ high │ true │ +╰───┴──────────────┴───────────┴─────────────────┴────────────┴───────────╯ + +# high-importance US releases in the next two weeks +> fxmacrodata calendar usd | where importance == high and release_time < ((date now) + 2wk) +``` diff --git a/modules/fxmacrodata/mod.nu b/modules/fxmacrodata/mod.nu new file mode 100644 index 00000000..4a0d827a --- /dev/null +++ b/modules/fxmacrodata/mod.nu @@ -0,0 +1,207 @@ +# FXMacroData API wrapper +# +# Economic indicator releases, release calendars and FX spot rates from +# https://api.fxmacrodata.com, returned as Nushell tables. +# +# USD data works without an API key. Other currencies and the FX endpoint need +# a key, which is read from `$env.FXMACRODATA_API_KEY` and sent in the +# `X-API-Key` header. + +const BASE_URL = "https://api.fxmacrodata.com/v1" + +def "nu-complete fxmacrodata currencies" [] { + [ + aud brl cad chf cnh cny dkk eur gbp huf ils + jpy krw myr ngn nok nzd pen sek thb twd usd + ] +} + +def api-headers []: nothing -> record { + let key = $env.FXMACRODATA_API_KEY? | default "" | str trim + if ($key | is-empty) { {} } else { {X-API-Key: $key} } +} + +# GET a path under /v1, dropping null query parameters +def api-get [path: string, query: record = {}]: nothing -> any { + let params = $query + | transpose key value + | where value != null + | each {|p| {key: $p.key, value: ($p.value | into string)} } + let url = if ($params | is-empty) { + $"($BASE_URL)($path)" + } else { + $"($BASE_URL)($path)?($params | transpose -r -d | url build-query)" + } + + # never follow redirects, so the key is not re-sent to another host + let response = http get --full --allow-errors --redirect-mode error --headers (api-headers) $url + if $response.status >= 400 { + let detail = if ($response.body | describe) =~ "^record" { + $response.body.detail? | default ($response.body | to json -r) + } else { + $response.body | into string + } + error make --unspanned { + msg: $"FXMacroData request failed with HTTP ($response.status): ($detail)" + } + } + let body = $response.body + if ($body | describe) !~ "^record" or ($body.detail? != null and $body.data? == null) { + error make --unspanned { + msg: "FXMacroData returned an unexpected response" + } + } + $body +} + +# Print free-tier notices to stderr, but only when they affected the result: +# the history window when it cut rows from this page, the release delay when +# it withheld a release +def report-free-tier [body: record] { + let window = $body.freemium_window? | default {} + let page = $body.pagination? | default {} + let returned = $page.returned_count? | default ($body.data? | default [] | length) + let trimmed = ( + ($window.applied? | default false) + and not ($page.has_more? | default false) + and $returned < ($page.limit? | default 0) + ) + if $trimmed { + print --stderr $"fxmacrodata: ($window.message? | default 'free tier history is limited')" + } + let delay = $body.freemium_delay? | default {} + let withheld = $delay.withheld_count? | default 0 + if ($delay.applied? | default false) and $withheld > 0 { + print --stderr $"fxmacrodata: ($delay.message? | default 'free tier data is delayed') \(($withheld) release\(s\) withheld\)" + } +} + +def epoch-to-datetime []: any -> any { + if $in == null { null } else { $in * 1_000_000_000 | into datetime } +} + +def date-to-datetime []: any -> any { + if ($in | is-empty) { null } else { $"($in)T00:00:00+00:00" | into datetime } +} + +# List the indicators published for a currency +# +# Each row is one indicator slug that can be passed to `fxmacrodata announcements`. +@example "USD indicators that have recent data" { fxmacrodata catalogue usd | where has_recent_data } +export def catalogue [ + currency: string@"nu-complete fxmacrodata currencies" = "usd" # three-letter currency code + --raw # return the unmodified JSON response +]: nothing -> any { + let body = api-get $"/data_catalogue/($currency | str lowercase)" + if $raw { return $body } + + $body + | transpose indicator meta + | each {|row| + let cov = $row.meta.coverage? | default {} + { + indicator: $row.indicator + name: $row.meta.name? + unit: $row.meta.unit? + frequency: $row.meta.frequency? + source: $row.meta.source? + earliest: ($cov.earliest_available_date? | date-to-datetime) + latest: ($cov.latest_available_date? | date-to-datetime) + latest_release: ($cov.latest_release_date? | date-to-datetime) + has_recent_data: $cov.has_recent_data? + requires_api_key: $cov.requires_api_key? + } + } +} + +# Get release history for one indicator, most recent first +# +# `date` is the reference period and `released` is when the figure was +# published. Without an API key, USD history covers roughly the last 90 days +# and new releases appear after a short delay; a notice is printed to stderr +# when either of those changes the result. +@example "Last 12 US CPI releases" { fxmacrodata announcements usd inflation --limit 12 } +@example "US payrolls since the start of 2025 (older history needs an API key)" { fxmacrodata announcements usd non_farm_payrolls --start 2025-01-01 } +export def announcements [ + currency: string@"nu-complete fxmacrodata currencies" # three-letter currency code + indicator: string # indicator slug, see `fxmacrodata catalogue` + --start: string # first period date, YYYY-MM-DD + --end: string # last period date, YYYY-MM-DD + --limit: int # maximum number of rows (1-100) + --raw # return the unmodified JSON response +]: nothing -> any { + let body = api-get $"/announcements/($currency | str lowercase)/($indicator)" { + start_date: $start + end_date: $end + limit: $limit + } + report-free-tier $body + if $raw { return $body } + + $body.data + | each {|row| + { + date: ($row.date | date-to-datetime) + value: $row.val + previous: $row.previous_value? + released: ($row.announcement_datetime? | epoch-to-datetime) + source: $row.source? + } + } +} + +# Get upcoming scheduled releases for a currency +@example "Upcoming US releases" { fxmacrodata calendar usd } +@example "Next US CPI releases" { fxmacrodata calendar usd --indicator inflation } +export def calendar [ + currency: string@"nu-complete fxmacrodata currencies" = "usd" # three-letter currency code + --indicator: string # only show releases for this indicator slug + --raw # return the unmodified JSON response +]: nothing -> any { + let body = api-get $"/calendar/($currency | str lowercase)" {indicator: $indicator} + if $raw { return $body } + + $body.data + | each {|row| + { + release_time: ($row.announcement_datetime | epoch-to-datetime) + indicator: $row.release + name: $row.name? + importance: $row.event_importance? + confirmed: $row.release_date_confirmed? + } + } +} + +# Get daily FX spot rates for a currency pair, most recent first +# +# Requires an API key in `$env.FXMACRODATA_API_KEY`. +@example "Last 30 EUR/USD fixes" { fxmacrodata forex eur usd --limit 30 } +export def forex [ + base: string@"nu-complete fxmacrodata currencies" # base currency code + quote: string@"nu-complete fxmacrodata currencies" # quote currency code + --start: string # first date, YYYY-MM-DD + --end: string # last date, YYYY-MM-DD + --limit: int # maximum number of rows (1-100) + --raw # return the unmodified JSON response +]: nothing -> any { + if ($env.FXMACRODATA_API_KEY? | is-empty) { + error make --unspanned { + msg: "fxmacrodata forex needs an API key: set $env.FXMACRODATA_API_KEY" + } + } + let body = api-get $"/forex/($base | str lowercase)/($quote | str lowercase)" { + start_date: $start + end_date: $end + limit: $limit + } + if $raw { return $body } + + $body.data + | each {|row| + { + date: ($row.date | date-to-datetime) + rate: $row.val + } + } +}