Skip to content

IRB Approach

The Internal Ratings-Based (IRB) approach allows banks with regulatory approval to use their own risk estimates for calculating RWA. This provides greater risk sensitivity than the Standardised Approach.

Overview

Two IRB variants are available:

Approach PD LGD EAD CCF
Foundation IRB (F-IRB) Bank Supervisory Supervisory Supervisory
Advanced IRB (A-IRB) Bank Bank Bank Bank

Basel 3.1 Restrictions

Under Basel 3.1, A-IRB is no longer permitted for: - Large corporates (revenue > £500m) - Banks/Financial Institutions - Equity exposures

IRB Formula

The core IRB formula calculates the capital requirement (K). See irb/formulas.py for the pure Polars implementation.

K = [LGD × N((1-R)^(-0.5) × G(PD) + (R/(1-R))^0.5 × G(0.999)) - LGD × PD] × MA

Where: - N() = Standard normal cumulative distribution function - G() = Inverse standard normal distribution function - R = Asset correlation - PD = Probability of Default - LGD = Loss Given Default - MA = Maturity Adjustment (for non-retail)

Then:

RWA = K × 12.5 × EAD × Scaling Factor

Stats Backend

The IRB formulas require statistical functions (normal CDF and inverse CDF). The calculator uses polars-normal-stats for native Polars CDF/PPF expressions.

Installation

uv add polars-normal-stats
Capital K Formula Implementation (formulas.py)

See the _polars_capital_k_expr() function in src/rwa_calc/engine/irb/formulas.py for the full Polars expression implementation.

Risk Parameters

Probability of Default (PD)

PD is the likelihood of default within one year.

Floors:

Exposure Class CRR Basel 3.1
Corporate 0.03% 0.05%
Large Corporate 0.03% 0.05%
Bank/Institution 0.03% 0.05%
Retail Mortgage 0.03% 0.05%
Retail QRRE (Transactor) 0.03% 0.03%
Retail QRRE (Revolver) 0.03% 0.10%
Retail Other 0.03% 0.05%
PD_effective = max(PD_estimated, PD_floor)

Loss Given Default (LGD)

LGD is the percentage of exposure lost after recoveries.

F-IRB Supervisory LGD:

Exposure Type CRR Basel 3.1
Senior Unsecured 45% 40%
Subordinated 75% 75%
Secured by Financial Collateral 0% 0%
Secured by Receivables 35% 20%
Secured by CRE/RRE 35% 20%
Secured by Other Collateral 40% 25%

A-IRB LGD Floors (Basel 3.1 only, PRA PS1/26):

Collateral Type LGD Floor
Unsecured Senior 25%
Unsecured Subordinated 50%
Financial Collateral 0%
Receivables 10%
Commercial Real Estate 10%
Residential Real Estate 5%
Other Physical 15%

Exposure at Default (EAD)

F-IRB: - On-Balance Sheet: Gross carrying amount - Off-Balance Sheet: Regulatory CCFs apply based on risk_type:

Risk Type SA CCF F-IRB CCF Notes
FR (full_risk) 100% 100% Guarantees, credit substitutes
MR (medium_risk) 50% 75% Committed undrawn, NIFs, RUFs
MLR (medium_low_risk) 20% 75% Documentary credits, trade finance
LR (low_risk) 0% 0% Unconditionally cancellable

CRR Art. 166(8): Under F-IRB, MR and MLR categories both use 75% CCF.

CRR Art. 166(9) Exception: Short-term letters of credit arising from the movement of goods retain 20% CCF under F-IRB. Flag these exposures with is_short_term_trade_lc = True.

A-IRB: - Bank estimates EAD using internal models - Provide modelled CCF in ccf_modelled column (0.0-1.5, can exceed 100% for Retail IRB) - When ccf_modelled is provided, it takes precedence over risk_type lookup - Subject to CCF floors under Basel 3.1

Maturity (M)

Effective maturity affects capital through the maturity adjustment.

  • Range: 1 year (floor) to 5 years (cap)
  • Retail exemption: No maturity adjustment for retail
# Effective maturity calculation
M = max(1, min(5, weighted_average_life))

Asset Correlation

Asset correlation (R) determines how sensitive the exposure is to systematic risk. See _polars_correlation_expr() for the pure Polars implementation.

Corporate/Bank Correlation

R = 0.12 × (1 - exp(-50 × PD)) / (1 - exp(-50)) +
    0.24 × [1 - (1 - exp(-50 × PD)) / (1 - exp(-50))]

This produces: - R = 24% for very low PD - R = 12% for high PD

SME Size Adjustment

For SME corporates (turnover €5m-€50m). The turnover_m column is provided in GBP millions; the calculator converts to EUR using config.eur_gbp_rate:

