You are here in the market-data-to-CVA chain A horizontal rail of eleven markers, one per chapter: IR-01 to IR-08 on the left, XVA-01 to XVA-03 on the right. IR-05 to IR-08 are drawn hollow under a bracket reading "written, not yet published". The chapter being read is ringed in crimson and named above the rail, and the object it consumes and the object it produces are printed below it. Market data to CVA · the whole chain, IR-01 to XVA-03 IR-01 · Bootstrap the SOFR curve IR-02 · Multi-curve bootstrapping IR-03 · Swaptions and the vol surface IR-04 · Why we model rates as random XVA-01 · Exposure XVA-02 · Netting, CSA and collateral XVA-03 · Credit Valuation Adjustment written, not yet published IR XVA 0102 0304 0506 0708 0102 03 INmarket quotes IN14 SOFR par quotes INESTR + EURIBOR INP(0,T), ATM vol grid INP(0,T) INV(t,ω), 5000 paths INEE(t) per trade INEEnet(t), dPD(t) OUTCVA OUTP(0,T), 14 pillars OUTtwo curves, a basis OUTσN, swaption prices OUTr(t,ω) under Q OUTEE(t), PFE(t,q) OUTEEnet(t) OUTCVA
You are here · IR-02 This chapter takes the two quote sets and hands on two curves and the basis between them.
Prerequisites

Requires IR-01: extracting $P(0,T)$ from swap quotes. No prior knowledge of multi-curve frameworks is assumed.

By the end you have two curves, OIS for discounting and EURIBOR for projection, and the basis between them.

Why multi-curve?

Before 2008 one LIBOR curve did both jobs: it discounted cash flows and it projected floating-leg rates. That is the curve we built in Chapter 01.

The 2008 crisis broke it. The spread between overnight indexed swap (OIS) rates and 3-month LIBOR, a handful of basis points before 2007, passed 300 bp at the October 2008 peak and never returned to zero. So which rate discounts a collateralised trade? Cash collateral earns the overnight rate set in the credit support annex (CSA), so the discount rate is a property of the collateral agreement, not of LIBOR and not of your own credit (Henrard, 2009). If the cash I am posted pays ESTR, I should not discount at LIBOR.

The cost of discounting a swap on the wrong curve Upper register: two bars giving the present value of one 10-year EUR swap, 100 million notional, struck at 2.00 percent. Discounted on the ESTR curve it is worth 7,221,033 euro; discounted on the EURIBOR curve, the pre-2008 habit, it is worth 7,089,275 euro. A gold bracket between the bar tops is labelled 131,757 euro. The bars start at 7.00 million, not at zero. Lower register: the same pricing error at two strikes, drawn from zero. Struck at 2.00 percent, an aged trade, the error is 131,757 euro. Struck at 2.81 percent, at par today, it is 13,423 euro, roughly a tenth as large. 10Y EUR SWAP · EUR 100m · STRUCK AT 2.00% EUR 7,221,033 EUR 7,089,275 EUR 131,757 discount on ESTR correct discount on EURIBOR the pre-2008 habit Bars start at EUR 7.00m, not at zero. PRICING ERROR BY STRIKE, SAME TRADE struck 2.00% EUR 131,757 struck 2.81% EUR 13,423 2.81% is where this swap prices at par today.
Figure 1 · IR-02 Discounting on the wrong curve is not an aesthetic preference. On an aged trade it is EUR 131,757, or 13.2 bp of notional. At par the error nearly vanishes, which is why single-curve pricing survived so long: the par rate itself moves only 0.15 bp. Source: xvafoundations.instruments.OISSwap priced on the two curves calibrated below, once with disc_curve = ESTR and once with disc_curve = EURIBOR.
The post-2008 framework

Discount curve: OIS (SOFR in USD, ESTR in EUR), the rate cash collateral earns under the CSA, used for every cash flow on a cash-collateralised trade. Projection curve: the tenor you are projecting (EURIBOR 3M, SOFR 3M). The difference between them is the basis.

