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. try_parse_tenor returns None
instead of failing.
For example:
tenor_apply(tenor_months(3i64), date(2025i64, 6i64, 15i64), RejectInvalidDay)// 2025-09-15, 92 days latertenor_apply(tenor_months(1i64), date(2025i64, 1i64, 31i64), ClampToMonthEnd)// 2025-02-28tenor_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. Counting from
a non-business trade date, the first step lands on the next business day.
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 the Japanese
year-end closures move the count to 6 January 2026, where a US count 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:
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
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
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