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), r(0), HW 1F INP(0,T), HW 1F, one trade INVk(t,ω) per trade INEE(t), P(0,t), a CDS quote OUTCVA OUTP(0,T), 14 pillars OUTtwo curves, a basis OUTσN, swaption prices OUTr(t,ω) under Q OUTEE(t), PFE(t,q) OUTEE(t) per netting set 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. Figure 1, just below, maps exactly what that takes.

Two quote sets, two solves, and the order between them An assembly map of this chapter, read top to bottom. Two navy boxes across the top hold everything that arrives from the market on 15 January 2026: fourteen ESTR quotes, being a one-week deposit, three FRAs and ten OIS swaps, on the left; and ten EURIBOR 3M par swaps on the right, which carry annual fixed and float legs on the same pillar grid. The ESTR quotes fall straight into the first solve. The EURIBOR quotes do not: their arrow runs down the right-hand edge, past the first solve, and enters the second one from the side, because they are known from the start and used only later. Solve one, a gold dashed box, is the IR-01 method unchanged on the ESTR quotes alone, three Newton steps, and it returns the discount curve P sub D of 0 comma T on fourteen pillars. That curve then crosses into the second solve on a solid navy arrow, because from there on it is a known input, not an unknown. A dashed aside pinned to that crossing records the structural fact that makes the order safe: there is no feedback, the EURIBOR quotes never move ESTR, so the sequential solve equals the joint solve here, the full system being block-triangular. Solve two, the second gold dashed box, takes the frozen discount curve and the EURIBOR quotes together. Its unknown, in crimson, is the vector of projection-curve zero rates R sub k; the equation is the par residual g sub k of R set to zero at all ten quotes; the solver is the same Newton system with the Jacobian taken by autograd, three steps, the Jacobian lower-triangular. It returns the forward curve P sub F of 0 comma T on ten pillars. The answer tile at the bottom holds what the chapter produces, and it is the end of the chain: two curves and the basis, the gap between them by tenor. Below the tile a dotted line, which in this series means a method is reused and no data crosses, leads to a closing note: what travels is the method, not the curves. Nothing downstream reads them, and IR-03 returns to the single USD SOFR curve of IR-01. TWO QUOTE SETS, ONE DATE 15 Jan 2026 14 ESTR quotes 1W deposit, 3 FRAs, 10 OIS swaps 10 EURIBOR 3M par swaps annual fixed and float legs on the same pillar grid BUILT HERE 14 quotes 10 par quotes not used yet solve 1 · the discount curve IR-01 method, unchanged, on ESTR 3 Newton steps PD(0,T), 14 pillars frozen: now an input, not an unknown no feedback EURIBOR quotes never move ESTR. Sequential equals joint here: the system is block-triangular solve 2 · the forward curve unknown Rk: the zero rates gk(R) = 0 at all 10 quotes the same Newton system, J by autograd 3 steps. J is lower-triangular. PF(0,T), 10 pillars WHAT COMES OUT two curves and the basis the gap between them, by tenor what travels is the method nothing downstream reads these two curves; IR-03 returns to SOFR
Figure 1 · IR-02 No forward curve is well posed until the discount curve is frozen. Navy carries objects; gold dashes are the two solves; the dotted foot is method, not data. Structural map; instrument, pillar and iteration counts and the valuation date are library output.

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 2 · IR-02 Discounting on the wrong curve costs EUR 131,757 on an aged trade, 13.2 bp of notional. At par the error nearly vanishes, which is why single-curve pricing survived so long. Source: xvafoundations.instruments.OISSwap on the two curves calibrated below, disc_curve ESTR then 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 3 · IR-02 The arrow that sets a payment's size no longer starts where the value arrow does. One annual payment is drawn; later payments repeat it. The gap between the curves 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 4 · IR-02 A staircase, not a matrix: each swap adds one unknown, solved by forward substitution. Row $k$ is residual $g_k$; a cell is filled when that swap moves with that pillar. Quotes: website/data/ir-02-multi-curve-bootstrapping.js, key projectionCurve. The 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$. The same holds one level up: no ESTR residual contains a projection rate, so the joint system is block lower-triangular and the sequential solve is the joint solution. 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

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 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 swaps quote above OIS at every tenor, and that gap is the premium on three-month unsecured lending.

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 whose shape is the chapter's result: widest at front, more than halved by 10Y. Each bar is $F_{\text{EUR}} - F_{\text{ESTR}}$ over an annual period. Source: YieldCurve.forward_rate, 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 in the strict order Figure 1 draws. 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.