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.
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(())
}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,
A forward tenor starts at and ends at .
Striking at par sets the fixed rate to the par rate on the market , the rate at which the two legs are worth the same:
with 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 |
| the spot start | spot_effective_date over the template's calendar, lag and convention |
Template.spot_date |
|
| , or an end date | the trade's length | with_maturity with a parsed Termination, or with_termination |
ProductBuilder.with_maturity, ProductBuilder.with_termination |
| the rate struck at par on | 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:
buildraisesValueError(Rust: an error) and points you atat_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_csaandwith_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.