Esc

    The API is under testing and may change before release.

    Templates and builders

    This page lists how a trade is drafted from a template: opening a builder, the terms each with_* call states, striking at par, and what a draft refuses. A template holds the conventions a desk never restates per trade, so a draft already carries all of them and you add only what the trade decides. Why booking works this way is covered in how trades are built.

    Quick example

    This drafts a five-year SOFR swap, starts one forward and ends one on a date, strikes one at par, and shows a draft with no rate being refused. The example reads its quotes from the desk sheets; load them once and it runs as shown.

    Rust
    use chrono::{Datelike, NaiveDate};
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        AnyTemplate, CollateralSpec, MarketContext, QuoteSheet, Side, Termination, spot_effective_date,
        template,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
        let market =
            MarketContext::as_of(close).with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?;
        let sofr = template("USD-SOFR-OIS")?;
    
        // The draft starts at the template's spot date and runs the stated tenor.
        let AnyTemplate::Ois(ois) = &sofr else { unreachable!("an overnight template") };
        let spot = spot_effective_date(&ois.calendar, close, ois.spot_lag, ois.business_day_convention);
        let swap = sofr
            .swap(close)?
            .with_maturity("5Y".parse::<Termination>()?)?
            .with_notional(25_000_000.0)
            .with_side(Side::Payer)
            .with_fixed_rate(0.0465)
            .build()?;
        let dates = swap.contract_dates();
        println!("spot          {spot}");
        println!("effective     {}", dates[0].1);
        println!("termination   {}", dates[1].1);
    
        // A forward tenor `2Y3Y` starts two years after spot and runs three; a stated
        // termination date ends the swap on that date instead of a tenor.
        let forward = sofr
            .swap(close)?
            .with_maturity("2Y3Y".parse::<Termination>()?)?
            .with_fixed_rate(0.0470)
            .build()?;
        let dated = sofr
            .swap(close)?
            .with_termination(date((2031, 10, 7)))?
            .with_fixed_rate(0.0470)
            .build()?;
        let (forward_dates, dated_dates) = (forward.contract_dates(), dated.contract_dates());
        println!("2Y3Y          {} to {}", forward_dates[0].1, forward_dates[1].1);
        println!("to a date     {} to {}", dated_dates[0].1, dated_dates[1].1);
    
        // The one term the market decides is the rate: `at_par` strikes it.
        let fair = sofr
            .swap(close)?
            .with_maturity("5Y".parse::<Termination>()?)?
            .with_notional(25_000_000.0)
            .with_side(Side::Payer)
            .at_par(&market)?;
        let par = fair.par_rate(&market)?;
        println!("struck at     {par:.8}");
        // Its value on that market is nothing, to the cent; abs drops the sign of rounding.
        println!("worth         {:.2}", fair.npv(&market)?.abs());
    
        // A draft with no rate and no market to strike it on refuses, and says how to
        // state one.
        let refused = sofr
            .swap(close)?
            .with_maturity("5Y".parse::<Termination>()?)?
            .build()
            .expect_err("a draft with no rate must refuse");
        let names_at_par = refused.to_string().contains("at_par");
        println!("refused, pointing at at_par: {}", if names_at_par { "yes" } else { "no" });
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE)
    sofr = hlx.templates.get("USD-SOFR-OIS")
    
    # The draft starts at the template's spot date and runs the stated tenor.
    spot = sofr.spot_date(SOFR_CLOSE_DATE)
    swap = (
        sofr.swap(SOFR_CLOSE_DATE)
        .with_maturity("5Y")
        .with_notional(25_000_000.0)
        .with_side("payer")
        .with_fixed_rate(0.0465)
        .build()
    )
    dates = dict(swap.contract_dates())
    print(f"spot          {spot}")
    print(f"effective     {dates['effective']}")
    print(f"termination   {dates['termination']}")
    
    # A forward tenor `2Y3Y` starts two years after spot and runs three; a stated
    # termination date ends the swap on that date instead of a tenor.
    forward = sofr.swap(SOFR_CLOSE_DATE).with_maturity("2Y3Y").with_fixed_rate(0.0470).build()
    dated = sofr.swap(SOFR_CLOSE_DATE).with_termination(dt.date(2031, 10, 7)).with_fixed_rate(0.0470).build()
    forward_dates, dated_dates = dict(forward.contract_dates()), dict(dated.contract_dates())
    print(f"2Y3Y          {forward_dates['effective']} to {forward_dates['termination']}")
    print(f"to a date     {dated_dates['effective']} to {dated_dates['termination']}")
    
    # The one term the market decides is the rate: `at_par` strikes it.
    fair = sofr.swap(SOFR_CLOSE_DATE).with_maturity("5Y").with_notional(25_000_000.0).with_side("payer").at_par(market)
    par = fair.par_rate(market)
    print(f"struck at     {par:.8f}")
    # Its value on that market is nothing, to the cent; abs drops the sign of rounding.
    print(f"worth         {abs(fair.npv(market)):.2f}")
    
    # A draft with no rate and no market to strike it on refuses, and says how to
    # state one.
    try:
        sofr.swap(SOFR_CLOSE_DATE).with_maturity("5Y").build()
    except ValueError as refused:
        no_rate = str(refused)
        print(f"refused, pointing at at_par: {'yes' if 'at_par' in no_rate else 'no'}")
    else:
        raise AssertionError("a draft with no rate must refuse")
    spot          2026-10-05
    effective     2026-10-05
    termination   2031-10-05
    2Y3Y          2028-10-05 to 2031-10-05
    to a date     2026-10-05 to 2031-10-07
    struck at     0.04710000
    worth         0.00
    refused, pointing at at_par: yes

    The struck rate is the sheet's five-year quote, because the curve was solved to reprice it, and the struck trade is worth nothing on that market.

    How it's priced

    A swap starts on the template's spot date: the trade date moved on by the spot lag on the template's calendar, then rolled by its business-day convention,

    ts=adjust⁡ ⁣(advance⁡(ttrade, lag), convention).t_s = \operatorname{adjust}\!\left(\operatorname{advance}(t_{\text{trade}},\ \text{lag}),\ \text{convention}\right).

    A forward tenor a×ba \times b starts at ts+at_s + a and ends at ts+a+bt_s + a + b.

    Striking at par sets the fixed rate to the par rate K∗K^\ast on the market MM, the rate at which the two legs are worth the same:

    K∗=Vfloat(M)A(M),V(K∗;M)=0,K^\ast = \frac{V_{\text{float}}(M)}{A(M)}, \qquad V(K^\ast; M) = 0,

    with AA the fixed leg's annuity.

    API

    Symbol Meaning Rust Python
    template a market's conventions under a code template(code) → AnyTemplate templates.get(code) → Template
    draft a builder carrying every convention AnyTemplate::swap(trade_date) → ProductBuilder Template.swap(trade_date) → products.ProductBuilder
    tst_s the spot start spot_effective_date over the template's calendar, lag and convention Template.spot_date
    a×ba \times b, or an end date the trade's length with_maturity with a parsed Termination, or with_termination ProductBuilder.with_maturity, ProductBuilder.with_termination
    K∗K^\ast the rate struck at par on MM ProductBuilder::at_par ProductBuilder.at_par
    the trade the booked instrument ProductBuilder::build ProductBuilder.build
    Argument Type Default Meaning
    templates.get(code) str required the template filed under code; the template registry lists every code
    Template.swap(trade_date) date required a draft traded on trade_date, carrying every convention of the template
    Template.spot_date(trade_date) date required the date a trade on trade_date starts
    ProductBuilder.with_maturity(term) str | date required, or with_termination a tenor from spot ("5Y"), a forward tenor ("2Y3Y") or an end date
    ProductBuilder.with_termination(end) str | date required, or with_maturity the same verb under its other name
    ProductBuilder.with_notional(amount) float 1_000_000.0 the notional, in the template's currency
    ProductBuilder.with_side(side) "payer" | "receiver" "receiver" a payer pays fixed, a receiver receives it
    ProductBuilder.with_fixed_rate(rate) float required, or at_par the fixed rate, as a decimal
    ProductBuilder.at_par(market) MarketContext the trade built with the fixed rate struck at par on market
    ProductBuilder.with_counterparty(counterparty) Counterparty none discount under the counterparty's CSA; see counterparties and CSA
    ProductBuilder.with_csa(csa) str none discount under a CSA posting csa, a currency or its overnight index
    ProductBuilder.with_discount_key(key) str the template's currency discount on the curve under key
    ProductBuilder.build() none the trade

    Defaults and limits

    • A draft with no rate is refused: build raises ValueError (Rust: an error) and points you at at_par, rather than guessing a rate.
    • Side defaults to receiver: a draft receives fixed unless you say with_side("payer").
    • Notional defaults to one million: in the template's currency.
    • The last collateral stated wins: between with_counterparty, with_csa and with_discount_key.
    • A tenor counts from spot, not from the trade date: "5Y" traded on a Thursday starts on the spot date two business days later.

    See also

    • Source: crates/helixrs/src/products.rs, crates/helixrs/src/statics/templates/registry.rs
    • Related pages:
      • How trades are built, for why a trade starts from a template.
      • Conventions and templates, for the dates a template's conventions produce.
      • Counterparties and CSA, for the collateral a draft discounts under.
      • The template registry, for every code a draft can open from.
      • Strike at the fair rate, for striking a trade as a task.

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