Esc

    The API is under testing and may change before release.

    Bond total return swap

    This page lists what you need to book and value a bond total return swap (TRS): the bond_total_return_swap call and its funding arguments, how each reset period is valued, and what happens when a valuation date falls after a reset. A bond TRS passes a bond's coupons and price change to one side in exchange for funding on the cash the bond would cost.

    Quick example

    The example reads its quotes from the desk sheets; load them once and it runs as shown.

    This swaps the return on 10 million of a ten-year 4.25% bond, struck at 99 and resetting quarterly, funded on USD GC. It values the swap on the close, then on a date a month after the first reset, where the swap has to be restated from that reset. Restated at the price struck then, 98, it's funded at a fixed margin, and one point less margin is checked by hand.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS};
    use helixrs::{
        AnyInstrument, BondTotalReturnSwap, CollateralSpec, Curve, DayCountFraction, FixedRateBond,
        FixingKey, FixingStore, Frequency, MarketContext, QuoteSheet, RepoSpread, YieldCurve,
    };
    
    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 market_on(day: NaiveDate) -> Outcome<MarketContext> {
        let sheet = QuoteSheet::from_rows(USD_SOFR_CLOSE.iter().copied())?;
        let prints: Vec<(FixingKey, NaiveDate, f64)> = USD_SOFR_PRINTS
            .iter()
            .map(|(index, d, value)| (FixingKey::Typed((*index).to_owned()), date(*d), *value))
            .collect();
        let gc = RepoSpread::new(day, &[(date((2027, 10, 1)), -0.0010)], DayCountFraction::Act360)?;
        Ok(MarketContext::as_of(day)
            .with_fixings(FixingStore::from_observations_checked(&prints)?)
            .with_recipe_quotes(&CollateralSpec::Currency("USD".parse()?), &sheet)?
            .with_repo_spread("USD", Curve::RepoSpread(gc), "discount(USD)".parse()?))
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let resets = vec![date((2026, 10, 2)), date((2027, 1, 4)), date((2027, 4, 1))];
        let restarted = resets[1..].to_vec();
        let bond = FixedRateBond::bullet(
            "USD".parse()?,
            date((2025, 8, 15)),
            date((2035, 8, 15)),
            0.0425,
            Frequency::SemiAnnual,
            DayCountFraction::ActActIcma,
            100.0,
        )?;
    
        // Struck at 99 on 10 million, resetting quarterly, funded on USD GC.
        let trs = AnyInstrument::BondTotalReturnSwap(Box::new(BondTotalReturnSwap::new(
            bond.clone(),
            10_000_000.0,
            99.0,
            resets,
            "USD",
        )?));
        let npv = trs.npv(&market_on(close)?)?;
        println!("npv  {npv:.2}");
        // The total-return receiver starts at 99 on a bond the curve prices lower, so
        // the price leg runs against it and the position is worth less than nothing.
    
        // A month after the first reset, the swap needs the price struck then.
        let reset_refusal = trs
            .npv(&market_on(date((2027, 2, 1)))?)
            .expect_err("a reset in the past must be restated")
            .to_string();
        println!(
            "refused, naming the 2027-01-04 reset: {}",
            if reset_refusal.contains("2027-01-04") { "yes" } else { "no" }
        );
    
        // Restated from the January reset at the price struck then, 98, the swap's
        // first funding period has already begun. Floating with no index to read,
        // that period's rate cannot be known, and the swap is refused by name.
        let later = market_on(date((2027, 2, 1)))?;
        let floating = AnyInstrument::BondTotalReturnSwap(Box::new(BondTotalReturnSwap::new(
            bond.clone(),
            10_000_000.0,
            98.0,
            restarted.clone(),
            "USD",
        )?));
        let funding_refusal = floating
            .npv(&later)
            .expect_err("a begun period floating on no index must be refused")
            .to_string();
        println!(
            "refused, asking for a fixed margin: {}",
            if funding_refusal.contains("fixed margin") { "yes" } else { "no" }
        );
    
        // Zero gearing funds at the margin alone: fixed coupons, known in advance.
        let at_margin = |margin: f64| -> Outcome<AnyInstrument> {
            Ok(AnyInstrument::BondTotalReturnSwap(Box::new(
                BondTotalReturnSwap::new(bond.clone(), 10_000_000.0, 98.0, restarted.clone(), "USD")?
                    .with_funding_gearing(0.0)
                    .with_funding_margin(margin),
            )))
        };
        let (four, three) = (at_margin(0.04)?.npv(&later)?, at_margin(0.03)?.npv(&later)?);
        // A point less margin saves one point on the financed cash, N·P0/100, over the
        // ACT/360 period, paid at its end and discounted there.
        let (start, end) = (restarted[0], restarted[1]);
        let (discount, _) = later.curve(&"discount(USD)".parse()?).expect("the USD curve");
        let saving = 10_000_000.0 * 98.0 / 100.0 * 0.01 * (end - start).num_days() as f64 / 360.0
            * discount.view::<f64>().discount(end)?;
        println!("npv at 4%   {four:.2}");
        println!("a point     {:.2}", three - four);
        println!("by hand     {saving:.2}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import SOFR_CLOSE_DATE, USD_SOFR_CLOSE, USD_SOFR_PRINTS
    
    RESETS = [dt.date(2026, 10, 2), dt.date(2027, 1, 4), dt.date(2027, 4, 1)]
    LATER = dt.date(2027, 2, 1)
    
    def market_on(day):
        gc = hlx.curves.RepoSpreadCurve(day, [(dt.date(2027, 10, 1), -0.0010)])
        return (
            hlx.MarketContext(day)
            .with_fixings(USD_SOFR_PRINTS)
            .with_recipe_quotes("USD", USD_SOFR_CLOSE)
            .with_repo_spread("USD", gc, "discount(USD)")
        )
    
    bond = hlx.products.fixed_rate_bond(
        "USD", dt.date(2025, 8, 15), dt.date(2035, 8, 15),
        coupon=0.0425, frequency="Semiannual", day_count="ACT/ACT.ICMA",
    )
    
    # Struck at 99 on 10 million, resetting quarterly, funded on USD GC.
    trs = hlx.products.bond_total_return_swap(
        bond, notional=10_000_000.0, initial_price=99.0, reset_schedule=RESETS, repo_key="USD"
    )
    npv = trs.npv(market_on(SOFR_CLOSE_DATE))
    print(f"npv  {npv:.2f}")
    # The total-return receiver starts at 99 on a bond the curve prices lower, so
    # the price leg runs against it and the position is worth less than nothing.
    
    # A month after the first reset, the swap needs the price struck then.
    try:
        trs.npv(market_on(LATER))
    except hlx.PricingError as refused:
        reset_refusal = str(refused)
    else:
        raise AssertionError("a reset in the past must be restated")
    print(f"refused, naming the 2027-01-04 reset: {'yes' if '2027-01-04' in reset_refusal else 'no'}")
    
    # Restated from the January reset at the price struck then, 98, the swap's first
    # funding period has already begun. Floating with no index to read, that
    # period's rate cannot be known, and the swap is refused by name.
    restarted = RESETS[1:]
    later = market_on(LATER)
    floating = hlx.products.bond_total_return_swap(
        bond, notional=10_000_000.0, initial_price=98.0, reset_schedule=restarted, repo_key="USD"
    )
    try:
        floating.npv(later)
    except hlx.PricingError as refused:
        funding_refusal = str(refused)
    else:
        raise AssertionError("a begun period floating on no index must be refused")
    print(f"refused, asking for a fixed margin: {'yes' if 'fixed margin' in funding_refusal else 'no'}")
    
    def at_margin(margin):
        # Zero gearing funds at the margin alone: fixed coupons, known in advance.
        return hlx.products.bond_total_return_swap(
            bond, notional=10_000_000.0, initial_price=98.0, reset_schedule=restarted, repo_key="USD",
            funding_gearing=0.0, funding_margin=margin,
        )
    
    four, three = at_margin(0.04).npv(later), at_margin(0.03).npv(later)
    # A point less margin saves one point on the financed cash, N·P0/100, over the
    # ACT/360 period, paid at its end and discounted there.
    start, end = restarted
    saving = 10_000_000.0 * 98.0 / 100.0 * 0.01 * (end - start).days / 360.0 * later.curve("discount(USD)").discount(end)
    print(f"npv at 4%   {four:.2f}")
    print(f"a point     {three - four:.2f}")
    print(f"by hand     {saving:.2f}")
    npv  -312168.69
    refused, naming the 2027-01-04 reset: yes
    refused, asking for a fixed margin: yes
    npv at 4%   -76866.09
    a point     23529.73
    by hand     23529.73

    The total-return receiver starts at 99 on a bond the curve prices lower, so the price leg runs against it and the swap is worth less than nothing. After the reset, a swap still struck from the original date is refused, naming the reset. Restated but floating on no index, its first funding period has begun with no rate to read, so it's refused too. At a fixed margin it prices, and a point of margin is worth exactly the financed cash's point over the period.

    How it's priced

    Over reset period [tk−1,tk][t_{k-1}, t_k], on notional NN, with bond price PkP_k per 100, the total-return receiver gets

    N100(Pk−Pk−1+coupons paid in (tk−1,tk])\frac{N}{100}\Bigl(P_k - P_{k-1} + \text{coupons paid in } (t_{k-1}, t_k]\Bigr)

    and pays funding on the financed cash NP0/100N P_0 / 100: NP0100 (gfk+m) αk\frac{N P_0}{100}\,(g f_k + m)\,\alpha_k at the period's funding rate fkf_k, scaled by the gearing gg, plus the margin mm, over the ACT/360 fraction αk\alpha_k. Each period's net payment is discounted on the funding curve, and forward bond prices come from the repo curve, as for a bond forward. Why the swap must be restated after a reset is covered in how financing works.

    API

    products.bond_total_return_swap(bond, *, notional, initial_price, reset_schedule, repo_key, ...):

    Argument Type Default Meaning
    bond FixedRateBond required the bond whose return is swapped
    notional float required the bond face the return is paid on
    initial_price float required the clean price per 100 the first period starts from
    reset_schedule list[date] required the reset dates in ascending order, the first being the start; at least two
    repo_key str required the repo curve the bond's forward prices are read on
    paying_total_return bool False pays the bond's return and receives the funding, instead of the reverse
    funding_margin float 0.0 the margin mm over the funding rate
    funding_index str none the index the funding floats on; without one it floats on the bond currency's cash collateral rate, ACT/360
    funding_gearing float 1.0 the gearing gg on the funding rate; 0.0 funds at the margin alone
    funding_notional float notional × initial_price / 100 the cash the funding accrues on
    funding_rate float none a fixed funding rate, in place of a margin, a gearing and an index

    bonds.fair_margin(trs, market) returns the funding margin that values the swap at zero. In Rust the swap is BondTotalReturnSwap::new(bond, notional, initial_price, resets, repo_key) in AnyInstrument::BondTotalReturnSwap, with .with_funding_margin(m), .with_funding_gearing(g) and .paying_total_return().

    Symbol Meaning Rust Python
    NN, P0P_0, the resets the swap BondTotalReturnSwap::new products.bond_total_return_swap
    mm the funding margin .with_funding_margin(m) funding_margin=
    gg the funding gearing .with_funding_gearing(g) funding_gearing=
    the value net of both legs AnyInstrument::npv Instrument.npv

    Defaults and limits

    • Side: the swap receives the total return and pays the funding unless paying_total_return=True.
    • What the market needs: the bond's discount curve and a repo spread curve under repo_key.
    • A reset in the past: a market dated after a reset doesn't hold the price struck then, so the swap is refused with a PricingError naming the reset. Restate it from that reset, with the struck price as initial_price.
    • A begun funding period: if the first funding period started before the valuation date and floats on no index, its rate can't be read, and the swap is refused with a PricingError asking for a fixed margin. Give a funding_index, or fund at the margin alone with funding_gearing=0.0, or set a funding_rate.
    • Coupons: a coupon the bond pays during a period is passed to the receiver at the period's reset.
    • Baskets: a total return swap on several bonds is products.basket_total_return_swap.

    See also

    • Source: crates/helixrs/src/instruments/financing.rs
    • Related pages:
      • How financing works, for resets and restating.
      • Bond forward, for the forward prices the price leg reads.
      • Repo, for the funding curve.
      • Equity total return swap, for the same swap on shares.

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