FX Conversion¶
The calculator supports multi-currency portfolios by converting all monetary values to a base currency (GBP by default) before performing calculations.
Why FX Conversion Matters¶
Regulatory thresholds (SME turnover, retail exposure limits) are defined in EUR or GBP. Without consistent currency conversion, exposures in foreign currencies would be incorrectly assessed against these thresholds.
What Gets Converted¶
| Data Type | Converted Fields |
|---|---|
| Exposures | Drawn amount, undrawn amount, nominal amount |
| Collateral | Market value, nominal value |
| Guarantees | Covered amount |
| Provisions | Provision amount |
Configuration¶
FX conversion is controlled via CalculationConfig:
from rwa_calc.contracts.config import CalculationConfig
from datetime import date
# FX conversion enabled by default (base_currency is always GBP)
config = CalculationConfig.crr(
reporting_date=date(2026, 12, 31),
apply_fx_conversion=True, # Enable/disable
)
Input Data¶
FX rates are provided via the fx_rates input table:
| Column | Type | Description |
|---|---|---|
currency_from |
String | Currency code (e.g., "USD", "EUR") |
currency_to |
String | Target currency (must match base_currency) |
rate |
Float | Exchange rate (source to target) |
Example FX Rates¶
fx_rates = pl.DataFrame({
"currency_from": ["USD", "EUR", "CHF"],
"currency_to": ["GBP", "GBP", "GBP"],
"rate": [0.79, 0.87, 0.89],
})
Pipeline Integration¶
FX conversion occurs early in the pipeline, during hierarchy resolution, so that all downstream calculations (threshold checks, collateral haircuts, RWA) use consistent GBP values.
flowchart TD
A[Raw Data - Mixed Currencies] --> B[FX Conversion]
B --> C[Hierarchy Resolution]
C --> D[Classification - GBP thresholds]
D --> E[CRM - GBP haircuts]
E --> F[Calculators - GBP RWA]
Audit Trail¶
The converter preserves original values for audit:
| Audit Field | Description |
|---|---|
original_currency |
Currency before conversion |
original_amount |
Amount before conversion |
fx_rate_applied |
Rate used for conversion |
Missing FX Rates¶
If a currency's FX rate is not provided:
- Values are left unchanged (not converted)
- fx_rate_applied is set to null
- No error is raised (graceful handling)
Regulatory Thresholds¶
Key thresholds that depend on correct FX conversion:
| Threshold | EUR Value | GBP Equivalent |
|---|---|---|
| SME turnover | EUR 50m | ~GBP 43.7m |
| SME exposure (SF tier) | EUR 2.5m | ~GBP 2.18m |
| Retail aggregate (CRR) | EUR 1m | ~GBP 873k (at 0.8732) |
| QRRE individual | EUR 100k | ~GBP 100k |
The EUR-to-GBP rate is configurable and defaults to 0.8732.
Basel 3.1 Fixed GBP Thresholds
Under Basel 3.1 (PRA PS1/26), the retail aggregate threshold is replaced with a fixed GBP 880,000 (Art. 123(1)(b)(ii)) and the QRRE individual limit with GBP 90,000 (Art. 147(5A)(c)). These do not require FX conversion. The table above applies to CRR only.
Auto-sync of eur_gbp_rate from the FX table¶
config.eur_gbp_rate has two separate uses under CRR:
- Deriving GBP equivalents of EUR regulatory thresholds from the rulepack pack EUR bases at read time (
engine/thresholds.py). - Converting GBP turnover to EUR inside the IRB SME correlation formula (CRR Art. 153(4)).
Exposure/collateral/guarantee/provision amounts are converted using the loaded fx_rates table (above). Historically, these two mechanisms did not talk to each other — a user could load an up-to-date fx_rates.parquet with (EUR, GBP, 0.90) and get all amounts converted at 0.90 while the SME correlation and derived GBP thresholds silently continued to use the default 0.8732.
The pipeline now auto-syncs config.eur_gbp_rate with the loaded table:
- If the
fx_ratesinput contains exactly one(currency_from="EUR", currency_to="GBP")row, the orchestrator replacesconfig.eur_gbp_rateviaconfig.with_fx_rate(derived_rate)(which carries only the rate); GBP thresholds are then re-derived from the pack EUR bases × the new rate at read time (engine/thresholds.py). - If the passed-in rate and the table rate differ, a
WARNINGis logged onrwa_calc.engine.pipelinenaming both values, so the mismatch is visible in the audit trail. - If the table contains more than one
(EUR, GBP)row, aWARNINGis logged onrwa_calc.engine.fx_rate_syncand the auto-sync is skipped — the caller's rate stands. - Under Basel 3.1, this is a no-op (thresholds are GBP-native per PRA PS1/26 Art. 153(4)).
Opting out¶
If you want the rate you pass into CalculationConfig.crr(eur_gbp_rate=...) to win unconditionally (regardless of the FX table contents), set the sync_eur_gbp_rate_from_fx_table flag to False:
from dataclasses import replace
config = replace(
CalculationConfig.crr(
reporting_date=date(2026, 12, 31),
eur_gbp_rate=Decimal("0.8732"),
),
sync_eur_gbp_rate_from_fx_table=False,
)
No WARNING is emitted in this mode, and the derived thresholds stay pinned to the rate you supplied.