Interest rate swap
This page lists everything you need to book and value a vanilla interest rate swap on a term index: the builder's arguments and defaults, how it's priced, and an example you can run. An interest rate swap exchanges a fixed rate for a floating term rate, such as EURIBOR 3M, on the same notional.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This books a five-year EURIBOR 3M swap receiving 2.30% fixed, values both sides, measures one basis point of fixed rate, and strikes a second swap at par. The market comes from a EUR close on the quote sheets and recipes.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE, Sheet};
use helixrs::{
AnyInstrument, AnyTemplate, CollateralSpec, MarketContext, QuoteSheet, Side,
Termination, 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 quote(rows: Sheet, template: &str, tenor: &str) -> f64 {
let (_, _, quote) = rows
.iter()
.find(|(t, l, _)| *t == template && *l == tenor)
.expect("quoted");
*quote
}
fn swap(irs: &AnyTemplate, close: NaiveDate, rate: f64, side: Side) -> Outcome<AnyInstrument> {
Ok(irs
.swap(close)?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(10_000_000.0)
.with_fixed_rate(rate)
.with_side(side)
.build()?)
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let market = MarketContext::as_of(close).with_recipe_quotes(
&CollateralSpec::Currency("EUR".parse()?),
&QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?,
)?;
let irs = template("EUR-EURIBOR-3M-IRS")?;
// Receive 2.30% fixed for five years against EURIBOR 3M.
let receiver = swap(&irs, close, 0.0230, Side::Receiver)?;
let npv = receiver.npv(&market)?;
let par = receiver.par_rate(&market)?;
println!("product {}", receiver.product_name());
println!("npv {npv:.2}");
println!("par rate {par:.8}");
// Paying fixed is the same two legs the other way round.
let payer = swap(&irs, close, 0.0230, Side::Payer)?.npv(&market)?;
println!("payer npv {payer:.2}");
// Value is linear in the fixed rate: the slope is the annuity, so moving the
// rate by one basis point moves the value by the same amount at every rate.
let value =
|rate: f64| -> Outcome<f64> { Ok(swap(&irs, close, rate, Side::Receiver)?.npv(&market)?) };
let step_low = value(0.0231)? - value(0.0230)?;
println!("value of 1bp of fixed rate {step_low:.2}");
// Struck at par, the swap is worth nothing on the market it was struck on.
let fair = irs
.swap(close)?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(10_000_000.0)
.at_par(&market)?;
println!("struck at {:.8}", fair.par_rate(&market)?);
Ok(())
}import helixio as hlx
from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("EUR", EUR_CLOSE)
irs = hlx.templates.get("EUR-EURIBOR-3M-IRS")
def swap(rate, side="receiver"):
return (
irs.swap(SOFR_CLOSE_DATE)
.with_maturity("5Y")
.with_notional(10_000_000.0)
.with_fixed_rate(rate)
.with_side(side)
.build()
)
# Receive 2.30% fixed for five years against EURIBOR 3M.
receiver = swap(0.0230)
npv = receiver.npv(market)
par = receiver.par_rate(market)
print(f"product {receiver.product()}")
print(f"npv {npv:.2f}")
print(f"par rate {par:.8f}")
# Paying fixed is the same two legs the other way round.
payer = swap(0.0230, "payer").npv(market)
print(f"payer npv {payer:.2f}")
# Value is linear in the fixed rate: the slope is the annuity, so moving the
# rate by one basis point moves the value by the same amount at every rate.
step_low = swap(0.0231).npv(market) - swap(0.0230).npv(market)
print(f"value of 1bp of fixed rate {step_low:.2f}")
# Struck at par, the swap is worth nothing on the market it was struck on.
fair = irs.swap(SOFR_CLOSE_DATE).with_maturity("5Y").with_notional(10_000_000.0).at_par(market)
print(f"struck at {fair.par_rate(market):.8f}")product VanillaSwap
npv -9394.85
par rate 0.02320000
payer npv 9394.85
value of 1bp of fixed rate 4697.42
struck at 0.02320000
The par rate is the sheet's five-year EURIBOR quote, because this swap is one of the instruments the curve was solved to reprice. Receiving 2.30% below it is worth less than nothing, and the payer holds exactly the opposite value.
How it's priced
Write for the fixed leg's annuity, the sum of each period's accrual times the discount factor to its payment date, and for the floating leg's value per unit notional:
The par rate is the fixed rate at which the swap is worth nothing, . Value is linear in the fixed rate with slope , and the payer holds the same legs the other way round, so . Why a EUR close carries both €STR and EURIBOR quotes is covered in how rates products work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
Template.swap(trade_date) |
date |
required | opens a draft on the template; Rust template(code)?.swap(d)? |
.with_maturity(end) |
date or tenor str |
"10Y" |
when the swap ends, as a date or a tenor from spot; also .with_termination; Rust takes a Termination |
.with_notional(notional) |
float |
10_000_000 |
the size |
.with_fixed_rate(rate) |
float |
none: set it, or use .at_par(market) |
the fixed rate; build() without one raises ValueError |
.with_side(side) |
"payer" or "receiver" |
"receiver" |
which way the fixed rate flows; Rust Side::Payer / Side::Receiver |
.at_par(market) |
MarketContext |
— | strikes the draft at the par rate instead of build() |
.with_csa(csa), .with_counterparty(c), .with_discount_key(key) |
str, Counterparty, str |
the template's discounting | collateral and discounting; see counterparties and CSAs |
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the market | €STR discount, EURIBOR projection | MarketContext::with_recipe_quotes on EUR |
MarketContext.with_recipe_quotes on "EUR" |
| , side | a trade on its template | template(code)?.swap(d)?…with_side(Side::Payer).build()? |
templates.get(code).swap(d)…with_side("payer").build() |
| the swap's value | AnyInstrument::npv |
Instrument.npv |
|
| the par rate | AnyInstrument::par_rate |
Instrument.par_rate |
|
| struck at par | ProductBuilder::at_par |
ProductBuilder.at_par |
Defaults and limits
- What the template holds: the template decides everything a desk does not restate per trade: calendars, day counts, leg frequencies and the floating index. You state the start, length, notional, fixed rate and side. The conventions catalog lists them in rates conventions.
- Side: the builder books the receiver of fixed unless you set the side to payer.
- Discounting: the swap discounts on the €STR curve and projects EURIBOR from its own curve, so the market needs both sets of quotes.
- Legs: to build and value the legs coupon by coupon, see legs and coupons.
See also
- Source:
crates/helixrs/src/instruments/rates.rs,crates/helixrs/src/products.rs - Related pages:
- Overnight indexed swap, for the swap on an overnight index.
- Tenor basis swap, for two floating legs.
- Templates and builders, for the builder.
- Quote sheets and recipes, for the EUR close.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.