That basis is two things at once: the default risk of lending to a bank for three months rather than overnight, and the cost of tying up funding for a longer horizon. Which dominates is regime-dependent (Filipović and Trolle, 2013). It is not an arbitrage; it compensates for real risk. Ametrano and Bianchetti (2013) give the arbitrage-free construction, Crépey (2015) the CSA-consistent XVA version.

Multi-curve swap pricing

Before and after 2008: where a cash flow's two arrows start Two registers drawn on identical geometry, with the same payment box in the same place in both. Upper register, headed before 2008, one curve: the LIBOR curve, and both arrows drop from it into the payment box, the arrow that sets the SIZE of the payment, F sub 1, and the arrow that sets its VALUE, P of 0 comma T sub 1. Lower register, headed after 2008, two curves: the ESTR OIS discount curve above the box, dashed crimson, and the EURIBOR 3M projection curve below it, solid teal. The VALUE arrow still drops from the discount curve into the top of the box. The SIZE arrow has moved: it now rises into the bottom of the box from the projection curve underneath. A dotted gold brace measures the gap between the two curves and is labelled basis. BEFORE 2008 · ONE CURVE LIBOR curve SIZE F1 VALUE P(0,T1) payment at T1 AFTER 2008 · TWO CURVES ESTR OIS · discount VALUE PD(0,T1) payment at T1 SIZE F1 EURIBOR 3M · projection basis SIZE comes from the index you are paid. VALUE comes from the cash you are posted.
Figure 2 · IR-02 One structural change: the arrow that sets a payment's size stopped starting on the same line as the arrow that sets its value. One annual payment is drawn; every later payment repeats the same two arrows. The gap the two curves open up is the basis, measured in Figure 5. Schematic; the curves carry no levels.

Subscript $D$ names the discount curve throughout and $F$ the projection curve, on bond prices and zero rates alike.

In the single-curve world, the floating leg telescopes to $P(0,0) - P(0,T_n)$ and no explicit forward rates are needed. In multi-curve, that simplification breaks down because discounting and projection use different curves.

Fixed leg paying on $T_{\alpha+1}, \ldots, T_\beta$, floating leg resetting on $t_0, t_1, \ldots, t_m$, with $\tau_j$ and $\delta_i$ the two accruals of Chapter 01:

Fixed leg PV (multi-curve) $$\text{Fixed PV} = S \sum_{j=\alpha+1}^{\beta} \tau_j \, P_D(0, T_j)$$
Floating leg PV (multi-curve) $$\text{Float PV} = \sum_{i=1}^{m} \delta_i \, P_D(0, t_i) \, F_i$$

where the forward rate from the projection curve is:

Simply-compounded forward rate $$F_i = \frac{1}{\delta_i} \left( \frac{P_F(0, t_{i-1})}{P_F(0, t_i)} - 1 \right)$$

$S$ is the quoted par rate and $F_i$ abbreviates $F(0;\,t_{i-1},t_i)$, the Brigo-Mercurio simply-compounded forward. $P_D$ is already calibrated and frozen; $P_F$ is still unknown. At par, $\text{PV} = 0$:

Par condition $$S \sum_{j=\alpha+1}^{\beta} \tau_j \, P_D(0, T_j) = \sum_{i=1}^{m} \delta_i \, P_D(0, t_i) \, F_i(R_1, R_2, \ldots, R_n)$$

where each $F_i$ depends on the unknowns through $P_F(0, T_k) = e^{-R_k \, T_k}$.

The system of equations and Jacobian

The unknowns are the projection curve's zero rates $R_k := R_F(0,T_k)$, entering through $P_F(0,T_k) = e^{-R_k T_k}$. On the common annual pillar grid the forward rate is:

Forward rate as a function of the unknowns $$F_i = \frac{1}{\tau_i} \left( \frac{P_F(0, T_{i-1})}{P_F(0, T_i)} - 1 \right) = \frac{e^{T_i\,R_i - T_{i-1}\,R_{i-1}} - 1}{\tau_i}, \qquad R_0 \equiv 0$$

