Esc

    The API is under testing and may change before release.

    From a schedule to a leg

    This page lists what turns a schedule into a leg: the schedule's periods, payment dates under a lag, and the stub rules a short period reads its index by. It's the one place the stub rules are written down; every other page links here.

    Quick example

    This builds quarterly periods with a short first one, pays each period two TARGET business days after it ends, values the stub under each stub rule, shows an interpolated stub refused on a market that projects only one tenor, and reads a single two-month coupon both ways. 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, IborIndex, Leg, LegTerms, MarketContext, PaymentRule,
        QuoteSheet, Schedule, StubRule, calendar, ibor_leg_on_index, ibor_leg_with_stubs, 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 euribor(code: &str) -> IborIndex {
        let Some(AnyIndex::Ibor(index)) = index(code) else {
            panic!("{code} is a registered term index");
        };
        index
    }
    
    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 target = calendar("EUTA")?;
    
        // Quarterly periods from 20 November 2026 to 5 October 2027, measured back from
        // the end: the first period is short.
        let schedule = Schedule::generate(
            date((2026, 11, 20)),
            date((2027, 10, 5)),
            "3M".parse()?,
            target.as_ref(),
            "ModFollowing".parse()?,
            "None".parse()?,
            "ShortInitial".parse()?,
            false,
            None,
            None,
        )?;
        for (start, end) in schedule.periods() {
            println!("{start}  {end}");
        }
    
        // Each period pays two TARGET business days after it ends.
        let paying = PaymentRule::business_days(2, target.clone());
        for (_, end) in schedule.periods() {
            let pay = paying.payment_date(end);
            println!("{end}  pays {pay}");
        }
    
        // The same strip under two stub rules. Projected over its own accrual, the
        // stub reads a forward over its six weeks; read at the index tenor, it reads
        // EURIBOR 3M's full window from its start. The next coupon is the same.
        let terms = LegTerms {
            notional: 10_000_000.0,
            day_count: "ACT/360".parse()?,
            currency: "EUR".parse()?,
            direction: Direction::Receive,
            payment: paying.clone(),
        };
        let three_month = euribor("EUR-EURIBOR-3M");
        let strip = |rule: StubRule| -> Outcome<Leg> {
            Ok(ibor_leg_with_stubs(
                &schedule,
                &terms,
                &three_month,
                1.0,
                0.0,
                &rule,
            )?)
        };
        let rates = |leg: &Leg| -> Outcome<Vec<f64>> {
            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").to_vec())
        };
        let own = rates(&strip(StubRule::OwnAccrual)?)?;
        let tenor = rates(&strip(StubRule::IndexTenor)?)?;
        println!("stub rate, own accrual   {:.8}", own[0]);
        println!("stub rate, index tenor   {:.8}", tenor[0]);
        println!("next coupon, both rules  {:.8}  {:.8}", own[1], tenor[1]);
    
        // Interpolating between two tenors reads both: a market that projects only
        // EURIBOR 3M cannot value it, and the refusal names the curve it lacks.
        let blended = strip(StubRule::Interpolated {
            shorter: euribor("EUR-EURIBOR-1M"),
            longer: three_month.clone(),
        })?;
        let refused = blended
            .npv_on(&market, &eur)
            .expect_err("an interpolated stub needs both tenors projected");
        let (kind, key) = refused.missing_input().expect("a missing input");
        let missing = format!("{kind}({key})");
        println!("refused: no {missing}");
    
        // A single two-month coupon on EURIBOR 3M elects the same reading with its
        // fixing window: by default it projects over its own accrual; with
        // `StubRule::IndexTenor` it reads the index's three-month window from its
        // start. Either way the amount accrues over the coupon's own two months.
        let two_months = Schedule::from_dates(vec![date((2026, 10, 5)), date((2026, 12, 7))])?;
        let single = LegTerms { payment: PaymentRule::none(), ..terms.clone() };
        let projected = ibor_leg_on_index(&two_months, &single, &three_month, 1.0, 0.0);
        let AnyCoupon::Ibor(coupon) = projected.coupons()[0].clone() else {
            unreachable!("an IBOR leg's coupon")
        };
        let windowed = Leg::new(
            vec![AnyCoupon::Ibor(coupon.with_stub(&three_month, &StubRule::IndexTenor)?)],
            Direction::Receive,
        );
        let (own_window, index_window) = (rates(&projected)?[0], rates(&windowed)?[0]);
        println!("own accrual  {own_window:.8}");
        println!("index tenor  {index_window:.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)
    target = hlx.Calendar("EUTA")
    
    # Quarterly periods from 20 November 2026 to 5 October 2027, measured back from
    # the end: the first period is short.
    schedule = hlx.dates.schedule(dt.date(2026, 11, 20), dt.date(2027, 10, 5), "3M", target)
    for start, end in schedule.periods():
        print(f"{start}  {end}")
    
    # Each period pays two TARGET business days after it ends.
    paying = schedule.paying(2, target)
    for (_, end), pay in zip(schedule.periods(), paying):
        print(f"{end}  pays {pay}")
    
    # The same strip under two stub rules. Projected over its own accrual, the
    # stub reads a forward over its six weeks; read at the index tenor, it reads
    # EURIBOR 3M's full window from its start. The next coupon is the same.
    
    def strip(stub):
        coupons = hlx.legs.ibor_leg(
            schedule, paying, "EUR", notional=10_000_000.0, index="EUR-EURIBOR-3M", day_count="ACT/360", stub=stub
        )
        return hlx.Leg(coupons, "Receive", "EUR")
    
    own = strip("own_accrual").cashflow_records(market).to_dict()["rate"]
    tenor = strip("index_tenor").cashflow_records(market).to_dict()["rate"]
    print(f"stub rate, own accrual   {own[0]:.8f}")
    print(f"stub rate, index tenor   {tenor[0]:.8f}")
    print(f"next coupon, both rules  {own[1]:.8f}  {tenor[1]:.8f}")
    
    # Interpolating between two tenors reads both: a market that projects only
    # EURIBOR 3M cannot value it, and the refusal names the curve it lacks.
    blended = strip(("EUR-EURIBOR-1M", "EUR-EURIBOR-3M"))
    try:
        blended.npv(market)
    except hlx.MissingMarketInputError as refused:
        missing = f"{refused.kind}({refused.key})"
    print(f"refused: no {missing}")
    
    # A single two-month coupon on EURIBOR 3M elects the same reading with its
    # fixing window: by default it projects over its own accrual; with
    # fixing_window="index_tenor" it reads the index's three-month window from its
    # start. Either way the amount accrues over the coupon's own two months.
    
    def stub_rate(**kw):
        coupon = hlx.legs.ibor_coupon(
            "EUR", dt.date(2026, 10, 5), dt.date(2026, 12, 7), dt.date(2026, 12, 7),
            notional=10_000_000.0, index="EUR-EURIBOR-3M", day_count="ACT/360", **kw,
        )
        return hlx.Leg([coupon], "Receive", "EUR").cashflow_records(market).to_dict()["rate"][0]
    
    own_window, index_window = stub_rate(), stub_rate(fixing_window="index_tenor")
    print(f"own accrual  {own_window:.8f}")
    print(f"index tenor  {index_window:.8f}")
    2026-11-20  2027-01-05
    2027-01-05  2027-04-05
    2027-04-05  2027-07-05
    2027-07-05  2027-10-05
    2027-01-05  pays 2027-01-07
    2027-04-05  pays 2027-04-07
    2027-07-05  pays 2027-07-07
    2027-10-05  pays 2027-10-07
    stub rate, own accrual   0.02053401
    stub rate, index tenor   0.02056213
    next coupon, both rules  0.02055978  0.02055978
    refused: no projection(EUR-EURIBOR-1M)
    own accrual  0.02054396
    index tenor  0.02056095

    Only the stub's rate depends on the stub rule; the next coupon reads the same rate under both. The interpolated stub needs the one-month curve too, and the refusal names it.

    How it's priced

    A payment lag of kk business days on calendar C\mathcal{C} pays period ii on Ti=add⁡C(Ei,k)T_i = \operatorname{add}_{\mathcal{C}}(E_i, k), where EiE_i is the period's end.

    A stub of length δ\delta between two published tenors δs<δ<δl\delta_s < \delta < \delta_l, read by linear interpolation, fixes at

    rstub=rs+δ−δsδl−δs (rl−rs),r_{\text{stub}} = r_s + \frac{\delta - \delta_s}{\delta_l - \delta_s}\,(r_l - r_s),

    where rsr_s and rlr_l are the shorter and longer tenors' rates, so valuing it needs both projected. Why a stub needs a rule at all is in how cashflows work.

    API

    Symbol Meaning Rust Python
    periods the accrual periods Schedule::generate, then Schedule::periods dates.schedule, then Schedule.periods
    TiT_i payment dates under a lag PaymentRule::business_days Schedule.paying
    add⁡C\operatorname{add}_{\mathcal{C}} business days added Calendar::add_business_days Calendar.add_business_days
    stub rule how a stub reads its index StubRule::OwnAccrual, IndexTenor, Interpolated stub= on legs.ibor_leg
    one coupon's window the same choice on a single coupon IborCoupon::with_stub fixing_window= on legs.ibor_coupon
    strip coupons over the schedule ibor_leg_with_stubs legs.ibor_leg

    Schedule.paying(...):

    Argument Type Default Meaning
    lag_days int required business days after each period's end; 0 pays on the end
    calendar Calendar required the calendar the lag counts on
    business_day_convention str "Following" how a payment date landing on a closed day rolls

    The stub= argument of legs.ibor_leg and legs.multi_reset_coupon:

    Argument Type Default Meaning
    stub str or tuple[str, str] "own_accrual" on ibor_leg, "index_tenor" on multi_reset_coupon one of the stub rules below
    Stub rules
    Rule Python Rust The stub reads
    own accrual "own_accrual" StubRule::OwnAccrual a forward over its own dates
    index tenor "index_tenor" StubRule::IndexTenor the index's full tenor from its start, as a regular period does
    interpolated ("EUR-EURIBOR-1M", "EUR-EURIBOR-3M") StubRule::Interpolated { shorter, longer } the two tenors either side of its length, blended by days

    A single coupon makes the same choice with fixing_window= on legs.ibor_coupon: None (the default) reads over its own accrual, "index_tenor" reads the index's full window from the accrual start's value date, and "value_dates" reads the index's value dates for each boundary. A (start, end) pair names the window outright. The amount always accrues over the accrual period.

    Defaults and limits

    • Where the stub goes: dates.schedule measures periods back from the end, so the first period is the short one. Rust's Schedule::generate takes the stub position as an argument ("ShortInitial" here).
    • Regular periods: a stub rule changes only the stub. Every regular period reads its index over the index's own window, whichever rule you choose.
    • Interpolated stubs need a registered term index for each tenor, and both projected in the market; a missing one is refused with the curve named.
    • projection="accrual_period" on legs.ibor_leg projects every coupon over its own accrual, and can't be combined with an elected stub.
    • Multi-reset coupons read a short final sub-period by the same rules; see multi-reset coupons.

    See also

    • Source: crates/helixrs/src/statics/schedules.rs, crates/helixrs/src/cashflows/legs.rs, crates/helixio/src/legs.rs
    • Related pages:
      • Build a leg from a schedule, the task step by step.
      • Legs and coupons, the strip constructors and the leg.
      • Schedules and calendars, for generating periods and counting a lag.

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