Esc

    The API is under testing and may change before release.

    Value a cross-currency swap leg by leg

    You'll value a EUR/USD basis swap one leg at a time, in each leg's own currency, in the swap's currency and in dollars, so you can see where its value comes from.

    Before you start

    • helixio installed: see installing.
    • Quotes for both currencies, the EUR/USD spot, and the cross-currency basis quotes. The basis builds the curve that discounts euros collateralised in dollars.

    Steps

    1. Build one market holding both currencies, the spot, and the basis under the key USD-CSA-EUR.
    2. Book the swap from its template, with the notional exchanged at the spot rate.
    3. Call leg_npvs(market, pnl_ccy="USD") (leg_npvs(&market, Some(usd)) in Rust). You get one row per leg with its value in its own currency (npv_local), in the swap's currency (npv) and in your P&L currency (npv_pnl).
    Rust
    use chrono::NaiveDate;
    use helixrs::{
        AnyInstrument, AnyTemplate, CollateralSpec, CrossCurrencySwap, CurrencyPair, FxRates,
        MarketContext, QuoteSheet, template,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real date")
    }
    
    fn main() -> Outcome<()> {
        let close = date(2026, 10, 1);
        let usd_sofr_close = [
            ("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),
        ];
        let eur_close = [
            ("EUR-ESTR-OIS", "1Y", 0.0195),
            ("EUR-ESTR-OIS", "2Y", 0.0200),
            ("EUR-ESTR-OIS", "5Y", 0.0215),
            ("EUR-ESTR-OIS", "10Y", 0.0240),
            ("EUR-ESTR-OIS", "20Y", 0.0262),
            ("EUR-ESTR-OIS", "30Y", 0.0265),
            ("EUR-EURIBOR-3M-IRS", "1Y", 0.0210),
            ("EUR-EURIBOR-3M-IRS", "2Y", 0.0216),
            ("EUR-EURIBOR-3M-IRS", "5Y", 0.0232),
            ("EUR-EURIBOR-3M-IRS", "10Y", 0.0257),
            ("EUR-EURIBOR-3M-IRS", "20Y", 0.0278),
            ("EUR-EURIBOR-3M-IRS", "30Y", 0.0280),
        ];
        let basis = [
            ("EUR-USD-ESTR-SOFR-BASIS", "1Y", -0.0010),
            ("EUR-USD-ESTR-SOFR-BASIS", "2Y", -0.0012),
            ("EUR-USD-ESTR-SOFR-BASIS", "5Y", -0.0015),
            ("EUR-USD-ESTR-SOFR-BASIS", "10Y", -0.0016),
        ];
    
        // 1. A market with both currencies, the spot, and the basis that discounts
        //    euros collateralised in dollars.
        let key = |spelling: &str| spelling.parse::<CollateralSpec>();
        let spot = FxRates::from_quotes(&[(CurrencyPair::new("EUR".parse()?, "USD".parse()?), 1.08)])?;
        let market = MarketContext::as_of(close)
            .with_recipe_quotes(&key("USD")?, &QuoteSheet::from_rows(usd_sofr_close.iter().copied())?)?
            .with_recipe_quotes(&key("EUR")?, &QuoteSheet::from_rows(eur_close.iter().copied())?)?
            .with_fx_rates(spot)
            .with_recipe_quotes(&key("USD-CSA-EUR")?, &QuoteSheet::from_rows(basis.iter().copied())?)?;
    
        // 2. Five years of €STR flat against SOFR, notionals exchanged at the spot.
        let AnyTemplate::Xccy(terms) = template("EUR-USD-ESTR-SOFR-BASIS")? else {
            unreachable!("a cross-currency template")
        };
        let swap = AnyInstrument::CrossCurrency(Box::new(CrossCurrencySwap::from_template(
            &terms, date(2026, 10, 5), date(2031, 10, 5), 10_000_000.0, 1.08, 0.0,
        )?));
    
        // 3. One row per leg: in its own currency, in the swap's (EUR), and in dollars.
        let rows = swap.leg_npvs(&market, Some("USD".parse()?))?;
        let text = |name: &str| rows[name].text().expect("a text column");
        let numbers = |name: &str| rows[name].numbers().expect("a number column");
        let (side, ccy) = (text("side"), text("ccy"));
        let (local, npv, pnl) = (numbers("npv_local"), numbers("npv"), numbers("npv_pnl"));
        println!("{:<8} {:<4} {:>12} {:>12} {:>12}", "side", "ccy", "local", "in EUR", "in USD");
        for i in 0..side.len() {
            println!("{:<8} {:<4} {:>12.2} {:>12.2} {:>12.2}", side[i], ccy[i], local[i], npv[i], pnl[i]);
        }
        let total = swap.npv(&market)?;
        println!("swap npv {total:.2} EUR");
    
        Ok(())
    }
    Python
    from datetime import date
    
    import helixio as hlx
    
    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,
        },
    }
    EUR_CLOSE = {
        "EUR-ESTR-OIS": {"1Y": 0.0195, "2Y": 0.0200, "5Y": 0.0215, "10Y": 0.0240, "20Y": 0.0262, "30Y": 0.0265},
        "EUR-EURIBOR-3M-IRS": {"1Y": 0.0210, "2Y": 0.0216, "5Y": 0.0232, "10Y": 0.0257, "20Y": 0.0278, "30Y": 0.0280},
    }
    BASIS = {"EUR-USD-ESTR-SOFR-BASIS": {"1Y": -0.0010, "2Y": -0.0012, "5Y": -0.0015, "10Y": -0.0016}}
    
    # 1. A market with both currencies, the spot, and the basis that discounts
    #    euros collateralised in dollars.
    market = (
        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)])
        .with_recipe_quotes("USD-CSA-EUR", BASIS)
    )
    
    # 2. Five years of €STR flat against SOFR, notionals exchanged at the spot.
    swap = hlx.templates.get("EUR-USD-ESTR-SOFR-BASIS").cross_currency_swap(
        date(2026, 10, 5), date(2031, 10, 5), notional=10_000_000.0, fx_rate=1.08, spread=0.0
    )
    
    # 3. One row per leg: in its own currency, in the swap's (EUR), and in dollars.
    rows = swap.leg_npvs(market, pnl_ccy="USD").to_dict()
    print(f"{'side':<8} {'ccy':<4} {'local':>12} {'in EUR':>12} {'in USD':>12}")
    for side, ccy, local, npv, pnl in zip(rows["side"], rows["ccy"], rows["npv_local"], rows["npv"], rows["npv_pnl"]):
        print(f"{side:<8} {ccy:<4} {local:>12.2f} {npv:>12.2f} {pnl:>12.2f}")
    total = swap.npv(market)
    print(f"swap npv {total:.2f} EUR")
    side     ccy         local       in EUR       in USD
    pay      USD        868.75       804.40       868.75
    receive  EUR      71503.11     71503.11     77223.36
    swap npv 72307.51 EUR

    Check it worked

    • The in EUR column adds up to the swap's value.
    • The in USD column adds up to that value converted at the spot.
    • The dollar leg's local and in USD figures are the same, because dollars are its own currency.
    • Cross-currency swap, the swap's arguments and its par spread.
    • How rates products work, which values each leg by hand on its own curve.
    • Cross-currency calibration, how the basis curve is solved.

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