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.
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(())
}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 applied to an accrual starting fixes at
with the index's fixing lag on its calendar and the value date advanced by the tenor under the index's convention and end-of-month rule. Off a discount curve , a term index projects simple interest over its own window on its own day count:
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 |
| the fixing date | IborIndex::fixing_date |
Index.fixing_date |
|
| , , | 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 |
| , | the window's value date and end | IborIndex::maturity_date |
Index.maturity_date |
| the index's day count | IborIndex::day_count |
Index.day_count |
|
| the discount curve projected off | DiscountCurve |
curves.DiscountCurve |
|
| 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.ibororIndex.overnightis 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
ValueErrornaming the kind rather than returningNone. - The fixing rule. For a valuation at , a reset on is read if and must have been published; read if and a print exists, projected otherwise; and projected if .
- 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.
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(())
}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.