Esc

    The API is under testing and may change before release.

    Cross-currency calibration

    This page lists what you need to solve one currency's discount curve under another currency's collateral from cross-currency basis swaps: the rungs, the solve, and the key the curve is filed under. It also says when to use this explicit solve and when to let a recipe build the same curve.

    Quick example

    This marks the dollar and euro curves and the EUR/USD spot, solves the euro curve under dollar collateral from four basis swaps, files it, and compares it with €STR's own discounting at ten years. The example reads its dollar quotes from the desk sheets; load them once and it runs as shown.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE};
    use helixrs::{
        CollateralSpec, CurrencyPair, FixingStore, FxRates, Interpolation, Locator, MarketContext,
        Period, QuoteSheet, Tolerance, YieldCurve, calibrate_cross_currency, cross_currency_rungs,
    };
    
    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 main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let eurusd = 1.17;
    
        // Both currencies marked from their sheets, and the spot. Each rung is a
        // EUR/USD basis swap at its quoted basis, its euro notional set at the spot.
        let usd_sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
        let mut eur_sheet = QuoteSheet::new();
        for (tenor, quote) in [("1Y", 0.0195), ("2Y", 0.0205), ("5Y", 0.0230), ("10Y", 0.0262)] {
            eur_sheet = eur_sheet.with("EUR-ESTR-OIS", Locator::parse(tenor)?, quote);
        }
        let fx = FxRates::from_quotes(&[("EUR/USD".parse::<CurrencyPair>()?, eurusd)])?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &usd_sheet)?
            .with_recipe_quotes(&CollateralSpec::Currency("EUR".parse()?), &eur_sheet)?
            .with_fx_rates(fx.clone());
        let tenors = ["1Y", "2Y", "5Y", "10Y"].iter().map(|t| t.parse::<Period>()).collect::<Result<Vec<_>, _>>()?;
        let bases = [-0.0010, -0.0012, -0.0014, -0.0015];
        let rungs = cross_currency_rungs("EUR-USD-ESTR-SOFR-BASIS", close, &tenors, &bases, 1.0, eurusd)?;
        for rung in &rungs {
            println!("{}  {}  basis {:.4}", rung.pillar, rung.instrument.product_name(), rung.quote);
        }
    
        // The euro curve under dollar collateral, solved against the dollar discount
        // and SOFR projection on the pay leg and the €STR projection on the receive
        // leg, then filed under the key a euro leg of a dollar-collateralised swap
        // declares.
        let (usd, _) = market.curve(&"discount(USD)".parse()?).expect("the USD curve");
        let (estr, _) = market.curve(&"projection(EUR-ESTR)".parse()?).expect("the ESTR projection");
        let solved = calibrate_cross_currency(
            close,
            &usd,
            &usd,
            Some(&estr),
            &rungs,
            &FixingStore::new(),
            &fx,
            Tolerance::default(),
            Interpolation::LogLinear,
        )?;
        let collateralised = market
            .clone()
            .with_discount(CollateralSpec::under_csa("EUR".parse()?, "USD".parse()?), solved.clone());
        for (role, key) in collateralised.contents() {
            if key.contains("CSA") {
                println!("filed as {role} {key}");
            }
        }
        let mut worst: f64 = 0.0;
        for rung in &rungs {
            worst = worst.max(rung.instrument.npv(&collateralised)?.abs());
        }
        println!("pillars {}, largest basis swap value {worst:.2}", solved.curve_value().parameter_count());
    
        // The basis moves euro discounting away from €STR's own: the two curves
        // differ, by an amount of the order of the basis over the horizon.
        let ten_years = date((2036, 10, 1));
        let (eur, _) = market.curve(&"discount(EUR)".parse()?).expect("the EUR curve");
        let own = eur.view::<f64>().discount(ten_years)?;
        let under_usd = solved.curve_value().view::<f64>().discount(ten_years)?;
        println!("10Y EUR discount, EUR collateral {own:.8}");
        println!("10Y EUR discount, USD collateral {under_usd:.8}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    calibration = hlx.calibration
    EUR_ESTR = {"EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0205, "5Y": 0.0230, "10Y": 0.0262}}
    EURUSD = 1.17
    
    # Both currencies marked from their sheets, and the spot. Each rung is a
    # EUR/USD basis swap at its quoted basis, its euro notional set at the spot.
    market = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_recipe_quotes("EUR", EUR_ESTR)
        .with_fx_rates([("EUR", "USD", EURUSD)])
    )
    tenors, bases = ["1Y", "2Y", "5Y", "10Y"], [-0.0010, -0.0012, -0.0014, -0.0015]
    rungs = calibration.cross_currency_rungs("EUR-USD-ESTR-SOFR-BASIS", SOFR_CLOSE_DATE, tenors, bases, 1.0, EURUSD)
    for pillar, swap, basis in rungs:
        print(f"{pillar}  {swap.product()}  basis {basis:.4f}")
    
    # The euro curve under dollar collateral, solved against the dollar discount
    # and SOFR projection on the pay leg and the €STR projection on the receive
    # leg, then filed under the key a euro leg of a dollar-collateralised swap
    # declares.
    usd = market.curve("discount(USD)")
    solved = calibration.cross_currency(
        SOFR_CLOSE_DATE, usd, usd, market.curve("projection(EUR-ESTR)"), rungs, market=market
    )
    collateralised = market.with_discount("EUR", solved.curve(), collateral="USD")
    for role, key in collateralised.contents():
        if "CSA" in key:
            print(f"filed as {role} {key}")
    worst = max(abs(swap.npv(collateralised)) for _, swap, _ in rungs)
    print(f"pillars {solved.curve().parameter_count()}, largest basis swap value {worst:.2f}")
    
    # The basis moves euro discounting away from €STR's own: the two curves
    # differ, by an amount of the order of the basis over the horizon.
    ten_years = dt.date(2036, 10, 1)
    own = market.curve("discount(EUR)").discount(ten_years)
    under_usd = solved.curve().discount(ten_years)
    print(f"10Y EUR discount, EUR collateral {own:.8f}")
    print(f"10Y EUR discount, USD collateral {under_usd:.8f}")
    2027-10-07  CrossCurrencySwap  basis -0.0010
    2028-10-10  CrossCurrencySwap  basis -0.0012
    2031-10-08  CrossCurrencySwap  basis -0.0014
    2036-10-08  CrossCurrencySwap  basis -0.0015
    filed as discount EUR-CSA-USD
    pillars 4, largest basis swap value 0.00
    10Y EUR discount, EUR collateral 0.76730551
    10Y EUR discount, USD collateral 0.77896217

    The basis is negative, so euros funded through dollar collateral are worth a little more: the curve under USD collateral discounts less than €STR's own.

    How it's priced

    Write D_{\}forthedollardiscountcurve,for the dollar discount curve,F_{$}andandF_{€}forthetwoprojections,andfor the two projections, andXforthespotindollarspereuro.Abasisswapwitheuronotionalfor the spot in dollars per euro. A basis swap with euro notionalN_{€},dollarnotional, dollar notional N_{$} = X N_{€}andbasisand basisb$ on the euro leg is worth, in dollars,

    V=X⋅PV€ ⁣(F€+b; D€∣$)−PV$ ⁣(F$; D$),V = X \cdot \mathrm{PV}_{€}\!\left(F_{€} + b;\ D_{€|\$}\right) - \mathrm{PV}_{\$}\!\left(F_{\$};\ D_{\$}\right),

    each leg including its notional exchanges. Only D_{€|\},theeurocurveunderdollarcollateral,isunknown,andeachquotedtenorfixesoneofitspillarsby, the euro curve under dollar collateral, is unknown, and each quoted tenor fixes one of its pillars by V = 0$. Why collateral changes discounting is in how calibration works.

    API

    Symbol Meaning Rust Python
    XX the spot FxRates::from_quotes, MarketContext::with_fx_rates MarketContext.with_fx_rates
    basis rungs basis swaps at each tenor cross_currency_rungs calibration.cross_currency_rungs
    D_{€\mid\}$ the collateral curve solved calibrate_cross_currency calibration.cross_currency
    filed curve the curve under EUR-CSA-USD MarketContext::with_discount(CollateralSpec::under_csa(..), ..) MarketContext.with_discount(.., collateral=)

    calibration.cross_currency_rungs(template, valuation, tenors, quotes, notional, fx_rate) builds one (pillar, instrument, basis) rung per tenor from a basis-swap template, such as EUR-USD-ESTR-SOFR-BASIS, dated by the template's conventions. calibration.cross_currency takes:

    Argument Type Default Meaning
    reference date required the curve's reference date
    pay_discount DiscountCurve required the pay leg's discount curve, here the dollar curve
    pay_projection DiscountCurve required the pay leg's projection, here SOFR
    recv_projection DiscountCurve or None required the receive leg's projection, here €STR; None for a fixed receive leg
    rungs list[Rung] or list[tuple[date, Instrument, float]] required the basis swaps with their quoted bases
    market MarketContext or None None where the spot, fixings and FX are read
    interpolation Interpolation or str "loglinear" how the solved curve reads between pillars
    tension float or None None the tension, for a tension spline
    end_slopes tuple[float, float] or None None the end slopes, for a clamped spline
    tolerance Tolerance or None the defaults how hard the solve tries; see bootstrapping

    The result is a Calibration, as for bootstrapping.

    Defaults and limits

    • What must be marked first: both currencies' own curves and the spot. The solve reads them; it doesn't build them.
    • The key: a curve filed with collateral="USD" on a "EUR" discount is keyed EUR-CSA-USD, the key a euro leg of a dollar-collateralised trade asks for.
    • Fixed receive legs: a fixed-float cross-currency swap, such as a lira leg against dollars, solves with recv_projection=None.
    • This solve or a recipe: use this solve when you have basis swap quotes and want the solve's report, its Jacobian, or your own rungs. When a counterparty's CSA decides the collateral and your sheet already lists the cross curve's FX swaps and basis, let the recipe build it with with_recipe_quotes on that cross curve's key. The counterparties and CSAs page does this for USD-CSA-EUR, dollars under euro collateral, the mirror of the curve solved here.

    See also

    • Source: crates/helixrs/src/calibration.rs, crates/helixrs/src/products.rs
    • Related pages:
      • Quote sheets and recipes, for the two currencies' own curves and the offshore recipes.
      • Bootstrapping, for the solve this one varies.
      • Counterparties and CSAs, for the recipe route keyed by a counterparty.
      • Cross-currency swaps, the trades this curve prices.

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