# Convert GBP turnover to EUR
S_eur = turnover_gbp / eur_gbp_rate  # e.g. £25m / 0.8732 = €28.6m

# Clamp to range and apply adjustment
S = min(50, max(5, S_eur))
R_adjusted = R - 0.04 × (1 - (S - 5) / 45)

This reduces correlation by up to 4 percentage points for smaller firms.

Retail Correlations

Retail Type Correlation
Residential Mortgage 15%
QRRE 4%
Other Retail 3-16% (PD-dependent)

Other Retail:

R = 0.03 × (1 - exp(-35 × PD)) / (1 - exp(-35)) +
    0.16 × [1 - (1 - exp(-35 × PD)) / (1 - exp(-35))]

Actual Correlation Implementation (formulas.py)

See _polars_correlation_expr() in src/rwa_calc/engine/irb/formulas.py for the full Polars expression implementation including SME adjustment.

Maturity Adjustment

For non-retail exposures. See _polars_maturity_adjustment_expr() for the pure Polars implementation.

# Maturity factor b
b = (0.11852 - 0.05478 × ln(PD))^2

# Maturity adjustment
MA = (1 + (M - 2.5) × b) / (1 - 1.5 × b)

Where M is effective maturity in years.

Actual Maturity Adjustment (formulas.py)

See _polars_maturity_adjustment_expr() in src/rwa_calc/engine/irb/formulas.py for the full Polars expression implementation.

Example values:

PD M=1yr M=2.5yr M=5yr
0.03% 0.853 1.000 1.221
0.10% 0.880 1.000 1.177
1.00% 0.934 1.000 1.099
5.00% 0.966 1.000 1.050

Scaling Factor

Framework Scaling Factor
CRR 1.06
Basel 3.1 1.00 (none)
# CRR
RWA = K × 12.5 × EAD × MA × 1.06

# Basel 3.1
RWA = K × 12.5 × EAD × MA

Expected Loss (EL)

IRB requires calculation of Expected Loss:

EL = PD × LGD × EAD

EL is compared to provisions: - EL > Provisions: Shortfall deducted from capital - EL < Provisions: Excess added to Tier 2 (with limits)

Detailed Calculation Example

Exposure: - Corporate loan, £50m - Bank-estimated PD: 0.50% - F-IRB (LGD = 45%) - Maturity: 3 years - Counterparty turnover: £25m (SME)

Step 1: Apply PD Floor

PD = max(0.0050, 0.0003) = 0.0050  # 0.50%

Step 2: Calculate Asset Correlation

# Base correlation
R_base = 0.12 × (1 - exp(-50 × 0.005)) / (1 - exp(-50)) +
         0.24 × (1 - (1 - exp(-50 × 0.005)) / (1 - exp(-50)))
R_base = 0.12 × 0.221 + 0.24 × 0.779 = 0.214

# SME adjustment (turnover £25m, converted to EUR)
S_eur = 25 / 0.8732 = 28.63  # GBP → EUR conversion
S = min(50, max(5, 28.63)) = 28.63
adjustment = 0.04 × (1 - (28.63 - 5) / 45) = 0.04 × 0.475 = 0.019
R = 0.214 - 0.019 = 0.195

Step 3: Calculate Capital Requirement (K)

# Intermediate calculations
G_PD = norm.ppf(0.005) = -2.576
G_999 = norm.ppf(0.999) = 3.090

term1 = (1 - R)^(-0.5) × G_PD = 1.115 × (-2.576) = -2.871
term2 = (R / (1-R))^0.5 × G_999 = 0.492 × 3.090 = 1.521

K_pre = LGD × N(term1 + term2) - LGD × PD
K_pre = 0.45 × N(-1.350) - 0.45 × 0.005
K_pre = 0.45 × 0.0885 - 0.00225
K_pre = 0.0398 - 0.00225 = 0.0376

Step 4: Calculate Maturity Adjustment

b = (0.11852 - 0.05478 × ln(0.005))^2 = (0.11852 + 0.290)^2 = 0.167
MA = (1 + (3 - 2.5) × 0.167) / (1 - 1.5 × 0.167)
MA = 1.0835 / 0.7495 = 1.446

Step 5: Calculate RWA

# CRR
RWA_CRR = K × 12.5 × EAD × MA × 1.06
RWA_CRR = 0.0376 × 12.5 × 50,000,000 × 1.446 × 1.06
RWA_CRR = £36,019,860

RW_CRR = RWA / EAD = 72.0%

# Basel 3.1 (no scaling)
RWA_B31 = 0.0376 × 12.5 × 50,000,000 × 1.446 × 1.00
RWA_B31 = £33,981,000

RW_B31 = 68.0%

