Standardised Approach¶
The Standardised Approach (SA) uses regulatory-prescribed risk weights based on external credit ratings and exposure characteristics. It is the default approach for institutions without IRB approval.
Overview¶
The SA calculation involves: 1. Determining the Exposure Class 2. Mapping to a Credit Quality Step (CQS) if rated 3. Looking up the Risk Weight 4. Applying Supporting Factors (CRR only)
Risk Weight Determination¶
flowchart TD
A[Exposure] --> B{Has External Rating?}
B -->|Yes| C[Map to CQS]
B -->|No| D[Unrated Treatment]
C --> E[Lookup RW by Class + CQS]
D --> F[Default RW for Class]
E --> G[Apply Supporting Factors]
F --> G
G --> H[Calculate RWA]
Credit Quality Steps (CQS)¶
External ratings are mapped to Credit Quality Steps:
| CQS | S&P/Fitch | Moody's | Description |
|---|---|---|---|
| CQS 1 | AAA to AA- | Aaa to Aa3 | Prime/High Grade |
| CQS 2 | A+ to A- | A1 to A3 | Upper Medium Grade |
| CQS 3 | BBB+ to BBB- | Baa1 to Baa3 | Lower Medium Grade |
| CQS 4 | BB+ to BB- | Ba1 to Ba3 | Non-Investment Grade |
| CQS 5 | B+ to B- | B1 to B3 | Highly Speculative |
| CQS 6 | CCC+ and below | Caa1 and below | Substantial Risk |
Risk Weights by Exposure Class¶
Sovereign Exposures¶
Exposures to governments and central banks:
| CQS | Risk Weight |
|---|---|
| CQS 1 | 0% |
| CQS 2 | 20% |
| CQS 3 | 50% |
| CQS 4 | 100% |
| CQS 5 | 100% |
| CQS 6 | 150% |
| Unrated | 100% |
UK Government
UK Government (HM Treasury) exposures receive 0% risk weight as a CQS 1 sovereign.
Institution Exposures¶
Exposures to banks and investment firms:
| CQS | CRR Risk Weight | Basel 3.1 (ECRA) |
|---|---|---|
| CQS 1 | 20% | 20% |
| CQS 2 | 30%* | 30% |
| CQS 3 | 50% | 50% |
| CQS 4 | 100% | 100% |
| CQS 5 | 100% | 100% |
| CQS 6 | 150% | 150% |
*UK deviation from standard 50% Basel weight
Unrated Institutions: - CRR: 40% risk weight (with due diligence assessment) - Basel 3.1: Use Standardised Credit Risk Assessment (SCRA)
Corporate Exposures¶
Exposures to non-financial corporates:
| CQS | CRR | Basel 3.1 |
|---|---|---|
| CQS 1 | 20% | 20% |
| CQS 2 | 50% | 50% |
| CQS 3 | 100% | 75% |
| CQS 4 | 100% | 100% |
| CQS 5 | 150% | 100% |
| CQS 6 | 150% | 150% |
| Unrated | 100% | 100% |
Corporate SME: - Same risk weights as Corporate - SME Supporting Factor may apply (CRR only)
Retail Exposures¶
Residential Mortgages (CRR):
| Criterion | Risk Weight |
|---|---|
| LTV ≤ 80%, performing | 35% |
| LTV > 80% | Split treatment: 35% on portion up to 80% LTV, 75% on excess |
Residential Mortgages (Basel 3.1):
| LTV | General (Whole Loan) | Income-Producing |
|---|---|---|
| ≤ 50% | 20% | 30% |
| 50-60% | 25% | 35% |
| 60-70% | 25% | 45% |
| 70-80% | 30% | 50% |
| 80-90% | 40% | 60% |
| 90-100% | 50% | 75% |
| > 100% | 70% | 105% |
QRRE (Qualifying Revolving Retail Exposures):
| Framework | Risk Weight |
|---|---|
| CRR | 75% |
| Basel 3.1 | 75% |
Other Retail:
| Framework | Risk Weight |
|---|---|
| CRR | 75% |
| Basel 3.1 | 75% |
Defaulted Exposures¶
Exposures where the counterparty is in default receive 100% risk weight under SA. Defaulted treatment with provision-coverage differentiation is handled through the IRB approach (see IRB Approach).
Equity Exposures¶
| Type | SA Risk Weight |
|---|---|
| Central bank | 0% |
| Listed / Exchange-traded / Government-supported | 100% |
| Unlisted / Private equity / CIU / Other | 250% |
| Speculative | 400% |
Commercial Real Estate¶
| Scenario | CRR | Basel 3.1 |
|---|---|---|
| Standard | 100% | 100% |
| CRR preferential (LTV ≤ 50%, income cover) | 50% | N/A |
| Income-Producing (LTV ≤ 60%) | N/A | 70% |
| Income-Producing (60-80%) | N/A | 90% |
| Income-Producing (LTV > 80%) | N/A | 110% |
| General (LTV ≤ 60%) | N/A | min(60%, Cpty RW) |
EAD Calculation¶
On-Balance Sheet¶
Off-Balance Sheet¶
Credit Conversion Factors by Risk Type (CRR Art. 111):
The risk_type column determines the CCF for off-balance sheet exposures:
| Risk Type | Code | SA CCF | Description |
|---|---|---|---|
| Full Risk | FR | 100% | Guarantees, acceptances, credit derivatives |
| Medium Risk | MR | 50% | NIFs, RUFs, standby LCs, committed undrawn |
| Medium-Low Risk | MLR | 20% | Documentary credits, short-term trade finance |
| Low Risk | LR | 0% | Unconditionally cancellable commitments |
Basel 3.1 Changes:
| Item Type | CRR CCF | Basel 3.1 CCF |
|---|---|---|
| Unconditionally Cancellable | 0% | 10% |
| Trade Finance (ST) | 20% | 20% |
| Undrawn Commitments (<1yr) | 20% | 40% |
| Undrawn Commitments (≥1yr) | 50% | 40% |
| NIFs/RUFs | 50% | 50% |
| Direct Credit Substitutes | 100% | 100% |
Credit Risk Mitigation¶
SA allows several CRM techniques:
Financial Collateral Simple Method¶
# Reduce RW based on collateral
Collateral_RW = Risk_Weight_of_Collateral_Issuer
# Apply lower of exposure RW or collateral RW
Effective_RW = min(Exposure_RW, Collateral_RW)
Financial Collateral Comprehensive Method¶
# Calculate adjusted values
E_adjusted = Exposure × (1 + H_e) # Exposure haircut
C_adjusted = Collateral × (1 - H_c - H_fx) # Collateral haircut
# Net exposure
E_star = max(0, E_adjusted - C_adjusted)
# RWA on net exposure
RWA = E_star × Risk_Weight
Supervisory Haircuts:
| Collateral Type | Haircut |
|---|---|
| Cash (same currency) | 0% |
| Government bonds ≤1yr | 0.5% |
| Government bonds 1-5yr | 2% |
| Government bonds >5yr | 4% |
| Corporate bonds AAA-AA ≤1yr | 1% |
| Corporate bonds AAA-AA 1-5yr | 4% |
| Corporate bonds AAA-AA >5yr | 8% |
| Main index equity | 15% |
| Other equity | 25% |
| Currency mismatch add-on | +8% |
Guarantees (Substitution Approach)¶
# Guaranteed portion treated as exposure to guarantor
Guaranteed_RWA = Guaranteed_Amount × Guarantor_RW
# Unguaranteed portion at counterparty RW
Unguaranteed_RWA = (EAD - Guaranteed_Amount) × Counterparty_RW
# Total RWA
RWA = Guaranteed_RWA + Unguaranteed_RWA
Supporting Factors (CRR Only)¶
Supporting factors are implemented in sa/supporting_factors.py.
SME Supporting Factor¶
Reduces RWA for SME exposures (CRR Art. 501):
Conceptual Logic
The following illustrates the SME factor calculation. See the actual implementation for the full Polars-based processing.
# Check eligibility
if turnover <= EUR_50m and is_sme:
threshold = EUR_2.5m # ~GBP 2.18m (converted via eur_gbp_rate)
if exposure <= threshold:
factor = 0.7619
else:
factor = (threshold * 0.7619 + (exposure - threshold) * 0.85) / exposure
RWA = RWA * factor
Actual Implementation (supporting_factors.py)
"""
CRR Supporting Factors for SA calculator (CRR2 Art. 501).
Applies SME and infrastructure supporting factors to RWA calculations.
These factors are CRR-specific and NOT available under Basel 3.1.
SME Supporting Factor - Tiered Approach (CRR2 Art. 501):
- Applies only to non-defaulted exposures (Art. 501 exclusion)
- Exposures up to EUR 2.5m (GBP 2.2m): factor of 0.7619
- Exposures above EUR 2.5m (GBP 2.2m): factor of 0.85
Formula:
factor = [min(D, threshold) × 0.7619 + max(D - threshold, 0) × 0.85] / D
Where D = drawn_amount + interest (on-balance-sheet amount owed)
The tier threshold is applied to drawn (on-balance-sheet) amounts only,
not the full post-CRM EAD which includes CCF-adjusted undrawn commitments.
The resulting blended factor is then applied to the full RWA.
Infrastructure Supporting Factor (CRR Art. 501a):
- Qualifying infrastructure: factor of 0.75
References:
- CRR2 Art. 501 (EU 2019/876 amending EU 575/2013)
- CRR Art. 501a: Infrastructure supporting factor
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import TYPE_CHECKING
import polars as pl
if TYPE_CHECKING:
from rwa_calc.contracts.config import CalculationConfig
@dataclass
class SupportingFactorResult:
"""Result of supporting factor calculation."""
factor: Decimal
was_applied: bool
description: str
class SupportingFactorCalculator:
"""
Calculate SME and infrastructure supporting factors for CRR.
The supporting factors reduce RWA for qualifying exposures:
- SME: Tiered factor (0.7619 up to threshold, 0.85 above)
- Infrastructure: Flat 0.75 factor
Under Basel 3.1, these factors are not available (returns 1.0).
"""
def calculate_sme_factor(
self,
total_exposure: Decimal,
config: CalculationConfig,
) -> Decimal:
"""
Calculate SME supporting factor based on total drawn exposure.
Args:
total_exposure: Total drawn (on-balance-sheet) amount to the SME
config: Calculation configuration
Returns:
Effective supporting factor (0.7619 to 0.85)
"""
if not config.supporting_factors.enabled:
return Decimal("1.0")
if total_exposure <= 0:
return Decimal("1.0")
# Get thresholds and factors from config
threshold_eur = config.supporting_factors.sme_exposure_threshold_eur
threshold_gbp = threshold_eur * config.eur_gbp_rate
factor_tier1 = config.supporting_factors.sme_factor_under_threshold
factor_tier2 = config.supporting_factors.sme_factor_above_threshold
# Use GBP threshold for GBP currency (default)
threshold = threshold_gbp
# Calculate tiered factor
tier1_amount = min(total_exposure, threshold)
tier2_amount = max(total_exposure - threshold, Decimal("0"))
weighted_factor = tier1_amount * factor_tier1 + tier2_amount * factor_tier2
return weighted_factor / total_exposure
def calculate_infrastructure_factor(
self,
config: CalculationConfig,
) -> Decimal:
"""
Get infrastructure supporting factor.
Args:
config: Calculation configuration
Returns:
Infrastructure factor (0.75 for CRR, 1.0 for Basel 3.1)
"""
if not config.supporting_factors.enabled:
return Decimal("1.0")
return config.supporting_factors.infrastructure_factor
def get_effective_factor(
self,
is_sme: bool,
is_infrastructure: bool,
total_exposure: Decimal,
config: CalculationConfig,
) -> Decimal:
"""
Get the most beneficial supporting factor.
If both SME and infrastructure apply, returns the lower factor
(more beneficial to the bank).
Args:
is_sme: Whether exposure qualifies for SME factor
is_infrastructure: Whether exposure qualifies for infrastructure
total_exposure: Total drawn (on-balance-sheet) amount for tier calc
config: Calculation configuration
Returns:
Most beneficial factor (lowest value)
"""
if not config.supporting_factors.enabled:
return Decimal("1.0")
factors = [Decimal("1.0")]
if is_sme:
factors.append(self.calculate_sme_factor(total_exposure, config))
if is_infrastructure:
factors.append(self.calculate_infrastructure_factor(config))
# Return lowest factor (most beneficial)
return min(factors)
def apply_factors(
self,
exposures: pl.LazyFrame,
config: CalculationConfig,
) -> pl.LazyFrame:
"""
Apply supporting factors to exposures LazyFrame.
The SME supporting factor threshold (EUR 2.5m) is applied at the
counterparty level using drawn (on-balance-sheet) amounts only.
All drawn amounts to the same counterparty are aggregated before
determining the tiered factor. The resulting blended factor is
then applied to each exposure's full RWA.
The tier calculation uses drawn_amount + interest ("amount owed"),
NOT ead_final which includes CCF-adjusted undrawn commitments.
Expects columns:
- is_sme: bool
- is_infrastructure: bool
- drawn_amount: float (on-balance-sheet drawn amount)
- interest: float (accrued interest)
- ead_final: float (fallback if drawn_amount not available)
- rwa_pre_factor: float (RWA before supporting factor)
- counterparty_reference: str (optional, for aggregation)
Adds columns:
- supporting_factor: float
- rwa_post_factor: float (RWA after supporting factor)
- supporting_factor_applied: bool
- total_cp_drawn: float (total counterparty drawn amount, for SME)
Args:
exposures: Exposures with RWA calculated
config: Calculation configuration
Returns:
Exposures with supporting factors applied
"""
if not config.supporting_factors.enabled:
# Basel 3.1: No supporting factors
return exposures.with_columns(
[
pl.lit(1.0).alias("supporting_factor"),
pl.col("rwa_pre_factor").alias("rwa_post_factor"),
pl.lit(False).alias("supporting_factor_applied"),
]
)
# Get threshold in GBP
threshold_gbp = float(
config.supporting_factors.sme_exposure_threshold_eur * config.eur_gbp_rate
)
factor_tier1 = float(config.supporting_factors.sme_factor_under_threshold)
factor_tier2 = float(config.supporting_factors.sme_factor_above_threshold)
infra_factor = float(config.supporting_factors.infrastructure_factor)
# Check for required columns
schema = exposures.collect_schema()
has_sme = "is_sme" in schema.names()
has_infra = "is_infrastructure" in schema.names()
has_counterparty = "counterparty_reference" in schema.names()
has_btl = "is_buy_to_let" in schema.names()
has_defaulted = "is_defaulted" in schema.names()
has_drawn = "drawn_amount" in schema.names()
# Build the drawn (on-balance-sheet) expression for tier calculation.
# Use drawn_amount + interest when available; fall back to ead_final.
if has_drawn:
drawn_expr = pl.col("drawn_amount").clip(lower_bound=0.0) + pl.col(
"interest"
).fill_null(0.0)
else:
drawn_expr = pl.col("ead_final")
# Build SME factor expression with counterparty-level aggregation
if has_sme:
if has_counterparty:
# Aggregate drawn amounts at counterparty level using window function
# Only aggregate SME exposures with valid counterparty references
total_cp_drawn_expr = (
pl.when(pl.col("is_sme") & pl.col("counterparty_reference").is_not_null())
.then(drawn_expr.sum().over("counterparty_reference"))
.otherwise(
# Fall back to individual drawn if no counterparty ref or not SME
drawn_expr
)
)
exposures = exposures.with_columns([total_cp_drawn_expr.alias("total_cp_drawn")])
# Use counterparty total drawn for tier calculation
ead_for_tier = pl.col("total_cp_drawn")
else:
# No counterparty reference column - use individual drawn amount
ead_for_tier = drawn_expr
# Calculate tiered factor based on aggregated drawn exposure
tier1_expr = (
pl.when(ead_for_tier <= threshold_gbp)
.then(ead_for_tier)
.otherwise(pl.lit(threshold_gbp))
)
tier2_expr = (
pl.when(ead_for_tier > threshold_gbp)
.then(ead_for_tier - threshold_gbp)
.otherwise(pl.lit(0.0))
)
# BTL exposures are excluded from the SME factor but still
# contribute to total_cp_drawn for tier calculation (CRR Art. 501)
is_btl = pl.col("is_buy_to_let") if has_btl else pl.lit(False)
# Defaulted exposures are excluded from SME factor (CRR Art. 501)
is_defaulted = pl.col("is_defaulted") if has_defaulted else pl.lit(False)
sme_factor_expr = (
pl.when(pl.col("is_sme") & (ead_for_tier > 0) & ~is_btl & ~is_defaulted)
.then((tier1_expr * factor_tier1 + tier2_expr * factor_tier2) / ead_for_tier)
.when(pl.col("is_sme") & (ead_for_tier <= 0) & ~is_btl & ~is_defaulted)
.then(
# Zero drawn = all within tier 1 → pure 0.7619
pl.lit(factor_tier1)
)
.otherwise(pl.lit(1.0))
)
else:
sme_factor_expr = pl.lit(1.0)
# Build infrastructure factor expression inline
if has_infra:
infra_factor_expr = (
pl.when(pl.col("is_infrastructure"))
.then(pl.lit(infra_factor))
.otherwise(pl.lit(1.0))
)
else:
infra_factor_expr = pl.lit(1.0)
# Compute minimum (most beneficial) factor
min_factor_expr = pl.min_horizontal(sme_factor_expr, infra_factor_expr)
# Single with_columns call for maximum performance
return exposures.with_columns(
[
min_factor_expr.alias("supporting_factor"),
(pl.col("rwa_pre_factor") * min_factor_expr).alias("rwa_post_factor"),
(min_factor_expr < 1.0).alias("supporting_factor_applied"),
]
)
def create_supporting_factor_calculator() -> SupportingFactorCalculator:
"""Create a SupportingFactorCalculator instance."""
return SupportingFactorCalculator()
Infrastructure Factor¶
Reduces RWA for qualifying infrastructure (CRR Art. 501a):
Calculation Example¶
Exposure: - Corporate loan, £10m drawn - Rated A+ (CQS 2) - SME counterparty (turnover £30m) - Unsecured
Calculation:
# Step 1: EAD
EAD = £10,000,000
# Step 2: Risk Weight (CQS 2 Corporate)
RW = 50%
# Step 3: Base RWA
Base_RWA = £10,000,000 × 50% = £5,000,000
# Step 4: SME Factor (CRR only)
# Exposure > threshold, so tiered
threshold = EUR 2,500,000 × 0.8732 = £2,183,000
factor = (2,183,000 × 0.7619 + 7,817,000 × 0.85) / 10,000,000
factor = (1,663,427 + 6,644,450) / 10,000,000
factor = 0.831
# Step 5: Final RWA (CRR)
Final_RWA = £5,000,000 × 0.831 = £4,153,939
# Basel 3.1 (no SME factor)
Final_RWA_B31 = £5,000,000
Implementation Notes¶
Calculator Usage¶
The SA calculator is implemented in sa/calculator.py.
import polars as pl
from rwa_calc.engine.sa.calculator import SACalculator
from rwa_calc.contracts.config import CalculationConfig
from datetime import date
# Create SA calculator
calculator = SACalculator()
# Calculate RWA for a single exposure via calculate_branch()
df = pl.DataFrame({
"exposure_reference": ["EX1"],
"ead": [10_000_000.0],
"exposure_class": ["CORPORATE"],
"cqs": [2],
"is_sme": [True],
}).lazy()
config = CalculationConfig.crr(reporting_date=date(2026, 12, 31))
result = calculator.calculate_branch(df, config).collect().to_dicts()[0]
# Access results
print(f"Risk Weight: {result['risk_weight']}")
print(f"RWA: {result['rwa']}")
Risk Weight Lookup¶
Risk weights are defined in:
- CRR:
data/tables/crr_risk_weights.py—get_combined_cqs_risk_weights(use_uk_deviation=True) - Basel 3.1:
data/tables/b31_risk_weights.py—get_b31_combined_cqs_risk_weights()
from rwa_calc.data.tables.crr_risk_weights import get_combined_cqs_risk_weights
# Get CRR risk weight lookup table
rw_table = get_combined_cqs_risk_weights(use_uk_deviation=True)
# Table includes: exposure_class, cqs, risk_weight
# Example: CORPORATE, CQS 2 -> 50%
Actual Risk Weight Application (calculator.py)
See the _apply_risk_weights method in src/rwa_calc/engine/sa/calculator.py for the
full implementation of risk weight lookups by exposure class and CQS.
Regulatory References¶
| Topic | CRR Article | BCBS CRE |
|---|---|---|
| Risk weight assignment | Art. 113-134 | CRE20-22 |
| CCFs | Art. 111 | CRE20.10 |
| CRM | Art. 192-241 | CRE22 |
| SME factor | Art. 501 | N/A |
| Real estate | Art. 124-125 | CRE20.70-90 |
Next Steps¶
- IRB Approach - Internal ratings-based methodology
- Credit Risk Mitigation - CRM techniques in detail
- Supporting Factors - SME and infrastructure factors