Books carry their market
This page lists how a book holds its trades and the market they're valued on, so every question you ask it takes only a date: the calls, their arguments, how a book in several currencies sums, and what it refuses.
Quick example
The example reads its quotes from the desk sheets; load them once and it runs as shown. It attaches a two-day market to a two-trade book, values it on each date and risks it on the close, shows a date the market doesn't hold being refused, and values a book in two currencies trade by trade.
use chrono::NaiveDate;
use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE, Sheet, USD_SOFR_CLOSE, USD_SOFR_PRINTS};
use helixrs::presets;
use helixrs::{
Book, CollateralSpec, CurrencyPair, FixingKey, FixingStore, FxRates, MarketContext,
QuoteSheet, Side, Termination, report_records, 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 sheet(rows: Sheet, shift: f64) -> Outcome<QuoteSheet> {
let shifted = rows.iter().map(|&(template, locator, quote)| (template, locator, quote + shift));
Ok(QuoteSheet::from_rows(shifted)?)
}
fn main() -> Outcome<()> {
let close = date(SOFR_CLOSE_DATE);
let next_day = date((2026, 10, 2));
let usd = CollateralSpec::Currency("USD".parse()?);
// Two trades: one traded before the close, which has accrued against the
// September prints, and one struck at the close. The market holds two days,
// and the prints both need: the second day also reads the 1 October print.
let sofr = template("USD-SOFR-OIS")?;
let seasoned = sofr
.swap(date((2026, 9, 24)))?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(50e6)
.with_fixed_rate(0.0466)
.build()?;
let fresh = sofr
.swap(close)?
.with_maturity("2Y".parse::<Termination>()?)?
.with_notional(25e6)
.with_side(Side::Payer)
.with_fixed_rate(0.0455)
.build()?;
let mut prints: Vec<(FixingKey, NaiveDate, f64)> = USD_SOFR_PRINTS
.iter()
.map(|(index, day, value)| (FixingKey::Typed((*index).to_owned()), date(*day), *value))
.collect();
prints.push((FixingKey::Typed("USD-SOFR".to_owned()), close, 0.0391));
let market = MarketContext::as_of(close)
.with_fixings(FixingStore::from_observations_checked(&prints)?)
.with_recipe_quotes(&usd, &sheet(USD_SOFR_CLOSE, 0.0)?)?
.on(next_day)
.with_recipe_quotes(&usd, &sheet(USD_SOFR_CLOSE, 0.0001)?)?;
let book = Book::new()
.add("SOFR-5Y-RCV", seasoned, 1.0)
.add("SOFR-2Y-PAY", fresh.clone(), 1.0)
.attach_market(market);
let dates = book.market_context().expect("attached").dates();
println!("trades {}", book.len());
println!(
"dates {}",
dates.iter().map(NaiveDate::to_string).collect::<Vec<_>>().join(", ")
);
// Each verb names the date; the book reads that date's snapshot.
let first = book.npv(close)?;
let second = book.npv(next_day)?;
let rows = report_records(&book.report(&[presets::DV01], close)?);
let dv01: f64 = rows["value"].numbers().expect("numbers").iter().sum();
println!("npv {close} {first:.2}");
println!("npv {next_day} {second:.2}");
println!("dv01 {close} {dv01:.2}");
// A date the market holds no snapshot for is refused by name, and the book
// keeps the market it had.
let refused = book
.npv(date((2026, 10, 5)))
.expect_err("a date the market does not hold must be refused");
let names_the_date = refused.to_string().contains("2026-10-05");
println!("refused, naming 2026-10-05: {}", if names_the_date { "yes" } else { "no" });
// A book in two currencies reports in the one it is told. Each trade is listed
// in its own currency and in that one, converted at the spot, and the converted
// column sums to the book's value.
let (eur, dollars) = ("EUR".parse()?, "USD".parse()?);
let euro = template("EUR-ESTR-OIS")?
.swap(close)?
.with_maturity("5Y".parse::<Termination>()?)?
.with_notional(20e6)
.with_fixed_rate(0.0200)
.build()?;
let two_currencies = MarketContext::as_of(close)
.with_recipe_quotes(&usd, &sheet(USD_SOFR_CLOSE, 0.0)?)?
.with_recipe_quotes(&CollateralSpec::Currency(eur), &sheet(EUR_CLOSE, 0.0)?)?
.with_fx_rates(FxRates::from_quotes(&[(CurrencyPair::new(eur, dollars), 1.08)])?);
let mixed = Book::new()
.add("SOFR-2Y-PAY", fresh, 1.0)
.add("ESTR-5Y-RCV", euro, 1.0)
.reporting_in(dollars)
.attach_market(two_currencies);
let rows = mixed.npv_by_trade(None)?;
let (trades, ccys, pnl_ccys) = (
rows["trade"].text().expect("text"),
rows["npv_ccy"].text().expect("text"),
rows["pnl_ccy"].text().expect("text"),
);
let (local, pnl) = (
rows["npv_local"].numbers().expect("numbers"),
rows["npv_pnl"].numbers().expect("numbers"),
);
println!("{:<12} {:<4} {:>12} {:>12}", "trade", "ccy", "local", "in USD");
for i in 0..trades.len() {
println!("{:<12} {:<4} {:>12.2} {:>12.2}", trades[i], ccys[i], local[i], pnl[i]);
}
println!("{:<12} {:<4} {:>12} {:>12.2}", "book", "USD", "", mixed.npv(None)?);
Ok(())
}import datetime as dt
import helixio as hlx
from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS
NEXT_DAY = dt.date(2026, 10, 2)
# Two trades: one traded before the close, which has accrued against the
# September prints, and one struck at the close. The market holds two days,
# and the prints both need: the second day also reads the 1 October print.
sofr = hlx.templates.get("USD-SOFR-OIS")
seasoned = sofr.swap(dt.date(2026, 9, 24)).with_maturity("5Y").with_notional(50e6).with_fixed_rate(0.0466).build()
fresh = sofr.swap(SOFR_CLOSE_DATE).with_maturity("2Y").with_notional(25e6).with_side("payer").with_fixed_rate(0.0455).build()
tuesday = {t: {k: v + 0.0001 for k, v in q.items()} for t, q in USD_SOFR_CLOSE.items()}
market = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_fixings([*USD_SOFR_PRINTS, ("USD-SOFR", SOFR_CLOSE_DATE, 0.0391)])
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.on(NEXT_DAY)
.with_recipe_quotes("USD", tuesday)
)
book = hlx.Book().add("SOFR-5Y-RCV", seasoned, 1.0).add("SOFR-2Y-PAY", fresh, 1.0)
book.attach_market(market)
print(f"trades {len(book)}")
print(f"dates {', '.join(d.isoformat() for d in book.market_context.dates())}")
# Each verb names the date; the book reads that date's snapshot.
first = book.npv(as_of=SOFR_CLOSE_DATE)
second = book.npv(as_of=NEXT_DAY)
dv01 = sum(book.report([hlx.risk.rates.DV01], as_of=SOFR_CLOSE_DATE).to_dict()["value"])
print(f"npv {SOFR_CLOSE_DATE} {first:.2f}")
print(f"npv {NEXT_DAY} {second:.2f}")
print(f"dv01 {SOFR_CLOSE_DATE} {dv01:.2f}")
# A date the market holds no snapshot for is refused by name, and the book
# keeps the market it had.
try:
book.npv(as_of=dt.date(2026, 10, 5))
except hlx.PricingError as error:
date_refusal = str(error)
print(f"refused, naming 2026-10-05: {'yes' if '2026-10-05' in date_refusal else 'no'}")
else:
raise AssertionError("a date the market does not hold must be refused")
# A book in two currencies reports in the one it is told. Each trade is listed
# in its own currency and in that one, converted at the spot, and the converted
# column sums to the book's value.
euro = hlx.templates.get("EUR-ESTR-OIS").swap(SOFR_CLOSE_DATE).with_maturity("5Y").with_notional(20e6).with_fixed_rate(0.0200).build()
two_currencies = (
hlx.MarketContext(SOFR_CLOSE_DATE)
.with_recipe_quotes("USD", USD_SOFR_CLOSE)
.with_recipe_quotes("EUR", EUR_CLOSE)
.with_fx_rates([("EUR", "USD", 1.08)])
)
mixed = hlx.Book().add("SOFR-2Y-PAY", fresh, 1.0).add("ESTR-5Y-RCV", euro, 1.0).reporting_in("USD")
mixed.attach_market(two_currencies)
rows = mixed.npv_by_trade().to_dict()
print(f"{'trade':<12} {'ccy':<4} {'local':>12} {'in USD':>12}")
for trade, ccy, local, pnl in zip(rows["trade"], rows["npv_ccy"], rows["npv_local"], rows["npv_pnl"]):
print(f"{trade:<12} {ccy:<4} {local:>12.2f} {pnl:>12.2f}")
print(f"{'book':<12} {'USD':<4} {'':>12} {mixed.npv():>12.2f}")trades 2
dates 2026-10-01, 2026-10-02
npv 2026-10-01 -84198.22
npv 2026-10-02 -101135.90
dv01 2026-10-01 -17868.38
refused, naming 2026-10-05: yes
trade ccy local in USD
SOFR-2Y-PAY USD 18970.81 18970.81
ESTR-5Y-RCV EUR -142946.93 -154382.68
book USD -135411.88
The book is net long duration, so its DV01 is negative and it loses value on the higher second sheet.
How it's priced
A book of trades in quantities , carrying market , is worth at
where is the market's snapshot at . Every verb reads the same . A book whose trades are in more than one currency sums them in a reporting currency it is given, each trade's value converting at the spot of :
Why a book should carry its market rather than be handed one each call is covered in how a market is built.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| a trade and its quantity | Book::add(id, instrument, quantity) |
Book.add(id, instrument, quantity) |
|
| the market the book carries | Book::attach_market |
Book.attach_market |
|
| the book's value at | Book::npv(t) |
Book.npv(as_of=t) |
|
| DV01 at | a risk measure at | Book::report(&[DV01], t) |
Book.report([risk.rates.DV01], as_of=t) |
| one trade on one snapshot | AnyInstrument::npv(&market.on(t)) |
Instrument.npv(market.on(t)) |
|
| the reporting currency | Book::reporting_in |
Book.reporting_in |
|
| a trade's npv currency | AnyInstrument::currency |
Instrument.npv_ccy |
|
| each term of the sum | a trade in and in | Book::npv_by_trade(t) |
Book.npv_by_trade(as_of=t) |
| Argument | Type | Default | Meaning |
|---|---|---|---|
Book.add(id, instrument, quantity) |
str, Instrument, float |
required | a trade under its id, held in a quantity |
Book.attach_market(market) |
MarketContext |
required | the market the book values on; attaching fetches what the book needs for the market's date from its data source, if it has one |
Book.npv(as_of=) |
date | None |
None |
the book's value; None values on the market's own date |
Book.report(measures, as_of=) |
list[Preset | Measure | str], date | None |
required, None |
named risk measures, such as risk.rates.DV01 |
Book.npv_by_trade(as_of=) |
date | None |
None |
each trade's value in its own currency and in the reporting currency |
Book.reporting_in(code) |
str |
required | the book reporting in code: every trade's value converted to it at the market's spot |
MarketContext.on(date) |
date |
required | the book's market at date, its other snapshots kept |
MarketContext.dates() |
none | the dates the market holds a snapshot for | |
MarketContext.without(date) |
date |
required | the market without its snapshot at date; attach the result and fetch again to refresh a date |
MarketContext.fixing_history() |
none | every version of every print, as index, date, known_on, source, value |
Defaults and limits
!!! warning "attach_market differs between the languages"
In Python, attach_market changes the book in place. In Rust it consumes the book and returns
it with the market attached, so write let book = book.attach_market(market);.
- A date the market doesn't hold is refused: the error names the date, and the book keeps the market it had.
- Several currencies need a reporting currency: a book holding trades in more than one currency
sums only after
reporting_in, converting at the market's spots. - DV01's sign: it is the value's change for a one basis point rise. A receiver of fixed loses when rates rise, so a book receiving fixed on more duration than it pays has a negative DV01.
See also
- Source:
crates/helixrs/src/trading.rs - Related pages:
- How a market is built, for the sum taken apart trade by trade.
- The dated market, for the snapshots a book reads.
- Data sources, for a book whose market fetches.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.