Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 F1F_1 and F2F_2 per unit notional and the spread leg's annuity AsA_s, the swap is worth

    V=N(F1+s As−F2)V = N\left(F_1 + s\,A_s - F_2\right)

    on the side that receives the spread leg, and the par spread is s∗=(F2−F1)/Ass^\ast = (F_2 - F_1)/A_s. Value is linear in ss with slope NAsN A_s. 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=
    s∗s^\ast the par spread AnyInstrument::par_spread Instrument.par_spread
    VV 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 to TenorBasisSwap::from_legs the 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.