Report a book's risk
You'll put trades in a book, see which risk measures the book can answer, and report its value, DV01 and par ladder in one call, reading one bucket of the ladder back.
Before you start
- helixio installed: see installing.
- A market marked from quotes. A par ladder needs the curve's calibrating quotes, so mark it with
with_recipe_quotesor a solve that keeps its ladder, not from bare factors. - The trades you hold. This guide books two SOFR swaps: a five-year receiver and a two-year payer.
Steps
- Mark the market from the SOFR swap quotes with
with_recipe_quotes. - Book the trades from their template, add them to a
Book, and attach the market. - List what the book can answer with
available_measures. A par measure appears only when every curve the trades read carries the quotes it was marked from. - Ask for value, DV01, par DV01 and the par ladder in one
reportcall, which prices the book once and reads every measure from that one pass. - Pick a bucket out of the ladder by its label, the tenor of the quote it moves.
The comments in the program match these steps.
Rust
use chrono::NaiveDate;
use helixrs::presets;
use helixrs::{Book, CollateralSpec, MarketContext, QuoteSheet, Side, Termination, report_records, template};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
const SOFR_CLOSE_DATE: (i32, u32, u32) = (2026, 10, 1);
const USD_SOFR_CLOSE: &[(&str, &str, f64)] = &[
("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),
];
fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
}
fn main() -> Outcome<()> {
// Step 1: mark the market at the close from the SOFR swap quotes.
let close = date(SOFR_CLOSE_DATE);
let usd = CollateralSpec::Currency("USD".parse()?);
let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
let market = MarketContext::as_of(close).with_recipe_quotes(&usd, &sheet)?;
// Step 2: put two swaps in a book and attach the market: receive 4.50% for five
// years on 10 million, pay 4.70% for two years on 20 million.
let sofr = template("USD-SOFR-OIS")?;
let receiver = sofr
.swap(close)?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(10_000_000.0)
.with_fixed_rate(0.0450)
.build()?;
let payer = sofr
.swap(close)?
.with_maturity("2Y".parse::<Termination>()?)?
.with_notional(20_000_000.0)
.with_fixed_rate(0.0470)
.with_side(Side::Payer)
.build()?;
let book = Book::new().add("RCV-5Y", receiver, 1.0).add("PAY-2Y", payer, 1.0).attach_market(market);
// Step 3: list the measures this book can answer on this market.
let names: Vec<&str> = book.available_measures(close)?.iter().map(|preset| preset.name()).collect();
println!("measures {}", names.join(", "));
// Step 4: report value, DV01, par DV01 and the par ladder in one call.
let rows = report_records(&book.report(
&[presets::NPV, presets::DV01, presets::PAR_DV01, presets::PAR_LADDER],
close,
)?);
let measures = rows["measure"].text().expect("text");
let labels = rows["label"].text().expect("text");
let values = rows["value"].numbers().expect("numbers");
for ((measure, label), value) in measures.iter().zip(labels).zip(values) {
println!("{measure:<10} {label:<4} {value:>14.2}");
}
// Step 5: read one bucket back: the five-year quote's row of the ladder.
let ladder: Vec<(&str, f64)> = measures
.iter()
.zip(labels)
.zip(values)
.filter(|((measure, _), _)| measure.as_str() == "PAR_LADDER")
.map(|((_, label), value)| (label.as_str(), *value))
.collect();
let bucket = |tenor: &str| ladder.iter().find(|(label, _)| *label == tenor).map(|(_, v)| *v).expect("a ladder row");
println!("5Y bucket {:.2}", bucket("5Y"));
Ok(())
}Python
from datetime import date
import helixio as hlx
# Step 1: mark the market at the close from the SOFR swap quotes.
SOFR_CLOSE_DATE = date(2026, 10, 1)
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,
},
}
market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE)
# Step 2: put two swaps in a book and attach the market: receive 4.50% for five
# years on 10 million, pay 4.70% for two years on 20 million.
sofr = hlx.templates.get("USD-SOFR-OIS")
receiver = sofr.swap(SOFR_CLOSE_DATE).with_maturity("5Y").with_notional(10_000_000.0).with_fixed_rate(0.0450).build()
payer = (
sofr.swap(SOFR_CLOSE_DATE)
.with_maturity("2Y")
.with_notional(20_000_000.0)
.with_fixed_rate(0.0470)
.with_side("payer")
.build()
)
book = hlx.Book().add("RCV-5Y", receiver, 1.0).add("PAY-2Y", payer, 1.0)
book.attach_market(market)
# Step 3: list the measures this book can answer on this market.
names = book.available_measures(as_of=SOFR_CLOSE_DATE).to_dict()["name"]
print(f"measures {', '.join(names)}")
# Step 4: report value, DV01, par DV01 and the par ladder in one call.
report = book.report(
[hlx.risk.NPV, hlx.risk.rates.DV01, hlx.risk.rates.PAR_DV01, hlx.risk.rates.PAR_LADDER],
as_of=SOFR_CLOSE_DATE,
).to_dict()
for measure, label, value in zip(report["measure"], report["label"], report["value"]):
print(f"{measure:<10} {label:<4} {value:>14.2f}")
# Step 5: read one bucket back: the five-year quote's row of the ladder.
ladder = {label: value for measure, label, value in zip(report["measure"], report["label"], report["value"]) if measure == "PAR_LADDER"}
print(f"5Y bucket {ladder['5Y']:.2f}")measures NPV, DV01, DV01_BUCKETED, DV01_BY_CURVE, DV01_LADDER, PAR_DV01, PAR_LADDER
NPV -134623.92
DV01 -618.09
PAR_DV01 -596.60
PAR_LADDER 1M 0.15
PAR_LADDER 3M -0.01
PAR_LADDER 1Y 3.59
PAR_LADDER 2Y 3801.64
PAR_LADDER 3Y 5.20
PAR_LADDER 4Y 7.07
PAR_LADDER 5Y -4414.23
PAR_LADDER 7Y 0.00
PAR_LADDER 10Y 0.00
PAR_LADDER 15Y 0.00
PAR_LADDER 20Y 0.00
PAR_LADDER 30Y 0.00
5Y bucket -4414.23
Check it worked
- The ladder has one row per quote the curve was marked from, labelled by its tenor.
- The ladder's rows sum to
PAR_DV01. - The receiver's risk sits on the 5Y quote and is negative: it loses when rates rise. The payer's sits on the 2Y quote and is positive.
DV01andPAR_DV01differ.DV01moves the curve's own node rates;PAR_DV01moves the quotes the curve was marked from, so it reads in the units a desk hedges in.- If a par measure is missing from
available_measures, or a report refuses it, the curve was pinned without its ladder: mark it from quotes instead.
Related
- Python reference, for every preset in
hlx.riskand the columns a report returns. - Provenance, for why a curve without its ladder has no par risk.
- Books carry their market, for attaching a market and valuing a book across days.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.