The residual of swap $k$ is floating PV minus fixed PV at par, the matched annual schedule collapsing $\delta_i = \tau_j = T_i - T_{i-1}$ to a single accrual $\tau_i$:

Residual (matched annual schedule, $\delta_i = \tau_j = \tau_i$) $$g_k(\mathbf{R}) = \sum_{i=1}^{k} \tau_i\, P_D(0, T_i)\, F_i(\mathbf{R}) \;-\; S_k \sum_{i=1}^{k} \tau_i\, P_D(0, T_i)$$

The accruals cancel before you differentiate: $\tau_i F_i = e^{T_i R_i - T_{i-1} R_{i-1}} - 1$, so the day-count convention never reaches the Jacobian $J_{ki} = \partial g_k / \partial R_i$. Its diagonal is:

Diagonal element $$J_{kk} = P_D(0, T_k) \cdot T_k \cdot e^{T_k\,R_k - T_{k-1}\,R_{k-1}}$$

Below the diagonal, $R_i$ reaches $g_k$ twice, once raising period $i$ and once lowering period $i+1$:

Sub-diagonal element, $i < k$ $$J_{ki} = P_D(0,T_i)\,T_i\,e^{T_i R_i - T_{i-1} R_{i-1}} - P_D(0,T_{i+1})\,T_i\,e^{T_{i+1} R_{i+1} - T_i R_i}$$

No $k$ survives on the right. Pillar $i$ enters every swap longer than it in the same way, so under the diagonal a column of $J$ is one number repeated: column 1 reads 0.0239956 in all nine rows beneath it. Only $\mathbf{R}$ is differentiated; every $P_D$ is a frozen weight.

The Jacobian sparsity pattern of the projection-curve bootstrap A five by five grid. Rows are the EURIBOR 3M swaps of maturity one to five years, quoted at 2.75, 2.63, 2.56, 2.56 and 2.59 percent. Columns are the unknown pillar rates R sub 1 to R sub 5. Row k has its first k minus 1 cells filled navy, meaning the swap depends on a pillar already solved, and its k-th cell outlined in crimson, the one new unknown that row introduces. Every cell to the right of the diagonal is empty. The filled cells therefore form a lower triangle, and an arrow down the left edge marks the solve order. Rows six to ten continue the same staircase. The discount curve is frozen: it enters every row and is unknown in none. SWAP k TOUCHES PILLARS 1 TO k, NEVER k+1 swap R1 R2 R3 R4 R5 quote EUR3M 1Y EUR3M 2Y EUR3M 3Y EUR3M 4Y EUR3M 5Y 2.75% 2.63% 2.56% 2.56% 2.59% already solved the one new unknown Rows 6 to 10 continue the same staircase. PD enters every row and is unknown in none.
Figure 3 · IR-02 The calibration is a staircase, not a matrix: each swap adds exactly one unknown, so the system solves by forward substitution. Row $k$ is residual $g_k$; a cell is filled when that swap's value moves with that pillar. Quotes: website/data/ir-02-multi-curve-bootstrapping.js, key projectionCurve. The sparsity pattern is exact, not illustrative.

$J$ is lower-triangular because $F_i$ involves only $P_F(0, T_{i-1})$ and $P_F(0, T_i)$, so $R_{k+1}$ never appears in residual $k$. Every $P_D$ and every exponential is positive, so the diagonal is positive, $J$ is non-singular at every iterate, and the Newton step always exists. Started at the quoted swap rates, convergence is locally quadratic (Ortega and Rheinboldt, 1970, Thm. 12.6.1), which is why three iterations clear both curves.

Newton-Raphson with autograd

The Newton step $\mathbf{R}^{(k+1)} = \mathbf{R}^{(k)} - J(\mathbf{R}^{(k)})^{-1} \mathbf{g}(\mathbf{R}^{(k)})$ is the one derived in Chapter 01. Only $J$ has changed.