Step 6: Check Output Floor (Basel 3.1)

# SA equivalent RWA (assume 100% RW corporate)
RWA_SA = 50,000,000 × 100% = £50,000,000

# Output floor (72.5%)
Floor = 50,000,000 × 0.725 = £36,250,000

# Final RWA
RWA_final = max(33,981,000, 36,250,000) = £36,250,000

Implementation

The IRB module provides a Polars namespace extension for fluent, chainable calculations:

import polars as pl
from datetime import date
from rwa_calc.contracts.config import CalculationConfig
import rwa_calc.engine.irb.namespace  # Registers .irb namespace

config = CalculationConfig.crr(reporting_date=date(2026, 12, 31))

# Create sample exposure data
exposures = pl.LazyFrame({
    "exposure_reference": ["EXP001"],
    "pd": [0.005],
    "lgd": [0.45],
    "ead_final": [50_000_000.0],
    "maturity": [3.0],
    "exposure_class": ["CORPORATE"],
    "turnover_m": [25.0],  # Annual turnover in millions
})

# Fluent IRB calculation pipeline
result = (
    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
    .collect()
)

# Access results
print(result.select([
    "pd_floored", "correlation", "k", "maturity_adjustment", "rwa", "expected_loss"
]))

Individual formula steps can also be chained:

result = (
    exposures
    .irb.apply_pd_floor(config)
    .irb.apply_lgd_floor(config)
    .irb.calculate_correlation(config)
    .irb.calculate_k(config)
    .irb.calculate_maturity_adjustment(config)
    .irb.calculate_rwa(config)
    .irb.calculate_expected_loss(config)
)

Using the IRB Calculator

from rwa_calc.engine.irb.calculator import IRBCalculator
from rwa_calc.contracts.config import CalculationConfig

# Create calculator
calculator = IRBCalculator()

# Calculate — takes a CRMAdjustedBundle, returns a LazyFrameResult
result = calculator.calculate(
    data=crm_adjusted_bundle,
    config=CalculationConfig.crr(reporting_date=date(2026, 12, 31))
)

# Result is a LazyFrameResult containing the LazyFrame and any errors
irb_rwa_df = result.data.collect()
print(irb_rwa_df.select("rwa", "expected_loss"))

Using IRB Formulas Directly

The IRB formulas are implemented in irb/formulas.py. Both scalar functions and pure Polars expression functions are available.

Implementation Architecture

  • Vectorized expressions: Pure Polars expressions for bulk processing (used by namespace and calculator)
  • Scalar wrappers: Thin wrappers around vectorized expressions for single-value calculations
from rwa_calc.engine.irb.formulas import (
    calculate_k,
    calculate_correlation,
    calculate_maturity_adjustment,
    calculate_irb_rwa,  # Convenience function for single exposures
)

# Calculate components
R = calculate_correlation(
    pd=0.005,
    exposure_class="CORPORATE",
    turnover_m=25.0,  # Turnover in GBP millions (converted to EUR internally)
)

MA = calculate_maturity_adjustment(pd=0.005, maturity=3)

K = calculate_k(pd=0.005, lgd=0.45, correlation=R)

# Calculate RWA
rwa = K * 12.5 * ead * MA * scaling_factor

# Or use the convenience function for complete calculation
result = calculate_irb_rwa(
    ead=50_000_000,
    pd=0.005,
    lgd=0.45,
    correlation=R,
    maturity=3.0,
    apply_scaling_factor=True,  # CRR 1.06 factor
)
print(f"RWA: {result['rwa']:,.0f}")
Scalar K Calculation (formulas.py)

See calculate_k() in src/rwa_calc/engine/irb/formulas.py for the scalar wrapper around the Polars expression.

Expected Loss Calculation

from rwa_calc.engine.irb.formulas import calculate_expected_loss

el = calculate_expected_loss(
    pd=0.005,
    lgd=0.45,
    ead=50_000_000
)
# el = 0.005 × 0.45 × 50,000,000 = £112,500

F-IRB vs A-IRB Comparison

Parameter F-IRB A-IRB
PD Bank estimate Bank estimate
LGD Supervisory (45%/75%) Bank estimate (floored)
EAD Regulatory Bank estimate
CCF Regulatory Bank estimate
Typical RW Higher Lower
Complexity Lower Higher
Approval Easier Harder

Regulatory References

Topic CRR Article BCBS CRE
IRB approach overview Art. 142-150 CRE30
K formula Art. 153 CRE31
PD estimation Art. 178-180 CRE32
LGD estimation Art. 181 CRE32
Correlation Art. 153 CRE31
Maturity adjustment Art. 162 CRE31
Supervisory LGD Art. 161 CRE32

Next Steps