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.
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(())
}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 pays at maturity
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 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.