Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 N((1+K)τ−1)N\left((1 + K)^{\tau} - 1\right) at maturity TT, so with the floating leg worth FF per unit notional the payer is worth

    Vpayer=N(F−((1+K)τ−1)P(T)).V_{\text{payer}} = N\left(F - \left((1 + K)^{\tau} - 1\right) P(T)\right).

    The par rate solves V=0V = 0: K∗=(1+F/P(T))1/τ−1K^\ast = \left(1 + F/P(T)\right)^{1/\tau} - 1. The value falls as KK 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
    K∗K^\ast the par rate AnyInstrument::par_rate Instrument.par_rate
    VV its value AnyInstrument::npv Instrument.npv

    Defaults and limits

    • Side: the swap is booked as the payer of fixed. Pass receiver=True in 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 N((1+K)5−1)N\left((1 + K)^5 - 1\right). The payment is read off the leg's cashflow table.

    Rust
    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(())
    }
    Python
    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.