Book a trade with a CSA counterparty
You'll book a USD swap facing a counterparty whose CSA posts EUR collateral, so it discounts on the USD-under-EUR curve, and check that's the curve it values on.
Before you start
- helixio installed: see installing.
- Quotes for both currencies' overnight curves, the EUR/USD spot, and a cross sheet: FX swaps quoted as outrights in EUR per USD, and EUR/USD basis swaps. The example states them inline.
- The counterparty's acronym and the overnight index of the collateral its CSA posts.
Steps
- State the close, the quotes, the spot and the cross sheet.
- Build the market. Add the spot with
with_fx_ratesbefore the cross sheet, thenwith_recipe_quotes("USD-CSA-EUR", cross)builds the curve a USD trade discounts on under a EUR CSA. - Put the counterparty master on the market with
with_counterparties, each counterparty asCounterparty(acronym, overnight_index). - Look the counterparty up with
market.counterparty(acronym)and book the trade facing it withwith_counterparty. - Value it, and read its discount curve from
market_data_needs.
The comments in the program match these steps.
Rust
use chrono::NaiveDate;
use helixrs::{
CollateralSpec, Counterparties, Counterparty, CsaSpec, CurrencyPair, FxRates, MarketContext,
QuoteSheet, Termination, market_data_needs, template,
};
type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
fn main() -> Outcome<()> {
// 1. The close, the quotes for both currencies, the EUR/USD spot, and the
// cross sheet: FX swaps as outrights in EUR per USD, then the basis.
let close = NaiveDate::from_ymd_opt(2026, 10, 1).expect("a real date");
let usd_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 cross = [
("USD-EUR-FXSWAP", "3M", 0.8496),
("USD-EUR-FXSWAP", "6M", 0.8441),
("USD-EUR-FXSWAP", "1Y", 0.8337),
("EUR-USD-ESTR-SOFR-BASIS", "2Y", -0.0009),
("EUR-USD-ESTR-SOFR-BASIS", "5Y", -0.0013),
("EUR-USD-ESTR-SOFR-BASIS", "10Y", -0.0012),
];
// 2. Build the market, with the curve a USD trade discounts on under a EUR CSA.
let market = MarketContext::as_of(close)
.with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &QuoteSheet::from_rows(usd_close)?)?
.with_recipe_quotes(&CollateralSpec::Currency("EUR".parse()?), &QuoteSheet::from_rows(eur_close)?)?
.with_fx_rates(FxRates::from_quotes(&[("EUR/USD".parse::<CurrencyPair>()?, 1.1720)])?)
.with_recipe_quotes(&"USD-CSA-EUR".parse::<CollateralSpec>()?, &QuoteSheet::from_rows(cross)?)?;
// 3. Put the counterparty master on the market: each counterparty's acronym
// and its CSA, named by the overnight index of the collateral it posts.
let market = market
.with_counterparties(Counterparties::new().with(Counterparty::new("EXCPTY", CsaSpec::new("EUR-ESTR"))));
// 4. Look the counterparty up and book the trade facing it.
let counterparty = market.counterparty("EXCPTY").expect("in the master").clone();
let draft = template("USD-SOFR-OIS")?
.swap(close)?
.with_maturity("3Y".parse::<Termination>()?)?
.with_notional(20_000_000.0)
.with_fixed_rate(0.0462);
let swap = draft.clone().with_counterparty(&counterparty).build()?;
// 5. Value it, and see which curve it discounts on.
let needs = market_data_needs(&swap)?.needs();
let discount = &needs.iter().find(|(role, _)| role == "discount").expect("a discount curve").1;
let npv = swap.npv(&market)?;
println!("discounts on {discount}");
println!("npv facing EXCPTY {npv:.2}");
println!("npv, USD CSA {:.2}", draft.clone().build()?.npv(&market)?);
let fair = draft.clone().with_counterparty(&counterparty).at_par(&market)?;
println!("par rate facing {:.8}", fair.par_rate(&market)?);
println!("npv at par {:.2}", fair.npv(&market)?.abs());
Ok(())
}Python
from datetime import date
import helixio as hlx
# 1. The close, the quotes for both currencies, the EUR/USD spot, and the
# cross sheet: FX swaps as outrights in EUR per USD, then the basis.
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},
}
CROSS = {
"USD-EUR-FXSWAP": {"3M": 0.8496, "6M": 0.8441, "1Y": 0.8337},
"EUR-USD-ESTR-SOFR-BASIS": {"2Y": -0.0009, "5Y": -0.0013, "10Y": -0.0012},
}
# 2. Build the market, with the curve a USD trade discounts on under a EUR CSA.
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.1720)])
.with_recipe_quotes("USD-CSA-EUR", CROSS)
)
# 3. Put the counterparty master on the market: each counterparty's acronym and
# its CSA, named by the overnight index of the collateral it posts.
market = market.with_counterparties([hlx.Counterparty("EXCPTY", "EUR-ESTR")])
# 4. Look the counterparty up and book the trade facing it.
counterparty = market.counterparty("EXCPTY")
draft = (
hlx.templates.get("USD-SOFR-OIS")
.swap(SOFR_CLOSE_DATE)
.with_maturity("3Y")
.with_notional(20_000_000.0)
.with_fixed_rate(0.0462)
)
swap = draft.with_counterparty(counterparty).build()
# 5. Value it, and see which curve it discounts on.
needs = dict(swap.market_data_needs())
npv = swap.npv(market)
print(f"discounts on {needs['discount']}")
print(f"npv facing EXCPTY {npv:.2f}")
print(f"npv, USD CSA {draft.build().npv(market):.2f}")
fair = draft.with_counterparty(counterparty).at_par(market)
print(f"par rate facing {fair.par_rate(market):.8f}")
print(f"npv at par {abs(fair.npv(market)):.2f}")discounts on USD-CSA-EUR
npv facing EXCPTY -33802.45
npv, USD CSA -33336.72
par rate facing 0.04680572
npv at par 0.00
Check it worked
- The trade discounts on
USD-CSA-EUR. - Its value differs from the same trade under a USD CSA.
- Struck at par facing the counterparty, it's worth nothing on the market.
Related
- Counterparties and CSA, the reference for counterparties, the cross curve and what its recipe refuses.
- How trades are built, for why collateral decides the curve.
- Cross-currency calibration, for solving the cross curve explicitly.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.