Esc

    The API is under testing and may change before release.

    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.

    Rust
    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(())
    }
    Python
    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. 12M equals 1Y and 7D equals 1W. 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: MAR26 runs 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.

    Rust
    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(())
    }
    Python
    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.