Esc

    The API is under testing and may change before release.

    Fixed-rate bond

    This page lists what you need to price a fixed-rate bond: the constructor and its options, the price, yield and risk calls, their defaults, and examples you can run. A fixed-rate bond pays a set coupon on a set schedule and repays its face at maturity; almost every other bond on these pages is a variation on it.

    Quick example

    The example reads its curve quotes from the desk sheets; load them once and it runs as shown.

    This takes a ten-year US Treasury-style note paying 4.25% semiannually on ACT/ACT ICMA, settling on 2 March 2026. It shows each call once, per 100 face: the accrued and the dirty price of a 98.50 clean quote, price and yield in both directions, duration, convexity and DV01, and the price off a SOFR curve instead of a yield. To quote a position of your own bond, see quote a bond from its yield.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::USD_CLOSE_MARCH;
    use helixrs::{
        CollateralSpec, Currency, DayCountFraction, FixedRateBond, Frequency, MarketContext,
        PriceBasis, QuoteSheet, YieldCurve, YieldQuoteConvention, macaulay_duration, yield_moments,
        z_spread,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    const COUPON: f64 = 0.0425;
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let settlement = date(2026, 3, 2);
        let usd = Currency::new("USD").expect("a registered currency");
        let bond = FixedRateBond::bullet(
            usd,
            date(2025, 5, 15),
            date(2035, 5, 15),
            COUPON,
            Frequency::SemiAnnual,
            DayCountFraction::ActActIcma,
            100.0,
        )?;
        let ust = YieldQuoteConvention::UsStreet;
        let ust_basis = ust.basis(bond.frequency).expect("a compounding convention");
    
        // A quote is clean. The buyer also pays the coupon the seller earned since
        // the last payment date, so the cash that changes hands is the dirty price.
        let quoted_clean = 98.50;
        let accrued = bond.accrued(settlement);
        let dirty = quoted_clean + accrued;
        println!("accrued         {accrued:.6}");
        println!("clean quote     {quoted_clean:.6}");
        println!("dirty (cash)    {dirty:.6}");
    
        // A price at a street yield, and the yield a price implies.
        let y = 0.044;
        let price_dirty = bond.price(y, settlement, PriceBasis::Dirty, ust, None);
        let implied = bond
            .yield_(price_dirty, settlement, PriceBasis::Dirty, ust, None)
            .expect("a bracketed solve");
        let quoted_yield = bond
            .yield_(quoted_clean, settlement, PriceBasis::Clean, ust, None)
            .expect("a bracketed solve");
        println!("dirty at 4.40%   {price_dirty:.6}");
        println!("yield back       {:.10}%", implied * 100.0);
        println!("98.50 clean is   {:.6}%", quoted_yield * 100.0);
    
        // Every yield-space risk number is a reading of the price and its first two
        // derivatives in yield, taken at the quoted yield.
        let moments = yield_moments(&bond, quoted_yield, settlement, ust_basis);
        let modified = moments.modified_duration();
        let macaulay = macaulay_duration(&bond, quoted_yield, settlement, ust_basis);
        let convexity = moments.convexity();
        let dv01 = moments.dv01();
        println!("modified duration   {modified:.6}");
        println!("Macaulay duration   {macaulay:.6}");
        println!("convexity           {convexity:.6}");
        println!("DV01 per 100        {dv01:.6}");
    
        // A curve prices each flow at its own discount factor instead of one yield.
        // The market is marked from the morning's swap sheet.
        let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied())?;
        let market = MarketContext::as_of(settlement)
            .with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?;
        let curve_dirty = bond.instrument().npv(&market)?;
        let curve_yield = bond
            .yield_(curve_dirty, settlement, PriceBasis::Dirty, ust, None)
            .expect("a bracketed solve");
        let spread = z_spread(&bond, &market, curve_dirty, PriceBasis::Dirty, None)?;
        println!("dirty off the curve   {curve_dirty:.6}");
        println!("clean off the curve   {:.6}", curve_dirty - accrued);
        println!("its street yield      {:.6}%", curve_yield * 100.0);
        println!("z-spread to the curve {:.6} bp", spread.abs() * 10_000.0);
    
        Ok(())
    }
    Python
    import datetime as dt
    import helixio as hlx
    from desk_sheets import USD_CLOSE_MARCH
    
    ISSUE = dt.date(2025, 5, 15)
    MATURITY = dt.date(2035, 5, 15)
    COUPON = 0.0425
    SETTLEMENT = dt.date(2026, 3, 2)
    BOND = hlx.products.fixed_rate_bond(
        "USD", ISSUE, MATURITY, coupon=COUPON, frequency=hlx.dates.Frequency.SemiAnnual,
        day_count=hlx.dates.DayCountFraction.ActActIcma,
    )
    UST = hlx.bonds.YieldConvention.UsStreet
    CLEAN = hlx.bonds.PriceBasis.Clean
    DIRTY = hlx.bonds.PriceBasis.Dirty
    
    # A quote is clean. The buyer also pays the coupon the seller earned since the
    # last payment date, so the cash that changes hands is the dirty price.
    quoted_clean = 98.50
    accrued = BOND.accrued(SETTLEMENT)
    dirty = quoted_clean + accrued
    print(f"accrued         {accrued:.6f}")
    print(f"clean quote     {quoted_clean:.6f}")
    print(f"dirty (cash)    {dirty:.6f}")
    
    # A price at a street yield, and the yield a price implies.
    y = 0.044
    price_dirty = BOND.price(y, SETTLEMENT, DIRTY, UST)
    implied = BOND.yield_(price_dirty, SETTLEMENT, DIRTY, UST)
    quoted_yield = BOND.yield_(quoted_clean, SETTLEMENT, CLEAN, UST)
    print(f"dirty at 4.40%   {price_dirty:.6f}")
    print(f"yield back       {implied * 100:.10f}%")
    print(f"98.50 clean is   {quoted_yield * 100:.6f}%")
    
    # Every yield-space risk number is a reading of the price and its first two
    # derivatives in yield, taken at the quoted yield.
    modified = BOND.modified_duration(quoted_yield, SETTLEMENT, UST)
    macaulay = BOND.macaulay_duration(quoted_yield, SETTLEMENT, UST)
    convexity = BOND.convexity(quoted_yield, SETTLEMENT, UST)
    dv01 = BOND.dv01(quoted_yield, SETTLEMENT, UST)
    print(f"modified duration   {modified:.6f}")
    print(f"Macaulay duration   {macaulay:.6f}")
    print(f"convexity           {convexity:.6f}")
    print(f"DV01 per 100        {dv01:.6f}")
    
    # A curve prices each flow at its own discount factor instead of one yield. The
    # market is marked from the morning's swap sheet.
    market = hlx.MarketContext(SETTLEMENT).with_recipe_quotes("USD", USD_CLOSE_MARCH)
    curve_dirty = BOND.instrument().npv(market)
    curve_yield = BOND.yield_(curve_dirty, SETTLEMENT, DIRTY, UST)
    spread = hlx.bonds.z_spread(BOND, market, curve_dirty, DIRTY)
    print(f"dirty off the curve   {curve_dirty:.6f}")
    print(f"clean off the curve   {curve_dirty - accrued:.6f}")
    print(f"its street yield      {curve_yield * 100:.6f}%")
    print(f"z-spread to the curve {abs(spread) * 10_000:.6f} bp")
    accrued         1.256215
    clean quote     98.500000
    dirty (cash)    99.756215
    dirty at 4.40%   100.125335
    yield back       4.4000000000%
    98.50 clean is   4.449635%
    modified duration   7.438360
    Macaulay duration   7.603850
    convexity           66.270537
    DV01 per 100        0.074202
    dirty off the curve   101.877897
    clean off the curve   100.621681
    its street yield      4.167287%
    z-spread to the curve 0.000000 bp

    The accrued is the coupon earned since 15 November, so the cash price sits above the quote. The yield that comes back from a price is the yield that went in. The curve price is higher than the quote, because the SOFR curve discounts the bond's flows at less than the 4.45% the quote implies; its z-spread to that curve is zero, since the curve itself set the price.

    How it's priced

    Each remaining flow aka_k is discounted at the yield yy, compounded mm times a year, with its time tkt_k counted in coupon periods. Off a curve each flow takes its own discount factor instead:

    Pdirty(y)=∑kak(1+ym)−m tk,Pcurve=∑kak DF(Tk),Pdirty=Pclean+AI.P_{\text{dirty}}(y) = \sum_k a_k \left(1 + \frac{y}{m}\right)^{-m\,t_k}, \qquad P_{\text{curve}} = \sum_k a_k\,DF(T_k), \qquad P_{\text{dirty}} = P_{\text{clean}} + AI.

    Duration, convexity and DV01 are readings of the first two yield derivatives of the dirty price. Why quotes are clean, why a yield needs its convention, and what each risk number means are covered in how bonds work.

    API

    Argument Type Default Meaning
    products.fixed_rate_bond(currency, issue, maturity, *, coupon, frequency, day_count, face) str, date, date, float, dates.Frequency, dates.DayCountFraction, float required; face=100.0 the bond, per 100 face; Rust FixedRateBond::bullet(ccy, issue, maturity, coupon, frequency, day_count, face)
    .with_first_coupon(date) date a regular first period an irregular first period, measured against notional regular periods
    .with_penultimate_coupon(date) date a regular last period an irregular final period
    .with_end_of_month() — off coupons on the last day of each month
    .with_ex_div(days, day_type) int, "Business" or "Calendar" no ex-dividend period a coupon is forgone after its ex-date; Rust with_ex_div(ExDivRule { days, day_type })
    .issued_by(issuer) str the bond is riskless tags the issuer, so a credit curve values it risky
    .accrued(settlement) date — accrued interest per 100 face
    .price(y, settlement, price_basis, convention) float, date, bonds.PriceBasis, convention — the price per 100 at a yield; Rust adds a trailing None
    .yield_(price, settlement, price_basis, convention) float, date, bonds.PriceBasis, convention — the yield a price implies, by a bracketed solve
    .modified_duration, .macaulay_duration, .convexity, .dv01 (y, settlement, convention) — yield-space risk; Rust reads them off yield_moments(&bond, y, s, basis)
    .flows(settlement) date — the remaining flows, with amount and time columns
    .instrument() — — the bond as an instrument, valued by npv(market) off the currency's discount curve
    bonds.z_spread(bond, market, price, price_basis) — — the spread over the curve that reprices a quote
    Symbol Meaning Rust Python
    AIAI accrued per 100 bond.accrued(s) FixedRateBond.accrued
    PcleanP_{\text{clean}}, PdirtyP_{\text{dirty}} price at a yield bond.price(y, s, PriceBasis::Clean, convention, None) FixedRateBond.price
    yy the yield a price implies bond.yield_(price, s, basis, convention, None) FixedRateBond.yield_
    mm the yield convention YieldQuoteConvention::UsStreet bonds.YieldConvention.UsStreet, or "ust"
    aka_k, tkt_k the remaining flows and times bond.timed_flows_per_100(s, None) FixedRateBond.flows(s)
    PcurveP_{\text{curve}} the bond off a curve bond.instrument().npv(&market) FixedRateBond.instrument().npv
    DF(Tk)DF(T_k) a curve's discount factor curve.view::<f64>().discount(d) curves.DiscountCurve.discount

    Defaults and limits

    • Yield conventions: "default" is the bond's own frequency, which for a semiannual bond is the street convention "ust". "annual", "semiannual", "quarterly" and "continuous" restate the same discount at another compounding. Each market's convention is in bond conventions.
    • Price basis: PriceBasis.Clean leaves the accrued out and PriceBasis.Dirty includes it. The risk calls work on the dirty price.
    • Face: prices, accrued and DV01 are per 100 face. Multiply by face / 100 for a position.
    • The solve: the price falls strictly as the yield rises, so each price has one yield.
    • Ex-dividend: after the ex-date a buyer forgoes the next coupon, and the accrued is negative.
    • Off a curve: the instrument discounts on the currency's discount curve, so the market needs it. Tagged to an issuer, it also needs that issuer's credit curve and recovery.

    More examples

    The same note, read as a yield under each convention, with a short final coupon, traded ex-dividend, and tagged to an issuer on a 2% hazard.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::USD_CLOSE_MARCH;
    use helixrs::{
        AnyInstrument, CallableBond, CollateralSpec, Currency, DayCountFraction, ExDivDayType,
        ExDivRule, FixedRateBond, FlatHazard, Frequency, MarketContext, PriceBasis, QuoteSheet,
        Survival, YieldQuoteConvention,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    const COUPON: f64 = 0.0425;
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let settlement = date(2026, 3, 2);
        let usd = Currency::new("USD").expect("a registered currency");
        let bond = FixedRateBond::bullet(
            usd,
            date(2025, 5, 15),
            date(2035, 5, 15),
            COUPON,
            Frequency::SemiAnnual,
            DayCountFraction::ActActIcma,
            100.0,
        )?;
    
        // One clean price, read as a yield under several conventions. The cash
        // flows and their times do not change; only the compounding the yield is
        // stated in.
        let names = ["default", "ust", "semiannual", "annual", "quarterly", "continuous"];
        let mut yields = Vec::new();
        for name in names {
            let convention: YieldQuoteConvention = name.parse()?;
            let value = bond
                .yield_(98.50, settlement, PriceBasis::Clean, convention, None)
                .expect("a bracketed solve");
            println!("{name:<11} {:.6}%", value * 100.0);
            yields.push(value);
        }
    
        // A note maturing on 15 February 2035 whose last regular coupon is on
        // 15 November 2034 has a short final period.
        let short_end = FixedRateBond::bullet(
            usd,
            date(2025, 5, 15),
            date(2035, 2, 15),
            COUPON,
            Frequency::SemiAnnual,
            DayCountFraction::ActActIcma,
            100.0,
        )?
        .with_penultimate_coupon(date(2034, 11, 15))?;
        let flows = short_end.timed_flows_per_100(settlement, None);
        let last_amount = flows[flows.len() - 1].1;
        println!("short final flow   {last_amount:.6}");
    
        // Traded ex-dividend seven business days before each coupon, a buyer
        // settling after the ex-date forgoes the coupon, and the accrued turns
        // negative.
        let ex_div = bond.clone().with_ex_div(ExDivRule {
            days: 7,
            day_type: ExDivDayType::Business,
        });
        let settle_late = date(2026, 5, 12);
        let negative = ex_div.accrued(settle_late);
        println!(
            "ex-dividend date   {}",
            ex_div.ex_div_date(date(2026, 5, 15)).expect("an ex-dividend rule")
        );
        println!(
            "ex-div on {settle_late}  {}",
            if ex_div.is_ex_div(settle_late) { "yes" } else { "no" }
        );
        println!("accrued ex-div     {negative:.6}");
    
        // Tagged to its issuer, the bullet is valued risky: each flow weighted by
        // the issuer's survival, with recovery of face on default. A bullet reaches
        // the risky valuation as a callable with no calls.
        let sheet = QuoteSheet::from_rows(USD_CLOSE_MARCH.iter().copied())?;
        let market = MarketContext::as_of(settlement)
            .with_recipe_quotes(&CollateralSpec::Currency(usd), &sheet)?;
        let issuer = "UST/SeniorUnSec/XR14/USD";
        let on_hazard = |hazard: f64| -> Result<MarketContext, Box<dyn std::error::Error>> {
            let curve = Survival::FlatHazard(FlatHazard::new(settlement, hazard, DayCountFraction::Act365Fixed)?);
            Ok(market
                .clone()
                .with_credit_curve(issuer.parse()?, std::sync::Arc::new(curve), Some(0.40)))
        };
        let tagged = AnyInstrument::Callable(Box::new(
            CallableBond::new(bond.clone(), Vec::new()).issued_by(issuer.parse()?),
        ));
        let curve_dirty = bond.instrument().npv(&market)?;
        let risky = tagged.npv(&on_hazard(0.02)?)?;
        let safe = tagged.npv(&on_hazard(0.0)?)?;
        println!("curve price      {curve_dirty:.6}");
        println!("risky, 2% hazard {risky:.6}");
        println!("risky, no hazard {safe:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    import helixio as hlx
    from desk_sheets import USD_CLOSE_MARCH
    
    ISSUE = dt.date(2025, 5, 15)
    MATURITY = dt.date(2035, 5, 15)
    COUPON = 0.0425
    SETTLEMENT = dt.date(2026, 3, 2)
    BOND = hlx.products.fixed_rate_bond(
        "USD", ISSUE, MATURITY, coupon=COUPON, frequency=hlx.dates.Frequency.SemiAnnual,
        day_count=hlx.dates.DayCountFraction.ActActIcma,
    )
    CLEAN = hlx.bonds.PriceBasis.Clean
    
    # One clean price, read as a yield under several conventions. The cash flows
    # and their times do not change; only the compounding the yield is stated in.
    names = ["default", "ust", "semiannual", "annual", "quarterly", "continuous"]
    yields = {name: BOND.yield_(98.50, SETTLEMENT, CLEAN, name) for name in names}
    for name in names:
        print(f"{name:<11} {yields[name] * 100:.6f}%")
    
    # A note maturing on 15 February 2035 whose last regular coupon is on
    # 15 November 2034 has a short final period.
    short_end = hlx.products.fixed_rate_bond(
        "USD", ISSUE, dt.date(2035, 2, 15), coupon=COUPON, frequency=hlx.dates.Frequency.SemiAnnual,
        day_count=hlx.dates.DayCountFraction.ActActIcma,
    ).with_penultimate_coupon(dt.date(2034, 11, 15))
    last_amount = short_end.flows(SETTLEMENT).to_dict()["amount"][-1]
    print(f"short final flow   {last_amount:.6f}")
    
    # Traded ex-dividend seven business days before each coupon, a buyer settling
    # after the ex-date forgoes the coupon, and the accrued turns negative.
    ex_div = BOND.with_ex_div(7, "Business")
    settle_late = dt.date(2026, 5, 12)
    negative = ex_div.accrued(settle_late)
    print(f"ex-dividend date   {ex_div.ex_div_date(dt.date(2026, 5, 15))}")
    print(f"ex-div on {settle_late}  {'yes' if ex_div.is_ex_div(settle_late) else 'no'}")
    print(f"accrued ex-div     {negative:.6f}")
    
    # Tagged to its issuer, the bullet is valued risky: each flow weighted by the
    # issuer's survival, with recovery of face on default.
    market = hlx.MarketContext(SETTLEMENT).with_recipe_quotes("USD", USD_CLOSE_MARCH)
    ISSUER = "UST/SeniorUnSec/XR14/USD"
    
    def on_hazard(hazard):
        return market.with_credit_curve(ISSUER, hlx.curves.SurvivalCurve.flat_hazard(SETTLEMENT, hazard), 0.40)
    
    curve_dirty = BOND.instrument().npv(market)
    risky = BOND.issued_by(ISSUER).npv(on_hazard(0.02))
    safe = BOND.issued_by(ISSUER).npv(on_hazard(0.0))
    print(f"curve price      {curve_dirty:.6f}")
    print(f"risky, 2% hazard {risky:.6f}")
    print(f"risky, no hazard {safe:.6f}")
    default     4.449635%
    ust         4.449635%
    semiannual  4.449635%
    annual      4.499133%
    quarterly   4.425158%
    continuous  4.400859%
    short final flow   101.080110
    ex-dividend date   2026-05-06
    ex-div on 2026-05-12  yes
    accrued ex-div     -0.035221
    curve price      101.877897
    risky, 2% hazard 93.270698
    risky, no hazard 101.877897

    Compounding less often needs a higher rate for the same discount, so the annual yield is the highest and the continuous one the lowest. The short final flow pays the days it runs out of a notional six-month period. On a 2% hazard the risky price is lower; with no hazard it is the curve price.

    See also

    • Source: crates/helixrs/src/instruments/bonds.rs, and crates/helixrs/src/statics/calc_methods.rs for the yield conventions.
    • Related pages:
      • How bonds work, for the ideas behind clean, dirty, yield and risk, and the flows worked by hand.
      • Quote a bond from its yield, step by step.
      • Spread measures, for the spreads between a price and a curve.
      • Day counts, for ACT/ACT ICMA and its siblings.
      • Bond conventions, for each market's day count, frequency and yield convention.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.