Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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 θi\theta_i in quantities qiq_i, carrying market M\mathcal{M}, is worth at tt

    Vbook(t)=∑iqi f ⁣(θi; Mt),V_{\text{book}}(t) = \sum_i q_i\, f\!\left(\theta_i;\ M_t\right),

    where MtM_t is the market's snapshot at tt. Every verb reads the same MtM_t. A book whose trades are in more than one currency sums them in a reporting currency rr it is given, each trade's value converting at the spot Xci→rX_{c_i \to r} of MtM_t:

    Vbook(t)=∑iqi f ⁣(θi; Mt)Xci→r.V_{\text{book}}(t) = \sum_i q_i\, f\!\left(\theta_i;\ M_t\right) X_{c_i \to r}.

    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
    qi,θiq_i, \theta_i a trade and its quantity Book::add(id, instrument, quantity) Book.add(id, instrument, quantity)
    M\mathcal{M} the market the book carries Book::attach_market Book.attach_market
    Vbook(t)V_{\text{book}}(t) the book's value at tt Book::npv(t) Book.npv(as_of=t)
    DV01 at tt a risk measure at tt Book::report(&[DV01], t) Book.report([risk.rates.DV01], as_of=t)
    f(θi;Mt)f(\theta_i; M_t) one trade on one snapshot AnyInstrument::npv(&market.on(t)) Instrument.npv(market.on(t))
    rr the reporting currency Book::reporting_in Book.reporting_in
    cic_i a trade's npv currency AnyInstrument::currency Instrument.npv_ccy
    each term of the sum a trade in cic_i and in rr 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.