Vocabularies
This page lists how the library's closed vocabularies work: how a spelling parses to a member, how a member prints, what a refused spelling tells you, and the grammar of a risk measure. Most options come from a closed set, such as a business-day convention, a day count or an interpolation scheme, and every parameter that takes one accepts either the typed member or its code string.
Quick example
This parses several spellings, shows three refused ones, and prints a member of each vocabulary with its code.
use helixrs::{
Aggregation, AssetSwapKind, BusinessDayConvention, CapFloorKind, DayCountFraction,
DeltaConvention, Direction, Frequency, Interpolation, LevelInterpolation, ResetCompounding,
RiskAxis, RollConvention, StrikeAxis, SwaptionEngine, YieldQuoteConvention,
};
use std::str::FromStr;
/// One row: where the Python class lives, its address, and the variant's code.
/// Returns whether the code parses back to the variant.
fn row<T: FromStr + PartialEq>(module: &str, address: &str, member: T, code: &str) -> bool {
println!("{module:<9} {address:<42} {code}");
code.parse::<T>().ok() == Some(member)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Several spellings of one value parse to one variant, and the variant
// prints the canonical code. The code parses back to the same variant.
let mut frequencies = Vec::new();
for spelling in ["6M", "SemiAnnual", "semi_annual"] {
let parsed: Frequency = spelling.parse()?;
frequencies.push(parsed);
println!("{spelling:<14} -> {}", parsed.code());
}
let day_count: DayCountFraction = "act/360".parse()?;
println!("{:<14} -> {}", "act/360", day_count.code());
let mut rolls = Vec::new();
for (spelling, expected) in [("eom", RollConvention::Eom), ("15", RollConvention::DayOfMonth(15))] {
let parsed: RollConvention = spelling.parse()?;
rolls.push((parsed, expected));
println!("{spelling:<14} -> {}", parsed.code());
}
let scheme = Interpolation::from_code("naturalcubic").expect("a scheme");
println!("{:<14} -> {}", "naturalcubic", scheme.code());
// A spelling outside the set is refused, and the message names the
// vocabulary, repeats the spelling, and lists every value the set accepts.
let refusals = [
"sideways".parse::<Frequency>().unwrap_err(),
"ACT/365".parse::<DayCountFraction>().unwrap_err(),
Interpolation::from_code("cubic").unwrap_err(),
];
for refusal in &refusals {
println!("{refusal}");
}
let mut round_trips = Vec::new();
// Every vocabulary is an enum with one variant per value, and the variant's
// code is the spelling its parser takes back. The first column names the
// Python module that carries the same vocabulary as a class.
let bdc = BusinessDayConvention::ModifiedFollowing;
round_trips.push(row("dates", "BusinessDayConvention.ModifiedFollowing", bdc, bdc.code()));
let level = LevelInterpolation::LogLinear;
round_trips.push(row("curves", "LevelInterpolation.LogLinear", level, level.code()));
let direction = Direction::Pay;
round_trips.push(row("legs", "Direction.Pay", direction, direction.code()));
let reset = ResetCompounding::SpreadExclusive;
round_trips.push(row("legs", "ResetCompounding.SpreadExclusive", reset, reset.code()));
let cap_floor = CapFloorKind::Floor;
round_trips.push(row("products", "CapFloorKind.Floor", cap_floor, cap_floor.code()));
let engine = SwaptionEngine::HullWhite;
round_trips.push(row("products", "SwaptionEngine.HullWhite", engine, engine.code()));
let package = AssetSwapKind::ParPar;
round_trips.push(row("products", "AssetSwapKind.ParPar", package, package.code()));
let delta = DeltaConvention::PaFwd;
round_trips.push(row("vols", "DeltaConvention.PaFwd", delta, delta.code()));
let strike_axis = StrikeAxis::SpreadToAtm;
round_trips.push(row("vols", "StrikeAxis.SpreadToAtm", strike_axis, strike_axis.code()));
let quoted = YieldQuoteConvention::UsStreet;
round_trips.push(row("bonds", "YieldConvention.UsStreet", quoted, quoted.code()));
let axis = RiskAxis::SecuritySpread;
round_trips.push(row("risk", "Axis.SecuritySpread", axis, axis.code()));
let by_curve = Aggregation::ByCurve;
round_trips.push(row("risk", "Aggregation.ByCurve", by_curve, by_curve.code()));
Ok(())
}import helixio as hlx
Frequency = hlx.dates.Frequency
DayCountFraction = hlx.dates.DayCountFraction
RollConvention = hlx.dates.RollConvention
Interpolation = hlx.curves.Interpolation
# Several spellings of one value parse to one member, and the member prints the
# canonical code. The code parses back to the same member.
PARSED = [
("6M", Frequency("6M"), Frequency.SemiAnnual),
("SemiAnnual", Frequency("SemiAnnual"), Frequency.SemiAnnual),
("semi_annual", Frequency("semi_annual"), Frequency.SemiAnnual),
("act/360", DayCountFraction("act/360"), DayCountFraction.Act360),
("eom", RollConvention("eom"), RollConvention.Eom),
("15", RollConvention("15"), RollConvention.day(15)),
("naturalcubic", Interpolation("naturalcubic"), Interpolation.NaturalCubic),
]
for spelling, parsed, member in PARSED:
print(f"{spelling:<14} -> {parsed.code()}")
# A spelling outside the set is refused, and the message names the vocabulary,
# repeats the spelling, and lists every value the set accepts.
REFUSED = [
lambda: Frequency("sideways"),
lambda: DayCountFraction("ACT/365"),
lambda: Interpolation("cubic"),
]
messages = []
for attempt in REFUSED:
try:
attempt()
message = ""
except ValueError as refusal:
message = str(refusal)
messages.append(message)
print(message)
# Every vocabulary is a class in the module that uses it, with one member per
# value. A member prints as its address, is never equal to its string, and its
# code is the string every parameter of that vocabulary also accepts.
MEMBERS = [
("dates", hlx.dates.BusinessDayConvention.ModifiedFollowing),
("curves", hlx.curves.LevelInterpolation.LogLinear),
("legs", hlx.legs.Direction.Pay),
("legs", hlx.legs.ResetCompounding.SpreadExclusive),
("products", hlx.products.CapFloorKind.Floor),
("products", hlx.products.SwaptionEngine.HullWhite),
("products", hlx.products.AssetSwapKind.ParPar),
("vols", hlx.vols.DeltaConvention.PaFwd),
("vols", hlx.vols.StrikeAxis.SpreadToAtm),
("bonds", hlx.bonds.YieldConvention.UsStreet),
("risk", hlx.risk.Axis.SecuritySpread),
("risk", hlx.risk.Aggregation.ByCurve),
]
for module, member in MEMBERS:
address = repr(member)
print(f"{module:<9} {address:<42} {member.code()}")6M -> 6M
SemiAnnual -> 6M
semi_annual -> 6M
act/360 -> ACT/360
eom -> EOM
15 -> 15
naturalcubic -> naturalcubic
unknown coupon frequency "sideways"; expected one of: Annual, SemiAnnual, Quarterly, BiMonthly, Monthly, Weekly, Daily, Once, or a period code such as 6M
unknown day count "ACT/365"; expected one of: ACT/360, ACT/365.FIXED, NL/365, 30/360, 30U/360, 30E/360, 30E/360.ISDA, ACT/ACT.ISDA, ACT/ACT.ICMA, ACT/ACT.AFB, BUS/252, 1/1
unknown interpolation scheme "cubic"; expected one of: loglinear, backwardflat, forwardflat, linearzero, anchoredlinearzero, naturalcubic, clampedcubic, notaknotcubic, tensionspline, monotoneconvex, monotonecubic, akima, kruger, monotonicnaturalcubic, krugerzero
dates BusinessDayConvention.ModifiedFollowing MODFOLLOWING
curves LevelInterpolation.LogLinear log_linear
legs Direction.Pay Pay
legs ResetCompounding.SpreadExclusive spread_exclusive
products CapFloorKind.Floor floor
products SwaptionEngine.HullWhite hull_white
products AssetSwapKind.ParPar par_par
vols DeltaConvention.PaFwd forward_premium
vols StrikeAxis.SpreadToAtm spread_to_atm
bonds YieldConvention.UsStreet UsStreet
risk Axis.SecuritySpread security_spread
risk Aggregation.ByCurve by-curve
How it's priced
Parsing follows one rule. A vocabulary is a finite set of members, each with a canonical code, and parsing maps a spelling to a member or refuses it:
Why the library refuses instead of guessing is covered in how dates and conventions work.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| a typed member | an enum variant, such as BusinessDayConvention::ModifiedFollowing |
a class member, such as dates.BusinessDayConvention |
|
| a spelling to a member | str::parse |
the member's class called with the spelling, or the string passed directly | |
| the canonical spelling | code |
code |
|
| a measure | the delta grammar | Measure, parsed or built |
risk.Measure |
| Argument | Type | Default | Meaning |
|---|---|---|---|
spelling |
str | required | a vocabulary class called with a spelling, such as dates.Frequency("6M"); Rust: "6M".parse::<Frequency>() |
spelling |
str | required | risk.Measure(spelling): a measure in the grammar delta:<axis>[:<basis>[:<aggregation>]][:<scale>] |
axis |
risk.Axis or str |
required | risk.delta: the axis the sensitivity is on |
basis |
risk.Basis or str |
"zero" |
risk.delta: what the sensitivity is against |
aggregation |
risk.Aggregation or str |
"total" |
risk.delta: how rows are collapsed |
scale |
risk.Scale, str or None |
None |
risk.delta: the unit the reading is in; None lets the report choose. Rust: Measure::sensitivity(axis, basis, aggregation, scale) |
Defaults and limits
- Several spellings, one member.
"6M","SemiAnnual"and"semi_annual"all parse to the semi-annual frequency, and the member always prints its canonical code. - A refused spelling raises
ValueErrorin Python, or a parse error in Rust, naming the vocabulary, repeating the spelling and listing every accepted code. - A member never equals its string, but every parameter that takes a vocabulary accepts both.
- Measure defaults (zero basis, total aggregation) are left out when a measure prints, so
delta:rateis the commonest spelling. A measure with no scale carries none, and the report chooses one. - The scale is the last segment.
delta:bp:rateis refused.
This program shows a member and its code giving the same curve, and builds a measure both ways:
use chrono::NaiveDate;
use helixrs::{Aggregation, Basis, Interpolation, Measure, PiecewiseDiscount, RiskAxis, Scale, YieldCurve};
fn date(year: i32, month: u32, day: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn days(from: NaiveDate, to: NaiveDate) -> f64 {
f64::from(i32::try_from((to - from).num_days()).expect("a span of days"))
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// A curve takes its interpolation scheme as a variant. Naming the variant
// and parsing its code give the same scheme, so every read agrees exactly.
let valuation = date(2026, 9, 25);
let pillars = [
(date(2027, 9, 29), 0.9612),
(date(2028, 9, 29), 0.9235),
(date(2031, 9, 29), 0.8187),
(date(2036, 9, 29), 0.6703),
];
let between = date(2030, 3, 29);
let curve = |scheme: Interpolation| {
PiecewiseDiscount::from_discount_factors(valuation, &pillars)
.and_then(|curve| curve.with_interpolation(scheme))
.expect("well-formed pillars")
};
let typed = curve(Interpolation::NaturalCubic);
let spelled = curve(Interpolation::from_code("naturalcubic").expect("a scheme"));
let log_linear = curve(Interpolation::LogLinear);
let cubic_factor: f64 = typed.view::<f64>().discount(between).expect("a date on the curve");
let spelled_factor: f64 = spelled.view::<f64>().discount(between).expect("a date on the curve");
// Log-linear is a straight line in log discount factors between the
// pillars either side, with time measured in days over 365 from the
// valuation date.
let ((left, df_left), (right, df_right)) = (pillars[1], pillars[2]);
let (t, t_left, t_right) = (
days(valuation, between) / 365.0,
days(valuation, left) / 365.0,
days(valuation, right) / 365.0,
);
let linear_factor: f64 = log_linear.view::<f64>().discount(between).expect("a date on the curve");
println!("natural cubic, typed member {cubic_factor:.10}");
println!("natural cubic, string code {spelled_factor:.10}");
println!("log-linear, typed member {linear_factor:.10}");
// A risk measure is a value with a grammar:
// delta:<axis>[:<basis>[:<aggregation>]][:<scale>]. Parsing a spelling and
// building one from typed parts give the same value.
let parsed: Measure = "delta:rate:par:ladder:bp".parse().expect("a measure");
let built =
Measure::sensitivity(RiskAxis::Rate, Basis::Par, Aggregation::Ladder, Some(Scale::Bp));
let Measure::Delta { axis, basis, aggregation, scale } = parsed else {
panic!("a delta spelling parses to a delta");
};
let stated = scale;
let scale = scale.expect("a stated scale");
// A scale is a multiplier on the per-unit reading: a basis point is
// 0.0001, and a custom scale states its own factor.
let per_million = "delta:rate:x1e6"
.parse::<Measure>()
.expect("a measure")
.scale()
.expect("a stated scale");
let spelled_out: Measure = "delta:rate:zero:total".parse().expect("a measure");
// The scale is the last segment. Anywhere else, the spelling is refused.
let misplaced_refused = "delta:bp:rate".parse::<Measure>().is_err();
println!("parsed {parsed}");
println!("built {built}");
println!("axis {}", axis.code());
println!("basis {}", basis.code());
println!("aggregation {}", aggregation.code());
println!("scale {scale}");
println!("custom scale {per_million}");
println!("defaults {spelled_out}");
println!("delta:bp:rate {}", if misplaced_refused { "refused" } else { "accepted" });
Ok(())
}import datetime as dt
import helixio as hlx
Interpolation = hlx.curves.Interpolation
risk = hlx.risk
# A curve takes its interpolation scheme as the typed member or as its code.
# The two curves are the same curve, so every read agrees exactly.
VALUATION = dt.date(2026, 9, 25)
PILLARS = [
(dt.date(2027, 9, 29), 0.9612),
(dt.date(2028, 9, 29), 0.9235),
(dt.date(2031, 9, 29), 0.8187),
(dt.date(2036, 9, 29), 0.6703),
]
BETWEEN = dt.date(2030, 3, 29)
typed = hlx.curves.DiscountCurve(VALUATION, PILLARS, Interpolation.NaturalCubic)
spelled = hlx.curves.DiscountCurve(VALUATION, PILLARS, "naturalcubic")
log_linear = hlx.curves.DiscountCurve(VALUATION, PILLARS, Interpolation.LogLinear)
cubic_factor = typed.discount(BETWEEN)
linear_factor = log_linear.discount(BETWEEN)
print(f"natural cubic, typed member {cubic_factor:.10f}")
print(f"natural cubic, string code {spelled.discount(BETWEEN):.10f}")
print(f"log-linear, typed member {linear_factor:.10f}")
# A risk measure is a value with a grammar:
# delta:<axis>[:<basis>[:<aggregation>]][:<scale>]. Parsing a spelling and
# building one from typed parts give the same value.
parsed = risk.Measure("delta:rate:par:ladder:bp")
built = risk.delta(
risk.Axis.Rate,
basis=risk.Basis.Par,
aggregation=risk.Aggregation.Ladder,
scale=risk.Scale.Bp,
)
# A scale is a multiplier on the per-unit reading: a basis point is 0.0001, and
# a custom scale states its own factor.
per_million = risk.Measure("delta:rate:x1e6").scale()
print(f"parsed {parsed.code()}")
print(f"built {built.code()}")
print(f"axis {parsed.axis().code()}")
print(f"basis {parsed.basis().code()}")
print(f"aggregation {parsed.aggregation().code()}")
print(f"scale {parsed.scale().code()}")
print(f"custom scale {per_million.code()}")
print(f"defaults {risk.Measure('delta:rate:zero:total').code()}")
# The scale is the last segment. Anywhere else, the spelling is refused.
try:
risk.Measure("delta:bp:rate")
misplaced_refused = False
except ValueError:
misplaced_refused = True
print(f"delta:bp:rate {'refused' if misplaced_refused else 'accepted'}")natural cubic, typed member 0.8696621267
natural cubic, string code 0.8696621267
log-linear, typed member 0.8696660281
parsed delta:rate:par:ladder:bp
built delta:rate:par:ladder:bp
axis rate
basis par
aggregation ladder
scale bp
custom scale x1000000
defaults delta:rate
delta:bp:rate refused
See also
- Source:
crates/helixrs/src/core/vocabulary.rs - Related pages:
- How dates and conventions work, for why vocabularies are closed.
- Day counts, for a vocabulary in use.
- Errors, for where a refused spelling sits.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.