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.
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(())
}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 , a cashflow in currency paid at is worth
where discounts currency under collateral in . When it is the currency's own overnight curve. When , the FX swaps fix it: an outright , in per , against spot satisfies
so the FX swaps on the cross sheet solve at their tenors, and the cross-currency basis swaps carry it further out.
API
| Symbol | Meaning | Rust | Python |
|---|---|---|---|
| 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) |
| , or under | the discount key a trade in gets | Counterparty::collateral_for |
Counterparty.collateral_for |
| 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.counterpartyreturnsNone(Rust: anOption<&Counterparty>). -
The last collateral stated wins: between
with_counterparty,with_csaandwith_discount_key. -
The cross curve needs the spot first: call
with_fx_ratesbeforewith_recipe_quoteson 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 , 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.