Retail Exposures¶
Retail exposures are claims on individuals or small businesses that meet specific criteria for size, product type, and portfolio management.
Definition¶
Retail exposures must meet ALL of the following criteria:
| Criterion | Requirement |
|---|---|
| Counterparty | Individual or small business |
| Product | Revolving credit, personal loans, mortgages, or small business facilities |
| Size | Total exposure ≤ EUR 1m (GBP 880k) |
| Management | Managed as part of a portfolio with similar characteristics |
Conceptual Logic
The following illustrates the retail classification decision logic. For the actual implementation,
see classifier.py:285-392.
# Conceptual overview - actual implementation in ExposureClassifier._apply_retail_classification
def is_retail(exposure, counterparty, lending_group_adjusted_exposure):
return (
counterparty.type in ["individual", "retail", "small_business"] and
lending_group_adjusted_exposure <= 1_000_000 and # EUR threshold
is_managed_as_retail_pool(exposure) # cp_is_managed_as_retail flag
)
Actual Implementation (classifier.py)
Compute all flags that depend only on raw input columns.
Uses two .with_columns() batches: the first pre-computes shared
intermediates (uppercase strings, entity-type mapping) that the
second batch references, avoiding redundant str.to_uppercase()
and replace_strict() calls.
Sets: exposure_class_sa, exposure_class_irb, exposure_class, is_mortgage,
is_defaulted, exposure_class_for_sa, is_infrastructure,
qualifies_as_retail, retail_threshold_exclusion_applied
"""
max_retail_exposure = float(config.retail_thresholds.max_exposure_threshold)
# SL override: exposures with sl_type (from specialised_lending join) get
# SPECIALISED_LENDING class regardless of counterparty entity_type.
sl_override = pl.col("sl_type").is_not_null()
# Batch 1: Pre-compute shared intermediates to avoid redundant work.
# - _sa_class: entity type → SA class mapping (used 3× below)
# - _irb_class: entity type → IRB class mapping
# - _pt_upper: product_type uppercased (used in is_mortgage, infrastructure)
exposures = exposures.with_columns(
[
pl.col("cp_entity_type")
.replace_strict(ENTITY_TYPE_TO_SA_CLASS, default=ExposureClass.OTHER.value)
.alias("_sa_class"),
pl.col("cp_entity_type")
.replace_strict(ENTITY_TYPE_TO_IRB_CLASS, default=ExposureClass.OTHER.value)
.alias("_irb_class"),
pl.col("product_type").str.to_uppercase().alias("_pt_upper"),
]
)
sl_class = pl.lit(ExposureClass.SPECIALISED_LENDING.value)
# Batch 2: Derive all flags from pre-computed intermediates.
return exposures.with_columns(
[
# --- Exposure class mappings (SL table overrides entity_type) ---
pl.when(sl_override)
.then(sl_class)
.otherwise(pl.col("_sa_class"))
.alias("exposure_class_sa"),
pl.when(sl_override)
.then(sl_class)
.otherwise(pl.col("_irb_class"))
.alias("exposure_class_irb"),
pl.when(sl_override)
.then(sl_class)
.otherwise(pl.col("_sa_class"))
.alias("exposure_class"),
# --- Mortgage flag ---
self._build_is_mortgage_expr(schema_names),
# --- Default flags ---
(pl.col("cp_default_status") == True) # noqa: E712
.alias("is_defaulted"),
pl.when(pl.col("cp_default_status") == True) # noqa: E712
.then(pl.lit(ExposureClass.DEFAULTED.value))
.when(sl_override)
Retail Sub-Classes¶
| Sub-Class | Description | IRB Correlation |
|---|---|---|
| Retail Mortgage | Residential mortgages | 15% |
| Retail QRRE | Qualifying revolving retail | 4% |
| Retail Other | All other retail | 3-16% |
Retail Mortgage¶
Definition¶
Exposures secured by residential property that is or will be: - Occupied by the borrower, OR - Rented out
SA Risk Weights (CRR)¶
| Criterion | Risk Weight |
|---|---|
| LTV ≤ 80%, performing | 35% |
| LTV > 80% | 75% |
| Non-performing | 100% |
SA Risk Weights (Basel 3.1)¶
Whole Loan Approach:
| LTV | Risk Weight |
|---|---|
| ≤ 50% | 20% |
| 50-60% | 25% |
| 60-70% | 30% |
| 70-80% | 40% |
| 80-90% | 50% |
| 90-100% | 70% |
| > 100% | Counterparty RW |
Income-Producing (Buy-to-Let):
| LTV | Risk Weight |
|---|---|
| ≤ 50% | 30% |
| 50-60% | 35% |
| 60-70% | 45% |
| 70-80% | 60% |
| 80-90% | 75% |
| 90-100% | 105% |
| > 100% | Counterparty RW |
IRB Treatment¶
Parameters: - PD: Bank estimate (floor 0.03% CRR / 0.05% Basel 3.1) - LGD: Bank estimate (floor 10% Basel 3.1) - Correlation: 15% (fixed)
No maturity adjustment for retail exposures.
QRRE (Qualifying Revolving Retail Exposures)¶
Definition¶
To qualify as QRRE, ALL criteria must be met:
| Criterion | Requirement |
|---|---|
| Counterparty | Individual (not corporate) |
| Product | Revolving credit line |
| Maximum limit | ≤ EUR 100,000 |
| Security | Unsecured |
| Cancellability | Unconditionally cancellable |
Examples: - Credit cards - Personal overdrafts - Revolving personal lines
SA Risk Weight¶
| Framework | Risk Weight |
|---|---|
| CRR | 75% |
| Basel 3.1 | 75% |
IRB Treatment¶
Parameters: - PD: Bank estimate - Floor: 0.03% (CRR/Basel 3.1 transactor) - Floor: 0.10% (Basel 3.1 revolver) - LGD: Bank estimate - Correlation: 4% (fixed)
Retail Other¶
Definition¶
All retail exposures not qualifying as mortgage or QRRE: - Personal loans - Auto finance - Consumer durable financing - Small business facilities (< EUR 1m total exposure)
SA Risk Weight¶
| Framework | Risk Weight |
|---|---|
| CRR | 75% |
| Basel 3.1 | 75% |
IRB Treatment¶
Parameters: - PD: Bank estimate (floor 0.03% CRR / 0.05% Basel 3.1) - LGD: Bank estimate (floor varies by collateral) - Correlation: PD-dependent (3% to 16%)
# Other retail correlation formula
R = 0.03 × (1 - exp(-35 × PD)) / (1 - exp(-35)) +
0.16 × [1 - (1 - exp(-35 × PD)) / (1 - exp(-35))]
| PD | Correlation |
|---|---|
| 0.03% | 16% |
| 0.5% | 12.4% |
| 2% | 7.2% |
| 10% | 3.6% |
| 20%+ | 3% |
Calculation Examples¶
Example 1: Residential Mortgage (SA)¶
Exposure: - £250,000 mortgage - Property value: £350,000 - LTV: 71.4% - Owner-occupied
CRR Calculation:
Basel 3.1 Calculation:
Example 2: Credit Card (QRRE)¶
Exposure: - £15,000 credit limit - £8,000 current balance - Unconditionally cancellable - Revolver (carries balance)
CRR Calculation:
# QRRE 75% RW
# CCF = 0% for unconditionally cancellable (CRR)
EAD = £8,000 # Current balance only
Risk_Weight = 75%
RWA = £8,000 × 75% = £6,000
Basel 3.1 Calculation:
# CCF = 10% for unconditionally cancellable
Undrawn = £15,000 - £8,000 = £7,000
EAD = £8,000 + (£7,000 × 10%) = £8,700
# Revolver = 75% RW
Risk_Weight = 75%
RWA = £8,700 × 75% = £6,525
Example 3: Retail IRB¶
Exposure: - £50,000 personal loan - Bank PD: 2% - Bank LGD: 40% - "Other retail" category
Calculation:
# Correlation (PD = 2%)
R = 0.03 × (1 - exp(-35 × 0.02)) / (1 - exp(-35)) +
0.16 × (1 - (1 - exp(-35 × 0.02)) / (1 - exp(-35)))
R = 0.072 # 7.2%
# K calculation (no maturity adjustment for retail)
K ≈ 0.0285
# RWA
RWA = K × 12.5 × EAD
RWA = 0.0285 × 12.5 × £50,000
RWA = £17,813
# Risk Weight equivalent
RW = 35.6%
Lending Groups and EUR 1m Threshold¶
Retail Lending Groups¶
For retail SME exposures, total exposure is calculated across the lending group: - Connected individuals/entities - Common ownership or control - Aggregated for threshold purposes
Residential Property Exclusion (CRR Art. 123(c))¶
Important: Exposures secured by residential property are excluded from the EUR 1m threshold calculation when they are assigned to the residential property exposure class under the Standardised Approach.
This exclusion applies because: - Per CRR Art. 123(c), exposures "fully and completely secured on residential property collateral that have been assigned to the exposure class laid down in point (i) of Article 112" are excluded from the aggregation - This means the collateral value (capped at the exposure amount) is deducted from the total amount owed
Key Rules:
| Approach | Residential Property Treatment |
|---|---|
| SA | Excluded from EUR 1m threshold; stays as residential mortgage |
| IRB | NOT excluded from EUR 1m threshold (per EBA Q&A 2018_4012) |
Conceptual Logic
The following illustrates the residential property exclusion logic. For the actual implementation,
see hierarchy.py:692-789.
# Conceptual overview - actual implementation in HierarchyResolver._calculate_residential_property_coverage
def calculate_adjusted_exposure(exposures, residential_collateral):
"""
Per CRR Art. 123(c), residential property secured exposures (SA)
are excluded from the EUR 1m retail threshold calculation.
"""
for exposure in exposures:
# Get residential collateral securing this exposure
res_collateral_value = residential_collateral.get(exposure.id, 0)
# Cap at exposure amount (can't exclude more than exposure)
exclusion = min(res_collateral_value, exposure.amount)
# Adjusted exposure for threshold
exposure.for_retail_threshold = exposure.amount - exclusion
return exposures
Actual Implementation (hierarchy.py)
The real implementation uses Polars LazyFrames for efficient processing:
contingent_ref_col = "contingent_reference"
if contingent_ref_col is None:
contingent_totals = pl.LazyFrame(
schema={
"aggregation_facility": pl.String,
"total_contingent": pl.Float64,
}
)
else:
# Filter mappings to only contingent children
if type_col is not None:
contingent_mappings = facility_mappings.filter(
pl.col(type_col).fill_null("").str.to_lowercase() == "contingent"
).unique(subset=["child_reference", "parent_facility_reference"])
else:
contingent_mappings = facility_mappings.unique(
subset=["child_reference", "parent_facility_reference"]
)
# Join contingents with their parent facility
contingent_with_parent = contingents.join(
contingent_mappings,
left_on="contingent_reference",
right_on="child_reference",
how="inner",
)
contingent_with_parent = _resolve_to_root_facility(contingent_with_parent, root_lookup)
contingent_totals = contingent_with_parent.group_by("aggregation_facility").agg(
[
pl.col("nominal_amount").clip(lower_bound=0.0).sum().alias("total_contingent"),
]
)
# Identify sub-facilities to exclude from output
# Sub-facilities appear as child_reference with child_type="facility"
# Anti-join with empty frame naturally returns all rows
sub_facility_refs = root_lookup.select(
pl.col("child_facility_reference").alias("_sub_ref"),
)
# Join with facilities to calculate undrawn
# Combine loan drawn + contingent utilisation
facility_with_drawn = (
facilities.join(
loan_drawn_totals,
left_on="facility_reference",
right_on="aggregation_facility",
how="left",
)
.join(
contingent_totals,
left_on="facility_reference",
right_on="aggregation_facility",
how="left",
)
.with_columns(
[
pl.col("total_drawn").fill_null(0.0),
pl.col("total_contingent").fill_null(0.0),
]
)
.with_columns(
[
(pl.col("total_drawn") + pl.col("total_contingent")).alias("total_utilised"),
(pl.col("limit") - (pl.col("total_drawn") + pl.col("total_contingent")))
.clip(lower_bound=0.0)
.alias("undrawn_amount"),
]
)
)
# Exclude sub-facilities: only root/standalone facilities produce undrawn exposures
facility_with_drawn = facility_with_drawn.join(
sub_facility_refs,
left_on="facility_reference",
right_on="_sub_ref",
how="anti",
)
# Get facility schema to check for optional columns
facility_schema = facilities.collect_schema()
facility_cols = set(facility_schema.names())
# Build select expressions with defaults for missing columns
# Note: parent_facility_reference is set to the source facility to enable
# facility-level collateral allocation to undrawn amounts
select_exprs = [
(pl.col("facility_reference") + "_UNDRAWN").alias("exposure_reference"),
pl.lit("facility_undrawn").alias("exposure_type"),
pl.col("product_type")
if "product_type" in facility_cols
else pl.lit(None).cast(pl.String).alias("product_type"),
pl.col("book_code").cast(pl.String, strict=False)
if "book_code" in facility_cols
else pl.lit(None).cast(pl.String).alias("book_code"),
Lending Group Threshold Check:
# Total adjusted exposure to lending group (from hierarchy resolver)
adjusted_group_exposure = sum(
exp.exposure_for_retail_threshold for entity in lending_group
for exp in entity.exposures
)
# Must be ≤ EUR 1m for retail treatment
if adjusted_group_exposure <= 1_000_000:
treatment = "RETAIL"
else:
treatment = "CORPORATE_SME" # SMEs retain firm-size adjustment
Treatment When Threshold Exceeded¶
| Counterparty Type | Exceeds Threshold | Treatment |
|---|---|---|
| Individual (mortgage) | Yes | Stays as RETAIL_MORTGAGE (SA Art. 112(i)) |
| Individual (other) | Yes | Reclassified to CORPORATE |
| SME (any product) | Yes | Reclassified to CORPORATE_SME |
Regulatory References: - CRR Art. 123(c) - Retail exclusion for residential property - EBA Q&A 2013_72 - SA residential property exclusion clarification - EBA Q&A 2018_4012 - IRB residential property NOT excluded
Example: Threshold Calculation with Exclusion¶
Scenario: Lending group with EUR 2m total exposure
| Exposure | Amount | Residential Collateral | For Threshold |
|---|---|---|---|
| Term loan | EUR 1m | EUR 0 | EUR 1m |
| Mortgage | EUR 1m | EUR 1m | EUR 0 |
| Total | EUR 2m | EUR 1m |
Result: Adjusted exposure = EUR 1m (at threshold) - qualifies as retail
CRM for Retail¶
Eligible Collateral¶
| Collateral Type | Treatment |
|---|---|
| Residential property | Mortgage RW |
| Financial collateral | Haircut method |
| Physical collateral | LGD reduction (IRB) |
Guarantees¶
Limited guarantee recognition for retail: - Government guarantees accepted - Institution guarantees under conditions - Individual guarantees generally not recognized
Regulatory References¶
| Topic | CRR Article | BCBS CRE | EBA Q&A |
|---|---|---|---|
| Retail definition | Art. 123 | CRE20.50-60 | - |
| EUR 1m threshold | Art. 123(c) | CRE20.65 | 2016_2626 |
| Residential property exclusion | Art. 123(c), Art. 112(i) | - | 2013_72, 2018_4012 |
| Retail mortgage | Art. 125 | CRE20.70-75 | - |
| QRRE | Art. 154 | CRE31.10-12 | - |
| Retail IRB | Art. 154 | CRE31 | - |
| Correlation | Art. 154 | CRE31.13-15 | - |