Zero-coupon swap
This page lists everything you need to book and value a zero-coupon swap: its arguments and defaults, how it's priced, and an example you can run. A zero-coupon swap pays one fixed amount at maturity, the fixed rate compounded over the life, against a floating leg.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This pays 2.20% compounded for five years against EURIBOR 3M, reads the par rate, and values a second swap struck 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};
use helixrs::{
AnyInstrument, AnyTemplate, CollateralSpec, MarketContext, Period, QuoteSheet,
ZeroCouponSwap, 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 swap(close: NaiveDate, rate: f64, receiver: bool) -> Outcome<AnyInstrument> {
let AnyTemplate::Zcs(zcs) = template("EUR-EURIBOR-3M-ZCS")? else {
unreachable!("a zero-coupon swap template")
};
let mut swap =
ZeroCouponSwap::from_template(&zcs, close, "5Y".parse::<Period>()?, 10_000_000.0, rate);
if receiver {
swap = swap.receiver();
}
Ok(AnyInstrument::Zcs(Box::new(swap)))
}
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())?,
)?;
// Pay 2.20% compounded for five years, receive EURIBOR 3M compounded.
let payer = swap(close, 0.0220, false)?;
let par = payer.par_rate(&market)?;
let npv = payer.npv(&market)?;
println!("par rate {par:.8}");
println!("payer npv {npv:.2}");
// At its par rate the swap is worth nothing, and above it the payer loses.
let at_par = swap(close, par, false)?.npv(&market)?;
println!("npv at par {at_par:.6}");
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)
zcs = hlx.templates.get("EUR-EURIBOR-3M-ZCS")
def swap(rate, receiver=False):
return zcs.zero_coupon_swap(
SOFR_CLOSE_DATE, "5Y", notional=10_000_000.0, fixed_rate=rate, receiver=receiver
)
# Pay 2.20% compounded for five years, receive EURIBOR 3M compounded.
payer = swap(0.0220)
par = payer.par_rate(market)
npv = payer.npv(market)
print(f"par rate {par:.8f}")
print(f"payer npv {npv:.2f}")
# At its par rate the swap is worth nothing, and above it the payer loses.
print(f"npv at par {swap(par).npv(market):.6f}")par rate 0.02325000
payer npv 61371.76
npv at par 0.000000
Paying 2.20% below par is worth something to the payer. At the par rate the swap is worth nothing, and above it the payer loses.
How it's priced
The fixed side pays at maturity , so with the floating leg worth per unit notional the payer is worth
The par rate solves : . The value falls as rises, and the receiver holds the other side. Who uses a swap that pays once is covered in how rates products work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
valuation |
date |
None |
the date tenor counts from, via the template's spot lag; give it with tenor |
tenor |
tenor str |
None |
the swap's length, such as "5Y" |
effective, maturity |
date |
None |
stated dates, instead of valuation and tenor; give both or neither |
notional |
float |
required | the size |
fixed_rate |
float |
required | the rate compounded to the single fixed payment |
receiver |
bool |
False |
True receives the fixed amount; Rust .receiver() on ZeroCouponSwap::from_template(&template, valuation, tenor, notional, rate) |
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the trade | a zero-coupon swap on its template | ZeroCouponSwap::from_template |
Template.zero_coupon_swap |
| receiver | the other side | ZeroCouponSwap::receiver |
receiver=True |
| the par rate | AnyInstrument::par_rate |
Instrument.par_rate |
|
| its value | AnyInstrument::npv |
Instrument.npv |
Defaults and limits
- Side: the swap is booked as the payer of fixed. Pass
receiver=Truein Python, or call.receiver()in Rust, for the other side. - Floating leg: it pays the term index each period, or compounds it to maturity, as the template says. The template holds the index and accrual rules; see rates conventions.
- Legs: to build the fixed and floating legs coupon by coupon, see legs and coupons.
The fixed leg on its own
legs.accumulated_fixed_leg builds the fixed side by itself: one coupon accruing fixed_rate into
a single payment at maturity. On 1/1, measured once over the whole term, it counts one accrual.
With frequency= (Rust accumulated_fixed_leg_by_period) it's measured over unadjusted periods of
that frequency and summed, so a five-year annual leg counts five and pays .
The payment is read off the leg's cashflow table.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE};
use helixrs::cashflows::legs::accumulated_fixed_leg_by_period;
use helixrs::{
AccrualCompounding, CollateralSpec, Direction, Leg, MarketContext, QuoteSheet, accumulated_fixed_leg,
};
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 main() -> Outcome<()> {
let eur = CollateralSpec::Currency("EUR".parse()?);
let sheet = QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(date(SOFR_CLOSE_DATE)).with_recipe_quotes(&eur, &sheet)?;
let (start, end) = (date((2026, 10, 5)), date((2031, 10, 5)));
// Five years at 3.00% on 1/1: once over the whole term, then year by year.
let once = accumulated_fixed_leg(
"EUR".parse()?,
start,
end,
10_000_000.0,
0.03,
"1/1".parse()?,
AccrualCompounding::Compounded,
Direction::Receive,
);
let yearly = accumulated_fixed_leg_by_period(
"EUR".parse()?,
start,
end,
"1Y".parse()?,
10_000_000.0,
0.03,
"1/1".parse()?,
AccrualCompounding::Compounded,
Direction::Receive,
)?;
// The year fraction the coupon counts, and the one payment its table shows.
let paid = |leg: &Leg| -> Outcome<(f64, f64)> {
let table = leg.cashflow_records(&market, &eur)?;
let amounts = table["amount"].numbers().expect("numbers");
let payment = *amounts.iter().find(|a| !a.is_nan()).expect("a payment");
Ok((leg.coupons()[0].year_fraction(), payment))
};
let ((once_years, once_amount), (yearly_years, yearly_amount)) = (paid(&once)?, paid(&yearly)?);
println!("one accrual {once_years:.0} year pays {once_amount:.2}");
println!("year by year {yearly_years:.0} years pays {yearly_amount:.2}");
Ok(())
}import datetime as dt
import math
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)
START, END = dt.date(2026, 10, 5), dt.date(2031, 10, 5)
# Five years at 3.00% on 1/1: once over the whole term, then year by year.
once = hlx.legs.accumulated_fixed_leg(
"EUR", START, END, notional=10_000_000.0, fixed_rate=0.03, day_count="1/1", discount="EUR",
)
yearly = hlx.legs.accumulated_fixed_leg(
"EUR", START, END, notional=10_000_000.0, fixed_rate=0.03, day_count="1/1", discount="EUR",
frequency="1Y",
)
def paid(leg):
"""The year fraction the coupon counts, and the one payment its table shows:
the resets carry an empty (NaN) amount, so the payment is the one that isn't."""
amounts = leg.cashflow_records(market).to_dict()["amount"]
return leg.coupons()[0].year_fraction(), [a for a in amounts if not math.isnan(a)][0]
(once_years, once_amount), (yearly_years, yearly_amount) = paid(once), paid(yearly)
print(f"one accrual {once_years:.0f} year pays {once_amount:.2f}")
print(f"year by year {yearly_years:.0f} years pays {yearly_amount:.2f}")one accrual 1 year pays 300000.00
year by year 5 years pays 1592740.74
| Argument | Type | Default | Meaning |
|---|---|---|---|
currency, effective, maturity |
str, date, date |
required | the leg's currency and term |
notional, fixed_rate |
float |
required | the size and the rate compounded to the payment |
day_count |
str |
required | the accrual day count, such as "1/1" |
accrual_compounding |
str |
"Compounded" |
how the rate accrues over the term |
direction |
str |
"Receive" |
"Receive" or "Pay" |
discount |
str |
"USD" |
the key the leg is discounted on |
frequency |
str or None |
None |
measure the accrual period by period, such as "1Y" |
See also
- Source:
crates/helixrs/src/instruments/rates.rs - Related pages:
- Interest rate swap, for the coupon-paying swap.
- Inflation swaps, for the zero-coupon inflation swap.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.