Esc

    The API is under testing and may change before release.

    Floating-rate note

    This page lists what you need to price a floating-rate note: the constructor, the coupon table it reports, its value off two curves, the discount and simple margins it trades on, its spread duration, and an example you can run. A floating-rate note pays a term index plus a fixed margin each period, and repays its face at maturity.

    Quick example

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

    This books five years of quarterly coupons on EURIBOR 3M plus 50 basis points, issued on 17 December 2025 and valued on 2 March 2026, so its first coupon's index has already fixed, at 2.05%, for a 2.55% coupon. It reads that coupon and the accrued, values the note on a EUR close, finds the margins that reprice a 99.20 clean quote, and reads the spread duration.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::EUR_CLOSE_MARCH;
    use helixrs::{
        AnyIndex, AnyInstrument, CollateralSpec, Column, Currency, Curve, DayCountFraction, FixingKey,
        FixingStore, FlatSpread, FloatingRateNote, IborIndex, MarketContext, PricingRiskFactors,
        QuoteSheet, Records, YieldCurve, discount_margin, index, instrument_cashflow_records,
        spread_duration,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    const INDEX: &str = "EUR-EURIBOR-3M";
    const MARGIN: f64 = 0.0050;
    const FIXING: f64 = 0.0205;
    
    fn euribor() -> IborIndex {
        match index(INDEX) {
            Some(AnyIndex::Ibor(index)) => index,
            _ => panic!("a registered term index"),
        }
    }
    
    fn note_with(security_spread: Option<&str>) -> AnyInstrument {
        let mut note = FloatingRateNote::new(
            &euribor(),
            date(2025, 12, 17),
            date(2030, 12, 17),
            MARGIN,
            100.0,
        );
        if let Some(key) = security_spread {
            note = note.spread_by(key);
        }
        AnyInstrument::FloatingRateNote(Box::new(note))
    }
    
    fn npv(note: &AnyInstrument, market: &MarketContext) -> f64 {
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted).expect("a market that binds");
        view.npv(note).expect("a priced note")
    }
    
    fn numbers(records: &Records, name: &str) -> Vec<f64> {
        match records.iter().find(|(n, _)| *n == name) {
            Some((_, Column::Number(values))) => values.clone(),
            _ => panic!("a number column"),
        }
    }
    
    fn dates(records: &Records, name: &str) -> Vec<Option<NaiveDate>> {
        match records.iter().find(|(n, _)| *n == name) {
            Some((_, Column::Date(values))) => values.clone(),
            _ => panic!("a date column"),
        }
    }
    
    fn texts(records: &Records, name: &str) -> Vec<String> {
        match records.iter().find(|(n, _)| *n == name) {
            Some((_, Column::Text(values))) => values.clone(),
            _ => panic!("a text column"),
        }
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let valuation = date(2026, 3, 2);
        let maturity = date(2030, 12, 17);
        let eur = Currency::new("EUR").expect("a registered currency");
        let fixings =
            FixingStore::from_observations_checked(&[(FixingKey::from(INDEX), date(2025, 12, 15), FIXING)])?;
        let sheet = QuoteSheet::from_rows(EUR_CLOSE_MARCH.iter().copied())?;
        let market = MarketContext::as_of(valuation)
            .with_fixings(fixings)
            .with_recipe_quotes(&CollateralSpec::Currency(eur), &sheet)?;
        let note = note_with(None);
        let lifted = market.lift::<f64>();
        let view = market.view(&lifted)?;
        let AnyInstrument::FloatingRateNote(frn) = &note else {
            unreachable!("built as a note")
        };
        let resolved = view.resolve(&frn.risk_factors().expect("a note declares its curves"))?;
    
        // The running coupon fixed two business days before it started. Its rate
        // is the published fixing plus the margin, whatever the curves now say.
        let rows = instrument_cashflow_records(&note, &resolved)?;
        let (rate, amount, pv) = (numbers(&rows, "rate"), numbers(&rows, "amount"), numbers(&rows, "pv"));
        let accrual_start = dates(&rows, "accrual_start");
        let fixed_rate = rate[0];
        let start = accrual_start[0].expect("a coupon accrues");
        let accrued = fixed_rate * DayCountFraction::Act360.year_fraction(start, valuation) * 100.0;
        println!(
            "period        {} to {}",
            start,
            dates(&rows, "accrual_end")[0].expect("a coupon accrues")
        );
        println!("fixed on      {}", dates(&rows, "fixing_date")[0].expect("a fixed coupon"));
        println!("coupon rate   {:.4}%", fixed_rate * 100.0);
        println!("coupon        {:.6}", amount[0]);
        println!("accrued now   {accrued:.6}");
    
        // Every later coupon projects the index's forward over its own window, adds
        // the margin, and is discounted on the other curve.
        let value = npv(&note, &market);
        for k in 1..4 {
            println!(
                "{}  rate {:.6}%  pv {:.6}",
                accrual_start[k].expect("a coupon accrues"),
                rate[k] * 100.0,
                pv[k]
            );
        }
        println!("coupons       {}, then the face", amount.len() - 1);
        println!("value         {value:.6}");
    
        // The note trades at 99.20 clean. The discount margin is the flat spread on
        // the discount curve that reprices that quote; the simple margin skips the
        // curves and spreads the pull to par over the remaining years.
        let quoted_clean = 99.20;
        let dm = discount_margin(&note, &market, quoted_clean)?;
        let sm = frn.simple_margin(quoted_clean, valuation);
        println!("quoted clean     {quoted_clean:.2}");
        println!("discount margin  {:.4} bp", dm * 10_000.0);
        println!("simple margin    {:.4} bp", sm * 10_000.0);
    
        // A floater's rate risk resets every quarter; its spread risk runs to
        // maturity.
        let sd = spread_duration(&note, &market)?;
        println!("spread duration     {sd:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    import helixio as hlx
    from desk_sheets import EUR_CLOSE_MARCH
    
    VALUATION = dt.date(2026, 3, 2)
    INDEX = "EUR-EURIBOR-3M"
    MARGIN = 0.0050
    MATURITY = dt.date(2030, 12, 17)
    FIXING = 0.0205
    ACT360 = hlx.dates.DayCountFraction.Act360
    
    # A EUR close on 2 March 2026: €STR swaps discount, and EURIBOR 3M swaps project.
    
    def note_with(security_spread=None):
        return hlx.products.floating_rate_note(
            INDEX, dt.date(2025, 12, 17), MATURITY, margin=MARGIN, face=100.0,
            security_spread=security_spread,
        )
    
    MARKET = (
        hlx.MarketContext(VALUATION)
        .with_fixings([(INDEX, dt.date(2025, 12, 15), FIXING)])
        .with_recipe_quotes("EUR", EUR_CLOSE_MARCH)
    )
    NOTE = note_with()
    
    # The running coupon fixed two business days before it started. Its rate is
    # the published fixing plus the margin, whatever the curves now say.
    rows = NOTE.cashflow_records(MARKET).to_dict()
    fixed_rate = rows["rate"][0]
    accrued = fixed_rate * ACT360.year_fraction(rows["accrual_start"][0], VALUATION) * 100.0
    print(f"period        {rows['accrual_start'][0]} to {rows['accrual_end'][0]}")
    print(f"fixed on      {rows['fixing_date'][0]}")
    print(f"coupon rate   {fixed_rate * 100:.4f}%")
    print(f"coupon        {rows['amount'][0]:.6f}")
    print(f"accrued now   {accrued:.6f}")
    
    # Every later coupon projects the index's forward over its own window, adds
    # the margin, and is discounted on the other curve.
    value = NOTE.npv(MARKET)
    for k in range(1, 4):
        print(f"{rows['accrual_start'][k]}  rate {rows['rate'][k] * 100:.6f}%  pv {rows['pv'][k]:.6f}")
    print(f"coupons       {len(rows['kind']) - 1}, then the face")
    print(f"value         {value:.6f}")
    
    # The note trades at 99.20 clean. The discount margin is the flat spread on
    # the discount curve that reprices that quote; the simple margin skips the
    # curves and spreads the pull to par over the remaining years.
    quoted_clean = 99.20
    dm = hlx.bonds.discount_margin(NOTE, MARKET, quoted_clean)
    sm = hlx.bonds.simple_margin(NOTE, quoted_clean, VALUATION)
    print(f"quoted clean     {quoted_clean:.2f}")
    print(f"discount margin  {dm * 10_000:.4f} bp")
    print(f"simple margin    {sm * 10_000:.4f} bp")
    
    # A floater's rate risk resets every quarter; its spread risk runs to maturity.
    sd = hlx.bonds.spread_duration(NOTE, MARKET)
    print(f"spread duration     {sd:.6f}")
    period        2025-12-17 to 2026-03-17
    fixed on      2025-12-15
    coupon rate   2.5500%
    coupon        0.637500
    accrued now   0.531250
    2026-03-17  rate 2.798724%  pv 0.710619
    2026-06-17  rate 2.798724%  pv 0.706679
    2026-09-17  rate 2.798651%  pv 0.695145
    coupons       20, then the face
    value         103.332291
    quoted clean     99.20
    discount margin  79.3512 bp
    simple margin    66.4477 bp
    spread duration     4.473866

    The running coupon pays the published fixing plus the margin; every later coupon projects the EURIBOR forward instead. The note pays 50 basis points over an index that projects above the €STR discount curve, so with its accrued it's worth more than par. A 99.20 quote sits below that value, so it needs a positive discount margin to reach it. The spread duration is close to the note's remaining life.

    How it's priced

    Each coupon pays the index rate LiL_i plus the margin mm over its accrual fraction; the value is every flow at its discount factor; and the discount margin dmdm is the flat spread that reprices a clean quote PP, the projected coupons held fixed:

    ci=100 τi (Li+m),V=∑ici DF(Ti)+100 DF(Tn),P+AI=∑iai DF(Ti) e−dm ti.c_i = 100\,\tau_i\,(L_i + m), \qquad V = \sum_i c_i\,DF(T_i) + 100\,DF(T_n), \qquad P + AI = \sum_i a_i\,DF(T_i)\,e^{-dm\,t_i}.

    The simple margin is 100−P100 n+m\frac{100 - P}{100\,n} + m over the nn years left, and spread duration is −1V∂V∂z-\frac{1}{V}\frac{\partial V}{\partial z} for a flat spread zz. Why a floater's rate risk resets while its spread risk doesn't is covered in how bonds work.

    API

    Argument Type Default Meaning
    index str required the term index the coupons fix on, such as "EUR-EURIBOR-3M"
    issue, maturity date required the first accrual start and the redemption
    margin float required the quoted margin over the index
    face float required the face per note
    security_spread str None a key whose spread curve the note discounts under, for bumping or marking a spread
    issuer str None tags the issuer, so a credit curve values the note risky
    gearing float None, a gearing of 1 a multiplier on the index rate
    cap, floor float None bounds on each coupon's rate
    outstanding list[float] None, a bullet the fraction of face outstanding in each period, starting at 1 and never rising
    day_count, accrual_calendar, business_day_convention str the index's the note's own accrual terms where they differ from the index's; fixing dates stay the index's
    projection str "index_tenor" the window each coupon forwards over: "index_tenor" or "accrual_period"

    products.floating_rate_note(index, issue, maturity, *, margin, face, ...) builds the note; in Rust, FloatingRateNote::new(&index, issue, maturity, margin, face), with .spread_by(key) for a security spread.

    Symbol Meaning Rust Python
    LiL_i the period's index rate IborIndex::forecast(&curve, start), or the stored fixing Index.forecast(curve, start), or MarketContext.with_fixings
    cic_i, τi\tau_i each flow and its accrual instrument_cashflow_records(&note, &resolved) Instrument.cashflow_records(market)
    VV value against the market MarketView::npv on AnyInstrument::FloatingRateNote Instrument.npv
    dmdm discount margin discount_margin(&note, &market, clean) bonds.discount_margin(note, market, clean)
    smsm simple margin FloatingRateNote::simple_margin(clean, settlement) bonds.simple_margin(note, clean, settlement)
    DsD_s spread duration spread_duration(&note, &market) bonds.spread_duration(note, market)
    zz a flat security spread MarketContext::with_security_spread(key, curve) MarketContext.with_security_spread(key, curve)

    Defaults and limits

    • Market inputs: the currency's discount curve, the index's projection curve, and the fixing of any coupon whose fixing date is on or before valuation. fixings_needed(valuation) lists those fixings; a market without one refuses the valuation and names it.
    • The running coupon pays the stored fixing plus the margin, whatever the curves now say.
    • The accrued isn't returned by the note: it's the running rate times the year fraction since the period started, on the index's day count.
    • Units: per 100 face, like the quote.

    See also

    • Source: crates/helixrs/src/instruments/bonds.rs
    • Related pages:
      • How bonds work, for discount margin, simple margin and spread duration.
      • Cashflow records, for every column of the coupon table.
      • Fixings: prints and projections, for how a stored fixing meets a projected rate.
      • Spread measures, for spreads on fixed-rate bonds.

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