> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ntropii.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Currency conversion

> ntro.capabilities.fx — convert amounts between currencies at a dated exchange rate, with a two-layer cache so the external feed is hit at most once per (pair, date).

**`ntro.capabilities.fx`** converts a monetary amount from one currency to
another at the exchange rate for a given **value date**. Use it from an
activity when a runbook handles multi-currency figures — e.g. normalising
invoices to a reporting currency before export.

It complements [`CurrencyCode`](/sdk/typing), which only *validates* ISO-4217
codes; `fx` is what actually **converts**.

## Convert

```python theme={null}
from datetime import date
from decimal import Decimal
from ntro.capabilities import fx

res = await fx.convert(Decimal("100"), "EUR", "GBP", on=date(2026, 3, 14))

res.amount       # Decimal("84.50") — converted, rounded to 2dp
res.rate         # Decimal("0.845") — units of GBP per 1 EUR (full precision)
res.rate_date    # date(2026, 3, 14)
res.source       # "frankfurter.dev"
res.original_amount  # Decimal("100")
```

* `on` is the **value date**. Pass it for a historical rate (typically the
  document/transaction date); omit it for the latest available rate.
* Same-currency conversions are a local no-op (`rate=1`, `source="identity"`) —
  no network call.
* The converted `amount` is quantised to 2 decimal places; the raw `rate` is
  returned at full precision so callers can re-round (minor-unit-aware rounding,
  e.g. JPY = 0dp, is a future refinement).
* Raises `fx.FxError` on an unsupported currency pair or a feed failure.

Just need the rate? `fx.rate(from_ccy, to_ccy, on=...)` returns an `FxRate`
(`rate`, `rate_date`, `source`).

## Rates + caching

Rates come from a free, key-less dated feed (**frankfurter.dev**, backed by the
ECB reference rates), keyed by the value date.

Rates for a past date are **immutable**, so resolution is write-once-read-many
across two layers — the external feed is hit **at most once per `(base, quote,
date)`**:

| Layer  | Where                                      | Lifetime                        |
| ------ | ------------------------------------------ | ------------------------------- |
| **L1** | in-process dict in the capability          | the worker process              |
| **L2** | `system.fx_rates` in the tenant data plane | durable; shared across replicas |

L2 is **opt-in**: pass `tenant_slug` to enable the persisted cache.

```python theme={null}
# L1 + L2 (durable, write-through) — typical in a worker activity:
res = await fx.convert(amount, ccy, "GBP", on=expense_date, tenant_slug=ctx.tenant_slug)

# L1 only (e.g. a managed-agent sandbox with no data plane):
res = await fx.convert(amount, ccy, "GBP", on=expense_date)
```

<Note>
  **The cache is not the audit record.** Stamp the *applied* rate on the
  consuming row — e.g. `ExpenseRow.fx_rate` / `fx_rate_date` / source — so the
  figure stays reproducible even if the cache is cleared. The two-layer cache is
  purely a performance + rate-limit concern.
</Note>

## Return shapes

```python theme={null}
class ConversionResult(BaseModel):
    amount: Decimal          # converted, rounded to minor units
    original_amount: Decimal
    from_ccy: str
    to_ccy: str
    rate: Decimal            # units of to_ccy per 1 from_ccy
    rate_date: date
    source: str

class FxRate(BaseModel):
    base: str
    quote: str
    rate: Decimal
    rate_date: date
    source: str
```

## Related

<CardGroup cols={2}>
  <Card title="Typing" icon="types" href="/sdk/typing">
    `CurrencyCode` — the ISO-4217 validator the amounts carry.
  </Card>

  <Card title="expenses" icon="receipt" href="/sdk/subledgers/types/expenses">
    Where converted amounts land + the audit-stamp pattern.
  </Card>

  <Card title="Data capability" icon="database" href="/sdk/capabilities/data">
    The tenant data plane the L2 cache (`system.fx_rates`) is written through.
  </Card>
</CardGroup>
