Esc

    The API is under testing and may change before release.

    Schedules

    This page lists everything you need to generate the dates a leg accrues between: the schedule call and every argument it takes, how stubs, stated dates, month ends and the termination are treated, and the cases where a generated grid disagrees with a term sheet.

    Quick example

    This generates two years of quarterly periods on New York's calendar under modified following, and prints each period's dates and length.

    Rust
    use chrono::{Datelike, NaiveDate};
    use helixrs::{
        BusinessDayConvention, Calendar, Frequency, Period,
        RollConvention, Schedule, StubPeriodType, add_period, calendar,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn tenor(spelling: &str) -> Period {
        spelling.parse().expect("a tenor spelling")
    }
    
    /// The generator with the arguments most regions leave at their defaults.
    fn generate(
        effective: NaiveDate,
        termination: NaiveDate,
        frequency: Period,
        calendar: &dyn Calendar,
        convention: BusinessDayConvention,
        roll: RollConvention,
        stub: StubPeriodType,
        end_of_month: bool,
    ) -> Schedule {
        Schedule::generate(
            effective, termination, frequency, calendar, convention, roll, stub, end_of_month, None,
            None,
        )
        .expect("a termination after the effective date")
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let new_york = calendar("USNY").expect("a registered business centre");
        let ny = new_york.as_ref();
        let modfollowing = BusinessDayConvention::ModifiedFollowing;
        let no_roll = RollConvention::None;
    
        // Two years of quarterly periods. Every boundary is the termination date
        // stepped back a whole number of quarters, then rolled under modified
        // following.
        let effective = date(2026, 1, 15);
        let termination = date(2028, 1, 15);
        let quarterly = Frequency::Quarterly.to_period();
        let grid = generate(
            effective, termination, quarterly, ny, modfollowing, no_roll,
            StubPeriodType::ShortInitial, false,
        );
        // Two years of quarters is eight periods.
        let periods = grid.periods().len() as i32;
        println!("{} boundaries, {periods} periods", grid.dates.len());
        for (start, end) in grid.periods() {
            println!("  {start} to {end}  {:>3} days", (end - start).num_days());
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    
    dates = hlx.dates
    BusinessDayConvention = dates.BusinessDayConvention
    Frequency = dates.Frequency
    NEW_YORK = hlx.Calendar("USNY")
    
    def show(label: str, boundaries: list[dt.date]) -> None:
        print(f"{label:<14} " + " ".join(d.strftime("%y-%m-%d") for d in boundaries))
    
    # Two years of quarterly periods. Every boundary is the termination date stepped
    # back a whole number of quarters, then rolled under modified following.
    EFFECTIVE = dt.date(2026, 1, 15)
    TERMINATION = dt.date(2028, 1, 15)
    grid = dates.schedule(
        EFFECTIVE, TERMINATION, Frequency.Quarterly, NEW_YORK, BusinessDayConvention.ModifiedFollowing
    )
    boundaries = grid.dates()
    # Two years of quarters is eight periods.
    periods = len(grid.periods())
    print(f"{len(grid)} boundaries, {periods} periods")
    for start, end in grid.periods():
        print(f"  {start} to {end}  {(end - start).days:>3} days")
    9 boundaries, 8 periods
      2026-01-15 to 2026-04-15   90 days
      2026-04-15 to 2026-07-15   91 days
      2026-07-15 to 2026-10-15   92 days
      2026-10-15 to 2027-01-15   92 days
      2027-01-15 to 2027-04-15   90 days
      2027-04-15 to 2027-07-15   91 days
      2027-07-15 to 2027-10-15   92 days
      2027-10-15 to 2028-01-18   95 days

    Every boundary is the termination stepped back a whole number of quarters, then rolled: two years of quarters is eight periods. Their lengths follow the calendar quarters (90 to 92 days), and the last runs to 95 because 15 January 2028 falls on a weekend and rolls to the 18th.

    How it's priced

    Boundaries are stepped from one anchor in whole multiples of the frequency: backwards from the termination for an initial stub, forwards from the effective date for a final one. With anchor aa and frequency pp, the unadjusted boundaries are

    bk=a+k p,k=0,±1,±2,…,b_k = a + k\,p, \qquad k = 0, \pm 1, \pm 2, \dots,

    in month arithmetic with clamping, and each is then adjusted on the calendar. Stepping from one anchor keeps the day of the month that repeated steps would lose. How term sheets state schedules is covered in how dates and conventions work.

    API

    Symbol Meaning Rust Python
    bkb_k boundaries from one anchor, adjusted Schedule::generate dates.schedule
    aa, pp the anchor and the frequency the effective or termination date, and the frequency you pass effective or termination, and frequency
    stated stubs first and last regular dates Schedule::generate_with_stubs dates.schedule with first_regular_start, last_regular_end
    the result boundaries and periods Schedule dates.Schedule
    Argument Type Default Meaning
    effective date required the first boundary, rolled like every other
    termination date required the last boundary, rolled by terminal_business_day_convention
    frequency Frequency, Period or str required the step, such as "3M" or Frequency.Quarterly
    calendar Calendar required the calendar each interior boundary is adjusted on
    business_day_convention BusinessDayConvention or str "ModFollowing" the roll for interior boundaries
    roll_convention RollConvention or str "None" snaps each boundary inside its month first, such as "IMM" or "IMM20"
    stub str "ShortInitial" "ShortInitial", "LongInitial", "ShortFinal" or "LongFinal": where the remainder goes and whether it merges
    end_of_month bool False from a month-end seed, roll every interior boundary to its month's end
    terminal_business_day_convention BusinessDayConvention, str or None None a separate roll for the termination; None means the same as business_day_convention, and "Unadjusted" or "None" keep it as stated
    date_generation str or None None "Cds" for the CDS rule set, dated on the twentieth of IMM months
    first_regular_start date or None None states where the regular periods start; a front stub runs from effective
    last_regular_end date or None None states where the regular periods end; a back stub runs to termination

    Defaults and limits

    • Every boundary is rolled, endpoints included. The termination takes terminal_business_day_convention when you pass one; "Unadjusted" keeps it as stated.
    • Boundaries are strictly ascending. Generated forwards, a boundary that rolls onto or past the termination merges into it, so the last period runs to the termination.
    • A long stub absorbs the short one, so it has one period fewer.
    • Month ends. With end_of_month=True and a seed on or after its month's last business day, each interior boundary moves to its calendar month's last day, then the convention adjusts it. Without the flag, the day of the month is kept and the grid can drift, for example to the 29th.
    • Stated dates are checked. first_regular_start or last_regular_end outside the term, or out of order, raises ValueError; neither combines with date_generation="Cds".
    • Templates generate forwards. A swap template's schedule runs forward from the effective date, the stub at the back. Template.with_date_generation("Backward") rolls back from the termination, the stub at the front.

    !!! warning "Write a 27-month swap as 27M" A swap builder's with_maturity reads "2Y3M" as a forward tenor: three months starting in two years. A 27-month swap is spelled "27M".

    More examples

    Every case above in one program: stubs, stated stub dates, month ends, the merge at the end, an interior roll, payment lags, the terminal date, a CDS schedule, stated dates, a bond on month end, an irregular first coupon, and a template's generation direction. The example reads no shared data.

    Rust
    use chrono::{Datelike, Duration, NaiveDate, Weekday};
    use helixrs::LagAdjustment;
    use helixrs::DateGenerationRule;
    use helixrs::PeriodContext;
    use helixrs::{
        BusinessDayConvention, Calendar, Currency, DayCountFraction, FixedRateBond, Frequency, Period,
        RollConvention, Schedule, ScheduleError, StubPeriodType, add_period, calendar, cds_standard_maturity, is_imm_date,
        last_day_of_month, prior_cds_imm_date, ImmCycle, AnyCoupon, AnyTemplate, Termination, template,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn tenor(spelling: &str) -> Period {
        spelling.parse().expect("a tenor spelling")
    }
    
    fn show(label: &str, boundaries: &[NaiveDate]) {
        let cells: Vec<String> = boundaries.iter().map(|d| d.format("%y-%m-%d").to_string()).collect();
        println!("{label:<14} {}", cells.join(" "));
    }
    
    /// The generator with the arguments most regions leave at their defaults.
    fn generate(
        effective: NaiveDate,
        termination: NaiveDate,
        frequency: Period,
        calendar: &dyn Calendar,
        convention: BusinessDayConvention,
        roll: RollConvention,
        stub: StubPeriodType,
        end_of_month: bool,
    ) -> Schedule {
        Schedule::generate(
            effective, termination, frequency, calendar, convention, roll, stub, end_of_month, None,
            None,
        )
        .expect("a termination after the effective date")
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let new_york = calendar("USNY").expect("a registered business centre");
        let ny = new_york.as_ref();
        let modfollowing = BusinessDayConvention::ModifiedFollowing;
        let no_roll = RollConvention::None;
    
        // The quick example's two-year quarterly grid, which later steps compare against.
        let effective = date(2026, 1, 15);
        let termination = date(2028, 1, 15);
        let quarterly = Frequency::Quarterly.to_period();
        let grid = generate(
            effective, termination, quarterly, ny, modfollowing, no_roll,
            StubPeriodType::ShortInitial, false,
        );
        let periods = grid.periods().len() as i32;
    
        // A term that is not a whole number of periods leaves a stub. Its place and
        // length are chosen: short or long, at the front or at the back.
        let stub_start = date(2026, 2, 10);
        let stub_end = date(2028, 1, 15);
        let mut lengths = Vec::new();
        for (label, stub) in [
            ("ShortInitial", StubPeriodType::ShortInitial),
            ("LongInitial", StubPeriodType::LongInitial),
            ("ShortFinal", StubPeriodType::ShortFinal),
            ("LongFinal", StubPeriodType::LongFinal),
        ] {
            let stub_grid = generate(
                stub_start, stub_end, Frequency::SemiAnnual.to_period(), ny, modfollowing, no_roll,
                stub, false,
            );
            lengths.push(stub_grid.periods().len());
            show(label, &stub_grid.dates);
        }
    
        // A confirmation can state where the regular periods start and end. The
        // regular periods run between the two stated dates, a front stub runs from
        // the effective date and a back stub runs to the termination date.
        let double_start = date(2026, 2, 10);
        let first_regular = date(2026, 4, 15);
        let last_regular = date(2027, 10, 15);
        let double_end = date(2027, 12, 10);
        let semi_annual = Frequency::SemiAnnual.to_period();
        let with_stubs = |first: Option<NaiveDate>, last: Option<NaiveDate>| {
            Schedule::generate_with_stubs(
                double_start, first, last, double_end, semi_annual, ny, modfollowing, no_roll,
                StubPeriodType::ShortInitial, false, None,
            )
        };
        let double = with_stubs(Some(first_regular), Some(last_regular)).expect("stub dates in the term");
        // Eighteen months between the stated dates is three regular periods.
        let mut unadjusted = vec![double_start];
        unadjusted.extend([3, 2, 1, 0].map(|k| add_period(last_regular, tenor(&format!("{}M", -6 * k)))));
        unadjusted.push(double_end);
        show("double stub", &double.dates);
    
        // A stated date outside the term is refused.
        let outside = match with_stubs(Some(date(2026, 1, 15)), None) {
            Err(ScheduleError::StubDateOutsideTerm { .. }) => "refused",
            _ => "",
        };
        println!("first regular start before the effective date: {outside}");
    
        // The seed, 29 February 2028, is a month end, so the end-of-month flag
        // rolls every interior boundary to the calendar month end before adjusting
        // it. Without the flag the day of the month is kept, and the grid drifts.
        let eom_start = date(2026, 2, 28);
        let eom_end = date(2028, 2, 29);
        let eom = |convention, flag| {
            generate(eom_start, eom_end, quarterly, ny, convention, no_roll, StubPeriodType::ShortInitial, flag)
        };
        let with_flag = eom(modfollowing, true);
        let without_flag = eom(modfollowing, false);
        show("end of month", &with_flag.dates);
        show("day kept", &without_flag.dates);
    
        // Under following, a weekend month end rolls into the next month in a
        // schedule, while a calendar advance under the same flag lands on the last
        // business day.
        let following = eom(BusinessDayConvention::Following, true);
        let schedule_boundary = following.dates[4];
        let advanced = ny.advance(date(2026, 11, 30), tenor("3M"), BusinessDayConvention::Following, true);
        println!("Feb 2027 boundary under following: schedule {schedule_boundary}, advance {advanced}");
    
        // Generated forward, a boundary that rolls onto or past the termination merges
        // into it rather than leaving a period of no length or one that runs backwards.
        // From Friday 30 January 2026, the month-end boundary is Saturday 28 February,
        // which following rolls to Monday 2 March.
        let merge_start = date(2026, 1, 30);
        let merge = |end: NaiveDate| {
            generate(
                merge_start, end, tenor("1M"), ny, BusinessDayConvention::Following, no_roll,
                StubPeriodType::ShortFinal, true,
            )
        };
        for merge_end in [date(2026, 3, 3), date(2026, 3, 2), date(2026, 2, 27)] {
            let merged = merge(merge_end);
            show(&format!("to {merge_end}"), &merged.dates);
        }
    
        // A roll convention snaps each interior boundary inside its month before it
        // is adjusted. From one IMM date to the next year's, the IMM roll keeps
        // every boundary on a third Wednesday; with no roll the start's day is kept.
        let imm_start = date(2026, 3, 18);
        let imm_end = date(2027, 3, 17);
        let imm_grid = generate(
            imm_start, imm_end, tenor("3M"), ny, modfollowing, RollConvention::Imm,
            StubPeriodType::ShortFinal, false,
        );
        let plain_grid = generate(
            imm_start, imm_end, tenor("3M"), ny, modfollowing, no_roll, StubPeriodType::ShortFinal,
            false,
        );
        show("IMM roll", &imm_grid.dates);
        show("no roll", &plain_grid.dates);
    
        // A period pays off its end date, here two New York business days later:
        // the lag an overnight-indexed swap carries so the final rate is published.
        let lag = LagAdjustment::business_days(2, BusinessDayConvention::Following, new_york.clone());
        let paid = grid.paying(&lag);
        for ((_, end), pays) in grid.periods().iter().zip(&paid) {
            println!("accrual ends {end}, pays {pays}");
        }
    
        // The last boundary can take its own convention. Left unadjusted it stays
        // on a Sunday termination, while the interior boundaries still roll.
        let sunday_end = date(2027, 1, 17);
        let terminal = |convention| {
            Schedule::generate(
                date(2026, 1, 17), sunday_end, tenor("6M"), ny, modfollowing, no_roll,
                StubPeriodType::ShortInitial, false, convention, None,
            )
            .expect("a termination after the effective date")
        };
        let kept = terminal(Some(BusinessDayConvention::None));
        let rolled = terminal(None);
        show("unadjusted end", &kept.dates);
        show("rolled end", &rolled.dates);
    
        // The CDS rule starts accrual at the CDS date on or before the trade date
        // and steps quarterly on the twentieth; the maturity itself is not rolled.
        let trade = date(2026, 1, 2);
        let maturity = cds_standard_maturity(trade, 1);
        let following = BusinessDayConvention::Following;
        let cds_grid = Schedule::generate(
            trade, maturity, tenor("3M"), ny, following, no_roll, StubPeriodType::ShortFinal, false,
            Some(BusinessDayConvention::None), Some(DateGenerationRule::Cds),
        )
        .expect("a termination after the effective date");
        let cds_dates = &cds_grid.dates;
        show("CDS grid", cds_dates);
    
        // Dates read off a confirmation are stated, not generated: nothing is
        // rolled or adjusted, even a weekend boundary. An ill-formed list is refused.
        let stated = Schedule::from_dates(vec![date(2026, 1, 15), date(2026, 7, 18), date(2027, 1, 15)])
            .expect("ascending dates");
        let refusal = if Schedule::from_dates(vec![date(2026, 7, 1), date(2026, 1, 1)]).is_err() {
            "refused"
        } else {
            ""
        };
        show("stated", &stated.dates);
        println!("descending dates: {refusal}");
    
        // A bond maturing on 30 November pays on 30 May and 30 November by default.
        // Rolled on the end of the month it pays on 31 May instead. Accrued interest
        // restarts at zero on a coupon date, which shows where the coupons fall.
        let usd = Currency::new("USD").expect("a currency code");
        let bond = FixedRateBond::bullet(
            usd, date(2025, 11, 30), date(2030, 11, 30), 0.05, Frequency::SemiAnnual,
            DayCountFraction::ActActIcma, 100.0,
        )
        .expect("a well-formed bond");
        let eom_bond = bond.clone().with_end_of_month(true);
        println!("{:<12} {:>18} {:>19}", "settlement", "accrued, day kept", "accrued, month end");
        for settle in [date(2026, 5, 30), date(2026, 5, 31)] {
            let (kept_accrued, eom_accrued) = (bond.accrued(settle), eom_bond.accrued(settle));
            println!("{:<12} {kept_accrued:>18.6} {eom_accrued:>19.6}", settle.to_string());
        }
    
        // Issued 10 March with its first coupon on 15 December, the bond has a long
        // first period that spans the notional coupon date of 15 June. Under
        // ACT/ACT.ICMA the accrual splits there, and each part is measured against
        // its own period.
        let issue = date(2026, 3, 10);
        let first = date(2026, 12, 15);
        let bond_maturity = date(2031, 6, 15);
        let settle = date(2026, 9, 10);
        let eur = Currency::new("EUR").expect("a currency code");
        let regular = FixedRateBond::bullet(
            eur, issue, bond_maturity, 0.04, Frequency::SemiAnnual, DayCountFraction::ActActIcma, 100.0,
        )
        .expect("a well-formed bond");
        let long_first = regular.clone().with_first_coupon(first).expect("a date on the coupon grid");
        let long_accrued = long_first.accrued(settle);
        // The regular grid pays on 15 June, so its accrual restarted there.
        println!("accrued on {settle}, regular grid      {:.6}", regular.accrued(settle));
        println!("accrued on {settle}, long first coupon {long_accrued:.6}");
    
        // A 27-month SOFR swap from spot does not divide into whole years, so one period is
        // a stub. Generated forward from the effective date, as every registered
        // template builds, the stub is at the back; generated backward from the
        // termination, it is at the front.
        let AnyTemplate::Ois(sofr) = template("USD-SOFR-OIS").expect("a registered template") else {
            unreachable!("a SOFR OIS template")
        };
        let fixed_periods = |template: AnyTemplate| -> Vec<(NaiveDate, NaiveDate)> {
            let swap = template
                .swap(date(2026, 10, 1))
                .expect("a swap builder")
                .with_maturity("27M".parse::<Termination>().expect("a tenor"))
                .expect("a maturity")
                .with_notional(1_000_000.0)
                .with_fixed_rate(0.04)
                .build()
                .expect("a swap");
            swap.legs()
                .expect("a swap has legs")
                .iter()
                .flat_map(|leg| leg.coupons().iter())
                .filter(|c| matches!(c, AnyCoupon::Fixed(_)))
                .map(|c| (c.accrual_start().expect("a start"), c.accrual_end().expect("an end")))
                .collect()
        };
        let forward = fixed_periods(AnyTemplate::Ois(sofr.clone()));
        let backward = fixed_periods(AnyTemplate::Ois(
            sofr.with_date_generation(DateGenerationRule::Backward),
        ));
        for (label, periods) in [("forward ", &forward), ("backward", &backward)] {
            let shown: Vec<String> = periods.iter().map(|(s, e)| format!("{s}..{e}")).collect();
            println!("{label}  {}", shown.join("  "));
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    
    dates = hlx.dates
    BusinessDayConvention = dates.BusinessDayConvention
    Frequency = dates.Frequency
    RollConvention = dates.RollConvention
    NEW_YORK = hlx.Calendar("USNY")
    
    def show(label: str, boundaries: list[dt.date]) -> None:
        print(f"{label:<14} " + " ".join(d.strftime("%y-%m-%d") for d in boundaries))
    
    # The quick example's two-year quarterly grid, which later steps compare against.
    EFFECTIVE = dt.date(2026, 1, 15)
    TERMINATION = dt.date(2028, 1, 15)
    grid = dates.schedule(
        EFFECTIVE, TERMINATION, Frequency.Quarterly, NEW_YORK, BusinessDayConvention.ModifiedFollowing
    )
    boundaries = grid.dates()
    
    # A term that is not a whole number of periods leaves a stub. Its place and
    # length are chosen: short or long, at the front or at the back.
    STUB_START = dt.date(2026, 2, 10)
    STUB_END = dt.date(2028, 1, 15)
    lengths = {}
    for stub in ("ShortInitial", "LongInitial", "ShortFinal", "LongFinal"):
        stub_grid = dates.schedule(
            STUB_START, STUB_END, Frequency.SemiAnnual, NEW_YORK, "ModFollowing", "None", stub
        )
        lengths[stub] = len(stub_grid.periods())
        show(stub, stub_grid.dates())
    
    # A confirmation can state where the regular periods start and end. The
    # regular periods run between the two stated dates, a front stub runs from the
    # effective date and a back stub runs to the termination date.
    DOUBLE_START = dt.date(2026, 2, 10)
    FIRST_REGULAR = dt.date(2026, 4, 15)
    LAST_REGULAR = dt.date(2027, 10, 15)
    DOUBLE_END = dt.date(2027, 12, 10)
    double = dates.schedule(
        DOUBLE_START,
        DOUBLE_END,
        Frequency.SemiAnnual,
        NEW_YORK,
        "ModFollowing",
        first_regular_start=FIRST_REGULAR,
        last_regular_end=LAST_REGULAR,
    )
    show("double stub", double.dates())
    
    # A stated date outside the term is refused.
    outside = ""
    try:
        dates.schedule(
            DOUBLE_START, DOUBLE_END, "6M", NEW_YORK, first_regular_start=dt.date(2026, 1, 15)
        )
    except ValueError:
        outside = "refused"
    print(f"first regular start before the effective date: {outside}")
    
    # The seed, 29 February 2028, is a month end, so the end-of-month flag rolls
    # every interior boundary to the calendar month end before adjusting it.
    # Without the flag the day of the month is kept, and the grid drifts to the 29th.
    EOM_START = dt.date(2026, 2, 28)
    EOM_END = dt.date(2028, 2, 29)
    with_flag = dates.schedule(
        EOM_START, EOM_END, Frequency.Quarterly, NEW_YORK, "ModFollowing", "None", "ShortInitial", True
    )
    without_flag = dates.schedule(
        EOM_START, EOM_END, Frequency.Quarterly, NEW_YORK, "ModFollowing", "None", "ShortInitial", False
    )
    show("end of month", with_flag.dates())
    show("day kept", without_flag.dates())
    
    # Under following, a weekend month end rolls into the next month in a schedule,
    # while a calendar advance under the same flag lands on the last business day.
    following = dates.schedule(
        EOM_START, EOM_END, Frequency.Quarterly, NEW_YORK, "Following", "None", "ShortInitial", True
    )
    schedule_boundary = following.dates()[4]
    advanced = NEW_YORK.advance(dt.date(2026, 11, 30), "3M", "Following", True)
    print(f"Feb 2027 boundary under following: schedule {schedule_boundary}, advance {advanced}")
    
    # Generated forward, a boundary that rolls onto or past the termination merges
    # into it rather than leaving a period of no length or one that runs backwards.
    # From Friday 30 January 2026, the month-end boundary is Saturday 28 February,
    # which following rolls to Monday 2 March.
    MERGE_START = dt.date(2026, 1, 30)
    for merge_end in (dt.date(2026, 3, 3), dt.date(2026, 3, 2), dt.date(2026, 2, 27)):
        merged = dates.schedule(MERGE_START, merge_end, "1M", NEW_YORK, "Following", "None", "ShortFinal", True)
        show(f"to {merge_end}", merged.dates())
    
    # A roll convention snaps each interior boundary inside its month before it is
    # adjusted. From one IMM date to the next year's, the IMM roll keeps every
    # boundary on a third Wednesday; with no roll the day of the start is kept.
    IMM_START = dt.date(2026, 3, 18)
    IMM_END = dt.date(2027, 3, 17)
    imm_grid = dates.schedule(
        IMM_START, IMM_END, "3M", NEW_YORK, "ModFollowing", RollConvention.Imm, "ShortFinal"
    )
    plain_grid = dates.schedule(IMM_START, IMM_END, "3M", NEW_YORK, "ModFollowing", "None", "ShortFinal")
    show("IMM roll", imm_grid.dates())
    show("no roll", plain_grid.dates())
    
    # A period pays off its end date, here two New York business days later: the
    # lag an overnight-indexed swap carries so the final rate can be published.
    paid = grid.paying(2, NEW_YORK)
    for (_, end), pays in zip(grid.periods(), paid):
        print(f"accrual ends {end}, pays {pays}")
    
    # The last boundary can take its own convention. Left unadjusted it stays on
    # a Sunday termination, while the interior boundaries still roll.
    SUNDAY_END = dt.date(2027, 1, 17)
    kept = dates.schedule(
        dt.date(2026, 1, 17),
        SUNDAY_END,
        "6M",
        NEW_YORK,
        "ModFollowing",
        terminal_business_day_convention=BusinessDayConvention.Unadjusted,
    )
    rolled = dates.schedule(dt.date(2026, 1, 17), SUNDAY_END, "6M", NEW_YORK, "ModFollowing")
    show("unadjusted end", kept.dates())
    show("rolled end", rolled.dates())
    
    # The CDS rule starts accrual at the CDS date on or before the trade date and
    # steps quarterly on the twentieth; the maturity itself is not rolled.
    TRADE = dt.date(2026, 1, 2)
    MATURITY = dates.cds_standard_maturity(TRADE, 1)
    cds_grid = dates.schedule(
        TRADE,
        MATURITY,
        "3M",
        NEW_YORK,
        "Following",
        stub="ShortFinal",
        terminal_business_day_convention="None",
        date_generation="Cds",
    )
    cds_dates = cds_grid.dates()
    show("CDS grid", cds_dates)
    
    # Dates read off a confirmation are stated, not generated: nothing is rolled
    # or adjusted, even a weekend boundary. An ill-formed list is refused.
    stated = dates.Schedule.from_dates([dt.date(2026, 1, 15), dt.date(2026, 7, 18), dt.date(2027, 1, 15)])
    refusal = ""
    try:
        dates.Schedule.from_dates([dt.date(2026, 7, 1), dt.date(2026, 1, 1)])
    except ValueError:
        refusal = "refused"
    show("stated", stated.dates())
    print(f"descending dates: {refusal}")
    
    # A bond maturing on 30 November pays on 30 May and 30 November by default.
    # Rolled on the end of the month it pays on 31 May instead. Accrued interest
    # restarts at zero on a coupon date, which shows where the coupons fall.
    bond = hlx.products.fixed_rate_bond(
        "USD",
        dt.date(2025, 11, 30),
        dt.date(2030, 11, 30),
        coupon=0.05,
        frequency="6M",
        day_count="ACT/ACT.ICMA",
    )
    eom_bond = bond.with_end_of_month()
    print(f"{'settlement':<12} {'accrued, day kept':>18} {'accrued, month end':>19}")
    for settle in (dt.date(2026, 5, 30), dt.date(2026, 5, 31)):
        kept_accrued, eom_accrued = bond.accrued(settle), eom_bond.accrued(settle)
        print(f"{settle!s:<12} {kept_accrued:>18.6f} {eom_accrued:>19.6f}")
    
    # Issued 10 March with its first coupon on 15 December, the bond has a long
    # first period that spans the notional coupon date of 15 June. Under ACT/ACT.ICMA
    # the accrual splits there, and each part is measured against its own period.
    ISSUE = dt.date(2026, 3, 10)
    FIRST = dt.date(2026, 12, 15)
    BOND_MATURITY = dt.date(2031, 6, 15)
    SETTLE = dt.date(2026, 9, 10)
    regular = hlx.products.fixed_rate_bond(
        "EUR", ISSUE, BOND_MATURITY, coupon=0.04, frequency="6M", day_count="ACT/ACT.ICMA"
    )
    long_first = regular.with_first_coupon(FIRST)
    long_accrued = long_first.accrued(SETTLE)
    # The regular grid pays on 15 June, so its accrual restarted there.
    print(f"accrued on {SETTLE}, regular grid      {regular.accrued(SETTLE):.6f}")
    print(f"accrued on {SETTLE}, long first coupon {long_accrued:.6f}")
    
    # A 27-month SOFR swap from spot does not divide into whole years, so one period is
    # a stub. Generated forward from the effective date, as every registered
    # template builds, the stub is at the back; generated backward from the
    # termination, it is at the front.
    sofr = hlx.templates.get("USD-SOFR-OIS")
    
    def fixed_periods(template):
        swap = template.swap(dt.date(2026, 10, 1)).with_maturity("27M").with_notional(1_000_000.0)\
            .with_fixed_rate(0.04).build()
        return [
            (c.accrual_start(), c.accrual_end())
            for leg in swap.legs()
            for c in leg.coupons()
            if c.kind() == "Fixed"
        ]
    
    forward = fixed_periods(sofr)
    backward = fixed_periods(sofr.with_date_generation("Backward"))
    for label, spans in [("forward ", forward), ("backward", backward)]:
        print(f"{label}  " + "  ".join(f"{s}..{e}" for s, e in spans))
    ShortInitial   26-02-10 26-07-15 27-01-15 27-07-15 28-01-18
    LongInitial    26-02-10 27-01-15 27-07-15 28-01-18
    ShortFinal     26-02-10 26-08-10 27-02-10 27-08-10 28-01-18
    LongFinal      26-02-10 26-08-10 27-02-10 28-01-18
    double stub    26-02-10 26-04-15 26-10-15 27-04-15 27-10-15 27-12-10
    first regular start before the effective date: refused
    end of month   26-02-27 26-05-29 26-08-31 26-11-30 27-02-26 27-05-28 27-08-31 27-11-30 28-02-29
    day kept       26-02-27 26-05-29 26-08-31 26-11-30 27-02-26 27-05-28 27-08-30 27-11-29 28-02-29
    Feb 2027 boundary under following: schedule 2027-03-01, advance 2027-02-26
    to 2026-03-03  26-01-30 26-03-02 26-03-03
    to 2026-03-02  26-01-30 26-03-02
    to 2026-02-27  26-01-30 26-02-27
    IMM roll       26-03-18 26-06-17 26-09-16 26-12-16 27-03-17
    no roll        26-03-18 26-06-18 26-09-18 26-12-18 27-03-17
    accrual ends 2026-04-15, pays 2026-04-17
    accrual ends 2026-07-15, pays 2026-07-17
    accrual ends 2026-10-15, pays 2026-10-19
    accrual ends 2027-01-15, pays 2027-01-20
    accrual ends 2027-04-15, pays 2027-04-19
    accrual ends 2027-07-15, pays 2027-07-19
    accrual ends 2027-10-15, pays 2027-10-19
    accrual ends 2028-01-18, pays 2028-01-20
    unadjusted end 26-01-20 26-07-17 27-01-17
    rolled end     26-01-20 26-07-17 27-01-19
    CDS grid       25-12-22 26-03-20 26-06-22 26-09-21 26-12-20
    stated         26-01-15 26-07-18 27-01-15
    descending dates: refused
    settlement    accrued, day kept  accrued, month end
    2026-05-30             0.000000            2.486264
    2026-05-31             0.013587            0.000000
    accrued on 2026-09-10, regular grid      0.950820
    accrued on 2026-09-10, long first coupon 2.016754
    forward   2026-10-05..2027-10-05  2027-10-05..2028-10-05  2028-10-05..2029-01-05
    backward  2026-10-05..2027-01-05  2027-01-05..2028-01-05  2028-01-05..2029-01-05

    See also

    • Source: crates/helixrs/src/statics/schedules.rs
    • Related pages:
      • How dates and conventions work, for how a term sheet states a schedule.
      • Dates and periods, for month arithmetic.
      • Calendars, for the adjustment of each boundary.
      • From a schedule to a leg, for the coupons a schedule becomes.

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