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.
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(())
}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 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_weekendandis_holidaygive 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 withfrom_holidays; every registry calendar resolves to exactly what it did before. - Ingest before drafting.
dates.ingest_calendarsbinds a code for every later lookup in the process; a trade drafted before keeps its dates. - Shipped centres include
USNY,GBLO,EUTAand 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.
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(())
}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.