Tenors and schedules
Modules: Shoals.Tenor, Shoals.Schedule.
A calendar tenor represents a Std.Datetime period, with months measured
by the calendar.
Money-market tenors such as overnight are counted in business days instead.
A schedule steps from a fixed anchor by whole tenors and can then roll each
date in a business calendar. Dates and periods come from Std.Datetime, and
business-day rules from Std.Datetime.Business.
Calendar tenors
Section titled “Calendar tenors”type Tenor -- opaque: a positive Std.Datetime.Period
def tenor_days(n: i64) -> Tenordef tenor_weeks(n: i64) -> Tenordef tenor_months(n: i64) -> Tenordef tenor_years(n: i64) -> Tenordef tenor_period(t: Tenor) -> Perioddef tenor_apply(t: Tenor, reference: Date, overflow: DayOverflow) -> Datedef parse_tenor(text: string) -> Tenordef try_parse_tenor(text: string) -> Option[Tenor]A week is seven days and a year is twelve months. Each constructor fails
with a domain error for a count below 1. tenor_apply adds the period with
Std.Datetime.date_add_period and takes the caller's DayOverflow policy,
because a month step from the 31st has no day-31 answer in a shorter month:
ClampToMonthEnd moves to the month's last day and RejectInvalidDay fails.
parse_tenor accepts exactly one or more ASCII digits with a value of at
least 1 followed by D, W, M, or Y, such as 3M or 10Y. It fails on
anything else, including a sign, a space, a lowercase unit, and ON, TN,
or SN, which are not calendar tenors. A count too large for i64, such
as 99999999999999999999D, is rejected the same way. try_parse_tenor
returns None instead of failing, and parse_tenor fails with
Shoals.Tenor.parse_tenor: domain: "..." is not a count of at least 1
followed by D, W, M or Y.
For example:
in_three_months = tenor_apply(tenor_months(3i64), date(2025i64, 6i64, 15i64), RejectInvalidDay)-- 2025-09-15, 92 days laterend_of_february = tenor_apply(tenor_months(1i64), date(2025i64, 1i64, 31i64), ClampToMonthEnd)-- 2025-02-28leap_anniversary = tenor_apply(tenor_years(1i64), date(2024i64, 2i64, 29i64), ClampToMonthEnd)-- 2025-02-28Business-day tenors and spot lags
Section titled “Business-day tenors and spot lags”type BusinessDayTenor = | BusinessDayTenor { lag: i64, length: i64 }
def overnight() -> BusinessDayTenor -- lag 0, length 1def tomorrow_next() -> BusinessDayTenor -- lag 1, length 1def spot_next(spot_days: i64) -> BusinessDayTenor -- lag spot_days, length 1def business_day_tenor_dates(t: BusinessDayTenor, trade: Date, calendar: BusinessCalendar, start: NonBusinessStart) -> (Date, Date)
type SpotLagdef two_calendar_lag(days: i64, count_calendar: BusinessCalendar, adjust_calendar: BusinessCalendar, roll: BusinessDayRoll) -> SpotLagdef lagged_date(lag: SpotLag, trade: Date) -> DateON, TN, and SN are a lag and a length in business days: the period starts
lag business days after the trade date and ends length business days
after its start. business_day_tenor_dates returns the start and end in the
given calendar; start states what a non-business trade date does, either
RejectNonBusinessStart or RollStartForward. Spot-next starts at spot, so
the caller states the spot lag. Over the 4 July 2025 holiday in
us_federal(), overnight from 3 July runs to 7 July.
A spot lag follows the two-calendar form of OpenGamma Strata's
DaysAdjustment: lagged_date counts days business days in
count_calendar, then rolls the result in adjust_calendar under roll,
one of Unadjusted, Following, Preceding, ModifiedFollowing, or
ModifiedPreceding from Std.Datetime.Business. Counting from a
non-business trade date, the first step lands on the next business day. A
zero-day lag counts nothing: the trade date itself is rolled in
adjust_calendar, so from Saturday 5 July 2025 in us_federal() a
Following roll gives Monday 7 July and a Preceding roll gives
Thursday 3 July.
For example, two Japanese bank days from 2 July 2025 reach 4 July,
which the US federal roll moves to 7 July. From 30 December 2025,
two_calendar_lag(2i64, japan_bank(), us_federal(), Following) reaches
6 January 2026 because of the Japanese year-end closures, while
two_calendar_lag(2i64, us_federal(), us_federal(), Following) reaches
2 January. A negative lag fails, and so does a date outside either
calendar's horizon.
The examples use us_federal(), the US federal government calendar, as a
calendar with known closures; it is not a USD settlement calendar (see
Holiday calendars).
Schedules
Section titled “Schedules”type StubConvention = | NoStub | ShortInitial | LongInitial | ShortFinal | LongFinal
def schedule_unadjusted(start: Date, end: Date, tenor: Tenor, stub: StubConvention, end_of_month: bool, overflow: DayOverflow) -> List[Date]def schedule(start: Date, end: Date, tenor: Tenor, stub: StubConvention, end_of_month: bool, overflow: DayOverflow, calendar: BusinessCalendar, roll: BusinessDayRoll) -> List[Date]schedule_unadjusted returns the dates from start to end inclusive,
strictly increasing. Each date is the anchor plus k whole tenors, never the
previous date plus one tenor, so a day of month clamped in February does not
drift into the following months. A final stub anchors at start and an
initial stub at end. A short stub keeps the irregular period on its own; a
long stub merges it into the neighboring regular period. NoStub requires
the tenor to divide the span exactly and fails otherwise. With
end_of_month true, a month-end anchor and a whole-month tenor keep every
date on its month end, and the month end decides the day, so overflow
has nothing to decide. Otherwise overflow applies only to the dates the
schedule emits. The schedule first selects its dates as whole-tenor steps
from the anchor, locating them by clamping, then applies overflow once to
each selected step. Under RejectInvalidDay an emitted date on a nonexistent
day fails, while a step past the end, or a step a long stub merges away, that
would land on one does not.
Whether the tenor divides the span is judged under the same policy, so the
period from 30 December to 28 February is two regular months when clamped
and a stub when rejected. A start that is not before the end fails.
schedule rolls each date of the unadjusted schedule in calendar under
roll. It fails if two dates roll onto the same business day, rather than
silently dropping one, or if a date lies outside the calendar's horizon.
For example:
monthly_dates = schedule_unadjusted(date(2025i64, 1i64, 31i64), date(2025i64, 5i64, 31i64), tenor_months(1i64), NoStub, false, ClampToMonthEnd)-- 31 Jan, 28 Feb, 31 Mar, 30 Apr, 31 May
long_stub_dates = schedule_unadjusted(date(2025i64, 1i64, 15i64), date(2025i64, 12i64, 31i64), tenor_months(3i64), ShortInitial, false, ClampToMonthEnd)-- 15 Jan, then 31 Mar, 30 Jun, 30 Sep, 31 Dec anchored at the end
adjusted_dates = schedule(date(2025i64, 5i64, 4i64), date(2025i64, 8i64, 4i64), tenor_months(1i64), NoStub, false, ClampToMonthEnd, us_federal(), Following)-- 5 May, 4 Jun, 7 Jul, 4 Aug: Sunday 4 May and the 4 July holiday roll forward