Overnight indexed swap
This page lists everything you need to book and value an overnight indexed swap (OIS), fresh, seasoned or forward-starting: the builder's arguments and defaults, how it's priced, and an example you can run. An OIS exchanges a fixed rate for the compounded daily overnight rate, such as SOFR.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown.
This prices a ten-year SOFR swap at par, lists the prints a swap traded on 24 September needs and values it, and reads the par rate of a two-year swap starting in two years. The market comes from the USD SOFR close and its prints on the quote sheets and recipes.
use chrono::NaiveDate;
use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS};
use helixrs::{
CollateralSpec, FixingKey, FixingStore, MarketContext, QuoteSheet, Side, Termination,
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 prints: Vec<(FixingKey, NaiveDate, f64)> = USD_SOFR_PRINTS
.iter()
.map(|(index, day, value)| (FixingKey::Typed((*index).to_owned()), date(*day), *value))
.collect();
let market = MarketContext::as_of(close)
.with_fixings(FixingStore::from_observations_checked(&prints)?)
.with_recipe_quotes(
&CollateralSpec::Currency("USD".parse()?),
&QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?,
)?;
let sofr = template("USD-SOFR-OIS")?;
// A ten-year swap from spot is one of the sheet's quotes, so it prices to par.
let fresh = sofr
.swap(close)?
.with_maturity("10Y".parse::<Termination>()?)?
.with_notional(25_000_000.0)
.at_par(&market)?;
let par = fresh.par_rate(&market)?;
println!("10Y par {par:.8}");
// Traded on 24 September, the swap has accrued since Monday 28 September and
// needs the three prints published since then — not the close's own print.
let seasoned = sofr
.swap(date((2026, 9, 24)))?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(50_000_000.0)
.with_fixed_rate(0.0466)
.with_side(Side::Payer)
.build()?;
let needed = seasoned.fixings_needed(close);
for (index, day) in &needed {
println!("needs {index} {day}");
}
let npv = seasoned.npv(&market)?;
println!("npv {npv:.2}");
// A forward start is spelled as two tenors: two years, starting in two.
let forward = sofr
.swap(close)?
.with_maturity("2Y2Y".parse::<Termination>()?)?
.with_notional(10_000_000.0)
.with_fixed_rate(0.0)
.build()?;
let dates = forward.contract_dates();
println!("effective {}", dates[0].1);
println!("termination {}", dates[1].1);
println!("2Y2Y par {:.8}", forward.par_rate(&market)?);
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_fixings(USD_SOFR_PRINTS)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
)
sofr = hlx.templates.get("USD-SOFR-OIS")
# A ten-year swap from spot is one of the sheet's quotes, so it prices to par.
fresh = sofr.swap(SOFR_CLOSE_DATE).with_maturity("10Y").with_notional(25_000_000.0).at_par(market)
par = fresh.par_rate(market)
print(f"10Y par {par:.8f}")
# Traded on 24 September, the swap has accrued since Monday 28 September and
# needs the three prints published since then — not the close's own print.
seasoned = (
sofr.swap(dt.date(2026, 9, 24))
.with_maturity("5Y")
.with_notional(50_000_000.0)
.with_fixed_rate(0.0466)
.with_side("payer")
.build()
)
needed = seasoned.fixings_needed(SOFR_CLOSE_DATE)
for index, day in needed:
print(f"needs {index} {day}")
npv = seasoned.npv(market)
print(f"npv {npv:.2f}")
# A forward start is spelled as two tenors: two years, starting in two.
forward = sofr.swap(SOFR_CLOSE_DATE).with_maturity("2Y2Y").with_notional(10_000_000.0).with_fixed_rate(0.0)
forward = forward.build()
dates = dict(forward.contract_dates())
print(f"effective {dates['effective']}")
print(f"termination {dates['termination']}")
print(f"2Y2Y par {forward.par_rate(market):.8f}")10Y par 0.04830000
needs USD-SOFR 2026-09-28
needs USD-SOFR 2026-09-29
needs USD-SOFR 2026-09-30
npv 103160.20
effective 2028-10-05
termination 2030-10-05
2Y2Y par 0.04799884
The ten-year swap reprices its own quote. The seasoned swap needs the three prints published since it started accruing, not the close's own print, which is published the next morning.
How it's priced
Over a period from to the floating leg pays the compounded rate
where is the overnight print for day and its accrual. Days already published use their prints; the rest project from the curve, which for a single curve collapses to over the unpublished part. The par rate, linearity in the fixed rate and the payer-receiver mirror are those of every swap: see interest rate swap. How OIS quotes build the discount curve is covered in how rates products work.
API
| Argument | Type | Default | Meaning |
|---|---|---|---|
Template.swap(trade_date) |
date |
required | opens a draft on the template; Rust template(code)?.swap(d)? |
.with_maturity(end) |
date or tenor str |
"10Y" |
when the swap ends, as a date or a tenor from spot; also .with_termination; Rust takes a Termination |
.with_notional(notional) |
float |
10_000_000 |
the size |
.with_fixed_rate(rate) |
float |
none: set it, or use .at_par(market) |
the fixed rate; build() without one raises ValueError |
.with_side(side) |
"payer" or "receiver" |
"receiver" |
which way the fixed rate flows; Rust Side::Payer / Side::Receiver |
.at_par(market) |
MarketContext |
— | strikes the draft at the par rate instead of build() |
.with_csa(csa), .with_counterparty(c), .with_discount_key(key) |
str, Counterparty, str |
the template's discounting | collateral and discounting; see counterparties and CSAs |
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| the published prints | MarketContext::with_fixings |
MarketContext.with_fixings |
|
| the curve | SOFR discount and projection | MarketContext::with_recipe_quotes on USD |
MarketContext.with_recipe_quotes on "USD" |
| the trade | a swap on its template | template("USD-SOFR-OIS")?.swap(d)?…build()? |
templates.get("USD-SOFR-OIS").swap(d)…build() |
| prints needed | the accrued days | AnyInstrument::fixings_needed |
Instrument.fixings_needed |
| the par rate | AnyInstrument::par_rate |
Instrument.par_rate |
Defaults and limits
- Prints: a seasoned swap needs every print between its start and the close, exclusive of the
close itself. Load them with
with_fixingsbefore you value it;fixings_neededlists them. - Forward starts: spell a forward start as two tenors, such as
2Y2Y: two years, starting in two. - Conventions: each currency's OIS conventions are in rates conventions.
- Coupons: the compounded coupon, print by print, is built on legs and coupons and fixings: prints and projections.
See also
- Source:
crates/helixrs/src/instruments/rates.rs - Related pages:
- Interest rate swap, for the term-rate swap.
- The dated market, for prints across dates.
- Your first trade, for the same swap end to end.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.