Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions modules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- [filesystem](#filesystem)
- [formats](#formats)
- [fun](#fun)
- [fxmacrodata](#fxmacrodata)
- [github](#github)
- [gitlab](#gitlab)
- [jc](#jc)
Expand Down Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions modules/fxmacrodata/README.md
Original file line number Diff line number Diff line change
@@ -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 <currency> <indicator>` | 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 <base> <quote>` | 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)
```
207 changes: 207 additions & 0 deletions modules/fxmacrodata/mod.nu
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading