Esc

    The API is under testing and may change before release.

    Inflation caps and floors

    This page lists what you need to price inflation caps and floors, both the zero-coupon kind and the year-on-year strip: every argument they take, the vol surfaces they read, and how each relates to its inflation swap. A cap pays when inflation runs above a strike; a floor pays when it falls short.

    Quick example

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

    This prices a five-year zero-coupon cap and floor and a year-on-year cap and floor at the same strike, each beside the inflation swap at that rate. The market is the SOFR close with a US CPI curve and normal vol surfaces 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::{
        AccrualCompounding, AnyInstrument, CapFloorKind, CollateralSpec, CpiVolSurface, Inflation,
        MarketContext, PiecewiseInflation, QuoteSheet, VolConvention, YoyInflationSwap,
        YoyInflationCapFloor, YoyVolSurface, ZeroCouponInflationCapFloor, ZeroCouponInflationSwap,
    };
    
    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")
    }
    
    const LEVELS: [((i32, u32, u32), f64); 6] = [
        ((2026, 7, 1), 321.0),
        ((2027, 7, 1), 329.0),
        ((2028, 7, 1), 337.2),
        ((2029, 7, 1), 345.5),
        ((2031, 7, 1), 362.6),
        ((2036, 7, 1), 407.0),
    ];
    
    fn cpi_curve(close: NaiveDate) -> Outcome<Inflation> {
        let nodes: Vec<(NaiveDate, f64)> = LEVELS.iter().map(|(d, level)| (date(*d), *level)).collect();
        Ok(Inflation::Piecewise(PiecewiseInflation::new(
            close,
            &nodes,
            "linear".parse()?,
            "ACT/365.FIXED".parse()?,
        )?))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let (expiries, strikes) = (vec![1.0, 3.0, 5.0, 10.0], vec![0.0, 0.02, 0.04]);
        let cpi_vols = CpiVolSurface::new(
            expiries.clone(),
            strikes.clone(),
            &vec![vec![0.010, 0.010, 0.011]; 4],
            VolConvention::Normal,
        )?;
        let yoy_vols = YoyVolSurface::new(
            expiries,
            strikes,
            &vec![vec![0.008, 0.008, 0.009]; 4],
            VolConvention::Normal,
        )?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(
                &CollateralSpec::Currency("USD".parse()?),
                &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
            )?
            .with_inflation_curve("US-CPI-U", cpi_curve(close)?)
            .with_cpi_vols("US-CPI-U", cpi_vols)
            .with_yoy_vols("US-CPI-U", yoy_vols);
        let (maturity, strike) = (date((2031, 10, 1)), 0.0250);
        let usd = || "USD".parse();
    
        // One option at maturity on five years of compounded CPI growth.
        let zc = |kind: CapFloorKind| -> Outcome<f64> {
            let option = ZeroCouponInflationCapFloor::new(
                usd()?,
                "US-CPI-U",
                close,
                maturity,
                10_000_000.0,
                strike,
                kind,
                3,
            )?;
            Ok(AnyInstrument::ZeroCouponInflationCapFloor(Box::new(option)).npv(&market)?)
        };
        let (c, f) = (zc(CapFloorKind::Cap)?, zc(CapFloorKind::Floor)?);
        let swap =
            ZeroCouponInflationSwap::new(usd()?, "US-CPI-U", close, maturity, 10_000_000.0, strike, 3)
                .accruing("ACT/365.FIXED".parse()?, AccrualCompounding::Compounded);
        let s = AnyInstrument::ZcInflationSwap(Box::new(swap)).npv(&market)?;
        println!("cap                 {c:.2}");
        println!("floor               {f:.2}");
        println!("cap - floor         {:.2}", c - f);
        println!("zero-coupon swap    {s:.2}");
    
        // A strip of options on each year's growth.
        let yoy = |kind: CapFloorKind| -> Outcome<f64> {
            let strip = YoyInflationCapFloor::new(
                usd()?,
                "US-CPI-U",
                close,
                maturity,
                10_000_000.0,
                strike,
                kind,
                3,
            )?;
            Ok(AnyInstrument::YoyInflationCapFloor(Box::new(strip)).npv(&market)?)
        };
        let (yc, yf) = (yoy(CapFloorKind::Cap)?, yoy(CapFloorKind::Floor)?);
        let yswap =
            YoyInflationSwap::new(usd()?, "US-CPI-U", close, maturity, 10_000_000.0, strike, 3)
                .accruing("ACT/365.FIXED".parse()?);
        let ys = AnyInstrument::YoyInflationSwap(Box::new(yswap)).npv(&market)?;
        println!("cap                 {yc:.2}");
        println!("floor               {yf:.2}");
        println!("cap - floor         {:.2}", yc - yf);
        println!("year-on-year swap   {ys:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    LEVELS = [
        (dt.date(2026, 7, 1), 321.0),
        (dt.date(2027, 7, 1), 329.0),
        (dt.date(2028, 7, 1), 337.2),
        (dt.date(2029, 7, 1), 345.5),
        (dt.date(2031, 7, 1), 362.6),
        (dt.date(2036, 7, 1), 407.0),
    ]
    EXPIRIES, STRIKES = [1.0, 3.0, 5.0, 10.0], [0.0, 0.02, 0.04]
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_inflation_curve("US-CPI-U", hlx.curves.InflationCurve(SOFR_CLOSE_DATE, LEVELS))
        .with_cpi_vols(
            "US-CPI-U", hlx.vols.CpiVolSurface(EXPIRIES, STRIKES, [[0.010, 0.010, 0.011]] * 4, "normal")
        )
        .with_yoy_vols(
            "US-CPI-U", hlx.vols.YoyVolSurface(EXPIRIES, STRIKES, [[0.008, 0.008, 0.009]] * 4, "normal")
        )
    )
    MATURITY, STRIKE = dt.date(2031, 10, 1), 0.0250
    TERMS = dict(notional=10_000_000.0, strike=STRIKE)
    
    # One option at maturity on five years of compounded CPI growth.
    cap = hlx.products.zero_coupon_inflation_cap_floor("USD", "US-CPI-U", SOFR_CLOSE_DATE, MATURITY, kind="cap", **TERMS)
    floor = hlx.products.zero_coupon_inflation_cap_floor("USD", "US-CPI-U", SOFR_CLOSE_DATE, MATURITY, kind="floor", **TERMS)
    swap = hlx.products.zero_coupon_inflation_swap(
        "USD", "US-CPI-U", SOFR_CLOSE_DATE, MATURITY, notional=10_000_000.0, fixed_rate=STRIKE
    )
    c, f, s = cap.npv(market), floor.npv(market), swap.npv(market)
    print(f"cap                 {c:.2f}")
    print(f"floor               {f:.2f}")
    print(f"cap - floor         {c - f:.2f}")
    print(f"zero-coupon swap    {s:.2f}")
    
    # A strip of options on each year's growth.
    ycap = hlx.products.yoy_inflation_cap_floor("USD", "US-CPI-U", SOFR_CLOSE_DATE, MATURITY, kind="cap", **TERMS)
    yfloor = hlx.products.yoy_inflation_cap_floor("USD", "US-CPI-U", SOFR_CLOSE_DATE, MATURITY, kind="floor", **TERMS)
    yswap = hlx.products.yoy_inflation_swap(
        "USD", "US-CPI-U", SOFR_CLOSE_DATE, MATURITY, notional=10_000_000.0, fixed_rate=STRIKE
    )
    yc, yf, ys = ycap.npv(market), yfloor.npv(market), yswap.npv(market)
    print(f"cap                 {yc:.2f}")
    print(f"floor               {yf:.2f}")
    print(f"cap - floor         {yc - yf:.2f}")
    print(f"year-on-year swap   {ys:.2f}")
    cap                 65171.92
    floor               80132.22
    cap - floor         -14960.30
    zero-coupon swap    -14960.30
    cap                 229981.50
    floor               243615.13
    cap - floor         -13633.63
    year-on-year swap   -13633.63

    In each case, the cap less the floor equals the swap.

    How it's priced

    A zero-coupon cap at strike KK pays at maturity

    Nmax⁡(I(T)I(T0)−(1+K)τ, 0),N\max\left(\frac{I(T)}{I(T_0)} - (1 + K)^{\tau},\ 0\right),

    and the floor pays the other side. Since a call less a put is the forward, a cap less a floor at the same strike is the zero-coupon swap at that rate. A year-on-year strip pays Nτimax⁡(I(ti)/I(ti−1)−1−K,0)N\tau_i\max(I(t_i)/I(t_{i-1}) - 1 - K, 0) each year, and a cap less a floor is the year-on-year swap. How these options are quoted is covered in how rates products work.

    API

    Symbol Meaning Rust Python
    CPI vols the zero-coupon surface CpiVolSurface::new, MarketContext::with_cpi_vols vols.CpiVolSurface, MarketContext.with_cpi_vols
    YoY vols the year-on-year surface YoyVolSurface::new, MarketContext::with_yoy_vols vols.YoyVolSurface, MarketContext.with_yoy_vols
    zero-coupon a cap or floor ZeroCouponInflationCapFloor::new products.zero_coupon_inflation_cap_floor
    year-on-year a cap or floor strip YoyInflationCapFloor::new products.yoy_inflation_cap_floor

    Both constructors take the same arguments:

    Argument Type Default Meaning
    currency str required the currency the option pays in
    index str required the inflation index, such as "US-CPI-U"
    base_date datetime.date required the date the index growth is measured from
    maturity datetime.date required when the option, or the strip's last period, ends
    notional float required the size
    strike float required an annual rate; for the zero-coupon option it compounds over the life, and it must be above −1
    kind CapFloorKind or str "cap" "cap" or "floor"; required in Rust
    lag_months int 3 the observation lag on the index; required in Rust
    day_count str, DayCountFraction or None None, meaning ACT/365F measures the life τ; Rust .accruing(day_count)

    Defaults and limits

    • Vols: each option reads its own surface by expiry and strike: the CPI surface for zero-coupon options, the year-on-year surface for strips. Both are normal vols here.
    • Strike: the zero-coupon strike is an annualised rate, compounded over the life.
    • Single coupons: for an option on one inflation coupon, see inflation coupons.

    See also

    • Source: crates/helixrs/src/instruments/cpi_cap_floor.rs, crates/helixrs/src/instruments/yoy_cap_floor.rs
    • Related pages:
      • Inflation swaps, for the swaps parity reproduces.
      • Inflation coupons, for options on one coupon.

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