SA-CCR — Supervisory delta (Art. 279a)¶
The supervisory delta is the trade-level sign-and-magnitude weight that converts a directional position in the primary risk driver into a signed contribution to the hedging-set add-on. The formula varies by instrument family:
| Instrument family | Formula | PRA / CRR reference |
|---|---|---|
| Linear (forward, swap, FRA) | δ = +1 (long) / δ = −1 (short) |
Art. 279a(1) |
| European option | Black-Scholes δ = ±Φ(±d1) |
Art. 279a(2) |
| CDO tranche | |δ| = 15 / ((1 + 14·A) · (1 + 14·D)) |
Art. 279a(3) |
The linear, option and CDO-tranche branches are all implemented. The asset-class scope of worked numeric examples below is IR + FX only — credit / equity / commodity option-delta worked examples land with engine batch P8.35–P8.38 alongside the corresponding asset-class add-on branches.
Linear instruments — Art. 279a(1)¶
For any trade that is not a European option and not a CDO tranche
(i.e. option_strike is null and cdo_attachment is null), the
supervisory delta is the signed unit:
is_long is the trade-level Boolean that indicates whether the
counterparty is long the primary risk driver — for an IR swap, "long" is
the receive-floating leg (long duration); for an FX forward, "long" is
the bought-currency leg; for a CDS, "long" is the protection-seller.
Engine: src/rwa_calc/engine/ccr/supervisory_delta.py::compute_supervisory_delta_linear.
"equity": _OPT_VOL_EQUITY_IDX,
"equity_sn": _OPT_VOL_EQUITY_SN,
"equity_idx": _OPT_VOL_EQUITY_IDX,
}
@cites("CRR Art. 279a")
def compute_supervisory_delta_linear(trades: pl.LazyFrame) -> pl.LazyFrame:
"""Supervisory delta for non-option directional trades per CRR Art. 279a(1).
delta = +1 for long positions in the primary risk driver
delta = -1 for short positions in the primary risk driver
The European-option Black-Scholes Phi(d1) branch (rows where
``option_strike`` is not null) and the CDO-tranche formula are handled by
:func:`compute_supervisory_delta_option` and
:func:`compute_supervisory_delta_cdo_tranche` respectively.
Args:
trades: LazyFrame containing an ``is_long`` Boolean column.
Returns:
The input LazyFrame with a new ``supervisory_delta: Float64`` column.
European options — Art. 279a(2)¶
For European-style options (rows that carry both option_strike and
option_underlying_price), the supervisory delta is the Black-Scholes
Φ(d1) adjusted for the call/put and long/short sign rule:
d1 = (ln(P/K) + 0.5 · σ² · T) / (σ · √T)
long call: δ = +Φ(d1)
short call: δ = −Φ(d1)
long put : δ = −Φ(−d1)
short put : δ = +Φ(−d1)
where:
P=option_underlying_price(price of the underlying at reporting date).K=option_strike(contractual strike).T=(maturity_date − start_date).days / 365— calendar-day basis, 365-day year. Differs from the SA-CCRstart_dateadjusted-notional floor (which uses the 250-business-day basis) — the option-delta T is a calendar-day count per BCBS CRE52.42.σ= asset-class supervisory volatility from the table below.Φ(·)= standard normal cumulative distribution function, evaluated viapolars_normal_stats(the same backend used by IRB Vasicek).
Supervisory option volatility table — Art. 279a(2) / BCBS CRE52.47¶
| Asset class | σ | Source constant |
|---|---|---|
| Interest rate | 0.50 | SA_CCR_OPTION_VOLATILITY_IR |
| FX | 0.15 | SA_CCR_OPTION_VOLATILITY_FX |
| Credit — single name | 1.00 | SA_CCR_OPTION_VOLATILITY_CREDIT_SN |
| Credit — index | 0.80 | SA_CCR_OPTION_VOLATILITY_CREDIT_IDX |
| Equity — single name | 1.20 | SA_CCR_OPTION_VOLATILITY_EQUITY_SN |
| Equity — index | 0.75 | SA_CCR_OPTION_VOLATILITY_EQUITY_IDX |
| Commodity — electricity | 1.50 | SA_CCR_OPTION_VOLATILITY_COMMODITY_ELECTRICITY |
| Commodity — other | 0.70 | SA_CCR_OPTION_VOLATILITY_COMMODITY_OTHER |
The TRADE_SCHEMA fixture carries only the coarse asset class (credit,
equity) without distinguishing single-name vs index. The engine defaults
the lookup to the index volatility for both credit and equity —
matching the lower-volatility leg of CRE52.47 and the P8.13 OPT_003
expected value. Firms that carry single-name vs index distinction in
upstream data should populate asset_class as credit_sn / credit_idx
/ equity_sn / equity_idx to override.
Sign rule — bought vs sold¶
The sign of the delta is determined by both the option type
(call vs put) and the trade direction (is_long). The four-way table
above gives the explicit sign for every combination — the engine encodes
this as a pl.when(...).then(...).otherwise(...) cascade, not a single
± magnitude:
d1 = ((p / k).log() + 0.5 * sigma * sigma * t_years) / (sigma * t_years.sqrt())
phi_d1 = normal_cdf(d1)
phi_neg_d1 = normal_cdf(-d1)
is_call = pl.col("option_type") == "call"
is_long = pl.col("is_long")
# Sign rule per CRR Art. 279a(2):
# long call -> +Phi(d1)
# short call -> -Phi(d1)
# long put -> -Phi(-d1)
# short put -> +Phi(-d1)
option_delta = (
pl.when(is_call & is_long)
.then(phi_d1)
Rows where option_strike is null fall back to the linear ±1 branch per
Art. 279a(1) — the option function preserves the linear behaviour for
non-option rows, so the orchestrator can call
compute_supervisory_delta_option on a mixed batch without filtering.
Engine: src/rwa_calc/engine/ccr/supervisory_delta.py::compute_supervisory_delta_option.
CDO tranches — Art. 279a(3)¶
For securitisation tranches (rows that carry both cdo_attachment and
cdo_detachment), the supervisory delta uses the closed-form attachment /
detachment formula from BCBS CRE52.43:
|δ| = 15 / ((1 + 14·A) · (1 + 14·D))
δ = +|δ| if is_long (long the tranche — protection seller)
δ = −|δ| if not is_long (short the tranche — protection buyer)
where:
A=cdo_attachment(loss attachment point, e.g. 0.03 for a 3%-attachment mezzanine).D=cdo_detachment(loss detachment point, e.g. 0.07 for a 3%-7% tranche).15and14are the regulatory constantsSA_CCR_CDO_TRANCHE_NUMERATORandSA_CCR_CDO_TRANCHE_COEFFICIENT.
The closed-form deliberately produces a magnitude greater than 1 for typical mezzanine tranches (e.g. A=3%, D=7% gives |δ| ≈ 5.34) — this reflects the leverage embedded in a thin tranche, where a small move in the underlying-pool loss percentage produces an outsized change in tranche value. The supervisory delta multiplies the tranche notional in the adjusted-notional formula, so the magnified |δ| is correct.
Rows where cdo_attachment is null fall back to the linear ±1 branch per
Art. 279a(1) — same fallback contract as the option function.
Engine: src/rwa_calc/engine/ccr/supervisory_delta.py::compute_supervisory_delta_cdo_tranche.
.alias("supervisory_delta")
)
.drop("_option_sigma")
)
@cites("CRR Art. 279a")
def compute_supervisory_delta_cdo_tranche(trades: pl.LazyFrame) -> pl.LazyFrame:
"""Supervisory delta for CDO tranches per CRR Art. 279a(3).
For rows that carry ``cdo_attachment`` AND ``cdo_detachment``, apply the
closed-form:
|delta| = 15 / ((1 + 14 * A) * (1 + 14 * D))
with sign +1 for long tranches and -1 for short tranches.
Rows where ``cdo_attachment`` is null fall back to the linear +/- 1 delta
per Art. 279a(1).
Args:
trades: LazyFrame with ``is_long``, ``cdo_attachment``, and
``cdo_detachment`` columns.
Returns:
The input LazyFrame with a new ``supervisory_delta: Float64`` column.
References:
CRR Art. 279a(3); BCBS CRE52.43.
"""
is_cdo = pl.col("cdo_attachment").is_not_null() & pl.col("cdo_detachment").is_not_null()
a = pl.col("cdo_attachment")
d = pl.col("cdo_detachment")
numerator = _CDO_TRANCHE_NUMERATOR
coefficient = _CDO_TRANCHE_COEFFICIENT
magnitude = numerator / ((1.0 + coefficient * a) * (1.0 + coefficient * d))
cdo_delta = pl.when(pl.col("is_long")).then(magnitude).otherwise(-magnitude)
linear_delta = pl.when(pl.col("is_long")).then(1.0).otherwise(-1.0)
return trades.with_columns(
Pipeline ordering¶
trades → years_to_maturity
→ compute_adjusted_notional_ir
→ compute_adjusted_notional_fx
→ compute_supervisory_delta_linear (or _option / _cdo_tranche)
→ compute_maturity_factor_unmargined
→ assign_hedging_set
→ compute_addon_per_asset_class
→ compute_pfe
Today the orchestrator at
engine/ccr/pipeline_adapter.py::ccr_rows_to_exposures calls the
linear variant unconditionally. Once the option / CDO branches are
chained in (engine batches P8.35–P8.38), the dispatcher will route on
option_strike.is_not_null() and cdo_attachment.is_not_null() and
emit a single supervisory_delta column that combines all three
branches. The fallback rules above guarantee idempotent composition —
_option and _cdo_tranche both preserve the linear ±1 result for
non-matching rows.
Worked numeric examples¶
The values below are pinned by
tests/unit/ccr/test_supervisory_delta_options.py and re-derived at
collection time from
tests/fixtures/ccr/option_delta_builder.py. Reporting date is set to
start_date = 2026-01-15, so T equals the difference in calendar days
between maturity_date and start_date divided by 365 (no time decay
between trade inception and reporting).
Linear branch — LIN_001 (IR swap)¶
Option branch — ATM long IR call (OPT_001)¶
asset_class = interest_rate, option_type = call, is_long = True
P = K = 0.03 (ATM), T = 1.0y, σ = 0.50
d1 = (ln(1) + 0.5 · 0.50² · 1.0) / (0.50 · √1.0) = 0.25
δ = +Φ(0.25) ≈ +0.598706 (Art. 279a(2))
Option branch — ATM long IR put (OPT_002)¶
asset_class = interest_rate, option_type = put, is_long = True
P = K = 0.03 (ATM), T = 1.0y, σ = 0.50
d1 = 0.25
δ = −Φ(−0.25) = −(1 − Φ(0.25)) ≈ −0.401294 (Art. 279a(2))
Verifies the long-put sign rule: a long put is short the underlying, so δ
is negative even though is_long = True.
Option branch — OTM short equity call (OPT_003)¶
asset_class = equity, option_type = call, is_long = False
P = 100, K = 110 (OTM), T = 91/365 ≈ 0.2493y, σ = 0.75 (equity-index)
d1 = (ln(100/110) + 0.5 · 0.75² · 0.2493) / (0.75 · √0.2493) ≈ −0.0673
δ = −Φ(d1) = −Φ(−0.0673) ≈ −0.47318 (Art. 279a(2))
Option branch — ITM long FX put (OPT_004)¶
asset_class = fx, option_type = put, is_long = True
P = 1.20, K = 1.30 (ITM put), T = 182/365 ≈ 0.4986y, σ = 0.15 (FX)
d1 = (ln(1.20/1.30) + 0.5 · 0.15² · 0.4986) / (0.15 · √0.4986) ≈ −0.7027
δ = −Φ(−d1) = −Φ(0.7027) ≈ −0.75889 (Art. 279a(2))
CDO branch — long mezzanine tranche (CDO_001)¶
asset_class = credit, is_long = True, A = 0.03, D = 0.07
(1 + 14·A) = 1.42; (1 + 14·D) = 1.98; product = 2.8116
|δ| = 15 / 2.8116 ≈ 5.335041
δ = +5.335041 (Art. 279a(3))
CDO branch — short mezzanine tranche (CDO_002)¶
asset_class = credit, is_long = False, A = 0.03, D = 0.07
|δ| = 5.335041 (as above)
δ = −5.335041 (Art. 279a(3))
Pending — credit / equity / commodity option worked examples¶
The four shipped option worked examples above cover the IR and FX asset
classes — sufficient to pin the Black-Scholes formula, the four-way
call/put × long/short sign rule, and the asset-class σ lookup. The
credit / equity / commodity option add-on aggregation (cross-HS ρ =
0.5 for credit and equity, ρ = 0.4 for commodity per Art. 280a / 280b /
280c) lands with engine batches P8.35–P8.38. Once those branches ship,
this page will gain three additional worked examples — one CDS option,
one equity-single-name option, one commodity option — wired into
matching CCR-A acceptance scenarios.
Placeholder — engine batch P8.35–P8.38
The credit / equity / commodity asset-class add-on branches are documented as Pending on the SA-CCR specification index. The supervisory delta formula is already asset-class-agnostic — the only thing waiting on those batches is end-to-end acceptance coverage that exercises the full chain (adjusted notional → delta → MF → hedging set → add-on → PFE → EAD) for a non-IR / non-FX trade.
References¶
- PRA PS1/26 Art. 279a(1) — linear instrument supervisory delta ±1.
- PRA PS1/26 Art. 279a(2) — European-option Black-Scholes supervisory delta and the σ table.
- PRA PS1/26 Art. 279a(3) — CDO-tranche supervisory delta closed-form.
- BCBS CRE52.41–43 — underlying methodology for linear, option and tranche deltas.
- BCBS CRE52.47 — supervisory option volatility table (verbatim source of the σ values).
src/rwa_calc/engine/ccr/supervisory_delta.py— engine implementation of all three branches.src/rwa_calc/rulebook/packs/common.py— cited pack paramssa_ccr_option_volatility_*andsa_ccr_cdo_tranche_numerator/sa_ccr_cdo_tranche_coefficient, read inengine/ccr/supervisory_delta.pyvia_PACK.scalar_param(...).tests/unit/ccr/test_supervisory_delta_options.py— pinned numeric examples for all six worked cases above.tests/fixtures/ccr/option_delta_builder.py— fixture rows for OPT_001–OPT_004, LIN_001 and CDO_001–CDO_002.