Tenor basis swap
This page lists everything you need to book and value a tenor basis swap: its arguments and defaults, how it's priced, and an example you can run. A tenor basis swap exchanges two floating rates in one currency, such as €STR against EURIBOR 3M, with one leg carrying a spread.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This values a five-year €STR against EURIBOR 3M basis swap at zero spread, reads its par spread, measures one basis point of spread, and moves the spread to the other leg. The market comes from a EUR close on the quote sheets and recipes.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE};
use helixrs::{
AnyInstrument, AnyTemplate, CollateralSpec, MarketContext, QuoteSheet, TenorBasisSwap,
template,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn swap(spread: f64) -> Outcome<AnyInstrument> {
let AnyTemplate::TenorBasis(basis) = template("EUR-ESTR-EURIBOR-BASIS")? else {
unreachable!("a tenor basis template")
};
let swap = TenorBasisSwap::from_template(
&basis,
date((2026, 10, 5)),
date((2031, 10, 6)),
10_000_000.0,
spread,
)?;
Ok(AnyInstrument::TenorBasis(Box::new(swap)))
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let market = MarketContext::as_of(close).with_recipe_quotes(
&CollateralSpec::Currency("EUR".parse()?),
&QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?,
)?;
// The spread that makes the five-year swap fair is the market's basis between
// the two curves.
let flat = swap(0.0)?;
let spread = flat.par_spread(&market)?;
println!("npv at zero spread {:.2}", flat.npv(&market)?);
println!("par spread {spread:.8}");
// Each basis point of spread is worth the spread leg's annuity, at any spread.
let value = |s: f64| -> Outcome<f64> { Ok(swap(s)?.npv(&market)?) };
let one = value(0.0001)? - value(0.0)?;
println!("value of 1bp spread {one:.2}");
// The spread sits on the template's spread index, the €STR leg here, received
// against EURIBOR 3M flat. Naming the other index with spread_leg= moves it:
// EURIBOR 3M plus the spread is received against €STR flat. EURIBOR fixes above
// €STR, so the par spread is positive on the €STR leg and negative on the other.
// In Rust the move is the template's two legs passed the other way round.
let AnyTemplate::TenorBasis(basis) = template("EUR-ESTR-EURIBOR-BASIS")? else {
unreachable!("a tenor basis template")
};
let on_estr = swap(0.0)?.par_spread(&market)?;
let moved = TenorBasisSwap::from_legs(
&basis.base,
&basis.spread,
date((2026, 10, 5)),
date((2031, 10, 6)),
10_000_000.0,
0.0,
)?;
let on_euribor = AnyInstrument::TenorBasis(Box::new(moved)).par_spread(&market)?;
println!("par spread on the ESTR leg {:.6} bp", on_estr * 10_000.0);
println!("par spread on the EURIBOR leg {:.6} bp", on_euribor * 10_000.0);
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("EUR", EUR_CLOSE)
basis = hlx.templates.get("EUR-ESTR-EURIBOR-BASIS")
START, END = dt.date(2026, 10, 5), dt.date(2031, 10, 6)
def swap(spread):
return basis.tenor_basis_swap(START, END, notional=10_000_000.0, spread=spread)
# The spread that makes the five-year swap fair is the market's basis between
# the two curves.
flat = swap(0.0)
spread = flat.par_spread(market)
print(f"npv at zero spread {flat.npv(market):.2f}")
print(f"par spread {spread:.8f}")
# Each basis point of spread is worth the spread leg's annuity, at any spread.
one = swap(0.0001).npv(market) - swap(0.0).npv(market)
print(f"value of 1bp spread {one:.2f}")
# The spread sits on the template's spread index, the €STR leg here, received
# against EURIBOR 3M flat. Naming the other index with spread_leg= moves it:
# EURIBOR 3M plus the spread is received against €STR flat. EURIBOR fixes above
# €STR, so the par spread is positive on the €STR leg and negative on the other.
on_estr = basis.tenor_basis_swap(START, END, notional=10_000_000.0, spread=0.0).par_spread(market)
on_euribor = basis.tenor_basis_swap(START, END, notional=10_000_000.0, spread=0.0, spread_leg="EUR-EURIBOR-3M")
on_euribor = on_euribor.par_spread(market)
print(f"par spread on the ESTR leg {on_estr * 10_000:.6f} bp")
print(f"par spread on the EURIBOR leg {on_euribor * 10_000:.6f} bp")npv at zero spread -65261.93
par spread 0.00135846
value of 1bp spread 4804.12
par spread on the ESTR leg 13.584579 bp
par spread on the EURIBOR leg -13.584579 bp
EURIBOR fixes above €STR, so the par spread is positive on the €STR leg and negative once you move it to the EURIBOR leg.
How it's priced
With the two legs worth and per unit notional and the spread leg's annuity , the swap is worth
on the side that receives the spread leg, and the par spread is . Value is linear in with slope . Why the two curves differ, and why that difference trades, is covered in how rates products work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
effective |
date |
required | when the swap starts |
termination |
date |
required | when it ends |
notional |
float |
required | the size |
spread |
float |
required | the spread on the spread leg, received |
spread_leg |
index str |
the template's spread index, the first in its code | which leg carries the spread; in Rust, swap the legs with TenorBasisSwap::from_legs |
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the trade | a basis swap on its template | TenorBasisSwap::from_template |
Template.tenor_basis_swap |
| spread leg | which index carries the spread | TenorBasisSwap::from_legs with the template's legs swapped |
spread_leg= |
| the par spread | AnyInstrument::par_spread |
Instrument.par_spread |
|
| its value | AnyInstrument::npv |
Instrument.npv |
Defaults and limits
- Which leg carries the spread: by default the spread sits on the template's spread index, the
first in its code, received against the other index flat. In Python,
spread_leg=names the other index to move it there. In Rust, pass the template's two legs toTenorBasisSwap::from_legsthe other way round. - Conventions: the template holds both legs' conventions; see rates conventions.
- Curves: the EUR recipe solves the €STR curve and the EURIBOR 3M projection separately; see quote sheets and recipes.
- Legs: to build the two floating legs coupon by coupon, see legs and coupons.
See also
- Source:
crates/helixrs/src/instruments/rates.rs - Related pages:
- Quote sheets and recipes, for the EUR recipe that solves both curves.
- Cross-currency swap, for basis across currencies.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.