Configuration API¶
The configuration module provides immutable dataclasses for controlling calculation
behavior. All configurations are frozen (@dataclass(frozen=True)) and use factory
methods for self-documenting creation.
Module: rwa_calc.contracts.config¶
PolarsEngine¶
Type alias controlling the Polars collection engine. "streaming" (default) processes
in batches for lower memory usage; "cpu" loads everything into memory.
CalculationConfig¶
Master configuration for RWA calculations. Bundles all framework-specific settings.
Use factory methods .crr() and .basel_3_1() instead of constructing directly.
@dataclass(frozen=True)
class CalculationConfig:
"""
Attributes:
framework: Regulatory framework (CRR or BASEL_3_1).
reporting_date: As-of date for the calculation.
base_currency: Currency for reporting (default "GBP").
apply_fx_conversion: Whether to convert exposures to base_currency.
pd_floors: PD floor configuration.
lgd_floors: LGD floor configuration (A-IRB).
supporting_factors: SME/infrastructure factors.
output_floor: Output floor configuration.
retail_thresholds: Retail classification thresholds.
permission_mode: STANDARDISED (all SA) or IRB (model permissions drive routing).
scaling_factor: 1.06 scaling for IRB K (CRR Art. 153), 1.0 for Basel 3.1.
eur_gbp_rate: EUR/GBP exchange rate for threshold conversion.
collect_engine: Polars engine for .collect() — "cpu" (default).
"""
framework: RegulatoryFramework
reporting_date: date
base_currency: str = "GBP"
apply_fx_conversion: bool = True
pd_floors: PDFloors
lgd_floors: LGDFloors
supporting_factors: SupportingFactors
output_floor: OutputFloorConfig
retail_thresholds: RetailThresholds
permission_mode: PermissionMode = PermissionMode.STANDARDISED
scaling_factor: Decimal = Decimal("1.06")
eur_gbp_rate: Decimal = Decimal("0.8732")
collect_engine: PolarsEngine = "cpu"
spill_dir: Path | None = None
Properties¶
@property
def is_crr(self) -> bool:
"""Check if using CRR framework."""
@property
def is_basel_3_1(self) -> bool:
"""Check if using Basel 3.1 framework."""
def get_output_floor_percentage(self) -> Decimal:
"""Get the applicable output floor percentage for the reporting date."""
Factory Methods¶
@classmethod
def crr(
cls,
reporting_date: date,
permission_mode: PermissionMode = PermissionMode.STANDARDISED,
eur_gbp_rate: Decimal = Decimal("0.8732"),
collect_engine: PolarsEngine = "cpu",
spill_dir: Path | None = None,
) -> CalculationConfig:
"""
Create CRR (Basel 3.0) configuration.
CRR characteristics:
- Single PD floor (0.03%) for all classes
- No LGD floors for A-IRB
- SME supporting factor (0.7619/0.85)
- Infrastructure supporting factor (0.75)
- No output floor
- 1.06 scaling factor for IRB K
Args:
reporting_date: As-of date for calculation.
permission_mode: STANDARDISED (all SA) or IRB (model permissions
drive routing).
eur_gbp_rate: EUR/GBP exchange rate for threshold conversion.
collect_engine: Polars collection engine.
spill_dir: Directory for temp files during streaming (None = system temp).
Example:
>>> config = CalculationConfig.crr(
... reporting_date=date(2026, 12, 31),
... permission_mode=PermissionMode.IRB,
... )
"""
@classmethod
def basel_3_1(
cls,
reporting_date: date,
permission_mode: PermissionMode = PermissionMode.STANDARDISED,
collect_engine: PolarsEngine = "cpu",
spill_dir: Path | None = None,
) -> CalculationConfig:
"""
Create Basel 3.1 (PRA PS1/26) configuration.
Basel 3.1 characteristics:
- Differentiated PD floors by exposure class
- LGD floors for A-IRB by collateral type
- No supporting factors
- Output floor (72.5%, transitional from 60% in 2027)
- 1.06 scaling factor removed
Args:
reporting_date: As-of date for calculation.
permission_mode: STANDARDISED (all SA) or IRB (model permissions
drive routing).
collect_engine: Polars collection engine.
spill_dir: Directory for temp files during streaming (None = system temp).
Example:
>>> config = CalculationConfig.basel_3_1(
... reporting_date=date(2027, 6, 30),
... permission_mode=PermissionMode.IRB,
... )
"""
PDFloors¶
PD floor values by exposure class. All values expressed as decimals (e.g., 0.0003 = 0.03%).
@dataclass(frozen=True)
class PDFloors:
"""
Under CRR: Single floor of 0.03% for all exposures (Art. 163).
Under Basel 3.1: Differentiated floors (CRE30.55, PS1/26 Ch.5).
"""
corporate: Decimal = Decimal("0.0003") # 0.03%
corporate_sme: Decimal = Decimal("0.0003") # 0.03%
retail_mortgage: Decimal = Decimal("0.0003") # 0.03%
retail_other: Decimal = Decimal("0.0003") # 0.03%
retail_qrre_transactor: Decimal = Decimal("0.0003") # 0.03%
retail_qrre_revolver: Decimal = Decimal("0.0003") # 0.03%
def get_floor(
self,
exposure_class: ExposureClass,
is_qrre_transactor: bool = False,
) -> Decimal:
"""Get the PD floor for a given exposure class.
For RETAIL_QRRE, distinguish transactors vs revolvers via is_qrre_transactor.
"""
@classmethod
def crr(cls) -> PDFloors:
"""CRR PD floors: single 0.03% floor for all classes."""
@classmethod
def basel_3_1(cls) -> PDFloors:
"""Basel 3.1 PD floors: differentiated by class (CRE30.55).
- Corporate/Corporate SME: 0.05%
- Retail mortgage: 0.05%
- Retail other: 0.05%
- QRRE transactors: 0.03%
- QRRE revolvers: 0.10%
"""
LGDFloors¶
LGD floor values by collateral type for A-IRB. Only applicable under Basel 3.1 (CRE30.41, PS1/26 Ch.5). CRR has no LGD floors.
@dataclass(frozen=True)
class LGDFloors:
unsecured: Decimal = Decimal("0.25") # 25%
subordinated_unsecured: Decimal = Decimal("0.50") # 50%
financial_collateral: Decimal = Decimal("0.0") # 0%
receivables: Decimal = Decimal("0.10") # 10%
commercial_real_estate: Decimal = Decimal("0.10") # 10%
residential_real_estate: Decimal = Decimal("0.05") # 5%
other_physical: Decimal = Decimal("0.15") # 15%
def get_floor(self, collateral_type: CollateralType) -> Decimal:
"""Get the LGD floor for a given collateral type.
Defaults to unsecured floor for unknown types."""
@classmethod
def crr(cls) -> LGDFloors:
"""CRR: No LGD floors (all zero)."""
@classmethod
def basel_3_1(cls) -> LGDFloors:
"""Basel 3.1 LGD floors (CRE30.41).
Note: Values reflect PRA implementation."""
SupportingFactors¶
Supporting factors for CRR (SME and infrastructure). Basel 3.1 removes these.
@dataclass(frozen=True)
class SupportingFactors:
"""
SME Supporting Factor (CRR Art. 501):
Factor 1: 0.7619 for exposure up to EUR 2.5m
Factor 2: 0.85 for exposure above EUR 2.5m
Infrastructure Supporting Factor (CRR Art. 501a):
Factor: 0.75
"""
sme_factor_under_threshold: Decimal = Decimal("0.7619")
sme_factor_above_threshold: Decimal = Decimal("0.85")
sme_exposure_threshold_eur: Decimal = Decimal("2500000") # EUR 2.5m
sme_turnover_threshold_eur: Decimal = Decimal("50000000") # EUR 50m
infrastructure_factor: Decimal = Decimal("0.75")
enabled: bool = True
@classmethod
def crr(cls) -> SupportingFactors:
"""CRR supporting factors enabled."""
@classmethod
def basel_3_1(cls) -> SupportingFactors:
"""Basel 3.1: Supporting factors disabled (all 1.0)."""
OutputFloorConfig¶
Output floor configuration for Basel 3.1. The output floor (CRE99.1-8, PS1/26 Ch.12) requires IRB RWAs to be at least 72.5% of the equivalent SA RWAs.
@dataclass(frozen=True)
class OutputFloorConfig:
enabled: bool = False
floor_percentage: Decimal = Decimal("0.725") # 72.5%
transitional_start_date: date | None = None
transitional_end_date: date | None = None
transitional_floor_schedule: dict[date, Decimal] = field(default_factory=dict)
def get_floor_percentage(self, calculation_date: date) -> Decimal:
"""Get the applicable floor percentage for a given date.
Returns 0% if floor is disabled or the calculation date precedes
the transitional start date (PS1/26: 1 Jan 2027 for UK firms).
Transitional schedule:
- 2027: 50%
- 2028: 55%
- 2029: 60%
- 2030: 65%
- 2031: 70%
- 2032+: 72.5%
"""
@classmethod
def crr(cls) -> OutputFloorConfig:
"""CRR: No output floor."""
@classmethod
def basel_3_1(cls) -> OutputFloorConfig:
"""Basel 3.1 output floor with PRA transitional schedule."""
RetailThresholds¶
Thresholds for retail exposure classification. Different thresholds apply under CRR vs Basel 3.1.
@dataclass(frozen=True)
class RetailThresholds:
# Maximum aggregated exposure to qualify as retail
max_exposure_threshold: Decimal = Decimal("1000000") # GBP 1m (CRR)
# QRRE specific limits
qrre_max_limit: Decimal = Decimal("100000") # GBP 100k
@classmethod
def crr(cls, eur_gbp_rate: Decimal = Decimal("0.8732")) -> RetailThresholds:
"""CRR retail thresholds (converted from EUR dynamically).
Args:
eur_gbp_rate: EUR/GBP exchange rate for threshold conversion.
CRR thresholds (EUR):
- Max exposure: EUR 1m (converted to GBP)
- QRRE max limit: EUR 100k (converted to GBP)
"""
@classmethod
def basel_3_1(cls) -> RetailThresholds:
"""Basel 3.1 retail thresholds (GBP).
- Max exposure: GBP 880k
- QRRE max limit: GBP 100k
"""
PermissionMode¶
High-level permission mode controlling whether the firm uses SA for all exposures or routes exposures to IRB based on model-level permissions.
| Mode | Behaviour |
|---|---|
STANDARDISED |
All exposures use the Standardised Approach. No IRB routing. |
IRB |
Approach routing is driven by the model_permissions input table. Each model's approved approach (AIRB, FIRB, slotting) is resolved per-exposure. Exposures without a matching model permission fall back to SA. If no model_permissions file is provided, all exposures fall back to SA with a warning. |
Model permissions required for IRB mode
When permission_mode=PermissionMode.IRB, the calculator requires
model_permissions input data to determine which exposures use FIRB,
AIRB, or slotting. Without it, the pipeline falls back to SA for all
exposures and emits a warning. See
Input Schemas — Model Permissions.
Usage Examples¶
CRR Configuration¶
from datetime import date
from decimal import Decimal
from rwa_calc.contracts.config import CalculationConfig
from rwa_calc.domain.enums import PermissionMode
# SA-only CRR configuration (default)
config = CalculationConfig.crr(
reporting_date=date(2026, 12, 31),
)
# CRR with IRB routing (requires model_permissions input data)
config = CalculationConfig.crr(
reporting_date=date(2026, 12, 31),
permission_mode=PermissionMode.IRB,
)
# Access configuration
print(f"Framework: {config.framework}") # RegulatoryFramework.CRR
print(f"Is CRR: {config.is_crr}") # True
print(f"Scaling factor: {config.scaling_factor}") # 1.06
print(f"SME enabled: {config.supporting_factors.enabled}") # True
Basel 3.1 Configuration¶
# Basel 3.1 with IRB routing (auto-calculates output floor from reporting_date)
config = CalculationConfig.basel_3_1(
reporting_date=date(2027, 6, 30),
permission_mode=PermissionMode.IRB,
)
# Check output floor percentage (transitional for 2027 = 60%)
floor = config.get_output_floor_percentage()
print(f"Output floor: {floor:.0%}") # 60%
# Fully phased-in (2030+)
config = CalculationConfig.basel_3_1(
reporting_date=date(2030, 1, 1),
permission_mode=PermissionMode.IRB,
)
floor = config.get_output_floor_percentage()
print(f"Output floor: {floor:.1%}") # 72.5%
Accessing Floors¶
from rwa_calc.domain.enums import ExposureClass, CollateralType
# PD floor lookup
pd_floor = config.pd_floors.get_floor(ExposureClass.CORPORATE)
print(f"Corporate PD floor: {pd_floor:.4%}") # 0.0500% (Basel 3.1)
# PD floor for QRRE (distinguish transactor vs revolver)
qrre_floor = config.pd_floors.get_floor(
ExposureClass.RETAIL_QRRE, is_qrre_transactor=True
)
print(f"QRRE transactor PD floor: {qrre_floor:.4%}") # 0.0300%
# LGD floor lookup
lgd_floor = config.lgd_floors.get_floor(CollateralType.IMMOVABLE)
print(f"CRE LGD floor: {lgd_floor:.0%}") # 10%
Permission Mode¶
from rwa_calc.domain.enums import PermissionMode
# SA-only — all exposures use standardised approach
config = CalculationConfig.crr(
reporting_date=date(2026, 12, 31),
permission_mode=PermissionMode.STANDARDISED, # default
)
# IRB mode — model_permissions input data drives approach routing
# Each exposure is matched to its model's approved approach (AIRB, FIRB, slotting)
# Exposures without a model permission fall back to SA
config = CalculationConfig.basel_3_1(
reporting_date=date(2027, 6, 30),
permission_mode=PermissionMode.IRB,
)