Finite differences need $2n$ function evaluations per iteration and carry truncation error from the choice of $\varepsilon$. Reverse-mode automatic differentiation (Adjoint Algorithmic Differentiation, AAD) returns the full $n \times n$ Jacobian in one torch.autograd.functional.jacobian call, exact to machine precision, with nothing to tune. It is the machinery production XVA engines use for market sensitivities.

Market data

Two quote sets, one valuation date, meeting on a common annual swap grid.

Par swap quotes, both curves
TenorESTREURIBOR 3M
1Y2.30%2.75%
2Y2.22%2.63%
3Y2.18%2.56%
4Y2.20%2.56%
5Y2.25%2.59%
6Y2.31%2.63%
7Y2.38%2.69%
8Y2.44%2.74%
9Y2.49%2.78%
10Y2.53%2.81%

Source: xvafoundations.data.estr, ESTR_INSTRUMENTS and EURIBOR_INSTRUMENTS, valuation date 15 Jan 2026, ACT/360 throughout. The ESTR curve also takes four short-end instruments: 1W deposit 2.37%, FRA 7x30 2.40%, FRA 30x90 2.42%, FRA 180x270 2.32%. A FRA fixes the curve at its end date, not its accrual length, so FRA 7x30 accrues 23 days and pins the 30-day pillar.

Step 1: ESTR OIS discount curve

Chapter 01's method, unchanged, on the ESTR quotes: $P_D(0,1) = 0.97749319$.

Step 2: EURIBOR 3M forward curve

The EURIBOR swaps carry annual fixed and float legs on the same pillar grid, and quote above OIS at every tenor: that gap is the premium on three-month unsecured lending.

Sequential calibration: freeze the discount curve, then build the projection curve on top A two-stage pipeline read downwards. Stage one: fourteen ESTR quotes, a one-week deposit, three FRAs and ten OIS swaps, enter the Stripper, which converges in three Newton steps and returns the discount curve P sub D of 0 comma T on fourteen pillars. A dashed gold gate then crosses the page, labelled frozen: P sub D is now an input, not an unknown. Stage two: ten EURIBOR 3M swap quotes enter the Stripper again, three more Newton steps, returning the projection curve P sub F of 0 comma T on ten pillars. A crimson rail carries the frozen P sub D down the left edge, crossing the gate, into the second solve, so every swap built there is discounted on ESTR. No arrow runs the other way: EURIBOR quotes never move the ESTR curve. STAGE 1 · THE DISCOUNT CURVE 14 ESTR quotes 1W deposit, 3 FRAs, 10 OIS swaps Stripper · 3 Newton steps PD(0,T), 14 pillars FROZEN · PD is now an input, not an unknown STAGE 2 · THE PROJECTION CURVE 10 EURIBOR 3M swap quotes Stripper · 3 Newton steps PF(0,T), 10 pillars PD No feedback: EURIBOR quotes never move ESTR.
Figure 4 · IR-02 Multi-curve calibration is sequential, not simultaneous: one curve stops being an unknown and becomes market data for the next. The crimson rail is the only thing that crosses the gate, and it crosses in one direction. A joint solve is possible; it is not what we do here. Schematic of the two calls in Snippets 2 and 3. Instrument counts and iteration counts are library output; the boxes carry no rates.

The first pillar sits at $T_1 = 1.0138889$ (ACT/360 from 15 Jan 2026 to 15 Jan 2027) and it is the one point on either curve you can check on paper. The 1Y EURIBOR swap has a single payment, so $P_D(0,T_1)$ appears on both sides of the par condition and cancels, leaving $F_1 = S_1 = 2.75\%$ with no reference to the discount curve at all:

First pillar, by hand $$P_F(0, T_1) = \frac{1}{1 + S_1 \tau_1} = \frac{1}{1 + 0.0275 \times 1.0138889} = 0.97287437$$

That is the calibrated value to eight decimals. The 2Y swap is where the difficulty starts: two payments, the discount factors no longer cancel, and the ESTR curve enters the answer for the first time. Snippet 3 below prints $P_F(0,1) = 0.97324094$, the curve read at the exact one-year point rather than at the pillar. Figure 5 uses that reading, not the pillar value.

