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.
npx -y skills add nusantara-ventures/exchangerate-skills --skill fx-rates-correctnessAssembled 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": trueanddata_updated_atpointing 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.3bugs become real money here. - Prefer the API's
convertedfield (already minor-unit-rounded server-side) over multiplyingrateclient-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_symbolsor 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; asource: livevalue 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_filledchecked, 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-codedround(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 viax-ratelimit-reset - Nothing settles or reconciles against an indicative rate