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.
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(())
}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 with forward , strike and normal vol over time is worth, under Bachelier,
A floorlet is the put. Since , a cap less a floor at the same strike is the floating leg less a fixed leg at on the same periods:
So is linear in , 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 |
|---|---|---|---|
| the cap vol surface | RateVolSurface::new, MarketContext::with_cap_vols |
vols.RateVolSurface, MarketContext.with_cap_vols |
|
| 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.