Esc

    The API is under testing and may change before release.

    Swaptions

    This page lists what you need to price a European swaption, the right to enter a swap on one expiry date at a fixed rate you agree today: every argument it takes, the vol surface it reads, and the defaults it falls back on.

    Quick example

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

    This prices a 1Y5Y SOFR payer and receiver at the money, then compares a payer less a receiver at 5% with the forward swap at 5%. The market comes from the SOFR close, with a normal vol surface added; 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, CollateralSpec, EuropeanSwaption, MarketContext, QuoteSheet, RateVolSurface,
        Side, SwaptionKind, SwaptionUnderlying, Termination, VolConvention, 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, rate: f64, side: Side) -> Outcome<AnyInstrument> {
        Ok(template("USD-SOFR-OIS")?
            .swap(close)?
            .with_maturity("1Y5Y".parse::<Termination>()?)?
            .with_notional(10_000_000.0)
            .with_fixed_rate(rate)
            .with_side(side)
            .build()?)
    }
    
    fn swaption(close: NaiveDate, strike: f64, kind: SwaptionKind) -> Outcome<AnyInstrument> {
        let side = if kind == SwaptionKind::Payer {
            Side::Payer
        } else {
            Side::Receiver
        };
        let AnyInstrument::Ois(swap) = underlying(close, strike, side)? else {
            unreachable!("a SOFR swap is an overnight indexed swap")
        };
        let option = EuropeanSwaption::new(
            SwaptionUnderlying::Ois(swap),
            date((2027, 10, 1)),
            strike,
            kind,
        )?;
        Ok(AnyInstrument::EuropeanSwaption(Box::new(option)))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let vols = RateVolSurface::new(
            vec![1.0, 2.0, 5.0],
            vec![2.0, 5.0, 10.0],
            &[
                vec![0.0100, 0.0105, 0.0110],
                vec![0.0102, 0.0107, 0.0112],
                vec![0.0100, 0.0104, 0.0108],
            ],
            VolConvention::Normal,
        )?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(
                &CollateralSpec::Currency("USD".parse()?),
                &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
            )?
            .with_swaption_vols("USD".parse()?, vols);
    
        // The forward swap rate is the par rate of the swap the option delivers.
        let forward = underlying(close, 0.0, Side::Payer)?.par_rate(&market)?;
        let payer = swaption(close, forward, SwaptionKind::Payer)?.npv(&market)?;
        let receiver = swaption(close, forward, SwaptionKind::Receiver)?.npv(&market)?;
        println!("forward       {forward:.8}");
        println!("payer         {payer:.2}");
        println!("receiver      {receiver:.2}");
    
        // Payer less receiver at a 5% strike is the forward payer swap at 5%.
        let difference = swaption(close, 0.05, SwaptionKind::Payer)?.npv(&market)?
            - swaption(close, 0.05, SwaptionKind::Receiver)?.npv(&market)?;
        let forward_swap = underlying(close, 0.05, Side::Payer)?.npv(&market)?;
        println!("payer - receiver  {difference:.2}");
        println!("forward swap      {forward_swap:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    VOLS = hlx.vols.RateVolSurface(
        [1.0, 2.0, 5.0],
        [2.0, 5.0, 10.0],
        [[0.0100, 0.0105, 0.0110], [0.0102, 0.0107, 0.0112], [0.0100, 0.0104, 0.0108]],
        "normal",
    )
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_swaption_vols("USD", VOLS)
    )
    sofr = hlx.templates.get("USD-SOFR-OIS")
    EXPIRY = dt.date(2027, 10, 1)
    
    def underlying(rate, side):
        return sofr.swap(SOFR_CLOSE_DATE).with_maturity("1Y5Y").with_notional(10_000_000.0)\
            .with_fixed_rate(rate).with_side(side).build()
    
    def swaption(strike, kind):
        side = "payer" if kind == "payer" else "receiver"
        return hlx.products.european_swaption(underlying(strike, side), EXPIRY, strike=strike, kind=kind)
    
    # The forward swap rate is the par rate of the swap the option delivers.
    forward = underlying(0.0, "payer").par_rate(market)
    payer = swaption(forward, "payer").npv(market)
    receiver = swaption(forward, "receiver").npv(market)
    print(f"forward       {forward:.8f}")
    print(f"payer         {payer:.2f}")
    print(f"receiver      {receiver:.2f}")
    
    # Payer less receiver at a 5% strike is the forward payer swap at 5%.
    difference = swaption(0.05, "payer").npv(market) - swaption(0.05, "receiver").npv(market)
    forward_swap = underlying(0.05, "payer").npv(market)
    print(f"payer - receiver  {difference:.2f}")
    print(f"forward swap      {forward_swap:.2f}")
    forward       0.04812298
    payer         176672.37
    receiver      176672.37
    payer - receiver  -79161.67
    forward swap      -79161.67

    At the money the payer and the receiver cost the same. At any strike, a payer less a receiver is the forward swap.

    How it's priced

    With SS the forward swap rate, AA the forward annuity, KK the strike and σ\sigma the normal vol to expiry tt, a payer is worth, under Bachelier,

    Vpayer=NA((S−K) Φ(d)+σt ϕ(d)),d=S−Kσt,V_{\text{payer}} = N A\left((S - K)\,\Phi(d) + \sigma\sqrt{t}\,\phi(d)\right), \qquad d = \frac{S - K}{\sigma\sqrt{t}},

    and a receiver is the put. At the money, K=SK = S, the two are equal. At any strike a payer less a receiver is the forward payer swap, NA(S−K)N A (S - K). How swaptions are quoted, and what a 1Y5Y is, is covered in how rates products work.

    API

    Symbol Meaning Rust Python
    σ\sigma the swaption vol surface RateVolSurface::new, MarketContext::with_swaption_vols vols.RateVolSurface, MarketContext.with_swaption_vols
    the underlying the forward swap template("USD-SOFR-OIS")?.swap(d)?…build()? templates.get("USD-SOFR-OIS").swap(d)…build()
    the option a European swaption EuropeanSwaption::new with SwaptionUnderlying::Ois products.european_swaption
    SS the forward rate AnyInstrument::par_rate on the underlying Instrument.par_rate on the underlying
    VV the premium AnyInstrument::npv Instrument.npv
    Argument Type Default Meaning
    underlying Instrument required the swap the option delivers, drafted from its template; in Rust a SwaptionUnderlying (SwaptionUnderlying::Ois for a SOFR swap)
    expiry datetime.date required the one exercise date; it must fall before the underlying's last fixed payment
    strike float required the fixed rate you may enter at
    kind SwaptionKind or str required "payer" or "receiver"
    engine SwaptionEngine or str "surface" "surface" prices off the currency's swaption vol surface; "hull_white" uses the Hull-White model. Rust: .with_engine(SwaptionEngine::HullWhite)
    settlement SwaptionSettlement or str "physical" "physical" pays on the swap's own annuity; "cash" on the par-yield annuity EUR swaptions settle on. Rust: .with_settlement(SwaptionSettlement::Cash)
    with_swaption_vols(currency, surface) str, RateVolSurface or SabrVolSurface required for "surface" pins the vol surface the option reads, on the market

    Defaults and limits

    • Model: the formula above prices off the normal vol the surface gives at the option's expiry, underlying tenor and strike. See rate vol surface for the grid, and SABR for a smile fitted across strikes.
    • Exercise: one date only. For several, use a Bermudan swaption.

    See also

    • Source: crates/helixrs/src/instruments/swaptions.rs
    • Related pages:
      • Bermudan swaptions, for several exercise dates.
      • Caps and floors, for options on single periods.
      • SABR, for a smile across strikes.

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