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.
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(())
}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 and frequency , the unadjusted boundaries are
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 |
|---|---|---|---|
| boundaries from one anchor, adjusted | Schedule::generate |
dates.schedule |
|
| , | 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_conventionwhen 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=Trueand 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_startorlast_regular_endoutside the term, or out of order, raisesValueError; neither combines withdate_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.
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(())
}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.