Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 VV is a finite set of members, each with a canonical code, and parsing maps a spelling to a member or refuses it:

    parse⁡:string→V∪{refused},parse⁡(code⁡(v))=vfor every v∈V.\operatorname{parse}: \text{string} \to V \cup \{\text{refused}\}, \qquad \operatorname{parse}(\operatorname{code}(v)) = v \quad \text{for every } v \in V.

    Why the library refuses instead of guessing is covered in how dates and conventions work.

    API

    Symbol Meaning Rust Python
    v∈Vv \in V a typed member an enum variant, such as BusinessDayConvention::ModifiedFollowing a class member, such as dates.BusinessDayConvention
    parse⁡\operatorname{parse} a spelling to a member str::parse the member's class called with the spelling, or the string passed directly
    code⁡(v)\operatorname{code}(v) 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 ValueError in 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:rate is the commonest spelling. A measure with no scale carries none, and the report chooses one.
    • The scale is the last segment. delta:bp:rate is refused.

    This program shows a member and its code giving the same curve, and builds a measure both ways:

    Rust
    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(())
    }
    Python
    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.