Basis spread

The basis is the difference between the EURIBOR and ESTR forward rates for each annual period.

The EURIBOR 3M over ESTR basis by tenor A bar chart of the basis in basis points against annual tenor from one to ten years, measured from a dashed zero line marked as what single-curve pricing assumes. EURIBOR 3M MINUS ESTR, BP 50 0 basis data unavailable Dashed line at zero: what single-curve pricing assumes. 7Y bump: unsmoothed quotes, not curve shape.
Figure 5 · IR-02 The basis is a term structure with a shape, and that shape is this chapter's result: widest at the front, compressing by more than half out to 10Y. Each bar is $F_{\text{EUR}} - F_{\text{ESTR}}$ over one annual period, both read off the curves calibrated above. Source: YieldCurve.forward_rate on both calibrated curves, via website/data/ir-02-multi-curve-bootstrapping.js, key basisTable. Simply-compounded annual forwards.

The first bar is one subtraction. Both curves are read at $T = 1$: $F_{\text{ESTR}} = 1/0.97749319 - 1 = 2.3025\%$ and $F_{\text{EUR}} = 1/0.97324094 - 1 = 2.7495\%$, so the spread is 44.7 bp. Substitute the EURIBOR pillar value $0.97287437$ for the one-year reading and you get 48.6 bp, which is two curves evaluated at two different dates rather than a basis.

Only $P_D$ is a price. $P_F$ is a bookkeeping device whose ratios carry the forward rates: multiply the whole $P_F$ curve by any constant and every $F_i$ is unchanged. Nobody trades at $P_F$.

The EURIBOR 3M curve gives a smaller discount factor than ESTR at every tenor from 1Y to 10Y, falling from 0.97324 to 0.75708 against 0.97749 to 0.77771.

Source: xvafoundations.calibration.Stripper, both curves read on the integer-year grid; website/data/ir-02-multi-curve-bootstrapping.js, key annualGrid.

Zero rates from both curves. The gap is not the same number as Figure 5: that differences simply-compounded forwards, this differences continuously compounded zeros, so at 1Y it reads 43.6 bp against 44.7 bp.

EURIBOR 3M zero rates run from 2.71 percent at 1Y to 2.78 percent at 10Y, above ESTR at 2.28 percent to 2.51 percent, with the gap narrowing from 43.6 to 26.9 basis points.

Source: xvafoundations.calibration.Stripper, continuously compounded zero rates on the same grid; website/data/ir-02-multi-curve-bootstrapping.js, key annualGrid.

Two curves is the teaching case, not the desk case. Every tenor gets its own projection curve, 1M, 3M, 6M, 12M, and the gaps between them are quoted directly as tenor basis swaps (3s6s, 1s3s) rather than backed out. In practice the 3M curve is usually built as OIS plus those quotes, which prices the basis to the market instead of inferring it from two independent calibrations.

Python implementation

Nothing is redefined here. For the deposit, FRA and swap pricing formulas, see Chapter 01.

Snippet 1: Imports

Python
import torch
from xvafoundations.curves import YieldCurve, InterpolationMethod
from xvafoundations.instruments import build_instruments, pillar_maturities, initial_rates
from xvafoundations.calibration import Stripper
from xvafoundations.data.estr import ESTR_INSTRUMENTS, EURIBOR_INSTRUMENTS, VALUATION_DATE

torch.set_default_dtype(torch.float64)

Snippet 2: ESTR OIS discount curve

Python
# ── Step 1: Bootstrap the ESTR OIS discount curve ──
pillars = torch.tensor(pillar_maturities(ESTR_INSTRUMENTS, VALUATION_DATE))
rates   = torch.tensor(initial_rates(ESTR_INSTRUMENTS))

def estr_factory(curve: YieldCurve) -> list:
    return build_instruments(ESTR_INSTRUMENTS, VALUATION_DATE, curve)

