Esc

    The API is under testing and may change before release.

    Multi-reset coupons

    This page lists what you need for a coupon that fixes its index more than once and pays once: the coupon and strip constructors, the four ways the resets combine, and how a short final sub-period reads the index.

    Quick example

    This values one six-month coupon on EURIBOR 3M, which resets twice, under each compounding method with a 100 basis point spread, then with no spread, then over five months, where the last sub-period is two months long. 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};
    use helixrs::{
        AnyCoupon, AnyIndex, CollateralSpec, Direction, Leg, MarketContext, MultiResetCoupon,
        QuoteSheet, ResetCompounding, StubRule, index, leg_cashflow_records,
    };
    
    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 main() -> Outcome<()> {
        let close = date(SOFR_CLOSE_DATE);
        let eur = CollateralSpec::Currency("EUR".parse()?);
        let sheet = QuoteSheet::from_rows(EUR_CLOSE.iter().copied())?;
        let market = MarketContext::as_of(close).with_recipe_quotes(&eur, &sheet)?;
        let Some(AnyIndex::Ibor(euribor)) = index("EUR-EURIBOR-3M") else {
            panic!("a registered term index");
        };
    
        // The coupon's rate over 5 January 2027 to `end`, read from its own table.
        let rate =
            |end: NaiveDate, method: ResetCompounding, spread: f64, stub: StubRule| -> Outcome<f64> {
                let coupon = MultiResetCoupon::on_index(
                    &euribor,
                    date((2027, 1, 5)),
                    end,
                    end,
                    10_000_000.0,
                    "ACT/360".parse()?,
                    "EUR".parse()?,
                    "3M".parse()?,
                    1.0,
                    spread,
                    method,
                    &stub,
                )?;
                let leg = Leg::new(vec![AnyCoupon::MultiReset(coupon)], Direction::Receive);
                let projections = leg.projections();
                let table = market.priced(&leg.risk_factors(&eur, &projections), |view| {
                    Ok(leg_cashflow_records(&leg, &view.disc, view, &view.fixings, close)?)
                })?;
                Ok(table["rate"].numbers().expect("numbers")[0])
            };
    
        // Two quarterly resets, a 100 basis point spread, four ways of combining them.
        let six_months = date((2027, 7, 5));
        let methods = [
            ("straight", ResetCompounding::Straight),
            ("flat", ResetCompounding::Flat),
            ("spread_exclusive", ResetCompounding::SpreadExclusive),
            ("averaged", ResetCompounding::Averaged),
        ];
        let mut rates = Vec::new();
        for (name, method) in methods {
            let value = rate(six_months, method, 0.01, StubRule::OwnAccrual)?;
            println!("{name:<17} {value:.8}");
            rates.push(value);
        }
    
        // With no spread there is nothing for the three compounding methods to treat
        // differently, and they agree to round-off.
        let mut bare = Vec::new();
        for (name, method) in &methods[..3] {
            let value = rate(six_months, *method, 0.0, StubRule::OwnAccrual)?;
            println!("spread 0, {name:<17} {value:.8}");
            bare.push(value);
        }
    
        // Five months of 3M resets leave a two-month final sub-period. Read over its own
        // accrual it projects two months; read at the index tenor it projects three.
        let five_months = date((2027, 6, 7));
        let own = rate(
            five_months,
            ResetCompounding::Straight,
            0.0,
            StubRule::OwnAccrual,
        )?;
        let tenor = rate(
            five_months,
            ResetCompounding::Straight,
            0.0,
            StubRule::IndexTenor,
        )?;
        println!("own accrual  {own:.8}");
        println!("index tenor  {tenor:.8}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    from desk_sheets import EUR_CLOSE, SOFR_CLOSE_DATE
    
    market = hlx.MarketContext(SOFR_CLOSE_DATE).with_recipe_quotes("EUR", EUR_CLOSE)
    
    def rate(end, *, compounding="straight", spread=0.0, stub="own_accrual"):
        """The coupon's rate over 5 January 2027 to `end`, read from its own table."""
        coupon = hlx.legs.multi_reset_coupon(
            "EUR", dt.date(2027, 1, 5), end, end, notional=10_000_000.0, index="EUR-EURIBOR-3M",
            day_count="ACT/360", reset_tenor="3M", compounding=compounding, spread=spread, stub=stub,
        )
        return hlx.Leg([coupon], "Receive", "EUR").cashflow_records(market).to_dict()["rate"][0]
    
    # Two quarterly resets, a 100 basis point spread, four ways of combining them.
    SIX_MONTHS = dt.date(2027, 7, 5)
    rates = {m: rate(SIX_MONTHS, compounding=m, spread=0.01) for m in ("straight", "flat", "spread_exclusive", "averaged")}
    for method, value in rates.items():
        print(f"{method:<17} {value:.8f}")
    
    # With no spread there is nothing for the three compounding methods to treat
    # differently, and they agree to round-off.
    bare = [rate(SIX_MONTHS, compounding=m) for m in ("straight", "flat", "spread_exclusive")]
    for method, value in zip(("straight", "flat", "spread_exclusive"), bare):
        print(f"spread 0, {method:<17} {value:.8f}")
    
    # Five months of 3M resets leave a two-month final sub-period. Read over its own
    # accrual it projects two months; read at the index tenor it projects three.
    FIVE_MONTHS = dt.date(2027, 6, 7)
    own = rate(FIVE_MONTHS, stub="own_accrual")
    tenor = rate(FIVE_MONTHS, stub="index_tenor")
    print(f"own accrual  {own:.8f}")
    print(f"index tenor  {tenor:.8f}")
    straight          0.03067746
    flat              0.03063905
    spread_exclusive  0.03061321
    averaged          0.03056008
    spread 0, straight          0.02061321
    spread 0, flat              0.02061321
    spread 0, spread_exclusive  0.02061321
    own accrual  0.02059675
    index tenor  0.02060354

    With a spread the four methods order from straight down to averaged. With no spread the three compounding methods agree. The short final sub-period reads a different rate under each stub rule.

    How it's priced

    With sub-periods jj of accrual τj\tau_j, rates rjr_j and spread ss, the coupon's rate cc over the whole accrual τ=∑jτj\tau = \sum_j \tau_j is, under each method,

    straight:τ c=∏j(1+(rj+s) τj)−1,flat:τ c=An,Aj=Aj−1+(rj+s) τj+Aj−1 rj τj, A0=0,spread-exclusive:τ c=∏j(1+rjτj)−1+s τ,averaged:τ c=∑j(rj+s) τj.\begin{aligned} \text{straight:}\quad & \tau\, c = \prod_j \bigl(1 + (r_j + s)\,\tau_j\bigr) - 1, \\ \text{flat:}\quad & \tau\, c = A_n, \quad A_j = A_{j-1} + (r_j + s)\,\tau_j + A_{j-1}\, r_j\, \tau_j,\ A_0 = 0, \\ \text{spread-exclusive:}\quad & \tau\, c = \prod_j \bigl(1 + r_j \tau_j\bigr) - 1 + s\,\tau, \\ \text{averaged:}\quad & \tau\, c = \sum_j (r_j + s)\,\tau_j. \end{aligned}

    Flat compounding accrues each sub-period's amount at its rate plus the spread, and compounds what has built up at the rate alone, so the spread's own accrual doesn't compound. For two resets the methods differ by products of accruals: straight less flat is (r1+s)τ1 s τ2/τ(r_1 + s)\tau_1\, s\,\tau_2 / \tau, flat less spread-exclusive is s τ1 r2τ2/τs\,\tau_1\, r_2 \tau_2 / \tau, and spread-exclusive less averaged is r1τ1 r2τ2/τr_1\tau_1\, r_2\tau_2 / \tau. With positive rates and spread,

    cstraight>cflat>cspread-exclusive>caveraged,c_{\text{straight}} > c_{\text{flat}} > c_{\text{spread-exclusive}} > c_{\text{averaged}},

    and with no spread the three compounding methods are the same product. Where these coupons come from is in how cashflows work.

    API

    Symbol Meaning Rust Python
    cc the coupon MultiResetCoupon::on_index legs.multi_reset_coupon
    method how resets combine ResetCompounding::Straight, Flat, SpreadExclusive, Averaged compounding=
    short final sub-period how it reads the index StubRule::OwnAccrual, IndexTenor stub=
    strip one coupon per period multi_reset_leg legs.multi_reset_leg

    legs.multi_reset_coupon(...):

    Argument Type Default Meaning
    currency str required the currency the coupon pays in
    accrual_start, accrual_end, payment_date date required the coupon's dates
    notional float required the size
    index str required the term index, such as "EUR-EURIBOR-3M"
    day_count str required the accrual day count
    reset_tenor str required how often the index resets, such as "3M"
    compounding str "straight" "straight", "flat", "spread_exclusive" or "averaged"
    gearing, spread float 1.0, 0.0 each sub-period's rate is gearing × fixing, and ss is spread
    stub str or pair "index_tenor" how a short final sub-period reads the index; see stub rules

    legs.multi_reset_leg(schedule, payment_dates, currency, ...) takes the same terms over a schedule.

    Defaults and limits

    • Stub default: a multi-reset coupon reads a short final sub-period at the index's own tenor by default, where an IBOR strip's stub reads over its own accrual.
    • Resets aren't exposed: the sub-periods are built inside the constructor; read the coupon's combined rate from its cashflow table.

    See also

    • Source: crates/helixrs/src/cashflows/coupons.rs, crates/helixrs/src/cashflows/legs.rs
    • Related pages:
      • Cross-currency swap, where a leg often pays less often than its index resets.
      • From a schedule to a leg, for the stub rules.

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