Esc

    The API is under testing and may change before release.

    Calendars

    This page lists everything you need to work with business-day calendars: how to resolve one from a centre code, the verbs that roll, step, advance and count dates on it, their arguments and defaults, and the edge cases worth knowing before a date comes out one day off from a counterparty's.

    Quick example

    This resolves New York (USNY) and London (GBLO) and asks each whether three days around Independence Day 2031 are open.

    Rust
    use chrono::NaiveDate;
    use helixrs::{
        Calendar, calendar,
    };
    
    fn date(year: i32, month: u32, day: u32) -> NaiveDate {
        NaiveDate::from_ymd_opt(year, month, day).expect("a real calendar date")
    }
    
    fn state(calendar: &dyn Calendar, day: NaiveDate) -> &'static str {
        if calendar.is_business_day(day) {
            "open"
        } else if calendar.is_holiday(day) {
            "holiday"
        } else {
            "weekend"
        }
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let new_york = calendar("USNY").expect("a registered business centre");
        let london = calendar("GBLO").expect("a registered business centre");
    
        // Friday 4 July 2031 is Independence Day: a listed closure in New York and
        // an ordinary working day in London. A calendar answers for its own market.
        let independence = date(2031, 7, 4);
    
        // A weekend day and a listed closure are different reasons for being shut,
        // and the two predicates never overlap.
        let saturday = date(2031, 7, 5);
    
        println!("{:<12} {:<9} {:<8} {:<8}", "date", "weekday", "USNY", "GBLO");
        for day in [date(2031, 7, 3), independence, saturday] {
            println!(
                "{:<12} {:<9} {:<8} {:<8}",
                day.to_string(),
                day.format("%A").to_string(),
                state(new_york.as_ref(), day),
                state(london.as_ref(), day)
            );
        }
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    
    NEW_YORK = hlx.Calendar("USNY")
    LONDON = hlx.Calendar("GBLO")
    
    # Friday 4 July 2031 is Independence Day: a listed closure in New York and an
    # ordinary working day in London. A calendar answers for its own market only.
    INDEPENDENCE = dt.date(2031, 7, 4)
    
    # A weekend day and a listed closure are different reasons for being shut, and
    # the two predicates never overlap.
    SATURDAY = dt.date(2031, 7, 5)
    
    def state(calendar: hlx.Calendar, day: dt.date) -> str:
        if calendar.is_business_day(day):
            return "open"
        return "holiday" if calendar.is_holiday(day) else "weekend"
    
    print(f"{'date':<12} {'weekday':<9} {'USNY':<8} {'GBLO':<8}")
    for day in (dt.date(2031, 7, 3), INDEPENDENCE, SATURDAY):
        print(f"{day!s:<12} {day.strftime('%A'):<9} {state(NEW_YORK, day):<8} {state(LONDON, day):<8}")
    date         weekday   USNY     GBLO    
    2031-07-03   Thursday  open     open    
    2031-07-04   Friday    holiday  open    
    2031-07-05   Saturday  weekend  weekend 

    A calendar answers for its own market only: Independence Day is a listed closure in New York and an ordinary working day in London, and a Saturday is a weekend in both, never a holiday.

    API

    Verb Meaning Rust Python
    a calendar resolved from a centre code or an expression calendar("USNY") Calendar("USNY")
    open or closed the business-day predicate everything else is built on Calendar::is_business_day Calendar.is_business_day
    adjust roll a closed date under a convention Calendar::adjust Calendar.adjust
    step move past nn open days Calendar::add_business_days Calendar.add_business_days
    advance add a tenor, then roll once Calendar::advance Calendar.advance
    count open days in a half-open interval Calendar::business_days_between Calendar.business_days_between
    joint two centres, one predicate JointCalendar, or calendar("GBLO,USNY") Calendar("GBLO,USNY"), Calendar.join
    settle a lag counted on one calendar, landing on two SettlementLag::with_settlement Calendar.settle(..., settlement=)
    your own a derived or ingested calendar amended, DataCalendar::new, ingest_calendars Calendar.amended, Calendar.from_holidays, dates.ingest_calendars
    Argument Type Default Meaning
    Calendar(code) str required a centre code such as "USNY", or a joint expression such as "GBLO,USNY"; case is ignored
    adjust(date, business_day_convention) date, BusinessDayConvention or str required the roll; "ModFollowing", "Following", "Preceding", "ModPreceding", "Nearest", "Unadjusted"
    add_business_days(date, n) date, int required steps n open days; a negative n steps back
    advance(date, tenor, ...) date, Period or str required month arithmetic, then one roll; "4B" steps business days instead
    advance(..., business_day_convention=) BusinessDayConvention or str "ModFollowing" the roll after the arithmetic
    advance(..., end_of_month=) bool False from a business month end, land on a business month end
    advance(..., roll_convention=) RollConvention, str or None None an explicit roll day for the arithmetic
    business_days_between(start, end, ...) date, date required counts open days in [start, end)
    business_days_between(..., include_first=, include_last=) bool or None None widen or narrow the interval at either end
    holidays(start, end, include_weekends=) date, date, bool include_weekends=False the listed closures in the range
    settle(date, n, unit=, settlement=, business_day_convention=) date, int, str, Calendar or None, convention unit="business_days", settlement=None, "Following" steps n on this calendar, then lands on a day open on both
    join(calendars, rule=) list[Calendar], JointCalendarRule or str rule="join_holidays" "join_holidays" is open where all are open; "join_business_days" where any is
    amended(closing=, opening=) list[date] or None None a new calendar with extra closures or openings; the original is untouched
    from_holidays(name, holidays, weekend=) str, list[date], list[str] or None weekend=None (Saturday and Sunday) a calendar from your own list; a registered centre's name is refused

    Defaults and limits

    • Weekends and holidays never overlap. is_weekend and is_holiday give different reasons for a day being shut; a Saturday is never a holiday.
    • Counting is half-open. business_days_between(start, end) counts the start when it's open and never the end, so adjacent intervals add up. An interval whose end isn't after its start counts nothing.
    • Stepping is not advancing. A T+2 lag steps two open days. Advancing by "2D" adds two calendar days first and rolls once, so over a holiday the two give different dates.
    • Calendars are never edited. A closure the shipped data lacks is added with amended, or a whole calendar with from_holidays; every registry calendar resolves to exactly what it did before.
    • Ingest before drafting. dates.ingest_calendars binds a code for every later lookup in the process; a trade drafted before keeps its dates.
    • Shipped centres include USNY, GBLO, EUTA and the centres non-deliverable forwards fix and settle in, such as Bogotá (COBO), Lima (PELI) and Santo Domingo (SNDA). There is no call that lists every shipped centre yet.

    !!! warning "4B and 4D give different dates" "4B" steps four business days on the calendar, the same date add_business_days gives. "4D" adds four calendar days and then adjusts once, so over a holiday the two part. Python's Calendar.advance reads the "B" spelling; in Rust, use add_business_days.

    !!! warning "An FX value date needs two calendars" A lag is counted on the market's business calendar, and the value date must also be open on the settlement currency's centre. Use settle(..., settlement=) rather than stepping on a joint calendar.

    More examples

    Every verb above in one program: adjusting under each convention, stepping against advancing, counting, joint calendars, settling on two axes, month ends, the holiday list, the non-deliverable centres, and business-day tenors. The example reads no shared data. Amending, building and ingesting your own calendar is on Use your own calendar.

    Rust
    use std::sync::Arc;
    
    use chrono::{Datelike, Duration, NaiveDate, Weekday};
    use helixrs::{
        BusinessDayConvention, Calendar, JointCalendar, JointCalendarRule, Period, SettlementLag,
        SettlementLagUnit, calendar, last_day_of_month,
    };
    
    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 state(calendar: &dyn Calendar, day: NaiveDate) -> &'static str {
        if calendar.is_business_day(day) {
            "open"
        } else if calendar.is_holiday(day) {
            "holiday"
        } else {
            "weekend"
        }
    }
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let new_york = calendar("USNY").expect("a registered business centre");
        let london = calendar("GBLO").expect("a registered business centre");
        let year_start = date(2031, 1, 1);
        let year_end = date(2032, 1, 1);
        let one_day = Duration::days(1);
    
        // Sunday 31 August 2031. Monday 1 September is Labor Day in New York, so
        // the open days either side are Friday 29 August and Tuesday 2 September.
        // Each convention is a rule for choosing between those two.
        let sunday = date(2031, 8, 31);
        let after = new_york.next_business_day(sunday);
        let before = new_york.previous_business_day(sunday);
    
        // The expectation for each convention is its definition, written out.
        let nearest = if after - sunday <= sunday - before { after } else { before };
        let same_month_after = if after.month() == sunday.month() { after } else { before };
        let same_month_before = if before.month() == sunday.month() { before } else { after };
        let expected = [
            ("Following", BusinessDayConvention::Following, after),
            ("ModifiedFollowing", BusinessDayConvention::ModifiedFollowing, same_month_after),
            ("Preceding", BusinessDayConvention::Preceding, before),
            ("ModifiedPreceding", BusinessDayConvention::ModifiedPreceding, same_month_before),
            ("Nearest", BusinessDayConvention::Nearest, nearest),
            ("Unadjusted", BusinessDayConvention::None, sunday),
        ];
        println!("{:<22} {:<14} adjusted", "convention", "code");
        for (name, convention, _) in expected {
            let adjusted = new_york.adjust(sunday, convention);
            println!("{name:<22} {:<14} {adjusted}", convention.code());
        }
    
        // Thursday 3 July 2031. A T+2 lag steps two open days, skipping the Friday
        // closure and the weekend. Advancing by the tenor "2D" adds two calendar
        // days first and rolls once, so it lands on the Saturday and rolls to Monday.
        let thursday = date(2031, 7, 3);
        let stepped = new_york.add_business_days(thursday, 2);
        let advanced =
            new_york.advance(thursday, tenor("2D"), BusinessDayConvention::Following, false);
    
        println!("start                {thursday} ({})", thursday.format("%a"));
        println!("step 2 business days {stepped} ({})", stepped.format("%a"));
        println!("advance by 2D        {advanced} ({})", advanced.format("%a"));
    
        // Business days in a year, three ways. The two-date count is half-open: it
        // counts the start when open and never the end, so adjacent intervals add.
        let mid_year = date(2031, 7, 1);
        let ny_days = new_york.business_days_between(year_start, year_end);
        let first_half = new_york.business_days_between(year_start, mid_year);
        let second_half = new_york.business_days_between(mid_year, year_end);
    
        // Listing the open days gives the same number, and so does the inclusive
        // form with the default endpoints (first counted, last not).
        let listed = new_york.business_days(year_start, year_end - one_day);
    
        println!("New York business days in 2031  {ny_days}");
        println!("  January to June               {first_half}");
        println!("  July to December              {second_half}");
    
        // A trade touching two centres needs a rule for joining them. The
        // expression "USNY,GBLO" is the union of closures: open only when both
        // centres are open. The intersection of closures is open when either is,
        // and is a `JointCalendar` under the other rule.
        let joint = calendar("USNY,GBLO").expect("a joint expression");
        let members: Vec<Arc<dyn Calendar>> = vec![new_york.clone(), london.clone()];
        let either = JointCalendar::new(members.clone(), JointCalendarRule::JoinBusinessDays);
        // The expression and the union rule are the same calendar.
        let union = JointCalendar::new(members, JointCalendarRule::JoinHolidays);
    
        let ldn_days = london.business_days_between(year_start, year_end);
        let union_days = joint.business_days_between(year_start, year_end);
        let either_days = either.business_days_between(year_start, year_end);
    
        println!("open days in 2031, New York                 {ny_days}");
        println!("open days in 2031, London                   {ldn_days}");
        println!("open in both (union of closures)            {union_days}");
        println!("open in either (intersection of closures)   {either_days}");
    
        // Settling has two axes: London decides which days count towards a lag, and
        // New York decides which days cash can land. A T+2 lag from Wednesday 2 July
        // steps two London days to Friday 4 July, which cannot settle in dollars.
        let wednesday = date(2031, 7, 2);
    
        let on_business_axis = london.add_business_days(wednesday, 2);
        let value_date = SettlementLag::with_settlement(
            2,
            SettlementLagUnit::BusinessDays,
            london.clone(),
            new_york.clone(),
            BusinessDayConvention::Following,
        )
        .apply(wednesday);
        // The value date is the London step, rolled forward until both centres open.
        let mut expected_value = on_business_axis;
        while !(london.is_business_day(expected_value) && new_york.is_business_day(expected_value)) {
            expected_value += one_day;
        }
    
        println!("trade date                    {wednesday}");
        println!("T+2 on the London axis        {on_business_axis}");
        println!("value date, settling in USD   {value_date}");
    
        // August 2031 ends on a Sunday. The business month end is the last open
        // day, the Friday.
        let mid_august = date(2031, 8, 12);
        let calendar_end = last_day_of_month(mid_august);
        let business_end = new_york.end_of_month(mid_august);
    
        // From the business month end, a one-month advance with the end-of-month
        // flag lands on September's business month end; without it the day is kept.
        let with_flag =
            new_york.advance(business_end, tenor("1M"), BusinessDayConvention::Following, true);
        let without_flag =
            new_york.advance(business_end, tenor("1M"), BusinessDayConvention::Following, false);
    
        println!("calendar month end        {calendar_end} ({})", calendar_end.format("%a"));
        println!("business month end        {business_end} ({})", business_end.format("%a"));
        println!("+1M, end-of-month flag    {with_flag}");
        println!("+1M, no flag              {without_flag}");
    
        // The listed closures in a year, weekends excluded. Together they account
        // for every weekday that is not a business day.
        let holidays = new_york.holidays(year_start, year_end - one_day, false);
        let mut weekdays = 0_i64;
        let mut day = year_start;
        while day < year_end {
            weekdays += i64::from(!matches!(day.weekday(), Weekday::Sat | Weekday::Sun));
            day += one_day;
        }
        let holiday_count = holidays.len() as i64;
    
        println!("New York closures in 2031: {holiday_count}");
        for holiday in &holidays {
            println!("  {holiday}  {}", holiday.format("%a"));
        }
    
        for code in ["COBO", "PELI", "SNDA"] {
            let open_days = calendar(code).expect("a shipped centre").business_days_between(year_start, year_end);
            println!("{code}  open days in 2031  {open_days}");
        }
    
        // A tenor in business days steps that many good business days on the
        // calendar: over London's new year that is past "4D", which counts calendar
        // days and then adjusts. Python spells the step "4B" on `Calendar.advance`;
        // in Rust it is `add_business_days`.
        let seed = NaiveDate::from_ymd_opt(2022, 12, 28).expect("a real calendar date");
        let four_b = london.add_business_days(seed, 4);
        let four_d = london.advance(seed, tenor("4D"), BusinessDayConvention::Following, false);
        println!("{seed} + 4B  {four_b}");
        println!("{seed} + 4D  {four_d}");
    
        Ok(())
    }
    Python
    import datetime as dt
    
    import helixio as hlx
    
    BusinessDayConvention = hlx.dates.BusinessDayConvention
    NEW_YORK = hlx.Calendar("USNY")
    LONDON = hlx.Calendar("GBLO")
    YEAR_START = dt.date(2031, 1, 1)
    YEAR_END = dt.date(2032, 1, 1)
    ONE_DAY = dt.timedelta(days=1)
    
    def state(calendar: hlx.Calendar, day: dt.date) -> str:
        if calendar.is_business_day(day):
            return "open"
        return "holiday" if calendar.is_holiday(day) else "weekend"
    
    # Sunday 31 August 2031. Monday 1 September is Labor Day in New York, so the
    # open days either side are Friday 29 August and Tuesday 2 September. Each
    # convention is a rule for choosing between those two.
    SUNDAY = dt.date(2031, 8, 31)
    after = NEW_YORK.next_business_day(SUNDAY)
    before = NEW_YORK.previous_business_day(SUNDAY)
    
    # The expectation for each convention is its definition, written out.
    nearest = after if (after - SUNDAY) <= (SUNDAY - before) else before
    expected = {
        BusinessDayConvention.Following: after,
        BusinessDayConvention.ModifiedFollowing: after if after.month == SUNDAY.month else before,
        BusinessDayConvention.Preceding: before,
        BusinessDayConvention.ModifiedPreceding: before if before.month == SUNDAY.month else after,
        BusinessDayConvention.Nearest: nearest,
        BusinessDayConvention.Unadjusted: SUNDAY,
    }
    print(f"{'convention':<22} {'code':<14} adjusted")
    for convention in expected:
        adjusted = NEW_YORK.adjust(SUNDAY, convention)
        name = repr(convention).split(".")[1]
        print(f"{name:<22} {convention.code():<14} {adjusted}")
    
    # Thursday 3 July 2031. A T+2 lag steps two open days, skipping the Friday
    # closure and the weekend. Advancing by the tenor "2D" adds two calendar days
    # first and rolls once, so it lands on the Saturday and rolls to Monday.
    THURSDAY = dt.date(2031, 7, 3)
    stepped = NEW_YORK.add_business_days(THURSDAY, 2)
    advanced = NEW_YORK.advance(THURSDAY, "2D", BusinessDayConvention.Following)
    
    print(f"start                {THURSDAY} ({THURSDAY.strftime('%a')})")
    print(f"step 2 business days {stepped} ({stepped.strftime('%a')})")
    print(f"advance by 2D        {advanced} ({advanced.strftime('%a')})")
    
    # Business days in a year, three ways. The two-date count is half-open: it
    # counts the start when open and never the end, so adjacent intervals add up.
    MID_YEAR = dt.date(2031, 7, 1)
    ny_days = NEW_YORK.business_days_between(YEAR_START, YEAR_END)
    first_half = NEW_YORK.business_days_between(YEAR_START, MID_YEAR)
    second_half = NEW_YORK.business_days_between(MID_YEAR, YEAR_END)
    
    print(f"New York business days in 2031  {ny_days}")
    print(f"  January to June               {first_half}")
    print(f"  July to December              {second_half}")
    
    # A trade touching two centres needs a rule for joining them. The expression
    # "USNY,GBLO" is the union of closures: open only when both centres are open.
    # The intersection of closures is open when either centre is, and is a joint
    # calendar under the other rule.
    JOINT = hlx.Calendar("USNY,GBLO")
    EITHER = hlx.Calendar.join([NEW_YORK, LONDON], hlx.dates.JointCalendarRule.JoinBusinessDays)
    
    ldn_days = LONDON.business_days_between(YEAR_START, YEAR_END)
    union_days = JOINT.business_days_between(YEAR_START, YEAR_END)
    either_days = EITHER.business_days_between(YEAR_START, YEAR_END)
    
    print(f"open days in 2031, New York                 {ny_days}")
    print(f"open days in 2031, London                   {ldn_days}")
    print(f"open in both (union of closures)            {union_days}")
    print(f"open in either (intersection of closures)   {either_days}")
    
    # Settling has two axes: London decides which days count towards a lag, and New
    # York decides which days cash can land. A T+2 lag from Wednesday 2 July steps
    # two London days to Friday 4 July, which cannot settle in dollars.
    WEDNESDAY = dt.date(2031, 7, 2)
    
    on_business_axis = LONDON.add_business_days(WEDNESDAY, 2)
    value_date = LONDON.settle(WEDNESDAY, 2, settlement=NEW_YORK)
    # The value date is the London step, rolled forward until both centres are open.
    expected_value = on_business_axis
    while not (LONDON.is_business_day(expected_value) and NEW_YORK.is_business_day(expected_value)):
        expected_value += ONE_DAY
    
    print(f"trade date                    {WEDNESDAY}")
    print(f"T+2 on the London axis        {on_business_axis}")
    print(f"value date, settling in USD   {value_date}")
    
    # August 2031 ends on a Sunday. The business month end is the last open day,
    # the Friday.
    MID_AUGUST = dt.date(2031, 8, 12)
    calendar_end = hlx.dates.last_day_of_month(MID_AUGUST)
    business_end = NEW_YORK.end_of_month(MID_AUGUST)
    
    # From the business month end, a one-month advance with the end-of-month flag
    # lands on September's business month end; without it the day is kept.
    with_flag = NEW_YORK.advance(business_end, "1M", BusinessDayConvention.Following, True)
    without_flag = NEW_YORK.advance(business_end, "1M", BusinessDayConvention.Following, False)
    
    print(f"calendar month end        {calendar_end} ({calendar_end.strftime('%a')})")
    print(f"business month end        {business_end} ({business_end.strftime('%a')})")
    print(f"+1M, end-of-month flag    {with_flag}")
    print(f"+1M, no flag              {without_flag}")
    
    # The listed closures in a year, weekends excluded. Together they account for
    # every weekday that is not a business day.
    holidays = NEW_YORK.holidays(YEAR_START, YEAR_END - ONE_DAY)
    holiday_count = len(holidays)
    
    print(f"New York closures in 2031: {holiday_count}")
    for holiday in holidays:
        print(f"  {holiday}  {holiday.strftime('%a')}")
    
    # The business centres a non-deliverable forward fixes and settles in ship as
    # calendars like any other. Each closes its own holidays, so each opens fewer
    # days in the year than New York's weekdays alone.
    for code in ("COBO", "PELI", "SNDA"):
        open_days = hlx.Calendar(code).business_days_between(YEAR_START, YEAR_END)
        print(f"{code}  open days in 2031  {open_days}")
    
    # A tenor in business days, "4B", steps four good business days on the
    # calendar: over London's new year that is past "4D", which counts calendar days
    # and then adjusts.
    SEED = dt.date(2022, 12, 28)
    four_b = LONDON.advance(SEED, "4B", BusinessDayConvention.Following)
    four_d = LONDON.advance(SEED, "4D", BusinessDayConvention.Following)
    print(f"{SEED} + 4B  {four_b}")
    print(f"{SEED} + 4D  {four_d}")
    convention             code           adjusted
    Following              FOLLOWING      2031-09-02
    ModifiedFollowing      MODFOLLOWING   2031-08-29
    Preceding              PRECEDING      2031-08-29
    ModifiedPreceding      MODPRECEDING   2031-08-29
    Nearest                NEAREST        2031-09-02
    Unadjusted             NONE           2031-08-31
    start                2031-07-03 (Thu)
    step 2 business days 2031-07-08 (Tue)
    advance by 2D        2031-07-07 (Mon)
    New York business days in 2031  250
      January to June               124
      July to December              126
    open days in 2031, New York                 250
    open days in 2031, London                   253
    open in both (union of closures)            245
    open in either (intersection of closures)   258
    trade date                    2031-07-02
    T+2 on the London axis        2031-07-04
    value date, settling in USD   2031-07-07
    calendar month end        2031-08-31 (Sun)
    business month end        2031-08-29 (Fri)
    +1M, end-of-month flag    2031-09-30
    +1M, no flag              2031-09-29
    New York closures in 2031: 11
      2031-01-01  Wed
      2031-01-20  Mon
      2031-02-17  Mon
      2031-05-26  Mon
      2031-06-19  Thu
      2031-07-04  Fri
      2031-09-01  Mon
      2031-10-13  Mon
      2031-11-11  Tue
      2031-11-27  Thu
      2031-12-25  Thu
    COBO  open days in 2031  243
    PELI  open days in 2031  249
    SNDA  open days in 2031  249
    2022-12-28 + 4B  2023-01-04
    2022-12-28 + 4D  2023-01-03

    See also

    • Source: crates/helixrs/src/statics/calendars/lookup.rs, crates/helixrs/src/statics/calendars/settlement_lag.rs
    • Related pages:
      • How dates and conventions work, for why every verb comes from one predicate, and the verbs rebuilt by hand.
      • Day counts, for BUS/252 on a calendar.
      • Schedules, for boundaries rolled on a calendar.
      • Conventions and templates, for the calendar a template carries.

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