disc_stripper = Stripper(pillars, rates, estr_factory)
disc_stripper.calibrate()
disc_curve = disc_stripper.get_curve()

t0 = torch.tensor(0.0)
print(f"P_D(0, 1) = {disc_curve.zc_price(t0, torch.tensor(1.0)):.8f}")

Snippet 3: The projection curve, built on the frozen discount curve

The factory captures disc_curve from the enclosing scope; the Stripper passes fwd_curve as the argument being calibrated. Each OISSwap gets both.

Python
# ── Step 2: Bootstrap the EURIBOR 3M forward curve ──
fwd_pillars = torch.tensor(pillar_maturities(EURIBOR_INSTRUMENTS, VALUATION_DATE))
fwd_rates   = torch.tensor(initial_rates(EURIBOR_INSTRUMENTS))

def euribor_factory(fwd_curve: YieldCurve) -> list:
    """disc_curve captured from enclosing scope; fwd_curve being calibrated."""
    return build_instruments(
        EURIBOR_INSTRUMENTS, VALUATION_DATE,
        curve=disc_curve,        # captured (frozen)
        fwd_curve=fwd_curve,     # being calibrated
    )

fwd_stripper = Stripper(fwd_pillars, fwd_rates, euribor_factory)
fwd_stripper.calibrate()
fwd_curve = fwd_stripper.get_curve()

print(f"P_F(0, 1) = {fwd_curve.zc_price(t0, torch.tensor(1.0)):.8f}")

Snippet 4: Basis spread computation

Python
# ── Basis spread: F_EURIBOR(T) - F_ESTR(T) for each annual period ──
t0 = torch.tensor(0.0)
print("\nBasis spread (EURIBOR 3M vs ESTR):")
for T in range(1, 11):
    T_prev = torch.tensor(float(T - 1))
    T_i    = torch.tensor(float(T))
    F_eur  = fwd_curve.forward_rate(t0, T_prev, T_i)
    F_estr = disc_curve.forward_rate(t0, T_prev, T_i)
    spread_bp = (F_eur - F_estr).item() * 10_000
    print(f"  {T:2d}Y: F_EUR={F_eur.item()*100:.4f}%, "
          f"F_ESTR={F_estr.item()*100:.4f}%, "
          f"spread={spread_bp:.1f}bp")

What this enables

Summary

The size of a cash flow and its value come from different curves. That is the whole of the post-2008 reform, and it costs one extra calibration: solve the discount curve, freeze it, solve the projection curve on top. The Jacobian stays lower-triangular, as in Chapter 01, and autograd computes it exactly. The basis it leaves, 44.7 bp at 1Y down to 17.9 bp at 10Y, shows up in every price downstream.

IR-03 takes the forward swap rate and the annuity built here and asks what a swaption is worth, which needs a volatility. It returns to the single USD SOFR curve of Chapter 01 to keep the change of numéraire uncluttered.

References

  1. D. Brigo and F. Mercurio, Interest Rate Models, Second Edition: Theory and Implementation, Springer, 2006.
  2. M. Henrard, "The Irony in the Derivatives Discounting Part II: The Crisis," SSRN, 2009.
  3. F. Ametrano and M. Bianchetti, "Everything You Always Wanted to Know About Multiple Interest Rate Curve Bootstrapping but Were Afraid to Ask," SSRN, 2013.
  4. L. B. G. Andersen and V. V. Piterbarg, Interest Rate Modeling, Volume I: Foundations and Vanilla Models, Atlantic Financial Press, 2010.
  5. S. Crépey, "Bilateral Counterparty Risk under Funding Constraints," Mathematical Finance, 25(1), 2015.
  6. D. Filipović and A. B. Trolle, "The Term Structure of Interbank Risk," Journal of Financial Economics, 109(3), 2013.
  7. J. M. Ortega and W. C. Rheinboldt, Iterative Solution of Nonlinear Equations in Several Variables, Academic Press, 1970.

All views expressed are strictly personal and do not represent any past or current employer. Content is educational; nothing here constitutes financial, investment, or trading advice.