Esc

    The API is under testing and may change before release.

    Caps and floors

    This page lists what you need to price a cap or a floor: the booking call and its arguments, the pricing formula, and the defaults you get. A cap is a strip of call options on a floating rate, one per coupon period; a floor is the strip of puts.

    Quick example

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

    This prices a five-year cap and floor on compounded SOFR struck at 4.50%, prints cap less floor at three strikes, and finds the strike where cap and floor are worth the same.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        AnyIndex, AnyInstrument, CapFloorKind, CollateralSpec, InterestRateCapFloor, MarketContext,
        Period, QuoteSheet, RateVolSurface, VolConvention, index,
    };
    
    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 strip(strike: f64, kind: CapFloorKind) -> Outcome<AnyInstrument> {
        let Some(AnyIndex::Overnight(sofr)) = index("USD-SOFR") else {
            unreachable!("SOFR is an overnight index")
        };
        let strip = InterestRateCapFloor::on_overnight(
            &sofr,
            date((2026, 10, 5)),
            date((2031, 10, 5)),
            "3M".parse::<Period>()?,
            10_000_000.0,
            strike,
            kind,
        )?;
        Ok(AnyInstrument::InterestRateCapFloor(Box::new(strip)))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let vols = RateVolSurface::new(
            vec![0.5, 1.0, 2.0, 5.0],
            vec![0.25],
            &[vec![0.0090], vec![0.0095], vec![0.0100], vec![0.0105]],
            VolConvention::Normal,
        )?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(
                &CollateralSpec::Currency("USD".parse()?),
                &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
            )?
            .with_cap_vols("USD".parse()?, vols);
    
        let cap = strip(0.0450, CapFloorKind::Cap)?.npv(&market)?;
        let floor = strip(0.0450, CapFloorKind::Floor)?.npv(&market)?;
        println!("cap 4.50%     {cap:.2}");
        println!("floor 4.50%   {floor:.2}");
    
        // Cap less floor is linear in the strike: equal steps in strike move it by
        // equal amounts, the struck leg's annuity per unit of rate.
        let gap = |strike: f64| -> Outcome<f64> {
            Ok(strip(strike, CapFloorKind::Cap)?.npv(&market)?
                - strip(strike, CapFloorKind::Floor)?.npv(&market)?)
        };
        let (low, mid, high) = (gap(0.0400)?, gap(0.0450)?, gap(0.0500)?);
        println!("cap - floor at 4.00%  {low:.2}");
        println!("cap - floor at 4.50%  {mid:.2}");
        println!("cap - floor at 5.00%  {high:.2}");
        // The strike where cap and floor are worth the same is the swap's par rate.
        let atm = 0.0450 - mid / ((high - low) / 0.0100);
        println!("atm strike            {atm:.8}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    VOLS = hlx.vols.RateVolSurface(
        [0.5, 1.0, 2.0, 5.0], [0.25], [[0.0090], [0.0095], [0.0100], [0.0105]], "normal"
    )
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_cap_vols("USD", VOLS)
    )
    
    def strip(strike, kind):
        return hlx.products.interest_rate_cap_floor(
            "USD-SOFR",
            dt.date(2026, 10, 5),
            dt.date(2031, 10, 5),
            notional=10_000_000.0,
            strike=strike,
            kind=kind,
            frequency="3M",
        )
    
    cap = strip(0.0450, "cap").npv(market)
    floor = strip(0.0450, "floor").npv(market)
    print(f"cap 4.50%     {cap:.2f}")
    print(f"floor 4.50%   {floor:.2f}")
    
    # Cap less floor is linear in the strike: equal steps in strike move it by
    # equal amounts, the struck leg's annuity per unit of rate.
    def gap(strike):
        return strip(strike, "cap").npv(market) - strip(strike, "floor").npv(market)
    
    low, mid, high = gap(0.0400), gap(0.0450), gap(0.0500)
    print(f"cap - floor at 4.00%  {low:.2f}")
    print(f"cap - floor at 4.50%  {mid:.2f}")
    print(f"cap - floor at 5.00%  {high:.2f}")
    # The strike where cap and floor are worth the same is the swap's par rate.
    atm = 0.0450 - mid / ((high - low) / 0.0100)
    print(f"atm strike            {atm:.8f}")
    cap 4.50%     298190.94
    floor 4.50%   240778.26
    cap - floor at 4.00%  282614.40
    cap - floor at 4.50%  57412.69
    cap - floor at 5.00%  -167789.03
    atm strike            0.04627469

    Cap less floor falls by the same amount for each half-point rise in the strike, because it is a swap: linear in the strike, as the next section shows.

    How it's priced

    A caplet on a period of accrual τ\tau with forward FF, strike KK and normal vol σ\sigma over time tt is worth, under Bachelier,

    c=NτP(T)((F−K) Φ(d)+σt ϕ(d)),d=F−Kσt.c = N\tau P(T)\left((F - K)\,\Phi(d) + \sigma\sqrt{t}\,\phi(d)\right), \qquad d = \frac{F - K}{\sigma\sqrt{t}}.

    A floorlet is the put. Since max⁡(F−K,0)−max⁡(K−F,0)=F−K\max(F - K, 0) - \max(K - F, 0) = F - K, a cap less a floor at the same strike is the floating leg less a fixed leg at KK on the same periods:

    C(K)−Fl(K)=N(∑iτiP(Ti)Fi−K∑iτiP(Ti)).C(K) - F_l(K) = N\left(\textstyle\sum_i \tau_i P(T_i) F_i - K \sum_i \tau_i P(T_i)\right).

    So C−FlC - F_l is linear in KK, and the strike where the two are equal is that swap's par rate. Who buys caps and floors, and why a SOFR cap earns less variance than a term-rate cap, is covered in how rates products work.

    API

    Symbol Meaning Rust Python
    σ\sigma the cap vol surface RateVolSurface::new, MarketContext::with_cap_vols vols.RateVolSurface, MarketContext.with_cap_vols
    C,FlC, F_l a cap or floor on SOFR InterestRateCapFloor::on_overnight products.interest_rate_cap_floor with frequency
    value its premium AnyInstrument::npv Instrument.npv
    Argument Type Default Meaning
    index str required the registered index, such as "USD-SOFR"; Rust takes the OvernightIndex
    effective date required the first period's start
    termination date required the last period's end
    notional float required the notional of every caplet
    strike float required the struck rate, as a decimal
    kind CapFloorKind or str "cap" "cap" or "floor"; Rust CapFloorKind::Cap or ::Floor
    first_caplet_excluded bool False drops the first caplet, whose rate is fixed at trade; Rust .without_first_caplet()
    frequency Frequency or str None the coupon frequency, such as "3M"; required for an overnight index

    Defaults and limits

    • Vols: cap vols are quoted by expiry, as normal vols in rate units; see rate vol surface.
    • SOFR periods: each caplet is struck on its quarter's compounded rate, which is known only when the quarter ends.
    • One coupon: an option on a single coupon, attached to a leg as a collar or a digital, is built on coupon options.

    See also

    • Source: crates/helixrs/src/instruments/cap_floor.rs
    • Related pages:
      • Coupon options, for options on one coupon.
      • Swaptions, for options on a whole swap.

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