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/ (package) |
Resolve hierarchies |
| Classifier | engine/classify/ (package) |
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/aggregator.py |
Combine results |
Each stage package is the single home of its component — there is no
top-level alias module. Import HierarchyResolver from
engine/hierarchy/, ExposureClassifier from engine/classify/,
RealEstateSplitter from engine/re_split/ and FXConverter from
engine/fx/.
Calculator / Domain Transforms¶
Calculator and domain logic is written as plain module-level typed functions
(fn(lf: LazyFrame, config, ...) -> LazyFrame) composed via
lf.pipe(fn, ...) — for example engine/sa/risk_weights.py,
engine/sa/rw_adjustments.py, engine/irb/transforms.py, and
engine/slotting/transforms.py. Each calculator class is a thin orchestrator
that pipes these transforms in regulatory order.
Polars namespace registrations (@pl.api.register_*_namespace) are extinct and
banned by scripts/arch_check.py check 14 — there is no lf.sa / lf.irb /
lf.slotting accessor and no IRBLazyFrame / SlottingLazyFrame export.
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. Suppresses the synthetic facility_undrawn exposure row when committed=False — uncommitted (unconditionally cancellable) facilities carry no commitment EAD because the bank can refuse to lend; loans and contingents mapped to the facility are unaffected and continue to flow as their own exposure rows |
_expand_mof_facility_undrawn() |
For Multiple Option Facility (MOF) parents — replaces the parent's single undrawn row with one row per committed descendant sub-facility (with positive headroom), allocated by waterfall in descending SA CCF order, capped per-sub at sub_limit − sub_drawn and globally at the parent's headroom. Emits a residual row at the parent's own risk_type when sub-limits don't cover the full parent limit. Uncommitted subs are skipped entirely. Each split row carries the sub's risk_type and counterparty_reference; provenance lives in mof_risk_type_source |
_derive_facility_share_members() |
For non-MOF Facility Shares (one facility any of several counterparties may draw) — derive the member set: the facility's own counterparty_reference (the owner is always a member) unioned with the distinct counterparties on its descendant loans and contingents, each resolved to its root facility. A union of more than one member is what makes the facility a share. MOF parents are anti-joined out, because each sub waterfall row already carries the right counterparty natively |
_apply_facility_share_fanout() |
Replicate a share's single undrawn row into one candidate row per member, each carrying the full headroom (each is "as if this member drew the whole line" — nothing is pro-rated and nothing is drawn-weighted). Each candidate takes exposure_reference = <facility>_UNDRAWN@<member>, the member as counterparty_reference, the owner in original_counterparty_reference, facility_share_group = <facility> and is_facility_share_candidate = True; source_exposure_reference stays <facility>, so reconciliation keys are unchanged. Nothing is ranked here |
_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)
- Multiple Option Facility (MOF) parents emit per-sub waterfall undrawn rows (any facility with at least one
child_type='facility'mapping is a MOF). Sub-facilities are sorted by descending SA CCF under the active framework and each takes the lesser of its own headroom (sub_limit − sub_drawn) and the parent's remaining headroom. Sub-limits beyond the parent's cap spill out, leftover parent headroom emits a residual row at the parent's ownrisk_type, uncommitted subs are skipped, and each row records its source inmof_risk_type_sourcefor audit. - A non-MOF Facility Share fans out into one undrawn candidate row per member, rather than being allocated here. The member set is the facility's owner unioned with the distinct counterparties on its descendant loans and contingents; more than one member makes it a share. Every candidate carries the full headroom, its own member as
counterparty_reference, the owner inoriginal_counterparty_referencefor audit, and thefacility_share_group/is_facility_share_candidatecarriers. Each then flows through the classifier, CRM and the calculators as an ordinary row of its own member — its own exposure class, model permission, PD/LGD and CRM — and the aggregator keeps one and drops the rest, so the allocation is decided on real priced RWA. MOF parents are excluded because each waterfall row already carries its sub-facility's own counterparty. The SA-equivalent risk-weight preview that used to choose the winner here was removed on 2026-09-05; see Facility Share Allocation. - 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
- Facility-mapping schema normalised at the resolver boundary: legacy
node_typeis accepted as an input alias and renamed tochild_type; missing column is synthesised as null. New producers MUST emitchild_type. - 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 retail threshold (EUR 1m CRR / GBP 880k B31)
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()
Derive requires_fi_scalar from user-supplied apply_fi_scalar flag.
(User sets apply_fi_scalar=True for LFSE — total assets ≥ EUR 70bn
under CRR Art. 142(1)(4), or ≥ GBP 79bn under PS1/26 Glossary p. 78 —
or for any unregulated FSE. No automatic threshold check in code.)
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: Assemble bundle
All exposures stay on the single unified frame; downstream
consumers filter on the `approach` column
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 retail threshold (EUR 1m CRR / GBP 880k Basel 3.1)
- 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 get_crm_unified_bundle(
self,
data: ClassifiedExposuresBundle,
config: CalculationConfig,
) -> CRMAdjustedBundle:
"""Apply CRM and return the unified bundle (no approach split)."""
...
Implementation¶
get_crm_unified_bundle() is the CRM stage's single entry point (the legacy
apply_crm()/get_crm_adjusted_bundle() dual path was deleted in migration
Phase 2). All exposures travel on one unified frame; the pipeline splits by
approach once, just before the calculators.
class CRMProcessor:
"""Process credit risk mitigation (Art. 111(1)(a)-(b) compliant)."""
def get_crm_unified_bundle(
self,
data: ClassifiedExposuresBundle,
config: CalculationConfig,
) -> CRMAdjustedBundle:
# Step 0: Funded-only two-layer protection look-through (Art. 191A)
# Step 1: Resolve provisions (before CCF)
# SA: drawn-first deduction, remainder reduces nominal
# IRB/Slotting: tracked but not deducted
# Step 2: Apply CCFs (uses nominal_after_provision)
# Step 3: Initialize EAD waterfall + crm_post_ead checkpoint
exposures = self._run_ead_pipeline(data, config)
# Step 4: Apply collateral (3 lookup collects: direct/facility/counterparty)
# + misdirected-AIRB diagnostics (CRM006)
exposures, applied = self._apply_collateral_unified_step(
exposures, collateral, config, errors
)
# Step 5: Apply guarantees (cross-approach CCF substitution),
# behind the crm_pre_guarantee_unified checkpoint
exposures = self._apply_guarantees_step(
exposures, guarantees, data, config, errors
)
# Step 6: Finalize EAD (no provision subtraction — already in ead_pre_crm)
# Step 7: Audit columns, then the crm_exit stage edge
return CRMAdjustedBundle(exposures=exposures, crm_errors=errors, ...)
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_branch(
self,
exposures: pl.LazyFrame,
config: CalculationConfig,
*,
errors: list[CalculationError] | None = None,
) -> pl.LazyFrame:
"""Calculate SA RWA on pre-filtered SA-only rows."""
...
def calculate_unified(
self,
exposures: pl.LazyFrame,
config: CalculationConfig,
*,
errors: list[CalculationError] | None = None,
) -> pl.LazyFrame:
"""SA risk weights on the unified frame (B3.1 output floor path)."""
...
Implementation¶
class SACalculator:
"""Calculate Standardised Approach RWA (thin orchestrator over the plain typed SA transforms)."""
def calculate_branch(self, exposures, config, *, errors=None):
if errors is not None:
self._warn_equity_in_main_table(exposures, errors) # SA005
return (
exposures.pipe(apply_risk_weights, config, pack=pack)
.pipe(apply_fcsm_rw_substitution, config)
.pipe(apply_life_insurance_rw_mapping)
.pipe(apply_guarantee_substitution, config, pack=pack)
.pipe(apply_currency_mismatch_multiplier, config, pack=pack)
.pipe(apply_due_diligence_override, config, errors=errors, pack=pack) # SA004
.pipe(calculate_rwa)
.pipe(apply_supporting_factors, config, errors=errors, pack=pack) # SF001
) # + approach_applied / rwa_final standardisation for the aggregator
The transforms (apply_risk_weights, apply_currency_mismatch_multiplier,
…) are plain typed functions imported from engine/sa/risk_weights.py,
engine/sa/rw_adjustments.py, and engine/sa/factors_output.py.
The optional errors accumulator is the branch-path error channel: the
pipeline passes one list into every calculate_branch call and merges the
accumulated CalculationErrors into the result bundle with their original
codes.
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_branch(
self,
exposures: pl.LazyFrame,
config: CalculationConfig,
*,
errors: list[CalculationError] | None = None,
) -> pl.LazyFrame:
"""Calculate IRB RWA on pre-filtered IRB-only rows."""
...
Implementation¶
The IRB Calculator is a thin orchestrator over plain typed transform functions
(engine/irb/transforms.py) composed via LazyFrame.pipe:
class IRBCalculator:
"""Calculate IRB RWA using K formula."""
def calculate_branch(self, exposures, config, *, errors=None):
exposures = (
exposures.pipe(classify_approach, config) # F-IRB vs A-IRB
.pipe(apply_firb_lgd, config, pack=resolved_pack) # Supervisory LGD for F-IRB
.pipe(prepare_columns, config, pack=resolved_pack) # Ensure required columns
.pipe(apply_all_formulas, config, pack=resolved_pack) # Full IRB calculation
.pipe(apply_post_model_adjustments, config, pack=resolved_pack)
.pipe(compute_el_shortfall_excess, errors=errors)
.pipe(apply_guarantee_substitution, config, pack=resolved_pack)
)
# Supporting factors (CRR only — Art. 501), then aggregator columns
exposures = self._apply_supporting_factors(exposures, config, errors=errors)
return exposures # + approach_applied / rwa_final / irb_maturity_m
IRB Transforms¶
The plain transform functions in engine/irb/transforms.py cover each
calculation step (composed via lf.pipe(fn, config)):
| Function | 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 |
| (PD floor) | Apply PD floor (0.03% CRR, 0.05% Basel 3.1) |
| (LGD floor) | Apply LGD floor (Basel 3.1 A-IRB only) |
| (correlation) | Calculate asset correlation with SME adjustment |
| (capital requirement K) | Calculate K |
| (maturity adjustment) | Calculate maturity adjustment |
apply_all_formulas(config) |
Run the complete calculation (floors, correlation, K, maturity adjustment, RWA, expected loss) |
Polars namespace registrations are banned by scripts/arch_check.py check 14 —
there is no .irb accessor or IRBLazyFrame class.
Key Features¶
- Composable transforms: plain typed functions piped in regulatory order
- 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_branch(
self,
exposures: pl.LazyFrame,
config: CalculationConfig,
*,
errors: list[CalculationError] | None = None,
) -> pl.LazyFrame:
"""Calculate Slotting RWA on pre-filtered slotting-only rows."""
...
Implementation¶
class SlottingCalculator:
"""Calculate Slotting RWA for specialised lending."""
def calculate_branch(self, exposures, config, *, errors=None):
exposures = (
exposures.pipe(prepare_columns, config)
.pipe(apply_slotting_weights, config, pack=pack) # Art. 153(5) tables
.pipe(calculate_rwa)
)
# Supporting factors (CRR Art. 501/501a), EL rates + shortfall/excess
exposures = self._apply_supporting_factors(exposures, config, errors=errors)
exposures = exposures.pipe(apply_el_rates, config, pack=pack).pipe(
compute_el_shortfall_excess, errors=errors
)
return exposures # + approach_applied / rwa_final
The slotting transforms (prepare_columns, apply_slotting_weights,
calculate_rwa, apply_el_rates, compute_el_shortfall_excess) are plain
typed functions in engine/slotting/transforms.py.
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 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_results: pl.LazyFrame,
irb_results: pl.LazyFrame,
slotting_results: pl.LazyFrame,
equity_bundle: EquityResultBundle | None,
config: CalculationConfig,
securitisation_audit: pl.LazyFrame | None = None,
) -> AggregatedResultBundle:
"""Aggregate results and apply final adjustments."""
...
Implementation¶
class OutputAggregator:
"""Aggregate calculation results."""
def aggregate(
self,
sa_results: pl.LazyFrame,
irb_results: pl.LazyFrame,
slotting_results: pl.LazyFrame,
equity_bundle: EquityResultBundle | None,
config: CalculationConfig,
securitisation_audit: pl.LazyFrame | None = None,
) -> AggregatedResultBundle:
# Combine the collected branch frames (+ equity results)
combined = pl.concat([
sa_results,
irb_results,
slotting_results,
*( [equity_bundle.results] if equity_bundle else [] ),
], how="diagonal_relaxed")
# Apply output floor — applicability resolves from the rulepack
# (a cited pack Feature read from the resolved pack), not an inline
# config.framework branch. The floor logic itself lives in
# engine/aggregator/_floor.py (apply_floor_with_impact).
if resolved_pack.feature("output_floor"):
combined = apply_floor_with_impact(combined, resolved_pack)
# 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 Internal Steps¶
| Step | Purpose |
|---|---|
resolve_facility_shares() (engine/aggregator/_facility_share.py) |
Runs at the head of aggregate(), on the three branch frames (sa_results, irb_results, slotting_results) plus the concatenation — before the securitisation views, the residual multiplier, the expected-loss summary and the output floor. Keeps exactly one undrawn candidate per facility_share_group and drops the rest from every frame, then collapses the winner's exposure_reference back to <facility>_UNDRAWN, so the aggregator exit keeps its one undrawn row per facility invariant. The metric is P0 (argmax own-approach RWA) under CRR, and under Basel 3.1 when the firm has elected own_approach; otherwise P2 — evaluate assignment A (argmax own RWA) and assignment B (argmax floored-branch marginal) end to end and keep the larger, ties to A. Emits the per-candidate facility_share_resolution audit frame, sets facility_share_metric_used / facility_share_trea_alternative on OutputFloorSummary, and raises an AGG003 warning where every candidate of a group carries a non-finite own-approach RWA and a deterministic fallback ordering picked the member instead. See Facility Share Allocation |
apply_floor_with_impact() (engine/aggregator/_floor.py) |
Basel 3.1 output floor at portfolio level, gated on the output_floor pack Feature and the entity-scope check; distributes the shortfall pro-rata by sa_rwa across floor-eligible rows |
The drop must precede the floor and the expected-loss summary: a loser left on
a branch frame inflates S-TREA, and its expected loss feeds the CET1 deduction and
therefore OF-ADJ. Filtering the combined frame alone is green on rwa_final and
wrong on OF-ADJ.
Key Features¶
- Facility-share candidate resolution before the floor
- 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