Esc

    The API is under testing and may change before release.

    Counterparties and CSA

    This page lists how a counterparty and its credit support annex (CSA) decide the curve a trade discounts on: the counterparty, the discount key it hands a trade, the cross curve that key reads, a counterparty master on the market, and what the cross curve's recipe refuses. Why collateral decides the discount curve is covered in how trades are built.

    Quick example

    This sets up two counterparties under different CSAs, builds a market with the USD-under-EUR cross curve, shows two misquoted FX swaps being refused, books one swap three ways, and looks the counterparties up in a master the market carries. Booking and striking a trade facing a counterparty, step by step, is the how-to book a trade with a CSA counterparty. The example reads its quotes from the desk sheets; load them once and it runs as shown.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE, Sheet, USD_SOFR_CLOSE};
    use helixrs::CsaSpec;
    use helixrs::{
        CollateralSpec, Counterparties, Counterparty, CurrencyPair, FxRates, MarketContext, QuoteSheet,
        Termination, template,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    // The USD-under-EUR curve is solved from EUR/USD FX swaps, quoted as outrights
    // in EUR per USD, beside the cross-currency basis the EUR-under-USD curve reads.
    const CROSS: Sheet = &[
        ("USD-EUR-FXSWAP", "3M", 0.8502),
        ("USD-EUR-FXSWAP", "6M", 0.8450),
        ("USD-EUR-FXSWAP", "1Y", 0.8345),
        ("EUR-USD-ESTR-SOFR-BASIS", "2Y", -0.0010),
        ("EUR-USD-ESTR-SOFR-BASIS", "5Y", -0.0012),
        ("EUR-USD-ESTR-SOFR-BASIS", "10Y", -0.0011),
    ];
    
    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);
    
        // A counterparty is an acronym and a CSA, named by its overnight index. The key
        // a trade discounts under is its own currency where that is the CSA's, and the
        // currency under the CSA's otherwise.
        let megabank = Counterparty::new("MEGABANK", CsaSpec::new("EUR-ESTR"));
        let acme = Counterparty::new("ACME", CsaSpec::new("USD-SOFR"));
        let usd = "USD".parse()?;
        println!(
            "MEGABANK posts {}: a USD trade discounts on {}",
            megabank.collateral_currency(),
            megabank.collateral_for(usd)
        );
        println!(
            "ACME posts {}: a USD trade discounts on {}",
            acme.collateral_currency(),
            acme.collateral_for(usd)
        );
    
        // Both closes, the spot, and the cross sheet: each curve from its recipe.
        let base = MarketContext::as_of(close)
            .with_recipe_quotes(&CollateralSpec::Currency(usd), &QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?)?
            .with_recipe_quotes(&CollateralSpec::Currency("EUR".parse()?), &QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?)?
            .with_fx_rates(FxRates::from_quotes(&[("EUR/USD".parse::<CurrencyPair>()?, 1.17)])?);
        let market = base
            .clone()
            .with_recipe_quotes(&"USD-CSA-EUR".parse::<CollateralSpec>()?, &QuoteSheet::from_rows(CROSS.iter().copied())?)?;
        let mut keys: Vec<String> = market
            .contents()
            .into_iter()
            .filter(|(role, _)| *role == "discount")
            .map(|(_, key)| key)
            .collect();
        keys.sort();
        println!("discount keys {}", keys.join(", "));
    
        // An FX swap rung reads an outright in EUR per USD. Forward points, or the rate
        // quoted the other way round, are refused before anything is solved, naming the
        // template, the tenor and the spot.
        let mut refusals = Vec::new();
        for (misquote, why) in [(0.0042, "forward points"), (1.17, "the other way round")] {
            let rows = CROSS.iter().map(|&(template, tenor, quote)| {
                (template, tenor, if (template, tenor) == ("USD-EUR-FXSWAP", "1Y") { misquote } else { quote })
            });
            let refused = base
                .clone()
                .with_recipe_quotes(&"USD-CSA-EUR".parse::<CollateralSpec>()?, &QuoteSheet::from_rows(rows)?)
                .expect_err("a misquoted outright must be refused");
            refusals.push((why, refused.to_string()));
            println!("refused 1Y at {misquote}: {why}");
        }
    
        // One draft, three ways to book it. Facing ACME is the default a USD swap
        // already has; facing MEGABANK moves it to the cross curve.
        let draft = template("USD-SOFR-OIS")?
            .swap(close)?
            .with_maturity("2Y".parse::<Termination>()?)?
            .with_notional(10_000_000.0)
            .with_fixed_rate(0.0450);
        let under_eur = draft.clone().with_counterparty(&megabank).build()?.npv(&market)?;
        let under_usd = draft.clone().with_counterparty(&acme).build()?.npv(&market)?;
        let house = draft.build()?.npv(&market)?;
        println!("facing MEGABANK  {under_eur:.2}");
        println!("facing ACME      {under_usd:.2}");
        println!("no counterparty  {house:.2}");
    
        // A desk resolves counterparties by acronym from a master the market carries.
        // An acronym the master does not hold is absent, not an error.
        let with_master = market.with_counterparties(Counterparties::new().with(megabank).with(acme));
        let facing = with_master.counterparty("MEGABANK").expect("in the master").clone();
        let nobody = with_master.counterparty("NOBODY").is_some();
        println!("MEGABANK in the master posts {}", facing.collateral_currency());
        println!("NOBODY in the master: {}", if nobody { "yes" } else { "no" });
    
        Ok(())
    }
    Python
    import helixio as hlx
    from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE, USD_SOFR_CLOSE
    
    # The USD-under-EUR curve is solved from EUR/USD FX swaps, quoted as outrights
    # in EUR per USD, beside the cross-currency basis the EUR-under-USD curve reads.
    CROSS = {
        "USD-EUR-FXSWAP": {"3M": 0.8502, "6M": 0.8450, "1Y": 0.8345},
        "EUR-USD-ESTR-SOFR-BASIS": {"2Y": -0.0010, "5Y": -0.0012, "10Y": -0.0011},
    }
    
    # A counterparty is an acronym and a CSA, named by its overnight index. The key
    # a trade discounts under is its own currency where that is the CSA's, and the
    # currency under the CSA's otherwise.
    megabank = hlx.Counterparty("MEGABANK", "EUR-ESTR")
    acme = hlx.Counterparty("ACME", "USD-SOFR")
    print(f"MEGABANK posts {megabank.collateral_currency()}: a USD trade discounts on {megabank.collateral_for('USD')}")
    print(f"ACME posts {acme.collateral_currency()}: a USD trade discounts on {acme.collateral_for('USD')}")
    
    # Both closes, the spot, and the cross sheet: each curve from its recipe.
    base = (
        hlx.MarketContext(SOFR_CLOSE_DATE)
        .with_recipe_quotes("USD", USD_SOFR_CLOSE)
        .with_recipe_quotes("EUR", EUR_CLOSE)
        .with_fx_rates([("EUR", "USD", 1.17)])
    )
    market = base.with_recipe_quotes("USD-CSA-EUR", CROSS)
    keys = sorted(key for role, key in market.contents() if role == "discount")
    print(f"discount keys {', '.join(keys)}")
    
    # An FX swap rung reads an outright in EUR per USD. Forward points, or the rate
    # quoted the other way round, are refused before anything is solved, naming the
    # template, the tenor and the spot.
    refusals = []
    for misquote, why in ((0.0042, "forward points"), (1.17, "the other way round")):
        sheet = {"USD-EUR-FXSWAP": {**CROSS["USD-EUR-FXSWAP"], "1Y": misquote}}
        try:
            base.with_recipe_quotes("USD-CSA-EUR", {**CROSS, **sheet})
        except ValueError as refused:
            refusals.append((why, str(refused)))
        else:
            raise AssertionError("a misquoted outright must be refused")
        print(f"refused 1Y at {misquote}: {why}")
    
    # One draft, three ways to book it. Facing ACME is the default a USD swap
    # already has; facing MEGABANK moves it to the cross curve.
    draft = (
        hlx.templates.get("USD-SOFR-OIS")
        .swap(SOFR_CLOSE_DATE)
        .with_maturity("2Y")
        .with_notional(10_000_000.0)
        .with_fixed_rate(0.0450)
    )
    under_eur = draft.with_counterparty(megabank).build().npv(market)
    under_usd = draft.with_counterparty(acme).build().npv(market)
    house = draft.build().npv(market)
    print(f"facing MEGABANK  {under_eur:.2f}")
    print(f"facing ACME      {under_usd:.2f}")
    print(f"no counterparty  {house:.2f}")
    
    # A desk resolves counterparties by acronym from a master the market carries.
    # An acronym the master does not hold is absent, not an error.
    with_master = market.with_counterparties([megabank, acme])
    facing = with_master.counterparty("MEGABANK")
    print(f"MEGABANK in the master posts {facing.collateral_currency()}")
    print(f"NOBODY in the master: {'yes' if with_master.counterparty('NOBODY') else 'no'}")
    MEGABANK posts EUR: a USD trade discounts on USD-CSA-EUR
    ACME posts USD: a USD trade discounts on USD
    discount keys EUR, EUR-CSA-USD, USD, USD-CSA-EUR
    refused 1Y at 0.0042: forward points
    refused 1Y at 1.17: the other way round
    facing MEGABANK  -17162.87
    facing ACME      -17073.73
    no counterparty  -17073.73
    MEGABANK in the master posts EUR
    NOBODY in the master: no

    The trade facing the USD-CSA counterparty is the trade with no counterparty; the one facing the EUR-CSA bank discounts on the cross curve, so it's worth a little less.

    How it's priced

    Under a CSA posting collateral in currency cc, a cashflow XX in currency dd paid at TT is worth

    V=X⋅Dd∣c(T),V = X \cdot D_{d \mid c}(T),

    where Dd∣cD_{d \mid c} discounts currency dd under collateral in cc. When c=dc = d it is the currency's own overnight curve. When c≠dc \ne d, the FX swaps fix it: an outright F(T)F(T), in cc per dd, against spot SS satisfies

    F(T)=S⋅Dd∣c(T)Dc(T),F(T) = S \cdot \frac{D_{d \mid c}(T)}{D_{c}(T)},

    so the FX swaps on the cross sheet solve Dd∣cD_{d \mid c} at their tenors, and the cross-currency basis swaps carry it further out.

    API

    Symbol Meaning Rust Python
    cc the CSA's collateral, named by its overnight index CsaSpec::new(index_code) the index code passed to Counterparty
    the counterparty an acronym and its CSA Counterparty::new(acronym, csa) Counterparty(acronym, index_code)
    dd, or dd under cc the discount key a trade in dd gets Counterparty::collateral_for Counterparty.collateral_for
    Dd∣cD_{d \mid c} the cross curve, from FX swaps and basis MarketContext::with_recipe_quotes on the USD-CSA-EUR key, after with_fx_rates MarketContext.with_recipe_quotes("USD-CSA-EUR", sheet) after MarketContext.with_fx_rates
    facing a draft booked against a counterparty ProductBuilder::with_counterparty ProductBuilder.with_counterparty
    the master counterparties resolved by acronym MarketContext::with_counterparties, MarketContext::counterparty MarketContext.with_counterparties, MarketContext.counterparty
    Argument Type Default Meaning
    Counterparty(acronym, csa) str, str required a counterparty; csa is the overnight index of the collateral it posts, such as "EUR-ESTR"
    Counterparty.acronym() none the counterparty's short identifier
    Counterparty.csa() none the CSA's overnight index code
    Counterparty.collateral_currency() none the currency the CSA posts
    Counterparty.collateral_for(currency) str required the discount key for a trade in currency: "USD" under a USD CSA, "USD-CSA-EUR" under a EUR one
    MarketContext.with_recipe_quotes(key, sheet) str, dict required with key a cross key such as "USD-CSA-EUR", solves the cross curve from FX swaps (USD-EUR-FXSWAP, outrights in EUR per USD) and basis swaps
    MarketContext.with_counterparties(counterparties) list[Counterparty] required the market carrying a counterparty master
    MarketContext.counterparty(acronym) str required the counterparty under acronym, or None
    ProductBuilder.with_counterparty(counterparty) Counterparty none the draft discounting under the counterparty's CSA

    Defaults and limits

    • No counterparty means the trade's own currency: a draft with no counterparty discounts on its currency's overnight curve, the same as facing a counterparty whose CSA posts that currency.

    • An unknown acronym is absent, not an error: MarketContext.counterparty returns None (Rust: an Option<&Counterparty>).

    • The last collateral stated wins: between with_counterparty, with_csa and with_discount_key.

    • The cross curve needs the spot first: call with_fx_rates before with_recipe_quotes on a cross key.

    • A misquoted FX swap is refused before anything is solved. The refusal names the template, the tenor, the quote and the spot. A quote is refused when it is:

      • not finite and positive;
      • more than three times spot, or under a third of it;
      • more than 10% from spot and nearer 1/S1/S, so quoted the other way round.

      Forward points are refused by the second rule. A high-yield forward far from spot, but on the right side of it, still builds.

    See also

    • Source: crates/helixrs/src/statics/counterparty.rs, crates/helixrs/src/statics/collateral.rs, crates/helixrs/src/products.rs
    • Related pages:
      • How trades are built, for why collateral decides the discount curve.
      • Book a trade with a CSA counterparty, the same steps as a task.
      • Cross-currency calibration, for solving the cross curve explicitly instead of through the recipe; use it when you need the rungs or the solve's report.
      • Quote sheets and recipes, for the recipes a key builds from.
      • Templates and builders, for the draft a counterparty is set on.

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