Component Overview¶
This document provides detailed documentation of each component in the RWA calculator.
Component Summary¶
| Component | Module | Purpose |
|---|---|---|
| Loader | engine/loader.py |
Load data from files |
| Hierarchy Resolver | engine/hierarchy.py |
Resolve hierarchies |
| Classifier | engine/classifier.py |
Classify exposures |
| CRM Processor | engine/crm/processor.py |
Apply CRM |
| SA Calculator | engine/sa/calculator.py |
Standardised RWA |
| IRB Calculator | engine/irb/calculator.py |
IRB RWA |
| Slotting Calculator | engine/slotting/calculator.py |
Slotting RWA |
| Equity Calculator | engine/equity/calculator.py |
Equity RWA |
| Aggregator | engine/aggregator.py |
Combine results |
Polars Namespace Extensions¶
The calculator uses Polars namespace extensions to provide fluent, chainable APIs for calculations. Each namespace is registered when its module is imported.
| Namespace | Module | Description |
|---|---|---|
lf.irb |
engine/irb/namespace.py |
IRB formulas, K calculation, floors |
lf.slotting |
engine/slotting/namespace.py |
Slotting risk weights |
All namespaces are automatically registered when importing from rwa_calc.engine:
Loader¶
Purpose¶
Load raw data from Parquet or CSV files into LazyFrames.
Interface¶
class LoaderProtocol(Protocol):
def load(self, path: Path) -> RawDataBundle:
"""Load raw data from the specified path."""
...
Implementation¶
class ParquetLoader:
"""Load data from Parquet files."""
def load(self, path: Path) -> RawDataBundle:
return RawDataBundle(
counterparties=pl.scan_parquet(path / "counterparties.parquet"),
facilities=pl.scan_parquet(path / "facilities.parquet"),
loans=pl.scan_parquet(path / "loans.parquet"),
contingents=self._load_optional(path / "contingents.parquet"),
collateral=self._load_optional(path / "collateral.parquet"),
guarantees=self._load_optional(path / "guarantees.parquet"),
provisions=self._load_optional(path / "provisions.parquet"),
ratings=self._load_optional(path / "ratings.parquet"),
org_mappings=self._load_optional(path / "org_mapping.parquet"),
lending_mappings=self._load_optional(path / "lending_mapping.parquet"),
fx_rates=self._load_optional(path / "fx_rates.parquet"),
facility_mappings=self._load_optional(path / "facility_mapping.parquet"),
model_permissions=self._load_optional(path / "model_permissions.parquet"),
)
def _load_optional(self, path: Path) -> pl.LazyFrame | None:
return pl.scan_parquet(path) if path.exists() else None
Key Features¶
- Lazy loading for performance
- Optional file handling
- Schema validation
- Error accumulation
Hierarchy Resolver¶
Purpose¶
Resolve counterparty and facility hierarchies, inherit ratings, unify exposures, and calculate facility undrawn amounts.
Interface¶
class HierarchyResolverProtocol(Protocol):
def resolve(
self,
raw_data: RawDataBundle,
config: CalculationConfig
) -> ResolvedHierarchyBundle:
"""Resolve hierarchies and inherit attributes."""
...
Implementation¶
The resolve() method orchestrates the full hierarchy resolution:
class HierarchyResolver:
"""Resolve counterparty, facility, and lending group hierarchies."""
def resolve(self, data: RawDataBundle, config: CalculationConfig) -> ResolvedHierarchyBundle:
# Step 1: Build counterparty hierarchy lookup
# → _build_ultimate_parent_lazy() - traverse org_mappings (up to 10 levels)
# → _build_rating_inheritance_lazy() - inherit ratings from parent if missing
# → Returns CounterpartyLookup (counterparties, parent_mappings,
# ultimate_parent_mappings, rating_inheritance)
# Step 2: Unify exposures (loans + contingents + facility undrawn)
# → _build_facility_root_lookup() - traverse facility hierarchies
# → _calculate_facility_undrawn() - limit minus aggregated drawn amounts
# → Combines all exposure types into single LazyFrame
# Step 2a: Apply FX conversion (exposures + CRM data)
# Step 2b: Add collateral LTV to exposures
# Step 3: Calculate residential property coverage
# Step 4: Calculate lending group totals (retail threshold)
# Step 5: Add lending group totals to exposures
return ResolvedHierarchyBundle(...)
Key Internal Methods¶
| Method | Purpose |
|---|---|
_build_counterparty_lookup() |
Build complete counterparty hierarchy with ratings |
_build_ultimate_parent_lazy() |
Traverse org_mappings to find ultimate parent (up to 10 levels) |
_build_rating_inheritance_lazy() |
Inherit ratings: own → parent → unrated |
_build_facility_root_lookup() |
Traverse facility-to-facility hierarchies to find root facility |
_calculate_facility_undrawn() |
Calculate undrawn = limit - sum(descendant drawn), excluding sub-facilities |
_unify_exposures() |
Combine loan, contingent, and facility_undrawn into single LazyFrame |
_calculate_lending_group_totals() |
Aggregate exposure by lending group for retail threshold |
_add_collateral_ltv() |
Add LTV from collateral (direct → facility → counterparty priority) |
_calculate_residential_property_coverage() |
Separate residential vs all-property collateral coverage |
_add_lending_group_totals_to_exposures() |
Join lending group totals to each exposure |
Key Features¶
- Iterative join-based hierarchy resolution (counterparty and facility)
- Support for deep hierarchies (up to 10 levels)
- Multi-level facility hierarchy: drawn amounts aggregated to root facility
- Sub-facility exclusion from undrawn exposure output (avoids double-counting)
- Rating inheritance from parent (own → parent → unrated)
- Lending group aggregation with residential property exclusion (CRR Art. 123(c))
- Multi-level collateral linking (direct, facility, counterparty) with pro-rata allocation
- FX conversion of exposures and CRM data
- Flexible type column detection (
child_type/node_type/ neither) - Non-blocking error accumulation
Classifier¶
Purpose¶
Assign regulatory exposure classes and calculation approaches based on counterparty entity type.
Interface¶
class ClassifierProtocol(Protocol):
def classify(
self,
resolved: ResolvedHierarchyBundle,
config: CalculationConfig
) -> ClassifiedExposuresBundle:
"""Classify exposures into regulatory classes."""
...
Entity Type Mappings¶
The classifier uses entity_type as the single source of truth for exposure class determination. Two separate mappings exist for SA and IRB approaches:
ENTITY_TYPE_TO_SA_CLASS - Maps to SA exposure class for risk weight lookup:
| Entity Type | SA Class |
|---|---|
sovereign, central_bank |
CENTRAL_GOVT_CENTRAL_BANK |
rgla_sovereign, rgla_institution |
RGLA |
pse_sovereign, pse_institution |
PSE |
mdb, international_org |
MDB |
institution, bank, ccp, financial_institution |
INSTITUTION |
corporate, company |
CORPORATE |
individual, retail |
RETAIL_OTHER |
specialised_lending |
SPECIALISED_LENDING |
ENTITY_TYPE_TO_IRB_CLASS - Maps to IRB exposure class for formula selection:
| Entity Type | IRB Class | Notes |
|---|---|---|
sovereign, central_bank |
CENTRAL_GOVT_CENTRAL_BANK | |
rgla_sovereign, pse_sovereign |
CENTRAL_GOVT_CENTRAL_BANK | Govt-backed = central govt IRB treatment |
rgla_institution, pse_institution |
INSTITUTION | Commercial = institution IRB treatment |
mdb, international_org |
CENTRAL_GOVT_CENTRAL_BANK | CRR Art. 147(3) |
institution, bank, ccp, financial_institution |
INSTITUTION | |
corporate, company |
CORPORATE | |
individual, retail |
RETAIL_OTHER | |
specialised_lending |
SPECIALISED_LENDING |
Classification Pipeline¶
The classify() method executes these steps in sequence:
Step 1: _add_counterparty_attributes()
Join exposures with counterparty data (entity_type, revenue, assets, etc.)
Step 2: _classify_exposure_class()
Map entity_type to exposure_class_sa and exposure_class_irb
Step 3: _apply_sme_classification()
Check annual_revenue < EUR 50m for CORPORATE -> CORPORATE_SME
Step 4: _apply_retail_classification()
Aggregate by lending group, check EUR 1m threshold
Apply mortgage classification for RETAIL_MORTGAGE
Step 5: _identify_defaults()
Check default_status, set exposure_class_for_sa = DEFAULTED
Step 5a: _apply_infrastructure_classification()
Check product_type for infrastructure lending
Step 5b: _apply_fi_scalar_classification()
Determine if FI scalar (1.25x correlation) applies:
- Large FSE: total_assets >= EUR 70bn
- Financial sector entity with apply_fi_scalar = True
Step 6: _determine_approach()
Assign SA/FIRB/AIRB/SLOTTING based on IRB permissions
Step 7: _add_classification_audit()
Build audit trail string for traceability
Step 7a: _enrich_slotting_exposures()
Add slotting_category, sl_type, is_hvcre for specialised lending
Step 8: Split by approach
Filter into sa_exposures, irb_exposures, slotting_exposures
FI Scalar (CRR Art. 153(2))¶
The 1.25x IRB correlation multiplier is controlled by the user-supplied apply_fi_scalar flag on counterparties. The classifier derives requires_fi_scalar directly from this flag.
Key Features¶
- Dual exposure class mapping: SA and IRB classes tracked separately
- Entity type as single source: No conflicting boolean flags
- SME identification: Corporate exposures with revenue < EUR 50m
- Retail threshold checking: Lending group aggregation against EUR 1m
- Mortgage detection: Product type pattern matching
- FI scalar: User-controlled
apply_fi_scalarflag - Infrastructure classification: For supporting factor eligibility
- Slotting enrichment: Category, type, HVCRE flags from patterns
- Full audit trail: Classification reasoning captured per exposure
Output Columns¶
The classifier adds these columns to exposures:
| Column | Description |
|---|---|
exposure_class |
SA exposure class (backwards compatible) |
exposure_class_sa |
SA exposure class (explicit) |
exposure_class_irb |
IRB exposure class |
is_sme |
SME classification flag |
is_mortgage |
Mortgage product flag |
is_defaulted |
Default status flag |
is_infrastructure |
Infrastructure lending flag |
requires_fi_scalar |
FI scalar required (1.25x correlation) |
qualifies_as_retail |
Meets retail threshold |
approach |
Assigned calculation approach (SA/FIRB/AIRB/SLOTTING) |
classification_reason |
Audit trail string |
See Classification for detailed documentation of the classification algorithm.
CRM Processor¶
Purpose¶
Apply credit risk mitigation (collateral, guarantees, provisions).
Interface¶
class CRMProcessorProtocol(Protocol):
def apply_crm(
self,
data: ClassifiedExposuresBundle,
config: CalculationConfig,
) -> LazyFrameResult:
"""Apply credit risk mitigation. Returns LazyFrameResult with CRM-adjusted
exposures and any errors."""
...
def get_crm_adjusted_bundle(
self,
data: ClassifiedExposuresBundle,
config: CalculationConfig,
) -> CRMAdjustedBundle:
"""Apply CRM and return as a bundle."""
...
Implementation¶
The CRMProcessor provides three public methods:
apply_crm()→LazyFrameResult— returns CRM-adjusted LazyFrame with errorsget_crm_adjusted_bundle()→CRMAdjustedBundle— wrapsapply_crm()result as a bundle with approach-split exposuresget_crm_unified_bundle()→CRMAdjustedBundle— unified path (no approach split), used for Basel 3.1 output floor calculation where SA-equivalent RWA is needed on all rows
class CRMProcessor:
"""Process credit risk mitigation (Art. 111(2) compliant)."""
def get_crm_adjusted_bundle(
self,
data: ClassifiedExposuresBundle,
config: CalculationConfig,
) -> CRMAdjustedBundle:
# Provisions resolved BEFORE CCF, then CRM waterfall after EAD init
# Step 1: Resolve provisions (before CCF)
# SA: drawn-first deduction, remainder reduces nominal
# IRB/Slotting: tracked but not deducted
after_provisions = self._resolve_provisions(
data.all_exposures, data.provisions, config
)
# Step 2: Apply CCFs (uses nominal_after_provision)
after_ccf = self._apply_ccf(after_provisions, config)
# Step 3: Initialize EAD waterfall + collect barrier
# (flattens deep plan to prevent 3× re-evaluation downstream)
after_init = self._initialize_ead(after_ccf)
# Step 4: Apply collateral (3 lookup collects: direct/facility/counterparty)
after_collateral = self._apply_collateral(
after_init, data.collateral, config
)
# Step 5: Apply guarantees (cross-approach CCF substitution)
after_guarantees = self._apply_guarantees(
after_collateral, data.guarantees, data.counterparty_lookup, config
)
# Step 6: Finalize EAD (no provision subtraction — already in ead_pre_crm)
final = self._finalize_ead(after_guarantees)
return CRMAdjustedBundle(exposures=final, ...)
def get_crm_unified_bundle(
self,
data: ClassifiedExposuresBundle,
config: CalculationConfig,
) -> CRMAdjustedBundle:
"""Same pipeline, but does not split by approach.
Used for Basel 3.1 output floor (SA-equiv RW on all rows)."""
Key Features¶
- Supervisory haircut application
- Currency mismatch handling
- Maturity mismatch adjustment
- Guarantee substitution
- Provision allocation
SA Calculator¶
Purpose¶
Calculate RWA using the Standardised Approach.
Interface¶
class SACalculatorProtocol(Protocol):
def calculate(
self,
exposures: pl.LazyFrame,
config: CalculationConfig
) -> SAResultBundle:
"""Calculate SA RWA."""
...
Implementation¶
class SACalculator:
"""Calculate Standardised Approach RWA."""
def calculate(
self,
exposures: pl.LazyFrame,
config: CalculationConfig
) -> SAResultBundle:
result = (
exposures
# Look up risk weight
.with_columns(
risk_weight=self._get_risk_weight(
pl.col("exposure_class"),
pl.col("cqs"),
config
)
)
# Calculate base RWA
.with_columns(
rwa_base=pl.col("ead") * pl.col("risk_weight")
)
)
# Apply supporting factors (CRR only)
if config.apply_sme_supporting_factor:
result = self._apply_sme_factor(result, config)
if config.apply_infrastructure_factor:
result = self._apply_infrastructure_factor(result, config)
return SAResultBundle(data=result)
Key Features¶
- Risk weight lookup by class and CQS
- LTV-based real estate weights (Basel 3.1)
- SME supporting factor application
- Infrastructure factor application
IRB Calculator¶
Purpose¶
Calculate RWA using IRB approaches (F-IRB and A-IRB).
Interface¶
class IRBCalculatorProtocol(Protocol):
def calculate(
self,
exposures: pl.LazyFrame,
config: CalculationConfig
) -> IRBResultBundle:
"""Calculate IRB RWA."""
...
Implementation¶
The IRB Calculator uses a Polars namespace extension (IRBLazyFrame) for fluent, chainable calculations:
class IRBCalculator:
"""Calculate IRB RWA using K formula."""
def get_irb_result_bundle(
self,
data: CRMAdjustedBundle,
config: CalculationConfig
) -> IRBResultBundle:
# Apply IRB calculations using namespace for fluent pipeline
exposures = (data.irb_exposures
.irb.classify_approach(config) # Determine F-IRB vs A-IRB
.irb.apply_firb_lgd(config) # Apply supervisory LGD for F-IRB
.irb.prepare_columns(config) # Ensure required columns exist
.irb.apply_all_formulas(config) # Run full IRB calculation
)
# Apply supporting factors (CRR only - Art. 501)
exposures = self._apply_supporting_factors(exposures, config)
return IRBResultBundle(
results=exposures,
expected_loss=exposures.irb.select_expected_loss(),
calculation_audit=exposures.irb.build_audit(),
errors=[],
)
IRB Namespace¶
The .irb namespace provides chainable methods for each calculation step:
| Method | Description |
|---|---|
classify_approach(config) |
Classify as F-IRB or A-IRB |
apply_firb_lgd(config) |
Apply supervisory LGD for F-IRB |
prepare_columns(config) |
Ensure required columns exist |
apply_pd_floor(config) |
Apply PD floor (0.03% CRR, 0.05% Basel 3.1) |
apply_lgd_floor(config) |
Apply LGD floor (Basel 3.1 A-IRB only) |
calculate_correlation(config) |
Calculate asset correlation with SME adjustment |
calculate_k(config) |
Calculate capital requirement K |
calculate_maturity_adjustment(config) |
Calculate maturity adjustment |
calculate_rwa(config) |
Calculate RWA |
calculate_expected_loss(config) |
Calculate expected loss |
apply_all_formulas(config) |
Run complete calculation pipeline |
Key Features¶
- Fluent API: Namespace enables readable, chainable method calls
- Pure Polars expressions: Full lazy evaluation with
polars-normal-statsfor statistical functions - Streaming-capable: No data materialization required, enabling large dataset processing
- PD and LGD floor application
- Correlation calculation with SME adjustment
- K formula implementation using
normal_cdfandnormal_ppf - Maturity adjustment
- Expected loss calculation
- CRR 1.06 scaling factor
Slotting Calculator¶
Purpose¶
Calculate RWA using the slotting approach for specialised lending.
Interface¶
class SlottingCalculatorProtocol(Protocol):
def calculate(
self,
exposures: pl.LazyFrame,
config: CalculationConfig
) -> SlottingResultBundle:
"""Calculate Slotting RWA."""
...
Implementation¶
class SlottingCalculator:
"""Calculate Slotting RWA for specialised lending."""
def calculate(
self,
exposures: pl.LazyFrame,
config: CalculationConfig
) -> SlottingResultBundle:
result = (
exposures
# Look up slotting risk weight
.with_columns(
risk_weight=self._get_slotting_weight(
pl.col("lending_type"),
pl.col("slotting_category"),
pl.col("is_pre_operational"), # For project finance
config
)
)
# Calculate RWA
.with_columns(
rwa=pl.col("ead") * pl.col("risk_weight")
)
)
# Apply infrastructure factor (CRR only)
if config.apply_infrastructure_factor:
result = self._apply_infrastructure_factor(result, config)
return SlottingResultBundle(data=result)
Key Features¶
- Slotting category to risk weight mapping
- Pre-operational project finance handling
- HVCRE treatment
- Infrastructure factor application
Equity Calculator¶
Purpose¶
Calculate RWA for equity exposures using SA (Article 133) or IRB Simple (Article 155) risk weights.
Interface¶
class EquityCalculatorProtocol(Protocol):
def calculate(
self,
data: CRMAdjustedBundle,
config: CalculationConfig,
) -> LazyFrameResult:
"""Calculate RWA for equity exposures."""
...
def get_equity_result_bundle(
self,
data: CRMAdjustedBundle,
config: CalculationConfig,
) -> EquityResultBundle:
"""Calculate equity RWA and return as bundle."""
...
Implementation¶
The approach is determined by the firm's IRB permissions:
- SA (Article 133): Default approach. Risk weights based on equity type (central bank 0%, listed 100%, unlisted 250%, speculative 400%).
- IRB Simple (Article 155): When IRB is permitted. Risk weights differ (private equity diversified 190%, exchange-traded 290%, other 370%).
class EquityCalculator:
"""Calculate equity exposure RWA."""
def get_equity_result_bundle(
self,
data: CRMAdjustedBundle,
config: CalculationConfig
) -> EquityResultBundle:
approach = self._determine_approach(config)
exposures = self._prepare_columns(data.equity_exposures, config)
if approach == "sa":
exposures = self._apply_equity_weights_sa(exposures, config)
else:
exposures = self._apply_equity_weights_irb_simple(exposures, config)
exposures = self._calculate_rwa(exposures)
audit = self._build_audit(exposures, approach)
return EquityResultBundle(
results=exposures,
calculation_audit=audit,
approach=approach,
errors=[],
)
Key Features¶
- Approach determination from IRB permissions
- SA Article 133 risk weight assignment
- IRB Simple Article 155 risk weight assignment
- Diversified portfolio treatment for private equity
- Equity exposures bypass CRM (no collateral applied)
- Full audit trail
Aggregator¶
Purpose¶
Combine results from all calculators, apply output floor.
Interface¶
class OutputAggregatorProtocol(Protocol):
def aggregate(
self,
sa_result: SAResultBundle,
irb_result: IRBResultBundle,
slotting_result: SlottingResultBundle,
equity_result: EquityResultBundle,
config: CalculationConfig
) -> AggregatedResultBundle:
"""Aggregate results and apply final adjustments."""
...
Implementation¶
class OutputAggregator:
"""Aggregate calculation results."""
def aggregate(
self,
sa_result: SAResultBundle,
irb_result: IRBResultBundle,
slotting_result: SlottingResultBundle,
equity_result: EquityResultBundle,
config: CalculationConfig
) -> AggregatedResultBundle:
# Combine all results
combined = pl.concat([
sa_result.data.with_columns(approach=pl.lit("SA")),
irb_result.data.with_columns(approach=pl.lit("IRB")),
slotting_result.data.with_columns(approach=pl.lit("SLOTTING")),
equity_result.results.with_columns(approach=pl.lit("EQUITY")),
])
# Apply output floor (Basel 3.1)
if config.framework == RegulatoryFramework.BASEL_3_1:
combined = self._apply_output_floor(
combined,
config.output_floor_config
)
# Calculate totals
totals = self._calculate_totals(combined)
return AggregatedResultBundle(
data=combined,
total_rwa=totals.rwa,
sa_rwa=totals.sa_rwa,
irb_rwa=totals.irb_rwa,
slotting_rwa=totals.slotting_rwa,
total_expected_loss=totals.expected_loss,
)
Key Features¶
- Result combination
- Output floor application
- Floor impact calculation
- Total aggregation
- Breakdown by approach/class
Next Steps¶
- API Reference - Complete API documentation
- Data Model - Schema definitions
- Development Guide - Extending the calculator