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.
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(())
}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 the forward swap rate, the forward annuity, the strike and the normal vol to expiry , a payer is worth, under Bachelier,
and a receiver is the put. At the money, , the two are equal. At any strike a payer less a receiver is the forward payer swap, . How swaptions are quoted, and what a 1Y5Y is, is covered in how rates products work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| 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 |
| the forward rate | AnyInstrument::par_rate on the underlying |
Instrument.par_rate on the underlying |
|
| 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.