Esc

    The API is under testing and may change before release.

    Provenance

    You'll see why a curve solved from quotes can report risk per quote while the same curve written down as discount factors can't, and what that means when you move curves between markets.

    Risk in the instruments you quote

    A desk hedges in the instruments it quotes. Its risk is useful as "the book loses this much if the five-year swap rate rises a basis point", not as risk to a discount factor in 2031. Turning one into the other needs to know which quote set which pillar, and how each pillar moves when a quote moves: the curve's provenance.

    A curve marked from a sheet has it. That ladder of quotes travels with the curve: read the curve out of its market and pin it into another, and the par risk comes with it. A curve written down as discount factors, even the same factors, has no ladder. It can report risk to a parallel move of rates, but not risk per quote.

    Turning risk to the curve's parameters into risk to each quote takes the solve's Jacobian, which the Jacobian derives. A curve with no ladder has no Jacobian, so a per-quote report is refused rather than guessed. A parallel move needs no Jacobian, so its DV01 is still there.

    Seeing it

    This re-pins a solved curve into a market of its own and reads its risk per quote. Then it pins a bare curve through two of its factors. That curve agrees with the solved one at those dates and reports a DV01, but refuses the per-quote ladder. With only two pillars it's a coarser curve, so its DV01 isn't the solved curve's; both are printed. The example reads its quotes from the desk sheets.

    Rust
    use std::sync::Arc;
    
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        AnyInstrument, Book, CollateralSpec, Curve, MarketContext, PiecewiseDiscount, PinnedCurve,
        QuoteSheet, Termination, YieldCurve, presets, 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 book_on(close: NaiveDate, swap: &AnyInstrument, curve: PinnedCurve) -> Outcome<Book> {
        let market = MarketContext::as_of(close)
            .with_discount(CollateralSpec::Currency("USD".parse()?), curve.clone())
            .with_projection("USD-SOFR", curve);
        Ok(Book::new().add("SOFR-5Y", swap.clone(), 1.0).attach_market(market))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let swap = template("USD-SOFR-OIS")?
            .swap(close)?
            .with_maturity("5Y".parse::<Termination>()?)?
            .with_notional(10_000_000.0)
            .with_fixed_rate(0.0450)
            .build()?;
        let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
        let marked = MarketContext::as_of(close)
            .with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?;
        let (solved, ladder) = marked.curve(&"discount(USD)".parse()?).expect("the USD curve");
    
        // The solved curve, re-pinned into a market of its own, still reports risk per
        // quote on the sheet: one ladder row per tenor.
        let carried = PinnedCurve::from(Arc::clone(&solved)).with_provenance(ladder.expect("a solved curve's ladder"))?;
        let rows = report_records(&book_on(close, &swap, carried)?.report(&[presets::PAR_LADDER], close)?);
        let (labels, values) = (rows["label"].text().expect("text"), rows["value"].numbers().expect("numbers"));
        println!("ladder rows  {}", labels.join(", "));
        let five = values[labels.iter().position(|l| l == "5Y").expect("5Y")];
        println!("5Y row       {five:.2}");
    
        // A bare curve through two of the solved curve's factors: the values agree at
        // those dates, and the ladder is refused, because nothing records what the
        // pillars came from. With only two pillars it's a coarser curve, so its DV01
        // differs from the solved curve's.
        let view = solved.view::<f64>();
        let dates = [date((2027, 10, 1)), date((2031, 10, 1))];
        let mut pillars = Vec::new();
        for d in dates {
            pillars.push((d, view.discount(d)?));
        }
        let bare_curve = Curve::Piecewise(PiecewiseDiscount::from_discount_factors(close, &pillars)?);
        let bare = PinnedCurve::from(bare_curve.clone());
        let refused = book_on(close, &swap, bare.clone())?.report(&[presets::PAR_LADDER], close);
        if refused.is_err() {
            println!("bare curve: no ladder to report per quote");
        }
        let rows = report_records(&book_on(close, &swap, bare)?.report(&[presets::DV01], close)?);
        let dv01: f64 = rows["value"].numbers().expect("numbers").iter().sum();
        println!(
            "factor at {}  solved {:.10}  bare {:.10}",
            dates[1],
            view.discount(dates[1])?,
            bare_curve.view::<f64>().discount(dates[1])?
        );
        let solved_rows = report_records(
            &book_on(close, &swap, PinnedCurve::from(Arc::clone(&solved)))?.report(&[presets::DV01], close)?,
        );
        let solved_dv01: f64 = solved_rows["value"].numbers().expect("numbers").iter().sum();
        println!("solved DV01  {solved_dv01:.2}");
        println!("bare DV01    {dv01:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    swap = (
        hlx.templates.get("USD-SOFR-OIS")
        .swap(SOFR_CLOSE_DATE)
        .with_maturity("5Y")
        .with_notional(10_000_000.0)
        .with_fixed_rate(0.0450)
        .build()
    )
    solved = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("USD", USD_SOFR_CLOSE).curve("discount(USD)")
    
    def book_on(curve):
        market = hlx.MarketContext(SOFR_CLOSE_DATE).with_discount("USD", curve).with_projection("USD-SOFR", curve)
        book = hlx.Book().add("SOFR-5Y", swap, 1.0)
        book.attach_market(market)
        return book
    
    # The solved curve, re-pinned into a market of its own, still reports risk per
    # quote on the sheet: one ladder row per tenor.
    rows = book_on(solved).report([hlx.risk.rates.PAR_LADDER], as_of=SOFR_CLOSE_DATE).to_dict()
    print(f"ladder rows  {', '.join(rows['label'])}")
    five = rows["value"][rows["label"].index("5Y")]
    print(f"5Y row       {five:.2f}")
    
    # A bare curve through two of the solved curve's factors: the values agree at
    # those dates, and the ladder is refused, because nothing records what the
    # pillars came from. With only two pillars it's a coarser curve, so its DV01
    # differs from the solved curve's.
    dates = [dt.date(2027, 10, 1), dt.date(2031, 10, 1)]
    bare = hlx.curves.DiscountCurve(SOFR_CLOSE_DATE, [(d, solved.discount(d)) for d in dates])
    try:
        book_on(bare).report([hlx.risk.rates.PAR_LADDER], as_of=SOFR_CLOSE_DATE)
    except hlx.NoLadderError:
        print("bare curve: no ladder to report per quote")
    else:
        raise AssertionError("a bare curve has no ladder")
    dv01 = sum(book_on(bare).report([hlx.risk.rates.DV01], as_of=SOFR_CLOSE_DATE).to_dict()["value"])
    print(f"factor at {dates[1]}  solved {solved.discount(dates[1]):.10f}  bare {bare.discount(dates[1]):.10f}")
    solved_dv01 = sum(book_on(solved).report([hlx.risk.rates.DV01], as_of=SOFR_CLOSE_DATE).to_dict()["value"])
    print(f"solved DV01  {solved_dv01:.2f}")
    print(f"bare DV01    {dv01:.2f}")
    ladder rows  1M, 3M, 1Y, 2Y, 3Y, 4Y, 5Y, 7Y, 10Y, 15Y, 20Y, 30Y
    5Y row       -4414.23
    bare curve: no ladder to report per quote
    factor at 2031-10-01  solved 0.7916588736  bare 0.7916588736
    solved DV01  -4537.06
    bare DV01    -4537.13

    In the API, a solved curve's ladder is the second element MarketContext::curve returns in Rust, and is carried by MarketContext.curve in Python. You re-pin it with PinnedCurve::from(curve).with_provenance(ladder) in Rust, or MarketContext.with_discount with a curve read from a market in Python. A per-quote report on a curve with no ladder is an Err from Book::report in Rust, and a NoLadderError in Python.

    See also

    • Source: crates/helixrs/src/market/store.rs, crates/helixrs/src/calibration.rs
    • Related pages:
      • The Jacobian, for how parameter risk becomes quote risk.
      • Quote sheets and recipes, for a curve solved with its ladder.

    From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.