Esc

    The API is under testing and may change before release.

    Repack notes

    This page lists what you need to book and price a repack note: the constructor and its two sides, the gap charge, the market it needs, and an example you can run. A repack note is issued by a special-purpose vehicle (SPV) that holds a bond and swaps its cash flows into what investors want; the bank on the other side of the swap carries the gap if the bond's issuer defaults.

    Quick example

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

    This packages a five-year 3.5% EUR corporate bond, starting on 5 October 2026, in an asset swap paying EURIBOR 3M plus 60 basis points, and books the note from the bank's side and the noteholder's. The issuer has a flat 2% hazard. It shows that the two sides together hold the bond, prices the gap charge, and repeats it at four recoveries.

    Rust
    use chrono::NaiveDate;
    use helixio_docs_sheets::{EUR_CLOSE, SOFR_CLOSE_DATE};
    use helixrs::{
        AnyInstrument, AssetSwap, Book, CallableBond, CollateralSpec, CreditCurveId, Currency,
        DayCountFraction, FixedRateBond, FlatHazard, Frequency, MarketContext, Period,
        PricingRiskFactors, QuoteSheet, RepackNote, RepackRole, Survival, YieldCurve, repack_gap,
    };
    
    type Outcome<T> = Result<T, Box<dyn std::error::Error>>;
    
    const ISSUER: &str = "ACME/SeniorUnSec/MM14/EUR";
    const FACE: f64 = 1_000_000.0;
    const INDEX: &str = "EUR-EURIBOR-3M";
    
    fn date((year, month, day): (i32, u32, u32)) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn bond(issuer: Option<&CreditCurveId>) -> Outcome<CallableBond> {
        let plain = FixedRateBond::bullet(
            Currency::new("EUR")?,
            date((2026, 10, 5)),
            date((2031, 10, 5)),
            0.035,
            Frequency::Annual,
            DayCountFraction::Thirty360,
            FACE,
        )?;
        let callable = CallableBond::new(plain, Vec::new());
        Ok(match issuer {
            Some(issuer) => callable.issued_by(issuer.clone()),
            None => callable,
        })
    }
    
    fn main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let start = date((2026, 10, 5));
        let issuer: CreditCurveId = ISSUER.parse()?;
        let sheet = QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?;
        let marked = MarketContext::as_of(close)
            .with_recipe_quotes(&CollateralSpec::Currency("EUR".parse()?), &sheet)?;
        let market_recovering = |recovery: f64| -> Outcome<MarketContext> {
            let hazard = Survival::FlatHazard(FlatHazard::new(close, 0.02, "ACT/365.FIXED".parse()?)?);
            Ok(marked
                .clone()
                .with_credit_curve(issuer.clone(), hazard, Some(recovery))
                .with_hull_white(Currency::new("EUR")?, 0.03, 0.01))
        };
    
        // The market: the EUR close, plus what no recipe builds — the issuer's
        // credit, and the Hull-White parameters the gap's swap exposure is read under.
        let market = market_recovering(0.05)?;
        let swap = AssetSwap::new(
            bond(None)?,
            start,
            INDEX,
            "3M".parse::<Period>()?,
            FACE,
            100.0,
            0.0060,
        )?;
        let bank = RepackNote::new(swap.clone(), issuer.clone(), FACE)?;
        let notes = bank.clone().with_role(RepackRole::Noteholder);
        let bank_side = AnyInstrument::RepackNote(Box::new(bank.clone()));
        let note_side = AnyInstrument::RepackNote(Box::new(notes));
        println!("bank side     {:.2}", bank_side.npv(&market)?);
        println!("one note      {:.6}", note_side.npv(&market)?);
    
        // Whatever the swap does, the bank and all the notes together hold the bond,
        // and nothing else: their values add to the credit-risky bond's value.
        let book = Book::new()
            .add("SWAP", bank_side, 1.0)
            .add("NOTES", note_side, FACE)
            .attach_market(market.clone());
        let held = book.npv(close)?;
        let collateral = AnyInstrument::Callable(Box::new(bond(Some(&issuer))?)).npv(&market)?;
        println!("bank + notes  {held:.2}");
        println!("collateral    {collateral:.2}");
    
        // The gap is what the bank loses on the swap when the issuer defaults and the
        // bond's recovery does not cover it. Its charge is the sum, over default
        // dates, of the default probability times the gap expected on that date,
        // discounted to today.
        let factors_of = AnyInstrument::RepackNote(Box::new(bank.clone()));
        let charge_on = |market: &MarketContext| -> Outcome<_> {
            let lifted = market.lift::<f64>();
            let view = market.view(&lifted)?;
            let resolved = view.resolve(&factors_of.risk_factors()?)?;
            Ok(repack_gap(&bank, &resolved)?)
        };
        let gap = charge_on(&market)?;
        println!("default dates {}", gap.rows.len());
        println!("gap charge    {:.2}", gap.charge);
    
        // The bank is paid from the bond's recovery first. The more the bond recovers,
        // the less of the swap is left unpaid, and at a high enough recovery the gap
        // is gone.
        let recoveries = [0.02, 0.05, 0.10, 0.40];
        let mut charges = Vec::new();
        for recovery in recoveries {
            charges.push(charge_on(&market_recovering(recovery)?)?.charge);
        }
        for (recovery, charge) in recoveries.iter().zip(&charges) {
            println!("recovery {recovery:.2}  charge {charge:.2}");
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
    
    ISSUER = "ACME/SeniorUnSec/MM14/EUR"
    FACE = 1_000_000.0
    INDEX = "EUR-EURIBOR-3M"
    START = dt.date(2026, 10, 5)
    
    def bond(issuer=None):
        plain = hlx.products.fixed_rate_bond(
            "EUR", START, dt.date(2031, 10, 5), coupon=0.035, frequency="Annual", day_count="30/360", face=FACE
        )
        return hlx.products.callable_bond(plain, [], issuer=issuer)
    
    # The market: the EUR close, plus what no recipe builds — the issuer's credit,
    # and the Hull-White parameters the gap's swap exposure is read under.
    def market_recovering(recovery):
        return (
            hlx.MarketContext(SOFR_CLOSE_DATE)
            .with_recipe_quotes("EUR", EUR_CLOSE)
            .with_credit_curve(ISSUER, hlx.curves.SurvivalCurve.flat_hazard(SOFR_CLOSE_DATE, 0.02), recovery)
            .with_hull_white("EUR", 0.03, 0.01)
        )
    
    market = market_recovering(0.05)
    swap = hlx.products.asset_swap(bond(), START, INDEX, "3M", notional=FACE, dirty_price=100.0, spread=0.0060)
    bank = hlx.products.repack_note(swap, ISSUER, notional=FACE)
    notes = hlx.products.repack_note(swap, ISSUER, notional=FACE, repack_role=hlx.products.RepackRole.Noteholder)
    print(f"bank side     {bank.npv(market):.2f}")
    print(f"one note      {notes.npv(market):.6f}")
    
    # The bank and all the notes together hold the bond, and nothing else.
    book = hlx.Book().add("SWAP", bank, 1.0).add("NOTES", notes, FACE)
    book.attach_market(market)
    held = book.npv(as_of=SOFR_CLOSE_DATE)
    collateral = bond(issuer=ISSUER).npv(market)
    print(f"bank + notes  {held:.2f}")
    print(f"collateral    {collateral:.2f}")
    
    # The gap is what the bank loses on the swap when the issuer defaults and the
    # bond's recovery does not cover it, summed over default dates.
    gap = hlx.credit.repack_gap(bank, market)
    rows = gap.rows().to_dict()
    print(f"default dates {len(rows['date'])}")
    print(f"gap charge    {gap.charge():.2f}")
    
    # The bank is paid from the bond's recovery first, so the more the bond
    # recovers, the smaller the gap.
    recoveries = (0.02, 0.05, 0.10, 0.40)
    charges = [hlx.credit.repack_gap(bank, market_recovering(r)).charge() for r in recoveries]
    for r, charge in zip(recoveries, charges):
        print(f"recovery {r:.2f}  charge {charge:.2f}")
    bank side     26190.12
    one note      0.945024
    bank + notes  971213.75
    collateral    971213.75
    default dates 20
    gap charge    403.15
    recovery 0.02  charge 1347.11
    recovery 0.05  charge 403.15
    recovery 0.10  charge 27.38
    recovery 0.40  charge 0.00

    The bank's side plus every note is the credit-risky bond, to the cent. The gap charge sums each default date's probability times the gap expected then, discounted. The more the bond recovers, the less of the swap is left unpaid, and at a 40% recovery the gap is gone.

    How it's priced

    With the bank's swap BB, nn notes VnoteV_{\text{note}}, and over default dates tkt_k the probability qkq_k of a default in the period, the gap GkG_k expected then and the discount factor D(tk)D(t_k):

    B+n Vnote=Vbondrisky,charge=∑kqk Gk D(tk).B + n\,V_{\text{note}} = V_{\text{bond}}^{\text{risky}}, \qquad \text{charge} = \sum_k q_k\,G_k\,D(t_k).

    How the flows run through the vehicle, and where the gap comes from, is covered in how bonds work.

    API

    Argument Type Default Meaning
    swap asset swap Instrument required the bond packaged against the floating index, from products.asset_swap
    issuer str required the bond issuer's credit key, which the market needs a credit curve for
    notional float required the notes' total face
    repack_role products.RepackRole or str "swap_counterparty" which side you book: the bank as swap counterparty, or RepackRole.Noteholder, one note per unit of face
    engine products.RepackEngine or str "independent" how the gap is priced: Independent of rates, or Correlated through the issuer's Gaussian intensity, which needs its intensity parameters
    trigger products.ValuationTrigger None an optional valuation trigger

    products.repack_note(swap, issuer, *, notional, repack_role, engine, trigger) books it; in Rust, RepackNote::new(swap, issuer, notional)?, with .with_role(RepackRole::Noteholder) for the notes. credit.repack_gap(note, market) prices the gap.

    Symbol Meaning Rust Python
    BB, VnoteV_{\text{note}} the note from either side RepackNote::new(..)?.with_role(..) products.repack_note(.., repack_role=..)
    VbondriskyV_{\text{bond}}^{\text{risky}} the bond, tagged to its issuer CallableBond::issued_by(credit) products.callable_bond(.., issuer=..)
    qkq_k, GkG_k, D(tk)D(t_k) each default date's row repack_gap(&note, &resolved)?.rows credit.repack_gap(note, market).rows()
    charge the sum of the rows GapCharge::charge GapCharge.charge()

    Defaults and limits

    • Market inputs: the currency's curves, the issuer's credit curve with its recovery, and Hull-White parameters for the currency, which the swap's exposure at each default date is read under.
    • Recovery: the bank is paid from the bond's recovery first, so the charge falls as recovery rises.
    • Rows: each row of rows() carries date, default_probability, expected_gap and contribution.
    • Monte Carlo: the gap can also be simulated, with crises and a valuation trigger, through simulate_repack_gap; this page shows the analytic charge.

    See also

    • Source: crates/helixrs/src/instruments/repack.rs, crates/helixrs/src/models/repack_gap.rs
    • Related pages:
      • How bonds work, for the structure and the gap.
      • Asset swap, for the swap inside the vehicle.
      • Survival curves, for the issuer's default probabilities.
      • Hull-White, for the model the swap's exposure is read under.

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