Callable swap
This page lists what you need to value a swap that one side may cancel on a set of dates: every argument it takes, who holds the right, and how the value splits into the plain swap and a Bermudan swaption.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This values a five-year SOFR payer swap at 4.80%, cancellable on each anniversary but the last, once with the right held by the holder and once sold to the counterparty. Each callable value is printed beside the plain swap plus or minus the matching Bermudan. The market is the SOFR close with a Hull-White model pinned; 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::instruments::swaptions::{CallableBy, CallableSwap};
use helixrs::{
AnyInstrument, BermudanSwaption, CollateralSpec, MarketContext, OvernightIndexedSwap,
QuoteSheet, Side, SwaptionKind, SwaptionUnderlying, Termination, template,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
const RATE: f64 = 0.0480;
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
/// A five-year SOFR swap from spot at 4.80%.
fn swap(close: NaiveDate, side: Side) -> Outcome<OvernightIndexedSwap> {
let AnyInstrument::Ois(swap) = template("USD-SOFR-OIS")?
.swap(close)?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(10_000_000.0)
.with_fixed_rate(RATE)
.with_side(side)
.build()?
else {
unreachable!("a SOFR swap is an overnight indexed swap")
};
Ok(swap)
}
/// The cancellation right: a Bermudan on the same swap and dates, struck at its rate.
fn bermudan(close: NaiveDate, dates: &[NaiveDate], kind: SwaptionKind) -> Outcome<AnyInstrument> {
let side = if kind == SwaptionKind::Payer {
Side::Payer
} else {
Side::Receiver
};
let underlying = SwaptionUnderlying::Ois(swap(close, side)?);
let option = BermudanSwaption::new(underlying, dates.to_vec(), RATE, kind)?;
Ok(AnyInstrument::BermudanSwaption(Box::new(option)))
}
fn callable(close: NaiveDate, dates: &[NaiveDate], by: CallableBy) -> Outcome<AnyInstrument> {
let underlying = SwaptionUnderlying::Ois(swap(close, Side::Payer)?);
let swap = CallableSwap::new(underlying, dates.to_vec(), by)?;
Ok(AnyInstrument::CallableSwap(Box::new(swap)))
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let market = MarketContext::as_of(close)
.with_recipe_quotes(
&CollateralSpec::Currency("USD".parse()?),
&QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
)?
.with_hull_white("USD".parse()?, 0.03, 0.01);
// Cancellable on each anniversary but the last.
let dates: Vec<NaiveDate> = (0..4).map(|i| date((2027 + i, 10, 5))).collect();
// Paying fixed, the holder cancels when rates fall: the right is a receiver
// Bermudan, bought.
let plain = AnyInstrument::Ois(swap(close, Side::Payer)?).npv(&market)?;
let holder = callable(close, &dates, CallableBy::Holder)?.npv(&market)?;
let holder_by_hand = plain + bermudan(close, &dates, SwaptionKind::Receiver)?.npv(&market)?;
println!("swap {plain:.2}");
println!("callable {holder:.2}");
println!("by hand {holder_by_hand:.2}");
// Sold to the counterparty, the right is theirs: they cancel when rates rise,
// into the swap's own direction, so the holder is short a payer Bermudan.
let sold = callable(close, &dates, CallableBy::Counterparty)?.npv(&market)?;
let sold_by_hand = plain - bermudan(close, &dates, SwaptionKind::Payer)?.npv(&market)?;
println!("callable {sold:.2}");
println!("by hand {sold_by_hand:.2}");
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_hull_white("USD", 0.03, 0.01)
)
sofr = hlx.templates.get("USD-SOFR-OIS")
RATE = 0.0480
# Cancellable on each anniversary but the last.
DATES = [dt.date(2027 + i, 10, 5) for i in range(4)]
def swap(side):
"""A five-year SOFR swap from spot at 4.80%."""
return sofr.swap(SOFR_CLOSE_DATE).with_maturity("5Y").with_notional(10_000_000.0)\
.with_fixed_rate(RATE).with_side(side).build()
def bermudan(kind):
"""The cancellation right: a Bermudan on the same swap and dates, struck at its rate."""
return hlx.products.bermudan_swaption(swap("payer" if kind == "payer" else "receiver"), DATES, strike=RATE, kind=kind)
# Paying fixed, the holder cancels when rates fall: the right is a receiver
# Bermudan, bought.
plain = swap("payer").npv(market)
holder = hlx.products.callable_swap(swap("payer"), DATES, callable_by="holder").npv(market)
holder_by_hand = plain + bermudan("receiver").npv(market)
print(f"swap {plain:.2f}")
print(f"callable {holder:.2f}")
print(f"by hand {holder_by_hand:.2f}")
# Sold to the counterparty, the right is theirs: they cancel when rates rise,
# into the swap's own direction, so the holder is short a payer Bermudan.
sold = hlx.products.callable_swap(swap("payer"), DATES, callable_by="counterparty").npv(market)
sold_by_hand = plain - bermudan("payer").npv(market)
print(f"callable {sold:.2f}")
print(f"by hand {sold_by_hand:.2f}")swap -39809.20
callable 139979.08
by hand 139979.08
callable -219737.06
by hand -219737.06
Holding the right makes the swap worth more than the plain one; selling it makes it worth less. Each callable value matches its rebuild.
How it's priced
Cancelling a swap on date is entering the reversed swap from , so a swap cancellable on dates is
where is a Bermudan swaption on the same swap, exercisable on those dates and struck at the swap's fixed rate. The holder's right is bought: for a payer swap it is a receiver Bermudan, . The counterparty's right is sold: seen from the holder, it is the swap's own kind, a payer Bermudan, . Why dealers write cancellable swaps, and who cancels when, is covered in how rates products work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the trade | a swap cancellable on dates | CallableSwap::new(swap, dates, callable_by) in AnyInstrument::CallableSwap |
products.callable_swap(swap, dates, callable_by=) |
| who holds the right | CallableBy::Holder, CallableBy::Counterparty |
callable_by="holder", "counterparty" |
|
| the Bermudan on the same dates | BermudanSwaption::new(swap, dates, strike, kind) |
products.bermudan_swaption(swap, dates, strike=, kind=) |
|
| the lattice | Hull-White parameters | MarketContext::with_hull_white |
MarketContext.with_hull_white |
| its value | AnyInstrument::npv |
Instrument.npv |
| Argument | Type | Default | Meaning |
|---|---|---|---|
swap |
Instrument |
required | a vanilla or overnight swap with one fixed rate; in Rust a SwaptionUnderlying |
exercise_dates |
list[datetime.date] |
required | the dates the swap may be cancelled on, strictly ascending |
callable_by |
str |
"holder" |
"holder" (a right you bought) or "counterparty" (a right you sold); Rust CallableBy::Holder or CallableBy::Counterparty, required |
with_hull_white(currency, mean_reversion, sigma) |
str, float, float or list[tuple[date, float]] |
required | pins the Hull-White model the lattice uses, on the market |
Defaults and limits
- Model: both sides are valued on the Hull-White lattice, so the market must carry a Hull-White
model for the currency (
with_hull_white), as on Bermudan swaptions. - Strike: the right is struck at the swap's fixed rate. A swap whose fixed coupons carry more than one rate has no single strike and is refused.
See also
- Source:
crates/helixrs/src/instruments/swaptions.rs - Related pages:
- Bermudan swaptions, for the right and the lattice it is valued on.
- Overnight indexed swap, for the swap itself.
- Callable bond, for the bond a callable swap often hedges.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.