← learnChapter 11 · Counterparty credit risk, deep dive6 min read

CVA from first principles

10.3 took a CVA sensitivity vector as given. Where does the CVA number underneath it actually come from? A Monte-Carlo integral over expected exposure and default probability, recorded once on the adjoint tape — and a real, verified gap between the counterparty curve the demo prints and the one it actually prices with.

10.3 handed you a CVA sensitivity vector and built a capital charge out of it. Where does the CVA number underneath that vector actually come from? An integral over a counterparty's expected exposure and default probability, simulated on a time grid and recorded once, the same way every other page in this Learn section has recorded a valuation.

The whole story

CVA is a sum over a time grid of expected positive exposure times a discount factor times the marginal default probability, loss-given-default applied throughout. A Hull-White short rate and a lognormal FX spot drive the mark-to-market at every date; a hazard curve, bootstrapped from real CDS quotes by bisection, supplies survival. One .greeks() sweep on the recorded tape returns the CVA number and its whole risk vector — IR delta and vega, CS01 by tenor bucket, FX delta, a recovery gradient — with no re-simulation. The expected-exposure profile is a real humped shape, peaking early and running off to zero as the netting set's trades mature. The central finding: a repo-wide search for where the bootstrapped hazard curve is actually used turns up exactly one call site, a printout — the Monte-Carlo integral's own hazard inputs come from a separate, hardcoded flat curve that never reacts to the counterparty's real CDS quotes at all.

Did you know?

Every quantity here — mark-to-market, exposure, the CVA integrand — is divided by MONEY_UNIT = 1,000,000 before it ever touches the tape. ExposureSimulation's own doc comment explains why: unscaled sums run past 1e10 in reporting-currency terms, "the difference between a stable and an unusable single-precision replay." Divide down to O(1) quantities, replay, then scale the price and gradient back up on the way out — the same trick that keeps this exact tape usable on cpu-jit's fp64 and a GPU backend's fp32 without two different implementations.

The CVA integral, in one line

CVA = LGD * sum_k  max(V(t_k) - C(t_k), 0) * D(t_k) * ( S(t_{k-1}) - S(t_k) )

Three ingredients, each already familiar from earlier chapters: V is a mark-to-market recorded the same way every option in this Learn section has been (here, two interest-rate swaps and an FX forward, summed); D is a discount factor off the same Hull-White path driving the trade values; S is the counterparty's survival probability, and its drop between two dates is the chance of default arriving exactly then.

Build a real hazard curve from CDS quotes

HazardCurve.bootstrap strips a piecewise-flat forward hazard from ascending CDS quotes, one segment at a time, by bisection:

double target = quote.parSpread();
for (int iteration = 0; iteration < 200; iteration++) {
  double mid = 0.5 * (lo + hi);
  hazards[segment] = mid;
  double modelSpread = parSpread(knots, hazards, segment + 1, lgd, discount);
  if (modelSpread > target) { hi = mid; } else { lo = mid; }
}

Fed counterparty A's real quotes — 90bp at 1y, 130bp at 3y, 150bp at 5y, 170bp at 10y — this gives real survival numbers: survival(1y) = 0.9851, survival(5y) = 0.8807, survival(10y) = 0.7473. The marginal default probability each step needs is built as an incremental hazard, S(t_{k-1}) · (1 - e^{-Δλ}), using a six-term Maclaurin series for 1 - e^{-x} instead of direct subtraction — Δλ is tiny at every step, and 1.0 - Math.exp(-x) for small x throws away almost every significant digit to cancellation before the series ever gets a chance to.

Simulate exposure, one sweep for the whole risk vector

A recorded Hull-White short rate and a lognormal FX spot drive every trade's mark-to-market at every grid date; one .greeks() run returns the CVA number and its complete gradient:

Nabla.TypedPricer<CvaMarket> pricer =
    (useFp64() ? model.fp64() : model.fp32()).greeks().on(engine).build();

