This chapter requires XVA-01: EE$(t)$ for a single trade, computed from simulated mark-to-market paths.
This chapter introduces netting sets and CSA (Credit Support Annex), the legal structures that reduce exposure. No prior knowledge of ISDA agreements or collateral mechanics is assumed.
By the end, you can collapse a portfolio into a single netted exposure profile and apply a CSA to reduce it further.
The input is a set of mark-to-market paths $V_k(t, \omega)$: one value per trade $k$, monitoring date $t$, and simulated scenario $\omega$.
From single trades to portfolios
A bank does not compute CVA trade by trade. It manages netting sets: collections of trades governed by a single ISDA Master Agreement with one counterparty. On default the bankruptcy trustee does not pay out a thousand individual claims; it nets them and pays the single net amount.
Netting: why it matters
xvafoundations.xva.NettingSet, seed 42, 5000 paths. Netting factor: Pykhtin & Zhu (2007).
A payer swap gains when rates rise; a receiver swap loses on the same move. Trade C is the receiver, so it is a liability exactly where A and B are assets.
Let $V_k(t)$ be the mark-to-market of trade $k$ at time $t$ on one scenario:
K: number of trades governed by the one ISDA Master Agreement
$V_{\text{net}}(t)$ is the only value the bankruptcy trustee recognises; it is used unchanged for the rest of the chapter
There are two ways to aggregate it, and an inequality between them:
Per the convention from XVA Chapter 01, $\text{EE}(t)$ is the time-varying expected positive exposure and EPE is its scalar time-average $\text{EPE} = \tfrac{1}{T}\int_0^T \text{EE}(t)\,dt$.
Collateral and the Credit Support Annex
Most derivative trades between investment-grade counterparties are governed by a Credit Support Annex (CSA): an agreement to post collateral against the net mark-to-market at regular intervals.
xvafoundations.xva.NettingSet, seed 42, 5000 paths.
$C(t)$ is the balance called on the net MTM one margin period earlier, at $t - \text{MPR}$, not this period's flow:
C(t) ≥ 0: collateral the bank has received, the right-hand branch of the ladder above and the only branch xvafoundations.xva.netting implements.
The left-hand branch, $\max(-V_{\text{net}}(t - \text{MPR}) - H,\, 0)$, can only lower $C$, so the one-way profile below is a lower bound on two-way residual exposure at every date.
Residual exposure is the positive part of the net MTM after deducting that balance, so it carries $E$ from XVA-01, not $V$:
The formula above and the library implementation both use a one-snapshot proxy: the balance at $t$ is computed from the lagged net MTM rather than from a running collateral account tracking every call, partial release and unposting in between. For monitoring grids coarse relative to the MPR this is adequate; for daily or intraday margining, a path-by-path balance simulation is closer to the true CSA mechanics. See Gregory (2020) Ch. 11 for the bilateral convention with thresholds, minimum transfer amounts, and independent amount.
The library takes the MPR in grid steps, not days. The run below is quarterly with mpr=2: a six-month lag against the 10 business days of a real CSA, so the collateralised profile is conservative. A genuine 10-day MPR needs a daily grid, which is a simulation cost, not a modelling change.
Portfolio-level CVA
Nothing about the CVA formula changes. Substitute the netting set's EE$(t)$ for the single-trade EE$(t)$ and the integral is the one XVA-03 derives, under the same assumption that the exposure and the default time $\tau$ are independent. The hazard rate and the recovery rate are the counterparty's, not any single trade's.
With the inputs XVA-03 uses, a flat 120 bp CDS spread and 40% recovery, the three profiles below give CVA of USD 37,602 gross, USD 14,227 netted and USD 3,303 collateralised. Netting removes 62.2% of the gross number, and the CSA removes a further 76.8% of what netting left.
Computing portfolio exposure in code
Part 1: the netting set
The aggregation lives in the library, so import it rather than retype it. All three methods return the profile EE$(t)$ of shape (num_times,).
from xvafoundations.xva.netting import NettingSet
# NettingSet takes a list of (num_paths, num_times) MTM tensors, one per
# trade -- the paths-first convention used throughout xvafoundations.
#
# gross_ee() sum_k mean_i max(V_k(t), 0)
# netted_ee() mean_i max(sum_k V_k(t), 0)
# collateralised_ee(H, mpr) mean_i max(V_net(t) - C(t), 0)
#
# with C(t) = max(V_net(t - mpr) - H, 0) and mpr counted in GRID STEPS.
Part 2: Example with a 3-trade netting set
from datetime import date
import torch
torch.set_default_dtype(torch.float64) # the library is float64 throughout
from xvafoundations.calibration import Stripper
from xvafoundations.conventions import DayCount, year_fraction
from xvafoundations.data.sofr import INSTRUMENTS, VALUATION_DATE
from xvafoundations.instruments import (build_instruments,
pillar_maturities, initial_rates)
from xvafoundations.models import HullWhite1F
from xvafoundations.pricing import SwapSpec, reprice_swap_paths
from xvafoundations.xva.netting import NettingSet
t0 = torch.tensor(0.0)
# The SOFR OIS curve of IR-01, rebuilt here so this block runs alone.
pillars = torch.tensor(pillar_maturities(INSTRUMENTS, VALUATION_DATE))
rates = torch.tensor(initial_rates(INSTRUMENTS))
stripper = Stripper(pillars, rates, instrument_factory=lambda c:
build_instruments(INSTRUMENTS, VALUATION_DATE, c))
stripper.calibrate()
curve = stripper.get_curve()
# ACT/360 year fractions of an annual 15-January schedule, so 10Y
# matures at 10.144444, not 10.0.
def annual_schedule(years):
v0 = VALUATION_DATE
return tuple(year_fraction(v0, date(v0.year + 1 + k, v0.month, v0.day),
DayCount.ACT360)
for k in range(years))
# a and sigma are ASSERTED, not calibrated: IR-05 and IR-06 are
# unwritten. They are in the range a USD desk would see, and the shapes
# below do not depend on the third decimal of either.
model = HullWhite1F(a=0.05, sigma=0.007, curve=curve)
# Three trades under one ISDA, each struck at its own par rate, so
# V(0) = 0 for every trade and therefore for the netting set:
# A 10Y payer 10,000,000 USD c = 4.1200%
# B 5Y payer 10,000,000 USD c = 3.9200%
# C 7Y receiver 12,000,000 USD c = 4.0200%
definitions = [("A", 10, 1.0e7, True),
("B", 5, 1.0e7, True),
("C", 7, 1.2e7, False)]
specs = {}
for key, years, notional, pay_fixed in definitions:
schedule = annual_schedule(years)
par = curve.par_swap_rate(t0, torch.tensor(schedule)).item()
specs[key] = SwapSpec(
notional=notional,
fixed_rate=par,
fixed_payment_times=schedule,
pay_fixed=pay_fixed,
)
# 41 steps to 10.25y is dt = 0.25 exactly, and 10.25 covers the longest
# maturity. One short-rate simulation drives all three trades.
short_rates, times = model.simulate(5_000, 41, 10.25, seed=42)
mtm = {key: reprice_swap_paths(model, spec, short_rates, times)
for key, spec in specs.items()}
netting_set = NettingSet([mtm["A"], mtm["B"], mtm["C"]])
# EE profiles: the per-time function EE(t), not the scalar EPE
gross = netting_set.gross_ee()
netted = netting_set.netted_ee()
coll = netting_set.collateralised_ee(threshold=0.0, mpr=2) # 2 steps = 0.5y
print(f"gross peak {gross.max():>12,.2f}") # 656,516.37
print(f"collateralised {coll.max():>12,.2f}"
f" at {times[coll.argmax()]:.2f}y") # 87,833.51 at 0.50y
The mark-to-market paths are repriced swaps, not scaled noise, so the net MTM is serially persistent: consecutive quarterly dates correlate 0.920 at 1.25y rising to 0.976 at 7y, and 0.919 across a full two-step margin period. That persistence is what makes collateral work at all: a margin call only protects you if today's MTM resembles the MTM one margin period ago.
What is asserted rather than calibrated is the pair $a = 0.05$ and $\sigma = 0.007$. IR-05 and IR-06 are unwritten, so nothing on this page claims these are the market's Hull-White parameters. They set the scale of every number in this chapter; they do not set its shape.
Gross vs. netted vs. collateralised EE profiles
References
- Brigo, D. & Mercurio, F. (2006). Interest Rate Models: Theory and Practice (2nd ed.). Springer. The canonical reference on rates models underlying XVA computations.
- Gregory, J. (2020). The xVA Challenge (4th ed.). Wiley. Comprehensive treatment of CVA, DVA, FVA, and counterparty risk in practice.
- Crépey, S. (2015). Counterparty Risk and Funding: A Tale of Two Puzzles. Chapman & Hall/CRC. Rigorous mathematical treatment of counterparty risk and funding costs.
- Pykhtin, M. & Zhu, S. (2007). "A Guide to Modelling Counterparty Credit Risk." GARP Risk Review. Excellent overview of EPE, netting, and collateral modelling.
XVA-03 takes this profile, adds default probabilities stripped from CDS spreads, and prices the credit risk.