Esc

    The API is under testing and may change before release.

    Bermudan swaptions

    This page lists what you need to price a Bermudan swaption, the right to enter a swap on any of several dates, on the Hull-White lattice: every argument it takes, the model it needs, and how accurate the lattice is.

    Quick example

    The example reads its quotes from the desk sheets; load them once and it runs as shown.

    This prices payer Bermudans with one, two and three exercise dates, receivers with one and three, and compares one-date parity on the lattice with the closed-form European. The market is the SOFR close with a Hull-White model pinned; see quote sheets and recipes for how a close becomes a market.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        AnyInstrument, BermudanSwaption, CollateralSpec, EuropeanSwaption, MarketContext,
        OvernightIndexedSwap, QuoteSheet, Side, SwaptionEngine, SwaptionKind, SwaptionUnderlying,
        Termination, 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 underlying(close: NaiveDate, kind: SwaptionKind) -> Outcome<OvernightIndexedSwap> {
        let side = if kind == SwaptionKind::Payer {
            Side::Payer
        } else {
            Side::Receiver
        };
        let AnyInstrument::Ois(swap) = template("USD-SOFR-OIS")?
            .swap(close)?
            .with_maturity("1Y5Y".parse::<Termination>()?)?
            .with_notional(10_000_000.0)
            .with_fixed_rate(0.0480)
            .with_side(side)
            .build()?
        else {
            unreachable!("a SOFR swap is an overnight indexed swap")
        };
        Ok(swap)
    }
    
    fn bermudan(close: NaiveDate, dates: &[NaiveDate], kind: SwaptionKind) -> Outcome<AnyInstrument> {
        let swap = SwaptionUnderlying::Ois(underlying(close, kind)?);
        let option = BermudanSwaption::new(swap, dates.to_vec(), 0.0480, kind)?;
        Ok(AnyInstrument::BermudanSwaption(Box::new(option)))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(
                &CollateralSpec::Currency("USD".parse()?),
                &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
            )?
            .with_hull_white("USD".parse()?, 0.03, 0.01);
        let (first, second, third) = (
            date((2027, 10, 5)),
            date((2028, 10, 5)),
            date((2029, 10, 5)),
        );
        let value = |dates: &[NaiveDate], kind: SwaptionKind| -> Outcome<f64> {
            Ok(bermudan(close, dates, kind)?.npv(&market)?)
        };
    
        // One date, two dates, three: each added right can only add value.
        let one = value(&[first], SwaptionKind::Payer)?;
        let two = value(&[first, second], SwaptionKind::Payer)?;
        let three = value(&[first, second, third], SwaptionKind::Payer)?;
        println!("one date      {one:.2}");
        println!("two dates     {two:.2}");
        println!("three dates   {three:.2}");
    
        // The same holds for the right to receive fixed.
        let r_one = value(&[first], SwaptionKind::Receiver)?;
        let r_three = value(&[first, second, third], SwaptionKind::Receiver)?;
        println!("receiver, one date     {r_one:.2}");
        println!("receiver, three dates  {r_three:.2}");
    
        // With one date the right to pay less the right to receive is the swap itself.
        // The Hull-White European is exact; the lattice is a discretisation of the same
        // model, so its parity holds to within its grid.
        let swap = AnyInstrument::Ois(underlying(close, SwaptionKind::Payer)?).npv(&market)?;
        let european = |kind: SwaptionKind| -> Outcome<f64> {
            let swap = SwaptionUnderlying::Ois(underlying(close, kind)?);
            let option = EuropeanSwaption::new(swap, first, 0.0480, kind)?
                .with_engine(SwaptionEngine::HullWhite);
            Ok(AnyInstrument::EuropeanSwaption(Box::new(option)).npv(&market)?)
        };
        let european_parity = european(SwaptionKind::Payer)? - european(SwaptionKind::Receiver)?;
        let lattice_parity = one - r_one;
        println!("forward swap           {swap:.2}");
        println!("european pay - rec     {european_parity:.2}");
        println!("one-date pay - rec     {lattice_parity:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_hull_white("USD", 0.03, 0.01)
    )
    sofr = hlx.templates.get("USD-SOFR-OIS")
    FIRST, SECOND, THIRD = dt.date(2027, 10, 5), dt.date(2028, 10, 5), dt.date(2029, 10, 5)
    
    def underlying(side):
        return sofr.swap(SOFR_CLOSE_DATE).with_maturity("1Y5Y").with_notional(10_000_000.0)\
            .with_fixed_rate(0.0480).with_side(side).build()
    
    def bermudan(dates, kind="payer"):
        side = "payer" if kind == "payer" else "receiver"
        return hlx.products.bermudan_swaption(underlying(side), dates, strike=0.0480, kind=kind)
    
    # One date, two dates, three: each added right can only add value.
    one = bermudan([FIRST]).npv(market)
    two = bermudan([FIRST, SECOND]).npv(market)
    three = bermudan([FIRST, SECOND, THIRD]).npv(market)
    print(f"one date      {one:.2f}")
    print(f"two dates     {two:.2f}")
    print(f"three dates   {three:.2f}")
    
    # The same holds for the right to receive fixed.
    r_one = bermudan([FIRST], "receiver").npv(market)
    r_three = bermudan([FIRST, SECOND, THIRD], "receiver").npv(market)
    print(f"receiver, one date     {r_one:.2f}")
    print(f"receiver, three dates  {r_three:.2f}")
    
    # With one date the right to pay less the right to receive is the swap itself.
    # The Hull-White European is exact; the lattice is a discretisation of the same
    # model, so its parity holds to within its grid.
    swap = underlying("payer").npv(market)
    
    def european(kind):
        side = "payer" if kind == "payer" else "receiver"
        return hlx.products.european_swaption(
            underlying(side), FIRST, strike=0.0480, kind=kind, engine="hull_white"
        ).npv(market)
    
    european_parity = european("payer") - european("receiver")
    lattice_parity = one - r_one
    print(f"forward swap           {swap:.2f}")
    print(f"european pay - rec     {european_parity:.2f}")
    print(f"one-date pay - rec     {lattice_parity:.2f}")
    one date      163218.94
    two dates     204679.50
    three dates   223085.87
    receiver, one date     157992.72
    receiver, three dates  217156.49
    forward swap           5186.68
    european pay - rec     5186.68
    one-date pay - rec     5226.23

    More dates never lower the value. With one date, payer less receiver is the forward swap: exact for the European, and to within the lattice's grid for the Bermudan.

    How it's priced

    On a lattice of the short rate under Hull-White, the option is valued backward from the last exercise date. At each exercise date tjt_j the holder takes the larger of exercising, the remaining swap's value UjU_j, and holding, the discounted expectation of the next node:

    Vj=max⁡(Uj, Ej ⁣[P(tj,tj+1) Vj+1]).V_j = \max\left(U_j,\ \mathbb{E}_j\!\left[P(t_j, t_{j+1})\,V_{j+1}\right]\right).

    Each added exercise date can only raise the value, since the holder can always ignore it. With a single exercise date TT the Bermudan is a European, and holding a payer while selling a receiver on the same swap and strike is the swap itself, so

    V0pay−V0rec=U0,V^{\text{pay}}_0 - V^{\text{rec}}_0 = U_0,

    the forward swap's value today. Where Bermudans appear, and why they need a short-rate model, is covered in how rates products work.

    API

    Symbol Meaning Rust Python
    a,σa, \sigma the model MarketContext::with_hull_white MarketContext.with_hull_white
    the option a Bermudan swaption BermudanSwaption::new with SwaptionUnderlying::Ois products.bermudan_swaption
    V0V_0 its value on the lattice AnyInstrument::npv Instrument.npv
    the European one exercise date, closed form EuropeanSwaption::new(..).with_engine(SwaptionEngine::HullWhite) products.european_swaption(..., engine="hull_white")
    U0U_0 the forward swap's value AnyInstrument::npv on the swap Instrument.npv on the swap
    Argument Type Default Meaning
    underlying Instrument required the swap the holder may enter; in Rust a SwaptionUnderlying (SwaptionUnderlying::Ois for a SOFR swap)
    exercise_dates list[datetime.date] required strictly ascending; the last must fall before the underlying's last fixed payment
    strike float required the fixed rate entered on exercise
    kind SwaptionKind or str required "payer" or "receiver"
    with_hull_white(currency, mean_reversion, sigma) str, float, float or list[tuple[date, float]] required pins the Hull-White model the lattice uses, on the market; sigma is one number or piecewise (date, σ) pieces

    Defaults and limits

    • Model: the market must carry a Hull-White model for the currency (with_hull_white); calibrate it to co-terminal Europeans on Hull-White.
    • Accuracy: the lattice runs about 50 time steps a year (DEFAULT_STEPS_PER_YEAR in the source), plus a step on each exercise date; the grid density is not a public argument. The example checks one-date parity to within 1e-5 of notional on the lattice, against 1e-9 of notional for the closed-form European.
    • Strike: a SOFR swap pays two days after each accrual end, so the option is struck at K+sK + s, where ss is the fixed spread that makes a par floating leg reprice those lagged coupons.
    • Lattice: the backward induction runs inside the pricer; no public call steps through it.

    See also

    • Source: crates/helixrs/src/instruments/swaptions.rs, crates/helixrs/src/models/lattice.rs
    • Related pages:
      • Swaptions, for one exercise date.
      • Callable swaps, a swap with a Bermudan inside it.
      • Hull-White, for the model.
      • Callable bonds, for the option inside a bond.
      • Rates conventions, for the swap conventions the underlying is drafted on.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.