Real numbers, 200,000 paths, 24 steps, cpu-jit, netting set NS-CPTY-A (a 7-year payer swap, a 5-year receiver, an FX forward): the expected positive exposure profile peaks early, $8.385m at t=0.29y, and runs off toward zero as trades mature, $0.167m at t=6.71y — a real, humped shape, not asserted. CVA(A) = $0.410m, from one sweep in 0.849 s. The same sweep also gives IR delta ($2,185) and vega ($41,565), CS01 by tenor bucket ($1,338 / $1,133 / $119 short/mid/long), FX delta ($9,917), and a recovery gradient (-$683,652) — no second run for any of them.

Unlike 10.3's netting set, the long CS01 bucket here is genuinely nonzero. 10.3's book matured at 5 years, so its hazard curve's 7.5-year vertex never touched any real exposure. This book's 7-year payer swap keeps exposure alive past that point, and the long bucket, $119, proves it.

Did you know?

A repo-wide search for .curve() — the accessor onto the bootstrapped HazardCurve above — turns up exactly one call site in the entire nablatensor-cva module and its examples: the survival() printout in CvaShowcase.main()'s [1/8] section. The Monte-Carlo integral's own hazard inputs (CvaMarket::hazardShort/hazardMid/hazardLong) come from a completely separate object, CvaMarket.demo() — a hardcoded flat 150bp curve, unrelated to whichever counterparty's HazardCurve gets bootstrapped and printed alongside it. The bootstrap is real; it just never reaches the kernel that prices the trade.

Try it yourself

If the counterparty's own CDS quotes actually drove the CVA number, a credit deterioration — every quote widened by 100bp — should make CVA worse. Bootstrapped a second curve from 190/230/250/270bp quotes, attached it to the same counterparty, and reran ExposureSimulation against it. Real numbers: survival(5y) genuinely moves, 0.8807 to 0.8097 — the bootstrap reacted exactly as it should. CVA(A) does not move at all: $0.410m, unchanged to the last printed digit. Predicting otherwise, then checking, is what surfaces the gap the sidenote above names — no source edit needed, both HazardCurve.bootstrap and ExposureSimulation.run are already public.

▶️ Run it

The same netting set, live: build the Hull-White-plus-FX tape, read the whole CVA risk vector off one adjoint sweep, then bootstrap a real hazard curve and watch survival move while CVA doesn't — right here, in your browser through TeaVM, on a stand-in for the real ExposureSimulation rewritten around chapter 1.3's cell's own limitation, rec.input() directly instead of Nabla.Inputs <CvaMarket>'s reflection layer. HazardCurve.bootstrap runs completely unmodified. The browser cell uses 2,000 paths × 8 steps so the full showcase finishes in a browser; the Maven command below retains the 200,000 × 24 defaults:

Java · compile and run in this browser

Or run the real thing:

mvn -o -q install
mvn -o -q -pl nablatensor-examples exec:java \
  -Dexec.mainClass=com.nablatensor.examples.CvaShowcase

Defaults to cpu-jit, 200,000 paths, 24 steps, no GPU. This runs the full 8-stage showcase — netting sets through the three PRA methods — this page only walked stages [1/8]–[3/8]; 10.3 already covered the SA-CVA aggregation stages, and BA-CVA/the PRA methods are Chapter 11.3's.

⚠️ What this doesn't do

The hazard-curve gap above is worth stating plainly rather than papering over: CvaMarket.demo()'s flat 150bp curve means every counterparty in every showcase this Learn section has used — A's real term structure, B's flat 110bp curve alike — gets priced against the identical hazard level, regardless of which HazardCurve object sits on its CreditName. The printed [1/8] survival numbers are real and correctly bootstrapped; they just describe a curve the pricing engine never sees. Everything else stays scoped as before: one netting set, no wrong-way risk beyond the fixed recovery assumption, and SaCvaParameters/CvaMarket's own values are explicitly indicative, not calibrated to a real desk.

What's next

→ Deeper: CvaShowcase.java runs the full portfolio (two netting sets, one under a CSA) through SA-CVA, BA-CVA and the three PRA methods in one pass. → Next: Hedging CVA — the same CvaShowcase book, now with a CDS hedge on top, and a real case where the hedge makes capital worse, not better.


Questions or corrections? open an issue