fx-rates-correctness
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.
Works with
---
name: fx-rates-correctness
description: 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.
license: MIT
---
# 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:
```python
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:
```python
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 rateMore Debugging skills
diagnosing-bugs
mattpocock/skills
Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
explore-code
lllllllama/rigorpilot-skills
Rigor Improve implementation leaf skill for auditable candidate implementation in deep learning research repositories. Use when the researcher explicitly authorizes exploratory work on an isolated branch or worktree to transplant modules, adapt a backbone, add LoRA or adapter layers, replace a head, or stitch together meaningful low-risk migration ideas with rollback-aware records in `explore_outputs/`. Do not use for end-to-end exploration orchestration on top of `current_research`, trusted baseline reproduction, conservative debugging, environment setup, verified contribution claims, or default repository analysis.
safe-debug
lllllllama/rigorpilot-skills
Rigor Debug / Rigor Audit skill for deep learning research work. Use when the user pastes a traceback, terminal error, CUDA OOM, checkpoint load failure, shape mismatch, NaN loss symptom, or training failure and wants conservative diagnosis before any patching, with debug fixes clearly separated from research contributions. Do not use for broad refactoring, speculative adaptation, automatic exploratory patching, or general repository familiarization.

