Dates and periods
This page lists everything you need to turn market lengths into dates: tenors such as 3M and 2Y
as values, the month arithmetic that adds them to a date, forward tenors such as 2Y2Y, and the
dates markets name rather than count, such as IMM dates, CDS dates and nth weekdays.
Quick example
This reads six tenor spellings and shows each one's canonical form and its length in months or in days.
use helixrs::{
ForwardTenor, Period,
};
fn tenor(spelling: &str) -> Period {
spelling.parse().expect("a tenor spelling")
}
fn show(value: Option<i32>) -> String {
value.map_or_else(|| "-".to_owned(), |v| v.to_string())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// A tenor is a count and a unit letter. The spelling is read
// case-insensitively and written back canonically; a month or year tenor
// has a length in months, a day or week tenor a length in days, and neither
// has both.
println!("{:<9} {:<10} {:>6} {:>5}", "spelling", "canonical", "months", "days");
for spelling in ["3m", "1Y", "18M", "2w", "7D", "-1Y"] {
let period = tenor(spelling);
let (months, days) = (period.in_months(), period.in_days());
println!(
"{spelling:<9} {:<10} {:>6} {:>5}",
period.to_string(),
show(months),
show(days)
);
}
Ok(())
}import helixio as hlx
dates = hlx.dates
def show(value: object) -> str:
return "-" if value is None else str(value)
# A tenor is a count and a unit letter. The spelling is read case-insensitively
# and written back canonically; a month or year tenor has a length in months,
# a day or week tenor a length in days, and neither has both.
print(f"{'spelling':<9} {'canonical':<10} {'months':>6} {'days':>5}")
for spelling in ("3m", "1Y", "18M", "2w", "7D", "-1Y"):
period = dates.Period(spelling)
months, days = period.in_months(), period.in_days()
print(f"{spelling:<9} {period!s:<10} {show(months):>6} {show(days):>5}")spelling canonical months days
3m 3M 3 -
1Y 1Y 12 -
18M 18M 18 -
2w 2W - 14
7D 7D - 7
-1Y -1Y -12 -
A month or year tenor has a length in months, a day or week tenor a length in days, and neither has both. Spellings are read case-insensitively and written back canonically.
API
| Name | Meaning | Rust | Python |
|---|---|---|---|
| tenor | a signed count and a unit, compared by duration | Period, parsed from its spelling |
dates.Period |
| month arithmetic | add a tenor to a date, clamping the day | add_period |
dates.add_period |
| a month's last day | the clamp's limit | last_day_of_month |
dates.last_day_of_month |
| forward tenor | a forward start and a term | ForwardTenor |
dates.forward_tenor |
| IMM date | the third Wednesday of an IMM month | next_imm_date, is_imm_date, imm_code_of |
dates.next_imm_date, dates.is_imm_date, dates.imm_code_of |
| IMM code | the period starting on the coded IMM date | imm_code_to_dates |
dates.imm_code_to_dates |
| CDS date | the twentieth of an IMM month | next_cds_imm_date, cds_standard_maturity |
dates.next_cds_imm_date, dates.cds_standard_maturity |
| roll convention | snap a date inside its month | RollConvention |
dates.roll, dates.RollConvention |
| Argument | Type | Default | Meaning |
|---|---|---|---|
Period(spelling) |
str |
required | "3M", "1Y", "2W", "7D", signed with -; a compound such as "1Y3M" is the sum of its segments |
add_period(date, tenor) |
date, Period or str |
required | month arithmetic for months and years, calendar days for days and weeks |
forward_tenor(spelling) |
str |
required | a forward start and a term, such as "2Y2Y" |
next_imm_date(date, cycle=) |
date, str |
cycle="Quarterly" |
the next IMM date strictly after date; "Monthly" uses every month |
is_imm_date(date, cycle=) |
date, str |
cycle="Quarterly" |
whether date is an IMM date of the cycle |
imm_date(month, year, cycle=, day=) |
int, int, str, str |
cycle="Quarterly", day="ThirdWednesday" |
the IMM date of a month; day="WednesdayOnOrAfterNinth" is the other day rule |
imm_code_of(date) |
date |
required | the code, such as MAR26, of an IMM date |
imm_code_to_dates(code, cycle=) |
str, str |
cycle="Quarterly" |
the period that starts on the coded date and runs to the next IMM date |
next_cds_imm_date(date) |
date |
required | the next twentieth of an IMM month |
cds_standard_maturity(date, tenor_years) |
date, int |
required | the on-the-run CDS maturity for a trade date |
nth_weekday(n, weekday, year, month) |
int, str, int, int |
required | the nth named weekday of a month |
roll(date, roll_convention) |
date, RollConvention or str |
required | snaps a date inside its month, such as "EOM" or "IMM20" |
Defaults and limits
- Clamping. Adding months keeps the day and clamps it to the target month's length. Clamping loses information: two one-month steps from 31 January land on 28 March, one two-month step lands on 31 March. That's why a schedule measures every boundary from one anchor.
- Tenors compare by duration.
12Mequals1Yand7Dequals1W. Comparing a month tenor with a day tenor, such as one month against thirty days, is refused rather than guessed. - Compound tenors sum descending segments of one family:
"1Y3M"is fifteen months,"2W3D"seventeen days. Mixing months with days is refused. - IMM codes name a period, not a day:
MAR26runs from the March IMM date to the June one, the same period a futures template's reference period gives. - CDS maturities roll twice a year. The trade date moves to the next roll date, 20 March or 20 September, the tenor is added, and the result steps back to the 20 June or 20 December on or before it. So an on-the-run 5Y maturity can sit up to three months short of five years: 2026-03-19 matures on 2030-12-20.
- Two IMM day rules. By default an IMM date is the third Wednesday.
"WednesdayOnOrAfterNinth"is the 9th rolled forward to a Wednesday, the 9th itself included; in a month whose 9th is a Wednesday, the two rules land a week apart.
!!! warning "2Y3M means different things in different places"
As a Period, "2Y3M" is one compound tenor, twenty-seven months. A swap builder's
with_maturity reads the same spelling as a forward tenor: three months starting in two years.
Write "27M" for a 27-month maturity, so the intent can't be misread.
More examples
Every feature above in one program: month arithmetic against the clamping rule, forward tenors, IMM dates and codes, CDS dates either side of the March roll, nth weekdays and rolls, compound tenors, and the two IMM day rules. The example reads no shared data.
use chrono::{Datelike, NaiveDate, Weekday};
use helixrs::{
AnyTemplate, DayOfWeek, ForwardTenor, ImmCycle, ImmDay, Period, RollConvention, add_period,
cds_standard_maturity, cds_standard_maturity_months, imm_code_of, imm_code_to_dates, imm_date,
imm_date_on,
is_imm_date, last_day_of_month, next_cds_imm_date, next_imm_date, next_weekday, nth_weekday,
prior_cds_imm_date, 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 main() -> Result<(), Box<dyn std::error::Error>> {
let trade_date = date(2026, 1, 2);
// Adding months keeps the day of the month, unless the target month is too
// short, when it clamps to that month's last day.
let cases = [
(date(2026, 1, 31), "1M"),
(date(2026, 1, 31), "2M"),
(date(2024, 2, 29), "1Y"),
(date(2026, 3, 31), "-1M"),
(date(2026, 5, 15), "6M"),
];
println!("{:<12} {:<6} result", "date", "tenor");
for (start, spelling) in cases {
let result = add_period(start, tenor(spelling));
println!("{:<12} {spelling:<6} {result}", start.to_string());
}
// Clamping is why a schedule measures every date from one anchor: two
// one-month steps from 31 January lose the 31st, one two-month step keeps it.
let chained = add_period(add_period(date(2026, 1, 31), tenor("1M")), tenor("1M"));
let anchored = add_period(date(2026, 1, 31), tenor("2M"));
println!("31 Jan + 1M + 1M = {chained}; 31 Jan + 2M = {anchored}");
// A forward tenor is one or two segments: a term from spot, or a forward
// start followed by a term. "2y2y" is a two-year term starting two years
// after spot.
println!("{:<9} {:<14} tenor", "spelling", "forward start");
for spelling in ["10Y", "2y2y", "6M5Y", "1Y1Y"] {
let parsed: ForwardTenor = spelling.parse().expect("a forward tenor");
let start = parsed.forward_start.map_or_else(|| "spot".to_owned(), |p| p.to_string());
println!("{spelling:<9} {start:<14} {}", parsed.tenor);
}
// Three segments, or a segment that is not a period, is refused by name.
let refused = ["1Y2Y3Y", "2Y X", "5"]
.iter()
.filter(|bad| bad.parse::<ForwardTenor>().is_err())
.count();
println!("refused spellings: {refused} of 3");
// An IMM date is the third Wednesday of March, June, September or December.
// Futures and IMM-dated swaps roll on it; the code names it as month and year.
let quarterly = ImmCycle::Imm3M;
let next_imm = next_imm_date(trade_date, quarterly);
let code = imm_code_of(next_imm).expect("an IMM date has a code");
let (start, end) = imm_code_to_dates(&code, quarterly).expect("a well-formed code");
// The code names the period that starts on its date and runs to the next IMM
// date: the contract's quarter, as a futures template reads it.
let AnyTemplate::Future(sofr) = template("USD-SOFR-3M-FUT").expect("a registered template") else {
panic!("USD-SOFR-3M-FUT is a futures template");
};
// The monthly cycle admits every month's third Wednesday; the quarterly
// does not.
let january = nth_weekday(3, DayOfWeek::Wednesday, 2026, 1).expect("a third Wednesday");
println!("trade date {trade_date}");
println!("next IMM date {next_imm} ({})", next_imm.format("%a"));
println!("its code {code}");
println!("reference period {start} to {end}");
let yes_no = |b: bool| if b { "yes" } else { "no" };
println!(
"third Wed of January {january}: monthly IMM {}, quarterly IMM {}",
yes_no(is_imm_date(january, ImmCycle::Imm1M)),
yes_no(is_imm_date(january, quarterly))
);
// CDS coupons fall on the twentieth of the IMM months. The standard maturity
// rolls twice a year, on 20 March and 20 September, and lands on a 20 June
// or 20 December. Two trade dates either side of the March roll show the step.
// The next CDS date is strictly after the trade date, so a trade on the
// twentieth itself reads the following quarter's.
println!("{:<12} {:<14} {:<12}", "trade date", "next CDS date", "5Y maturity");
let mut maturities = Vec::new();
for traded in [date(2026, 3, 19), date(2026, 3, 20)] {
let next_cds = next_cds_imm_date(traded);
let maturity = cds_standard_maturity(traded, 5);
maturities.push(maturity);
println!(
"{:<12} {:<14} {:<12}",
traded.to_string(),
next_cds.to_string(),
maturity.to_string()
);
}
// Crossing the roll moves the on-the-run maturity out by six months.
// Some dates are named by position: the third Friday, the next Monday. A
// roll snaps a date inside its own month to the day a convention names.
let third_friday = nth_weekday(3, DayOfWeek::Friday, 2026, 6).expect("a third Friday");
let next_monday = next_weekday(trade_date, DayOfWeek::Monday);
let any_day = date(2026, 2, 10);
let rolls = [
("EOM", RollConvention::Eom),
("IMM", RollConvention::Imm),
("day 30", RollConvention::DayOfMonth(30)),
("IMM twentieth", RollConvention::ImmTwentieth),
];
println!("third Friday of June 2026 {third_friday}");
println!("next Monday after {trade_date} {next_monday}");
println!("rolls of {any_day}:");
for (label, convention) in rolls {
let rolled = convention.apply(any_day);
println!(" {label:<14} {rolled}");
}
// A tenor of descending segments in one family reads as their sum: a
// fifteen-month tenor can be written "1Y3M". Months and days do not mix.
let fifteen = tenor("1Y3M");
let spot = date(2026, 10, 5);
println!("1Y3M is {} months", fifteen.in_months().expect("a month tenor"));
println!("2W3D is {} days", tenor("2W3D").in_days().expect("a day tenor"));
println!("spot + 1Y3M {}", add_period(spot, fifteen));
let mixed = "1M3D".parse::<Period>().is_err();
println!("1M3D refused {}", if mixed { "yes" } else { "no" });
// Beside the third Wednesday, an IMM date can be the first Wednesday on or
// after the 9th: in October 2024 the 9th is itself a Wednesday.
let third = imm_date(10, 2024, ImmCycle::Imm1M).expect("a cycle month");
let ninth = imm_date_on(10, 2024, ImmCycle::Imm1M, ImmDay::WednesdayOnOrAfterNinth)
.expect("a cycle month");
let january_9th = imm_date_on(1, 2023, ImmCycle::Imm1M, ImmDay::WednesdayOnOrAfterNinth)
.expect("a cycle month");
println!("third Wednesday, October 2024 {third}");
println!("on or after the 9th, October 2024 {ninth}");
println!("on or after the 9th, January 2023 {january_9th}");
Ok(())
}import datetime as dt
import helixio as hlx
dates = hlx.dates
TRADE_DATE = dt.date(2026, 1, 2)
# Adding months keeps the day of the month, unless the target month is too
# short, when it clamps to that month's last day.
cases = [
(dt.date(2026, 1, 31), "1M"),
(dt.date(2026, 1, 31), "2M"),
(dt.date(2024, 2, 29), "1Y"),
(dt.date(2026, 3, 31), "-1M"),
(dt.date(2026, 5, 15), "6M"),
]
print(f"{'date':<12} {'tenor':<6} result")
for start, spelling in cases:
result = dates.add_period(start, spelling)
print(f"{start!s:<12} {spelling:<6} {result}")
# Clamping is why a schedule measures every date from one anchor: two
# one-month steps from 31 January lose the 31st, one two-month step keeps it.
chained = dates.add_period(dates.add_period(dt.date(2026, 1, 31), "1M"), "1M")
anchored = dates.add_period(dt.date(2026, 1, 31), "2M")
print(f"31 Jan + 1M + 1M = {chained}; 31 Jan + 2M = {anchored}")
# A forward tenor is one or two segments: a term from spot, or a forward start
# followed by a term. "2y2y" is a two-year term starting two years after spot.
print(f"{'spelling':<9} {'forward start':<14} tenor")
for spelling in ("10Y", "2y2y", "6M5Y", "1Y1Y"):
start, term = dates.forward_tenor(spelling)
print(f"{spelling:<9} {start or 'spot':<14} {term}")
# Three segments, or a segment that is not a period, is refused by name.
refused = 0
for bad in ("1Y2Y3Y", "2Y X", "5"):
try:
dates.forward_tenor(bad)
except ValueError:
refused += 1
print(f"refused spellings: {refused} of 3")
# An IMM date is the third Wednesday of March, June, September or December.
# Futures and IMM-dated swaps roll on it; the code names it as month and year.
next_imm = dates.next_imm_date(TRADE_DATE)
code = dates.imm_code_of(next_imm)
start, end = dates.imm_code_to_dates(code)
# The monthly cycle admits every month's third Wednesday; the quarterly does not.
january = dates.nth_weekday(3, "Wednesday", 2026, 1)
print(f"trade date {TRADE_DATE}")
print(f"next IMM date {next_imm} ({next_imm.strftime('%a')})")
print(f"its code {code}")
print(f"reference period {start} to {end}")
monthly = "yes" if dates.is_imm_date(january, "Monthly") else "no"
quarterly = "yes" if dates.is_imm_date(january) else "no"
print(f"third Wed of January {january}: monthly IMM {monthly}, quarterly IMM {quarterly}")
# CDS coupons fall on the twentieth of the IMM months. The standard maturity
# rolls twice a year, on 20 March and 20 September, and lands on a 20 June or
# 20 December. Two trade dates either side of the March roll show the step.
# The next CDS date is strictly after the trade date, so a trade on the
# twentieth itself reads the following quarter's.
print(f"{'trade date':<12} {'next CDS date':<14} {'5Y maturity':<12}")
maturities = []
for traded in (dt.date(2026, 3, 19), dt.date(2026, 3, 20)):
next_cds = dates.next_cds_imm_date(traded)
maturity = dates.cds_standard_maturity(traded, 5)
maturities.append(maturity)
print(f"{traded!s:<12} {next_cds!s:<14} {maturity!s:<12}")
# Crossing the roll moves the on-the-run maturity out by six months.
# Some dates are named by position: the third Friday, the next Monday. A roll
# snaps a date inside its own month to the day a convention names.
third_friday = dates.nth_weekday(3, "Friday", 2026, 6)
next_monday = dates.next_weekday(TRADE_DATE, "Monday")
ANY_DAY = dt.date(2026, 2, 10)
rolls = [
("EOM", dates.RollConvention.Eom),
("IMM", dates.RollConvention.Imm),
("day 30", dates.RollConvention.day(30)),
("IMM twentieth", dates.RollConvention.ImmTwentieth),
]
print(f"third Friday of June 2026 {third_friday}")
print(f"next Monday after {TRADE_DATE} {next_monday}")
print(f"rolls of {ANY_DAY}:")
for label, convention in rolls:
rolled = dates.roll(ANY_DAY, convention)
print(f" {label:<14} {rolled}")
# A tenor of descending segments in one family reads as their sum: a
# fifteen-month tenor can be written "1Y3M". Months and days do not mix.
fifteen = dates.Period("1Y3M")
spot = dt.date(2026, 10, 5)
print(f"1Y3M is {fifteen.in_months()} months")
print(f"2W3D is {dates.Period('2W3D').in_days()} days")
print(f"spot + 1Y3M {dates.add_period(spot, '1Y3M')}")
try:
dates.Period("1M3D")
mixed = False
except ValueError:
mixed = True
print(f"1M3D refused {'yes' if mixed else 'no'}")
# Beside the third Wednesday, an IMM date can be the first Wednesday on or
# after the 9th: in October 2024 the 9th is itself a Wednesday.
third = dates.imm_date(10, 2024, "Monthly")
ninth = dates.imm_date(10, 2024, "Monthly", "WednesdayOnOrAfterNinth")
january_9th = dates.imm_date(1, 2023, "Monthly", "WednesdayOnOrAfterNinth")
print(f"third Wednesday, October 2024 {third}")
print(f"on or after the 9th, October 2024 {ninth}")
print(f"on or after the 9th, January 2023 {january_9th}")date tenor result
2026-01-31 1M 2026-02-28
2026-01-31 2M 2026-03-31
2024-02-29 1Y 2025-02-28
2026-03-31 -1M 2026-02-28
2026-05-15 6M 2026-11-15
31 Jan + 1M + 1M = 2026-03-28; 31 Jan + 2M = 2026-03-31
spelling forward start tenor
10Y spot 10Y
2y2y 2Y 2Y
6M5Y 6M 5Y
1Y1Y 1Y 1Y
refused spellings: 3 of 3
trade date 2026-01-02
next IMM date 2026-03-18 (Wed)
its code MAR26
reference period 2026-03-18 to 2026-06-17
third Wed of January 2026-01-21: monthly IMM yes, quarterly IMM no
trade date next CDS date 5Y maturity
2026-03-19 2026-03-20 2030-12-20
2026-03-20 2026-06-20 2031-06-20
third Friday of June 2026 2026-06-19
next Monday after 2026-01-02 2026-01-05
rolls of 2026-02-10:
EOM 2026-02-28
IMM 2026-02-18
day 30 2026-02-28
IMM twentieth 2026-02-20
1Y3M is 15 months
2W3D is 17 days
spot + 1Y3M 2028-01-05
1M3D refused yes
third Wednesday, October 2024 2024-10-16
on or after the 9th, October 2024 2024-10-09
on or after the 9th, January 2023 2023-01-11
See also
- Source:
crates/helixrs/src/statics/time/period.rs,crates/helixrs/src/statics/imm.rs - Related pages:
- How dates and conventions work, for why markets quote lengths, and month arithmetic rebuilt by hand.
- Schedules, for dates stepped from one anchor.
- Calendars, for rolling a date to a business day.
- Listed futures, for contracts dated by IMM months.
From the 0.0 line at revision bf7f2e2. Every printed number is that revision's own output.