Strike a trade at its fair rate
You'll book a trade at the rate that makes it worth nothing today: a FRA at its forward, and an OIS at its par rate. This is how a new trade is usually struck.
Before you start
- helixio installed: see installing.
- A market for each trade's currency. Here that is a EUR close for the FRA and a USD SOFR close for the OIS.
Steps
- Draft the trade at any rate. The rate doesn't matter yet.
- Read its fair rate on your market:
par_rategives a FRA's forward and a swap's par rate. - Book the trade again at that rate.
- For a swap you can do steps 1 to 3 in one call:
at_par(market)books it at its par rate.
Rust
use chrono::NaiveDate;
use helixrs::{
AnyInstrument, AnyTemplate, CollateralSpec, ForwardRateAgreement, MarketContext, Period,
QuoteSheet, Termination, template,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn fra(close: NaiveDate, rate: f64) -> Outcome<AnyInstrument> {
let AnyTemplate::Fra(fras) = template("EUR-EURIBOR-3M-FRA")? else {
unreachable!("a FRA template")
};
let start = "3M".parse::<Period>()?;
Ok(AnyInstrument::Fra(ForwardRateAgreement::from_template(
&fras, close, Some(start), 10_000_000.0, rate,
)))
}
fn main() -> Outcome<()> {
let close = NaiveDate::from_ymd_opt(2026, 10, 1).expect("a real date");
let eur_close = [
("EUR-ESTR-OIS", "1Y", 0.0195),
("EUR-ESTR-OIS", "2Y", 0.0200),
("EUR-ESTR-OIS", "5Y", 0.0215),
("EUR-ESTR-OIS", "10Y", 0.0240),
("EUR-ESTR-OIS", "20Y", 0.0262),
("EUR-ESTR-OIS", "30Y", 0.0265),
("EUR-EURIBOR-3M-IRS", "1Y", 0.0210),
("EUR-EURIBOR-3M-IRS", "2Y", 0.0216),
("EUR-EURIBOR-3M-IRS", "5Y", 0.0232),
("EUR-EURIBOR-3M-IRS", "10Y", 0.0257),
("EUR-EURIBOR-3M-IRS", "20Y", 0.0278),
("EUR-EURIBOR-3M-IRS", "30Y", 0.0280),
];
let usd_sofr_close = [
("USD-SOFR-OIS", "1M", 0.0391),
("USD-SOFR-OIS", "3M", 0.0402),
("USD-SOFR-OIS", "1Y", 0.0439),
("USD-SOFR-OIS", "2Y", 0.0459),
("USD-SOFR-OIS", "3Y", 0.0468),
("USD-SOFR-OIS", "4Y", 0.0469),
("USD-SOFR-OIS", "5Y", 0.0471),
("USD-SOFR-OIS", "7Y", 0.0475),
("USD-SOFR-OIS", "10Y", 0.0483),
("USD-SOFR-OIS", "15Y", 0.0494),
("USD-SOFR-OIS", "20Y", 0.0499),
("USD-SOFR-OIS", "30Y", 0.0492),
];
let eur = MarketContext::as_of(close).with_recipe_quotes(
&CollateralSpec::Currency("EUR".parse()?),
&QuoteSheet::from_rows(eur_close.iter().copied())?,
)?;
let usd = MarketContext::as_of(close).with_recipe_quotes(
&CollateralSpec::Currency("USD".parse()?),
&QuoteSheet::from_rows(usd_sofr_close.iter().copied())?,
)?;
// A FRA: draft it at any rate, read its forward, and rebook at the forward.
let draft = fra(close, 0.0200)?;
let forward = draft.par_rate(&eur)?;
let fair_fra = fra(close, forward)?;
println!("FRA forward {forward:.8}");
println!("FRA npv {:.6}", fair_fra.npv(&eur)?);
// An OIS: the same, or in one call with at_par.
let ois = template("USD-SOFR-OIS")?
.swap(close)?
.with_maturity("7Y".parse::<Termination>()?)?
.with_notional(25_000_000.0)
.at_par(&usd)?;
let par = ois.par_rate(&usd)?;
println!("OIS par rate {par:.8}");
println!("OIS npv {:.6}", ois.npv(&usd)?);
Ok(())
}Python
from datetime import date
import helixio as hlx
SOFR_CLOSE_DATE = date(2026, 10, 1)
EUR_CLOSE = {
"EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0200, "5Y": 0.0215, "10Y": 0.0240, "20Y": 0.0262, "30Y": 0.0265},
"EUR-EURIBOR-3M-IRS": {"1Y": 0.0210, "2Y": 0.0216, "5Y": 0.0232, "10Y": 0.0257, "20Y": 0.0278, "30Y": 0.0280},
}
USD_SOFR_CLOSE = {
"USD-SOFR-OIS": {
"1M": 0.0391, "3M": 0.0402, "1Y": 0.0439, "2Y": 0.0459, "3Y": 0.0468, "4Y": 0.0469,
"5Y": 0.0471, "7Y": 0.0475, "10Y": 0.0483, "15Y": 0.0494, "20Y": 0.0499, "30Y": 0.0492,
},
}
eur = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("EUR", EUR_CLOSE)
usd = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE)
# A FRA: draft it at any rate, read its forward, and rebook at the forward.
fras = hlx.templates.get("EUR-EURIBOR-3M-FRA")
draft = fras.forward_rate_agreement(SOFR_CLOSE_DATE, "3M", notional=10_000_000.0, struck_rate=0.0200)
forward = draft.par_rate(eur)
fra = fras.forward_rate_agreement(SOFR_CLOSE_DATE, "3M", notional=10_000_000.0, struck_rate=forward)
print(f"FRA forward {forward:.8f}")
print(f"FRA npv {fra.npv(eur):.6f}")
# An OIS: the same, or in one call with at_par.
sofr = hlx.templates.get("USD-SOFR-OIS")
ois = sofr.swap(SOFR_CLOSE_DATE).with_maturity("7Y").with_notional(25_000_000.0).at_par(usd)
par = ois.par_rate(usd)
print(f"OIS par rate {par:.8f}")
print(f"OIS npv {ois.npv(usd):.6f}")FRA forward 0.02055978
FRA npv 0.000000
OIS par rate 0.04750000
OIS npv 0.000000
Check it worked
- Both trades are worth zero, to the printed precision, on the market you read the rate from.
- The 7Y OIS strikes at the 7Y quote on the SOFR sheet, because that quote is what the curve was solved to reprice.
- On a different market, such as tomorrow's close, the same trade is no longer worth zero.
Related
- Forward-rate agreement, the FRA's arguments and its forward.
- Overnight indexed swap, the OIS's arguments.
- Price a swap from today's quotes, to value a trade at a rate you chose.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.