Esc

    The API is under testing and may change before release.

    Indices

    This page lists everything you need to work with the rates floating coupons read: resolving an index from the registry, its conventions, building one the registry doesn't carry, dating a term fixing, projecting a rate, and the rule every product applies when it decides to read a print or project one.

    Quick example

    This resolves five registered indices, prints the conventions each carries, and shows that an overnight index refuses to invent a tenor.

    Rust
    use helixrs::{AnyIndex, DayCountFraction, index};
    
    fn code_of(index: &AnyIndex) -> String {
        match index {
            AnyIndex::Overnight(i) => i.code(),
            AnyIndex::Ibor(i) => i.code(),
            AnyIndex::Price(i) => i.code(),
            other => other.name().to_owned(),
        }
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        // A registry code resolves to an index carrying its conventions. Matching
        // ignores case; the code written back is the registered one.
        println!("{:<15} {:<10} {:<14} {:<16} {:>3}", "code", "kind", "day count", "fixing calendar", "lag");
        for spelling in ["usd-sofr", "EUR-ESTR", "GBP-SONIA", "EUR-EURIBOR-3M", "EUR-EURIBOR-6M"] {
            let index = index(spelling).expect("a registered index");
            let (day_count, calendar_name, lag) = match &index {
                AnyIndex::Overnight(i) => (i.day_count, i.fixing_calendar.name().to_owned(), i.publication_lag_days),
                AnyIndex::Ibor(i) => (i.day_count, i.fixing_calendar.name().to_owned(), i.fixing_lag_days),
                _ => unreachable!("rate indices only"),
            };
            println!(
                "{:<15} {:<10} {:<14} {calendar_name:<16} {lag:>3}",
                code_of(&index),
                index.kind(),
                day_count.code()
            );
        }
    
        // A term index has a tenor; an overnight index has no tenor field at all.
        let Some(AnyIndex::Ibor(euribor)) = index("EUR-EURIBOR-3M") else {
            panic!("a term index")
        };
        let refused = if matches!(index("USD-SOFR"), Some(AnyIndex::Overnight(_))) { "refused" } else { "" };
        println!("tenor of USD-SOFR: {refused}");
    
        Ok(())
    }
    Python
    import helixio as hlx
    
    # A registry code resolves to an index carrying its conventions. Matching
    # ignores case; the code written back is the registered one.
    print(f"{'code':<15} {'kind':<10} {'day count':<14} {'fixing calendar':<16} {'lag':>3}")
    for spelling in ("usd-sofr", "EUR-ESTR", "GBP-SONIA", "EUR-EURIBOR-3M", "EUR-EURIBOR-6M"):
        index = hlx.Index(spelling)
        print(
            f"{index.code():<15} {index.kind():<10} {index.day_count():<14} "
            f"{index.fixing_calendar().name():<16} {index.fixing_lag_days():>3}"
        )
    
    # A term index has a tenor; an overnight index refuses to invent one.
    refused = ""
    try:
        hlx.Index("USD-SOFR").tenor()
    except ValueError:
        refused = "refused"
    print(f"tenor of USD-SOFR: {refused}")
    code            kind       day count      fixing calendar  lag
    USD-SOFR        Overnight  ACT/360        USNY.SOFR          1
    EUR-ESTR        Overnight  ACT/360        EUTA               1
    GBP-SONIA       Overnight  ACT/365.FIXED  GBLO               0
    EUR-EURIBOR-3M  Ibor       ACT/360        EUTA               2
    EUR-EURIBOR-6M  Ibor       ACT/360        EUTA               2
    tenor of USD-SOFR: refused

    Matching ignores case, and the code written back is the registered one. Every registered index, with its code and conventions, is listed in the rates conventions catalog.

    How it's priced

    A term index of tenor pp applied to an accrual starting ss fixes at

    tf=s−L business days,[v,e]=[s, adj⁡(s+p)],t_f = s - L \text{ business days}, \qquad [v, e] = [s,\ \operatorname{adj}(s + p)],

    with LL the index's fixing lag on its calendar and ee the value date advanced by the tenor under the index's convention and end-of-month rule. Off a discount curve DD, a term index projects simple interest over its own window on its own day count:

    F(v,e)=1τ(v,e)(D(v)D(e)−1),F(v, e) = \frac{1}{\tau(v, e)}\left(\frac{D(v)}{D(e)} - 1\right),

    which is not the curve's continuously compounded forward over the same dates. When indices publish, and why a desk at a close holds every print before it, is covered in how dates and conventions work.

    API

    Symbol Meaning Rust Python
    index resolved by code index(code) Index
    tft_f the fixing date IborIndex::fixing_date Index.fixing_date
    ss, pp, LL the accrual start, the tenor, the fixing lag the date you pass, IborIndex::tenor, IborIndex::fixing_lag_days the date you pass, Index.tenor, Index.fixing_lag_days
    vv, ee the window's value date and end IborIndex::maturity_date Index.maturity_date
    τ\tau the index's day count IborIndex::day_count Index.day_count
    DD the discount curve projected off DiscountCurve curves.DiscountCurve
    F(v,e)F(v, e) the projected rate AnyIndex::forecast Index.forecast
    prints what the index published FixingStore, MarketContext::with_fixings MarketContext.with_fixings, MarketContext.fixings
    resets needed every reset before the valuation date AnyInstrument::fixings_needed Instrument.fixings_needed
    Argument Type Default Meaning
    Index(code) str required a registry code or display name, such as "USD-SOFR" or "EUR-EURIBOR-3M"; case is ignored
    Index.codes() none none every registered index as (kind, name, code)
    Index.ibor(name, currency, tenor, day_count, fixing_calendar, fixing_lag_days, ...) str, str, str, day count, Calendar, int required a term index the registry doesn't carry
    Index.ibor(..., business_day_convention=, end_of_month=) convention or None, bool None, True the window's roll and end-of-month rule
    Index.overnight(name, currency, day_count, fixing_calendar, ...) str, str, day count, Calendar required an overnight index the registry doesn't carry
    Index.overnight(..., publication_lag_days=, business_day_convention=, end_of_month=) int, convention or None, bool 0, None, False when a print is published, and the roll
    fixing_date(accrual_start) date required the term fixing for an accrual starting on that date
    maturity_date(value_date) date required the end of the index's window
    forecast(curve, value_date) DiscountCurve, date required the projected rate over the index's window

    Defaults and limits

    • Building registers nothing. An index built with Index.ibor or Index.overnight is a value you pass around; its code still doesn't resolve, and an unknown code is refused with the list of registered ones.
    • Accessors a kind doesn't carry raise. An overnight index has no tenor and a price index has none either; asking raises ValueError naming the kind rather than returning None.
    • The fixing rule. For a valuation at tt, a reset on tft_f is read if tf<tt_f < t and must have been published; read if tf=tt_f = t and a print exists, projected otherwise; and projected if tf>tt_f > t.
    • A print is known from the date the market learnt it. A print dated after the market's date isn't read until the market moves past it.
    • A typed code the registry doesn't carry is refused when it is published into a market.

    !!! warning "A seasoned trade needs its past prints" Valuing a trade whose resets fell before the valuation date raises MissingMarketInputError, naming the first missing date. fixings_needed lists every reset the valuation will read.

    More examples

    Every case above in one program: the registry, a term index's window, your own index, a forecast, prints, and the fixing rule on a seasoned swap. The example reads its curve quotes and prints from the desk sheets.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS};
    use helixrs::{
        AnyIndex, BusinessDayConvention, CollateralSpec, Currency, Curve, DayCountFraction, FixingKey,
        FixingStore, IborIndex, MarketContext, Period, PiecewiseDiscount, QuoteSheet,
        Termination, YieldCurve, calendar, codes, index, template,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn ymd((year, month, day): (i32, u32, u32)) -> NaiveDate {
        date(year, month, day)
    }
    
    fn code_of(index: &AnyIndex) -> String {
        match index {
            AnyIndex::Overnight(i) => i.code(),
            AnyIndex::Ibor(i) => i.code(),
            AnyIndex::Price(i) => i.code(),
            other => other.name().to_owned(),
        }
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let valuation = date(2026, 1, 2);
        let curve = Curve::Piecewise(
            PiecewiseDiscount::from_discount_factors(
                valuation,
                &[(date(2026, 7, 2), 0.9802), (date(2027, 1, 4), 0.9608), (date(2028, 1, 3), 0.9231)],
            )
            .expect("well-formed pillars"),
        );
        let view = curve.view::<f64>();
    
        // A term index has a tenor; an overnight index has no tenor field at all
        // (the quick example shows both).
        let Some(AnyIndex::Ibor(euribor)) = index("EUR-EURIBOR-3M") else {
            panic!("a term index")
        };
        let refused = if matches!(index("USD-SOFR"), Some(AnyIndex::Overnight(_))) { "refused" } else { "" };
    
        // The registry lists every index it carries as (kind, accessor, code), and
        // every listed code resolves back to itself.
        let listed = codes();
        let mut counts: std::collections::BTreeMap<&str, i32> = std::collections::BTreeMap::new();
        for (kind, _accessor, _code) in &listed {
            *counts.entry(kind).or_insert(0) += 1;
        }
        let total = listed.len() as i32;
        for (kind, count) in &counts {
            println!("{kind:<10} {count:>3}");
        }
        println!("{:<10} {total:>3}", "total");
    
        // A term rate for an accrual starting on 18 March fixes two TARGET business
        // days earlier, and quotes over the index's own window: the value date
        // advanced by the tenor under the index's convention and end-of-month rule.
        let accrual_start = date(2026, 3, 18);
        let fixing = euribor.fixing_date(accrual_start);
        let window_end = euribor.maturity_date(accrual_start);
        println!("accrual start  {accrual_start}");
        println!("fixing date    {fixing} ({})", fixing.format("%a"));
        println!("window end     {window_end}");
    
        // An index the registry does not carry is built from its conventions.
        // Building one registers nothing: the code it would carry still does not
        // resolve.
        let new_york = calendar("USNY").expect("a registered business centre");
        let three_months: Period = "3M".parse().expect("a tenor");
        let desk = IborIndex::new(
            "DESKIBOR",
            "USD",
            three_months,
            DayCountFraction::Act360,
            new_york.clone(),
            2,
            BusinessDayConvention::ModifiedFollowing,
            true,
        );
        let unresolved = if index(&desk.code()).is_none() { "not registered" } else { "" };
        let desk_fixing = desk.fixing_date(accrual_start);
        println!("built {}: {unresolved}", desk.code());
        println!("fixes for {accrual_start} on {desk_fixing}");
    
        // An index projects its rate off a discount curve: simple interest over its
        // own window, on its own day count. That is not the curve's continuously
        // compounded forward over the same dates.
        let Some(sofr) = index("USD-SOFR") else { panic!("a registered index") };
        let value_date = date(2026, 3, 18);
        let term = desk.forecast(&view, value_date).expect("a curve that covers the window");
        let window = desk.maturity_date(value_date);
        let overnight = sofr.forecast(&view, value_date).expect("a rate index");
        let continuous = view.forward_rate(value_date, window).expect("inside the curve");
        println!("DESKIBOR 3M forecast for {value_date}  {term:.8}");
        println!("SOFR overnight forecast              {overnight:.8}");
        println!("curve forward, continuous, same dates {continuous:.8}");
    
        // A market holds what an index printed. Reading a date the store holds
        // returns the print; `try_fixing` answers None for a date nothing was
        // published on.
        let prints = [
            ("USD-SOFR", date(2025, 12, 30), 0.0431),
            ("USD-SOFR", date(2025, 12, 31), 0.0433),
        ];
        let observations: Vec<(FixingKey, NaiveDate, f64)> =
            prints.iter().map(|(code, day, rate)| (FixingKey::from(*code), *day, *rate)).collect();
        let store = FixingStore::from_observations_checked(&observations).expect("registered keys");
        let market = MarketContext::as_of(valuation).with_fixings(store);
        let read = market.fixings().view::<f64>();
        for ((code, day), rate) in market.fixings().parameter_keys().iter().zip(market.fixings().parameters()) {
            println!("{code:<10} {day}  {rate:.4}");
        }
    
        // A typed code the registry does not carry is refused when it is published,
        // and so is one date printed twice at two different rates.
        let typo = [(FixingKey::from("USD-S0FR"), date(2025, 12, 30), 0.0431)];
        let twice = [
            (FixingKey::from("USD-SOFR"), date(2025, 12, 30), 0.0431),
            (FixingKey::from("USD-SOFR"), date(2025, 12, 30), 0.0435),
        ];
        let refusals = [&typo[..], &twice[..]]
            .iter()
            .filter(|bad| FixingStore::from_observations_checked(bad).is_err())
            .count();
        println!("refused publications: {refusals}");
    
        // A swap traded on 24 September 2026 has accrued since spot, 28 September. At
        // the 1 October close its valuation needs every reset strictly before the
        // close; valuing without them fails, naming the first missing date.
        let close = ymd(SOFR_CLOSE_DATE);
        let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied()).expect("a sheet locator");
        let close_market = MarketContext::as_of(close)
            .with_recipe_quotes(&CollateralSpec::Currency(Currency::new("USD").expect("a currency")), &sheet)
            .expect("the USD recipe");
        let seasoned = template("USD-SOFR-OIS")
            .expect("a registered template")
            .swap(date(2026, 9, 24))
            .expect("a trade date")
            .with_maturity("5Y".parse::<Termination>().expect("a tenor"))
            .expect("a maturity")
            .with_notional(1_000_000.0)
            .with_fixed_rate(0.0466)
            .build()
            .expect("a complete trade");
        let needed = seasoned.fixings_needed(close);
        let published: Vec<(String, NaiveDate)> =
            USD_SOFR_PRINTS.iter().map(|(code, day, _)| ((*code).to_owned(), ymd(*day))).collect();
        let refusal = seasoned.npv(&close_market).expect_err("a seasoned trade needs its prints").to_string();
        let missing =
            format!("refused, naming 2026-09-28: {}", if refusal.contains("2026-09-28") { "yes" } else { "no" });
    
        // The reset on the close itself is not needed: its print is published the next
        // morning, so it is projected. Once a print for it exists, it is read instead.
        let pinned = |extra: Option<f64>| {
            let mut all: Vec<(FixingKey, NaiveDate, f64)> = USD_SOFR_PRINTS
                .iter()
                .map(|(code, day, rate)| (FixingKey::from(*code), ymd(*day), *rate))
                .collect();
            if let Some(rate) = extra {
                all.push((FixingKey::from("USD-SOFR"), close, rate));
            }
            close_market.clone().with_fixings(FixingStore::from_observations_checked(&all).expect("registered keys"))
        };
        let projected = seasoned.npv(&pinned(None)).expect("every past reset published");
        let printed = seasoned.npv(&pinned(Some(0.0500))).expect("every past reset published");
        println!("resets needed on {close}: {}, the last on {}", needed.len(), needed[needed.len() - 1].1);
        println!("value without them             {missing}");
        println!("value, today's reset projected {projected:.6}");
        println!("value, today's reset printed   {printed:.6}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS
    
    VALUATION = dt.date(2026, 1, 2)
    CURVE = hlx.curves.DiscountCurve(
        VALUATION,
        [(dt.date(2026, 7, 2), 0.9802), (dt.date(2027, 1, 4), 0.9608), (dt.date(2028, 1, 3), 0.9231)],
    )
    
    # The registry lists every index it carries as (kind, name, code), and every
    # listed code resolves back to itself.
    codes = hlx.Index.codes()
    counts = {}
    for kind, _, _ in codes:
        counts[kind] = counts.get(kind, 0) + 1
    total = len(codes)
    for kind in sorted(counts):
        print(f"{kind:<10} {counts[kind]:>3}")
    print(f"{'total':<10} {total:>3}")
    
    # A term rate for an accrual starting on 18 March fixes two TARGET business
    # days earlier, and quotes over the index's own window: the value date
    # advanced by the tenor under the index's convention and end-of-month rule.
    EURIBOR = hlx.Index("EUR-EURIBOR-3M")
    ACCRUAL_START = dt.date(2026, 3, 18)
    fixing = EURIBOR.fixing_date(ACCRUAL_START)
    window_end = EURIBOR.maturity_date(ACCRUAL_START)
    print(f"accrual start  {ACCRUAL_START}")
    print(f"fixing date    {fixing} ({fixing.strftime('%a')})")
    print(f"window end     {window_end}")
    
    # An index the registry does not carry is built from its conventions. Building
    # one registers nothing: the code it would carry still does not resolve.
    desk = hlx.Index.ibor(
        "DESKIBOR", "USD", "3M", "ACT/360", hlx.Calendar("USNY"), 2, "ModFollowing", True
    )
    unresolved = ""
    try:
        hlx.Index(desk.code())
    except ValueError:
        unresolved = "not registered"
    desk_fixing = desk.fixing_date(ACCRUAL_START)
    print(f"built {desk.code()}: {unresolved}")
    print(f"fixes for {ACCRUAL_START} on {desk_fixing}")
    
    # An index projects its rate off a discount curve: simple interest over its
    # own window, on its own day count. That is not the curve's continuously
    # compounded forward over the same dates.
    SOFR = hlx.Index("USD-SOFR")
    VALUE_DATE = dt.date(2026, 3, 18)
    term = desk.forecast(CURVE, VALUE_DATE)
    window = desk.maturity_date(VALUE_DATE)
    overnight = SOFR.forecast(CURVE, VALUE_DATE)
    continuous = CURVE.forward_rate(VALUE_DATE, window)
    print(f"DESKIBOR 3M forecast for {VALUE_DATE}  {term:.8f}")
    print(f"SOFR overnight forecast              {overnight:.8f}")
    print(f"curve forward, continuous, same dates {continuous:.8f}")
    
    # A market holds what an index printed. Reading a date the store holds returns
    # the print; `try_fixing` answers None for a date nothing was published on.
    PRINTS = [
        ("USD-SOFR", dt.date(2025, 12, 30), 0.0431),
        ("USD-SOFR", dt.date(2025, 12, 31), 0.0433),
    ]
    market = hlx.MarketContext(VALUATION).with_fixings(PRINTS)
    table = market.fixings()
    for code, day, rate in table.to_rows():
        print(f"{code:<10} {day}  {rate:.4f}")
    
    # A typed code the registry does not carry is refused when it is published,
    # and so is one date printed twice at two different rates.
    refusals = 0
    for bad in (
        [("USD-S0FR", dt.date(2025, 12, 30), 0.0431)],
        [("USD-SOFR", dt.date(2025, 12, 30), 0.0431), ("USD-SOFR", dt.date(2025, 12, 30), 0.0435)],
    ):
        try:
            hlx.MarketContext(VALUATION).with_fixings(bad)
        except hlx.MarketDataError:
            refusals += 1
    print(f"refused publications: {refusals}")
    
    # A swap traded on 24 September 2026 has accrued since spot, 28 September. At
    # the 1 October close its valuation needs every reset strictly before the
    # close; valuing without them fails, naming the first missing date.
    close_market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE)
    seasoned = (
        hlx.templates.get("USD-SOFR-OIS")
        .swap(dt.date(2026, 9, 24))
        .with_maturity("5Y")
        .with_notional(1_000_000.0)
        .with_fixed_rate(0.0466)
        .build()
    )
    needed = seasoned.fixings_needed(SOFR_CLOSE_DATE)
    missing = ""
    try:
        seasoned.npv(close_market)
    except hlx.MissingMarketInputError as error:
        missing = f"refused, naming 2026-09-28: {'yes' if '2026-09-28' in str(error) else 'no'}"
    
    # The reset on the close itself is not needed: its print is published the next
    # morning, so it is projected. Once a print for it exists, it is read instead.
    projected = seasoned.npv(close_market.with_fixings(USD_SOFR_PRINTS))
    printed = seasoned.npv(close_market.with_fixings([*USD_SOFR_PRINTS, ("USD-SOFR", SOFR_CLOSE_DATE, 0.0500)]))
    print(f"resets needed on {SOFR_CLOSE_DATE}: {len(needed)}, the last on {needed[-1][1]}")
    print(f"value without them             {missing}")
    print(f"value, today's reset projected {projected:.6f}")
    print(f"value, today's reset printed   {printed:.6f}")
    Ibor        26
    Overnight   26
    Price        4
    total       56
    accrual start  2026-03-18
    fixing date    2026-03-16 (Mon)
    window end     2026-06-18
    built USD-DESKIBOR-3M: not registered
    fixes for 2026-03-18 on 2026-03-16
    DESKIBOR 3M forecast for 2026-03-18  0.03997916
    SOFR overnight forecast              0.03977851
    curve forward, continuous, same dates 0.04032876
    USD-SOFR   2025-12-30  0.0431
    USD-SOFR   2025-12-31  0.0433
    refused publications: 2
    resets needed on 2026-10-01: 3, the last on 2026-09-30
    value without them             refused, naming 2026-09-28: yes
    value, today's reset projected -2063.204005
    value, today's reset printed   -2093.657477

    See also

    • Source: crates/helixrs/src/statics/indices/registry.rs, crates/helixrs/src/statics/indices/term.rs, crates/helixrs/src/marketdata/fixings.rs
    • Related pages:
      • How dates and conventions work, for when indices publish, and the fixing rule written out by hand.
      • Rates conventions, every registered index with its code.
      • The dated market, for when a print becomes known.
      • Calendars, for the fixing calendar.
      • Multi-reset coupons, for coupons reading several resets.

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