agentsclimarketplace

Fx rates correctness

Skill nusantara-ventures/exchangerate-skills/skills/fx-rates-correctness

Agent Skills for exchangerate.dev — FX rates API. Currency conversion, FX correctness, portfolio translation, multi-currency pricing. Works keyless.

Install
npx -y skills add nusantara-ventures/exchangerate-skills --skill fx-rates-correctness

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Correctness pitfalls when working with foreign-exchange rate data in code — weekend and holiday gaps, forward-filled historical rates, timezone-of-record ambiguity, per-currency decimal precision and rounding, triangulated (derived) cross rates, and stale-rate detection. Use this skill whenever code fetches, stores, joins, backtests, or displays currency exchange rates — even a "simple" USD-to-EUR conversion — because naive FX handling produces silently wrong numbers, not errors. Also use when debugging why converted amounts, historical FX joins, or currency charts look wrong.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

7.4 KB, as published. Nobody here has run it

FX rates correctness

Exchange-rate bugs rarely throw. They return a plausible number that is wrong — a Friday rate silently used for Sunday, a JPY amount rounded like EUR, a backtest joining Tokyo closes against a CET fix. This skill catalogs the failure modes and the code patterns that avoid them. Examples use exchangerate.dev (keyless, https://api.exchangerate.dev), but the pitfalls apply to any FX source.

1. There is no rate for Saturday

FX reference rates (ECB, FRED) publish on business days only. Two different API behaviors exist, and confusing them corrupts data:

  • Snapshot endpoints forward-fill. GET /v1/2024-01-14?base=USD&symbols=EUR (a Sunday) returns Friday's fix with "is_forward_filled": true and data_updated_at pointing at the Friday. If you store this row keyed by the Sunday date without checking the flag, your database now claims a Sunday fix existed.
  • Range endpoints omit. GET /v1/range?... returns business days only — weekends are absent rows, not nulls. A naive "365 rows per year" assumption breaks; so does positional alignment against a calendar array.

Pattern:

row = get_historical("2024-01-14", base="USD", symbols=["EUR"])
if row["is_forward_filled"]:
    # The requested date had no published fix. Decide explicitly:
    # - display/UX: fine to show, but label it ("as of Fri 2024-01-12")
    # - accounting/audit: use data_updated_at's date as the rate date, not the requested date
    # - research: usually better to drop the date or forward-fill deliberately in your own pipeline
    ...

The same trap exists on holidays (ECB holidays ≠ your market's holidays — Jan 1 has no fix, but neither does Easter Monday).

2. Forward-filling in research: fill AFTER the join, never before

For backtests and analytics, join FX to your asset data on exact dates first, then decide fill policy. Forward-filling FX before the join lets a Friday rate leak into Monday-morning calculations that should have used Monday's rate — look-ahead bias's quieter sibling. Standard pandas shape:

fx = fetch_range(base="USD", symbols=["EUR"], start_date=..., end_date=...)  # business days only
df = prices.join(fx, how="left")       # exact-date join; weekend/holiday FX is NaN
df["EUR"] = df["EUR"].ffill()          # explicit, auditable fill as the LAST step

3. "The rate for date D" is ambiguous — pin the timezone-of-record

An ECB fix dated 2024-01-15 is a ~16:00 CET snapshot. A Tokyo close on the same calendar date happened 8 hours earlier; a New York close 6 hours later. Any join of FX against another time-series must state which timestamp convention both sides use. Ask: rate as of which market's close? If the answer matters (portfolio valuation, P&L), see the portfolio-currency-translation skill — mixing conventions shifts daily returns by up to a full day.

With intraday sources, use the response's own clock: exchangerate.dev returns timestamp and data_updated_at (UTC) on every call — store those alongside the rate instead of stamping rows with your server's local time.

4. Precision is per-pair, not global

JPY pairs quote at 2–3 decimal places; most others at 4–5. Two distinct concepts:

  • Rate precision (decimals) — how many places the quote carries. Blanket-rounding all rates to 4 dp destroys JPY-pair information and adds noise elsewhere.
  • Minor units (minor_units) — how many places a cash amount has: 2 for USD/EUR, 0 for JPY (there are no yen cents), 3 for KWD/BHD.

Get both from GET /v1/currencies ({"code": "JPY", "decimals": 3, "minor_units": 0}) instead of hard-coding. Rounding rules:

  • Round once, at the end of the calculation chain — never round the rate, then the product.
  • Round amounts to the target currency's minor units (a USD→JPY conversion yields whole yen).
  • Store amounts as integers in minor units or as decimals — never as binary floats. 0.1 + 0.2 != 0.3 bugs become real money here.
  • Prefer the API's converted field (already minor-unit-rounded server-side) over multiplying rate client-side.

5. Derived crosses are triangulated — know when that matters

Most FX APIs quote only a few dozen pairs natively; everything else is triangulated (EUR/GBP = EUR/USD ÷ GBP/USD). exchangerate.dev makes this visible: derived: true / derived_symbols, with the error bound in derivation_bps_max (typically 1–2 bps).

  • Display, pricing, dashboards: derived is fine — 1–2 bps is far inside the indicative-rate band.
  • Research comparing your numbers against Bloomberg/Reuters native crosses: filter derived_symbols or expect small systematic drift.
  • Doing your own triangulation? Divide full-precision rates and round once at the end; triangulating pre-rounded rates compounds the error.

6. "Current" is a claim — verify it before making it

A rate fetched on Sunday is not current, whatever your cache says. Check the session context before labeling:

  • market_session: open → live market, safe to present as current.
  • market_session: weekend → interbank closed; a source: live value is the last trading-week consensus. Display it, but label it ("last updated Fri 21:59 UTC").
  • market_session: interbank_closed → the post-Friday-close / pre-Sydney-open gap.
  • source: ecb_daily → a once-a-day fix that can be up to ~24 h old on a weekday and ~72 h old on Sunday — even though the API call succeeded seconds ago.

Cache accordingly: caching a live-session rate for 24 h is a staleness bug; re-fetching an ECB daily fix every minute is a rate-limit bug. Key cache TTL off source, and surface data_updated_at to users rather than "now".

7. Indicative vs settlement — don't let display rates touch money movement

Aggregated indicative rates (~5–15 bps band) are for display, analytics, and internal tools. The rate at which money actually moves comes from your payment provider/bank at execution time, including their spread. Never reconcile books against a display-rate conversion, and never promise a customer the indicative number as the transaction rate — quote it as an estimate. For rate-of-record conventions in invoicing and bookkeeping, use the fx-accounting-rates skill.

Quick checklist

Before shipping FX-touching code, verify:

  • Weekend/holiday dates handled explicitly (is_forward_filled checked, or absent range rows expected)
  • Research pipelines join on exact dates, fill afterwards
  • Timezone-of-record chosen and documented for any cross-series join
  • Precision from /v1/currencies, no hard-coded round(x, 4), no float money
  • Derived pairs identified and acceptable for the use case
  • UI labels weekend/stale rates instead of implying live
  • Cache TTL varies by source; 429s back off via x-ratelimit-reset
  • Nothing settles or reconciles against an indicative rate

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.