use crate::calendar::{
BusinessDayConvention, Calendar, CalendarError, EndOfMonth, EndpointInclusion, TargetCalendar,
};
use crate::daycount::{ActualActualIcma, DeterminationPeriod, IcmaError};
use crate::error::IdentifierError;
use crate::identifier::{Entry, Identifiers};
use crate::time::{Date, DateError, Frequency, Period, TimeUnit};
use std::error::Error as StdError;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::str::FromStr;
mod cds;
mod cds_maturity;
mod old_cds;
mod roll;
pub use cds::{CdsCouponDates, CdsDatePlan};
pub use cds_maturity::{
CdsMaturityConvention, CdsMaturityOutcome, CdsMaturityResolution, resolve_cds_maturity,
};
pub use old_cds::OldCdsProvenance;
pub use roll::{RollProvenance, SpecialRollRule};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum DateGenerationRule {
Forward,
Backward,
Zero,
Roll(SpecialRollRule),
OldCds,
Cds,
Cds2015,
}
impl DateGenerationRule {
pub const ALL: [Self; 10] = [
Self::Forward,
Self::Backward,
Self::Zero,
Self::Roll(SpecialRollRule::ThirdWednesday),
Self::Roll(SpecialRollRule::ThirdWednesdayInclusive),
Self::Roll(SpecialRollRule::Twentieth),
Self::Roll(SpecialRollRule::TwentiethIMM),
Self::OldCds,
Self::Cds,
Self::Cds2015,
];
#[must_use]
pub const fn canonical_identifier(self) -> &'static str {
match self {
Self::Forward => "Forward",
Self::Backward => "Backward",
Self::Zero => "Zero",
Self::Roll(rule) => rule.canonical_identifier(),
Self::OldCds => "OldCDS",
Self::Cds => "CDS",
Self::Cds2015 => "CDS2015",
}
}
}
const RULE_IDS: Identifiers<DateGenerationRule> = Identifiers::new(
"date generation rule",
&[
Entry {
value: DateGenerationRule::Forward,
canonical: "Forward",
aliases: &[],
},
Entry {
value: DateGenerationRule::Backward,
canonical: "Backward",
aliases: &[],
},
Entry {
value: DateGenerationRule::Zero,
canonical: "Zero",
aliases: &[],
},
Entry {
value: DateGenerationRule::Roll(SpecialRollRule::ThirdWednesday),
canonical: "ThirdWednesday",
aliases: &[],
},
Entry {
value: DateGenerationRule::Roll(SpecialRollRule::ThirdWednesdayInclusive),
canonical: "ThirdWednesdayInclusive",
aliases: &[],
},
Entry {
value: DateGenerationRule::Roll(SpecialRollRule::Twentieth),
canonical: "Twentieth",
aliases: &[],
},
Entry {
value: DateGenerationRule::Roll(SpecialRollRule::TwentiethIMM),
canonical: "TwentiethIMM",
aliases: &[],
},
Entry {
value: DateGenerationRule::OldCds,
canonical: "OldCDS",
aliases: &[],
},
Entry {
value: DateGenerationRule::Cds,
canonical: "CDS",
aliases: &[],
},
Entry {
value: DateGenerationRule::Cds2015,
canonical: "CDS2015",
aliases: &[],
},
],
&[],
);
impl fmt::Display for DateGenerationRule {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_identifier())
}
}
impl FromStr for DateGenerationRule {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
RULE_IDS.parse(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StubPolicy {
None,
Short,
Long,
}
impl StubPolicy {
pub const ALL: [Self; 3] = [Self::None, Self::Short, Self::Long];
#[must_use]
pub const fn canonical_identifier(self) -> &'static str {
match self {
Self::None => "None",
Self::Short => "Short",
Self::Long => "Long",
}
}
}
const STUB_IDS: Identifiers<StubPolicy> = Identifiers::new(
"stub policy",
&[
Entry {
value: StubPolicy::None,
canonical: "None",
aliases: &[],
},
Entry {
value: StubPolicy::Short,
canonical: "Short",
aliases: &[],
},
Entry {
value: StubPolicy::Long,
canonical: "Long",
aliases: &[],
},
],
&[],
);
impl fmt::Display for StubPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_identifier())
}
}
impl FromStr for StubPolicy {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
STUB_IDS.parse(text)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PeriodRegularity {
Regular,
Stub,
NonPeriodic,
}
impl PeriodRegularity {
pub const ALL: [Self; 3] = [Self::Regular, Self::Stub, Self::NonPeriodic];
#[must_use]
pub const fn canonical_identifier(self) -> &'static str {
match self {
Self::Regular => "Regular",
Self::Stub => "Stub",
Self::NonPeriodic => "NonPeriodic",
}
}
}
const REGULARITY_IDS: Identifiers<PeriodRegularity> = Identifiers::new(
"period regularity",
&[
Entry {
value: PeriodRegularity::Regular,
canonical: "Regular",
aliases: &[],
},
Entry {
value: PeriodRegularity::Stub,
canonical: "Stub",
aliases: &[],
},
Entry {
value: PeriodRegularity::NonPeriodic,
canonical: "NonPeriodic",
aliases: &[],
},
],
&[],
);
impl fmt::Display for PeriodRegularity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.canonical_identifier())
}
}
impl FromStr for PeriodRegularity {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
REGULARITY_IDS.parse(text)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ScheduleCalendar {
Plain(Calendar),
Target(TargetCalendar),
}
impl ScheduleCalendar {
fn validate_span(&self, start: Date, end: Date) -> Result<(), ScheduleError> {
self.validate_span_for(start, end, "coverage")
}
fn validate_span_for(
&self,
start: Date,
end: Date,
phase: &'static str,
) -> Result<(), ScheduleError> {
if let Self::Target(calendar) = self {
calendar
.business_days_between(
start,
end,
EndpointInclusion::Include,
EndpointInclusion::Include,
)
.map_err(|cause| ScheduleError::Calendar {
index: None,
phase,
cause,
})?;
}
Ok(())
}
fn adjust(
&self,
date: Date,
convention: BusinessDayConvention,
index: usize,
) -> Result<Date, ScheduleError> {
let result = match self {
Self::Plain(calendar) => calendar.adjust(date, convention),
Self::Target(calendar) => calendar.adjust(date, convention),
};
result.map_err(|cause| ScheduleError::Calendar {
index: Some(index),
phase: "adjustment",
cause,
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExplicitStubDates {
first: Option<Date>,
penultimate: Option<Date>,
}
impl ExplicitStubDates {
pub fn new(first: Option<Date>, penultimate: Option<Date>) -> Result<Self, ScheduleError> {
if first.is_none() && penultimate.is_none() {
return Err(ScheduleError::EmptyExplicitStubDates);
}
if let (Some(first), Some(penultimate)) = (first, penultimate)
&& first > penultimate
{
return Err(ScheduleError::ReversedExplicitStubDates { first, penultimate });
}
Ok(Self { first, penultimate })
}
#[must_use]
pub const fn first(self) -> Option<Date> {
self.first
}
#[must_use]
pub const fn penultimate(self) -> Option<Date> {
self.penultimate
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ScheduleBuilder {
start: Date,
end: Date,
calendar: ScheduleCalendar,
convention: BusinessDayConvention,
termination_convention: BusinessDayConvention,
}
impl ScheduleBuilder {
pub const MIN_BOUNDARIES: u32 = 2;
pub const MAX_BOUNDARIES: u32 = 3_652_059;
pub fn new(
start: Date,
end: Date,
calendar: ScheduleCalendar,
convention: BusinessDayConvention,
termination_convention: BusinessDayConvention,
) -> Result<Self, ScheduleError> {
if start >= end {
return Err(ScheduleError::InvalidRange { start, end });
}
Ok(Self {
start,
end,
calendar,
convention,
termination_convention,
})
}
#[must_use]
pub const fn start(&self) -> Date {
self.start
}
#[must_use]
pub const fn end(&self) -> Date {
self.end
}
#[must_use]
pub const fn calendar(&self) -> &ScheduleCalendar {
&self.calendar
}
#[must_use]
pub const fn convention(&self) -> BusinessDayConvention {
self.convention
}
#[must_use]
pub const fn termination_convention(&self) -> BusinessDayConvention {
self.termination_convention
}
pub fn build_recurrent(
&self,
tenor: Period,
rule: DateGenerationRule,
stub: StubPolicy,
eom: EndOfMonth,
max_boundaries: u32,
) -> Result<Schedule, ScheduleError> {
validate_limit(max_boundaries)?;
let forward = match rule {
DateGenerationRule::Forward => true,
DateGenerationRule::Backward => false,
DateGenerationRule::Zero
| DateGenerationRule::Roll(_)
| DateGenerationRule::OldCds
| DateGenerationRule::Cds
| DateGenerationRule::Cds2015 => {
return Err(ScheduleError::InvalidRule { rule });
}
};
if tenor.length() <= 0 {
return Err(ScheduleError::InvalidTenor { tenor });
}
if eom == EndOfMonth::Preserve && matches!(tenor.unit(), TimeUnit::Days | TimeUnit::Weeks) {
return Err(ScheduleError::IncompatibleEndOfMonth {
tenor,
end_of_month: eom,
});
}
self.calendar.validate_span(self.start, self.end)?;
let (anchor, endpoint) = if forward {
(self.start, self.end)
} else {
(self.end, self.start)
};
let preserve = eom == EndOfMonth::Preserve && anchor.is_end_of_month();
let mut rolling = Rolling::new(anchor)?;
loop {
let candidate = anchored_candidate(
anchor,
tenor,
rolling.count,
forward,
preserve,
rolling.dates.len(),
)?;
if let Some(date) = candidate
&& if forward {
date < endpoint
} else {
date > endpoint
}
{
let required = rolling.count + if stub == StubPolicy::Long { 1 } else { 2 };
if required > max_boundaries {
return Err(ScheduleError::BoundaryLimitExceeded {
maximum: max_boundaries,
required_at_least: required,
});
}
rolling.push(date, PeriodRegularity::Regular)?;
continue;
}
if candidate == Some(endpoint) {
rolling.push_limited(endpoint, PeriodRegularity::Regular, max_boundaries)?;
} else {
match stub {
StubPolicy::None => {
return Err(ScheduleError::StubRequired {
rule,
tenor,
start: self.start,
end: self.end,
regular_boundary: rolling.dates.last().copied().unwrap_or(anchor),
});
}
StubPolicy::Short => {
rolling.push_limited(endpoint, PeriodRegularity::Stub, max_boundaries)?
}
StubPolicy::Long => {
if rolling.regularity.is_empty() {
return Err(ScheduleError::LongStubWithoutRegularPeriod {
rule,
tenor,
start: self.start,
end: self.end,
});
}
rolling.dates.pop();
rolling.regularity.pop();
rolling.count -= 1;
rolling.push_limited(endpoint, PeriodRegularity::Stub, max_boundaries)?;
}
}
}
break;
}
if !forward {
rolling.dates.reverse();
rolling.regularity.reverse();
}
self.finish(rolling, rule, Some(tenor), Some(stub), Some(eom), None)
}
pub fn build_explicit(
&self,
tenor: Period,
rule: DateGenerationRule,
dates: ExplicitStubDates,
eom: EndOfMonth,
max_boundaries: u32,
) -> Result<Schedule, ScheduleError> {
validate_limit(max_boundaries)?;
let forward = match rule {
DateGenerationRule::Forward => true,
DateGenerationRule::Backward => false,
DateGenerationRule::Zero
| DateGenerationRule::Roll(_)
| DateGenerationRule::OldCds
| DateGenerationRule::Cds
| DateGenerationRule::Cds2015 => {
return Err(ScheduleError::InvalidRule { rule });
}
};
if tenor.length() <= 0 {
return Err(ScheduleError::InvalidTenor { tenor });
}
if eom == EndOfMonth::Preserve && matches!(tenor.unit(), TimeUnit::Days | TimeUnit::Weeks) {
return Err(ScheduleError::IncompatibleEndOfMonth {
tenor,
end_of_month: eom,
});
}
for (role, date) in [("first", dates.first), ("penultimate", dates.penultimate)] {
if let Some(date) = date
&& (date <= self.start || date >= self.end)
{
return Err(ScheduleError::ExplicitStubOutsideRange {
role,
date,
start: self.start,
end: self.end,
});
}
}
self.calendar.validate_span(self.start, self.end)?;
let regular_start = dates.first.unwrap_or(self.start);
let regular_end = dates.penultimate.unwrap_or(self.end);
let (anchor, endpoint, leading, trailing) = if forward {
(
regular_start,
regular_end,
dates.first.map(|_| self.start),
dates.penultimate.map(|_| self.end),
)
} else {
(
regular_end,
regular_start,
dates.penultimate.map(|_| self.end),
dates.first.map(|_| self.start),
)
};
let preserve = eom == EndOfMonth::Preserve && anchor.is_end_of_month();
let extra = u32::from(leading.is_some()) + u32::from(trailing.is_some());
let required = extra + if anchor == endpoint { 1 } else { 2 };
if required > max_boundaries {
return Err(ScheduleError::BoundaryLimitExceeded {
maximum: max_boundaries,
required_at_least: required,
});
}
let mut rolling = Rolling::new(leading.unwrap_or(anchor))?;
if let Some(outer) = leading {
let adjacent = anchored_candidate(anchor, tenor, 1, !forward, preserve, 0)?;
rolling.push(anchor, outer_regularity(adjacent, outer))?;
}
let mut step = 0;
if anchor != endpoint {
loop {
step += 1;
let candidate = anchored_candidate(
anchor,
tenor,
step,
forward,
preserve,
rolling.dates.len(),
)?;
if candidate == Some(endpoint) {
rolling.push(endpoint, PeriodRegularity::Regular)?;
break;
}
if let Some(date) = candidate
&& if forward {
date < endpoint
} else {
date > endpoint
}
{
let required = rolling.count + 2 + u32::from(trailing.is_some());
if required > max_boundaries {
return Err(ScheduleError::BoundaryLimitExceeded {
maximum: max_boundaries,
required_at_least: required,
});
}
rolling.push(date, PeriodRegularity::Regular)?;
} else {
return Err(ScheduleError::IrregularExplicitMiddle {
rule,
tenor,
anchor,
endpoint,
regular_boundary: rolling.dates.last().copied().unwrap_or(anchor),
});
}
}
}
if let Some(outer) = trailing {
let adjacent = anchored_candidate(
anchor,
tenor,
step + 1,
forward,
preserve,
rolling.dates.len(),
)?;
rolling.push_limited(outer, outer_regularity(adjacent, outer), max_boundaries)?;
}
if !forward {
rolling.dates.reverse();
rolling.regularity.reverse();
}
self.finish(rolling, rule, Some(tenor), None, Some(eom), Some(dates))
}
pub fn build_zero(&self, max_boundaries: u32) -> Result<Schedule, ScheduleError> {
validate_limit(max_boundaries)?;
self.calendar.validate_span(self.start, self.end)?;
let mut rolling = Rolling::new(self.start)?;
rolling.push_limited(self.end, PeriodRegularity::NonPeriodic, max_boundaries)?;
self.finish(rolling, DateGenerationRule::Zero, None, None, None, None)
}
fn finish(
&self,
rolling: Rolling,
rule: DateGenerationRule,
tenor: Option<Period>,
stub: Option<StubPolicy>,
eom: Option<EndOfMonth>,
explicit: Option<ExplicitStubDates>,
) -> Result<Schedule, ScheduleError> {
let mut adjusted = Vec::new();
adjusted
.try_reserve_exact(rolling.dates.len())
.map_err(|_| ScheduleError::AllocationFailed {
phase: "adjusted boundaries",
requested: rolling.dates.len(),
})?;
for (index, &date) in rolling.dates.iter().enumerate() {
let convention = if index + 1 == rolling.dates.len() {
self.termination_convention
} else {
self.convention
};
let value = self.calendar.adjust(date, convention, index)?;
if let Some(&previous_adjusted) = adjusted.last()
&& value <= previous_adjusted
{
return Err(ScheduleError::AdjustedOrder {
index,
previous_contractual: rolling.dates[index - 1],
contractual: date,
previous_adjusted,
adjusted: value,
});
}
adjusted.push(value);
}
Ok(Schedule {
contractual: rolling.dates,
adjusted,
regularity: rolling.regularity,
generation_rule: rule,
tenor,
stub_policy: stub,
end_of_month: eom,
explicit_stub_dates: explicit,
roll_provenance: None,
old_cds_provenance: None,
calendar: self.calendar.clone(),
convention: self.convention,
termination_convention: self.termination_convention,
})
}
}
#[derive(Clone, Debug)]
pub struct Schedule {
contractual: Vec<Date>,
adjusted: Vec<Date>,
regularity: Vec<PeriodRegularity>,
generation_rule: DateGenerationRule,
tenor: Option<Period>,
stub_policy: Option<StubPolicy>,
end_of_month: Option<EndOfMonth>,
explicit_stub_dates: Option<ExplicitStubDates>,
roll_provenance: Option<RollProvenance>,
old_cds_provenance: Option<OldCdsProvenance>,
calendar: ScheduleCalendar,
convention: BusinessDayConvention,
termination_convention: BusinessDayConvention,
}
impl Schedule {
pub const MIN_ICMA_PERIODS: u32 = 1;
pub const MAX_ICMA_PERIODS: u32 = 3_652_058;
pub fn build_icma_context(
&self,
frequency: Frequency,
max_periods: u32,
) -> Result<ActualActualIcma, ScheduleError> {
if !(Self::MIN_ICMA_PERIODS..=Self::MAX_ICMA_PERIODS).contains(&max_periods) {
return Err(ScheduleError::InvalidIcmaPeriodLimit {
requested: max_periods,
minimum: Self::MIN_ICMA_PERIODS,
maximum: Self::MAX_ICMA_PERIODS,
});
}
let lattice = match self.generation_rule {
DateGenerationRule::Forward | DateGenerationRule::Backward => self
.end_of_month
.zip(self.lattice_anchor())
.map(|(eom, anchor)| IcmaLattice::Anchored {
anchor,
preserve: eom == EndOfMonth::Preserve && anchor.is_end_of_month(),
}),
DateGenerationRule::Roll(_) => self.roll_provenance.map(IcmaLattice::Roll),
DateGenerationRule::OldCds => {
self.old_cds_provenance
.map(|provenance| IcmaLattice::Anchored {
anchor: provenance.recurrence_anchor(),
preserve: false,
})
}
DateGenerationRule::Zero | DateGenerationRule::Cds | DateGenerationRule::Cds2015 => {
None
}
};
let (Some(tenor), Some(lattice)) = (self.tenor, lattice) else {
return Err(ScheduleError::NonPeriodicIcma {
rule: self.generation_rule,
});
};
let Some(expected_tenor) = frequency.period() else {
return Err(ScheduleError::InvalidIcmaFrequency { frequency });
};
if tenor != expected_tenor {
return Err(ScheduleError::IcmaFrequencyMismatch {
tenor,
frequency,
expected_tenor,
});
}
let (Some(&start), Some(&end)) = (self.contractual.first(), self.contractual.last()) else {
return Err(ScheduleError::NonPeriodicIcma {
rule: self.generation_rule,
});
};
let anchor = lattice.anchor()?;
let remaining_upper = u32::from(anchor < end);
let mut count = 0;
let mut boundaries = Vec::new();
reserve_one(&mut boundaries, "ICMA reference boundaries")?;
boundaries.push(anchor);
let mut lower = anchor;
let mut step = 0;
while lower > start {
check_icma_count(count + 1 + remaining_upper, max_periods)?;
step += 1;
lower = lattice.date(anchor, tenor, step, false, boundaries.len())?;
reserve_one(&mut boundaries, "ICMA reference boundaries")?;
boundaries.push(lower);
count += 1;
}
boundaries.reverse();
let mut upper = anchor;
step = 0;
while upper < end {
check_icma_count(count + 1, max_periods)?;
step += 1;
upper = lattice.date(anchor, tenor, step, true, boundaries.len())?;
reserve_one(&mut boundaries, "ICMA reference boundaries")?;
boundaries.push(upper);
count += 1;
}
self.calendar
.validate_span_for(lower, upper, "ICMA reference coverage")?;
let mut periods = Vec::new();
periods
.try_reserve_exact(boundaries.len() - 1)
.map_err(|_| ScheduleError::AllocationFailed {
phase: "ICMA determination periods",
requested: boundaries.len() - 1,
})?;
for pair in boundaries.windows(2) {
let period = DeterminationPeriod::new(pair[0], pair[1])
.map_err(|cause| ScheduleError::Icma { cause })?;
periods.push(period);
}
ActualActualIcma::new(periods, frequency).map_err(|cause| ScheduleError::Icma { cause })
}
#[must_use]
pub fn contractual_dates(&self) -> &[Date] {
&self.contractual
}
#[must_use]
pub fn adjusted_dates(&self) -> &[Date] {
&self.adjusted
}
#[must_use]
pub fn regularity(&self) -> &[PeriodRegularity] {
&self.regularity
}
#[must_use]
pub const fn generation_rule(&self) -> DateGenerationRule {
self.generation_rule
}
#[must_use]
pub const fn tenor(&self) -> Option<Period> {
self.tenor
}
#[must_use]
pub const fn stub_policy(&self) -> Option<StubPolicy> {
self.stub_policy
}
#[must_use]
pub const fn end_of_month(&self) -> Option<EndOfMonth> {
self.end_of_month
}
#[must_use]
pub const fn explicit_stub_dates(&self) -> Option<ExplicitStubDates> {
self.explicit_stub_dates
}
#[must_use]
pub const fn roll_provenance(&self) -> Option<&RollProvenance> {
self.roll_provenance.as_ref()
}
#[must_use]
pub const fn old_cds_provenance(&self) -> Option<&OldCdsProvenance> {
self.old_cds_provenance.as_ref()
}
#[must_use]
pub fn lattice_anchor(&self) -> Option<Date> {
match self.generation_rule {
DateGenerationRule::Forward => self
.explicit_stub_dates
.and_then(ExplicitStubDates::first)
.or_else(|| self.contractual.first().copied()),
DateGenerationRule::Backward => self
.explicit_stub_dates
.and_then(ExplicitStubDates::penultimate)
.or_else(|| self.contractual.last().copied()),
DateGenerationRule::Zero
| DateGenerationRule::Roll(_)
| DateGenerationRule::OldCds
| DateGenerationRule::Cds
| DateGenerationRule::Cds2015 => None,
}
}
#[must_use]
pub const fn calendar(&self) -> &ScheduleCalendar {
&self.calendar
}
#[must_use]
pub const fn convention(&self) -> BusinessDayConvention {
self.convention
}
#[must_use]
pub const fn termination_convention(&self) -> BusinessDayConvention {
self.termination_convention
}
}
impl PartialEq for Schedule {
fn eq(&self, other: &Self) -> bool {
self.contractual == other.contractual
&& self.adjusted == other.adjusted
&& self.regularity == other.regularity
&& self.generation_rule == other.generation_rule
&& stored_tenor(self.tenor) == stored_tenor(other.tenor)
&& self.stub_policy == other.stub_policy
&& self.end_of_month == other.end_of_month
&& self.explicit_stub_dates == other.explicit_stub_dates
&& self.roll_provenance == other.roll_provenance
&& self.old_cds_provenance == other.old_cds_provenance
&& self.calendar == other.calendar
&& self.convention == other.convention
&& self.termination_convention == other.termination_convention
}
}
impl Eq for Schedule {}
impl Hash for Schedule {
fn hash<H: Hasher>(&self, state: &mut H) {
self.contractual.hash(state);
self.adjusted.hash(state);
self.regularity.hash(state);
self.generation_rule.hash(state);
stored_tenor(self.tenor).hash(state);
self.stub_policy.hash(state);
self.end_of_month.hash(state);
self.explicit_stub_dates.hash(state);
self.roll_provenance.hash(state);
self.old_cds_provenance.hash(state);
self.calendar.hash(state);
self.convention.hash(state);
self.termination_convention.hash(state);
}
}
fn stored_tenor(tenor: Option<Period>) -> Option<(i32, TimeUnit)> {
tenor.map(|value| (value.length(), value.unit()))
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScheduleError {
#[non_exhaustive]
InvalidCdsTenor {
trade_date: Date,
tenor: Period,
convention: CdsMaturityConvention,
},
#[non_exhaustive]
CdsMaturityDate {
trade_date: Date,
tenor: Period,
convention: CdsMaturityConvention,
cause: DateError,
},
#[non_exhaustive]
InvalidCdsMaturity {
maturity: Date,
convention: CdsMaturityConvention,
},
#[non_exhaustive]
InvalidCdsCouponLimit {
requested: u32,
minimum: u32,
maximum: u32,
},
#[non_exhaustive]
CdsCouponLimitExceeded {
maximum: u32,
required: u32,
},
#[non_exhaustive]
NoEligibleCdsPayment {
trade_date: Date,
payment_selection_cutoff: Date,
unadjusted_maturity: Date,
final_payment: Date,
},
#[non_exhaustive]
CdsDate {
trade_date: Date,
unadjusted_maturity: Date,
role: &'static str,
input: Date,
cause: DateError,
},
#[non_exhaustive]
CdsCalendar {
trade_date: Date,
unadjusted_maturity: Date,
phase: &'static str,
index: Option<usize>,
start: Date,
end: Date,
cause: CalendarError,
},
#[non_exhaustive]
CdsAdjustedOrder {
index: usize,
previous_contractual: Date,
contractual: Date,
previous_adjusted: Date,
adjusted: Date,
},
#[non_exhaustive]
InvalidRollTenor {
rule: SpecialRollRule,
tenor: Period,
},
#[non_exhaustive]
ResolvedRollRange {
rule: SpecialRollRule,
tenor: Period,
requested_start: Date,
requested_end: Date,
resolved_start: Date,
resolved_end: Date,
},
#[non_exhaustive]
RollDate {
rule: SpecialRollRule,
tenor: Period,
role: &'static str,
input: Date,
cause: DateError,
},
#[non_exhaustive]
InvalidOldCdsTenor {
generation_start: Date,
requested_end: Date,
tenor: Period,
},
#[non_exhaustive]
OldCdsDate {
generation_start: Date,
requested_end: Date,
tenor: Period,
role: &'static str,
input: Date,
cause: DateError,
},
#[non_exhaustive]
InvalidRange {
start: Date,
end: Date,
},
#[non_exhaustive]
InvalidBoundaryLimit {
requested: u32,
minimum: u32,
maximum: u32,
},
#[non_exhaustive]
InvalidRule {
rule: DateGenerationRule,
},
#[non_exhaustive]
InvalidTenor {
tenor: Period,
},
#[non_exhaustive]
IncompatibleEndOfMonth {
tenor: Period,
end_of_month: EndOfMonth,
},
EmptyExplicitStubDates,
#[non_exhaustive]
ReversedExplicitStubDates {
first: Date,
penultimate: Date,
},
#[non_exhaustive]
ExplicitStubOutsideRange {
role: &'static str,
date: Date,
start: Date,
end: Date,
},
#[non_exhaustive]
IrregularExplicitMiddle {
rule: DateGenerationRule,
tenor: Period,
anchor: Date,
endpoint: Date,
regular_boundary: Date,
},
#[non_exhaustive]
InvalidIcmaPeriodLimit {
requested: u32,
minimum: u32,
maximum: u32,
},
#[non_exhaustive]
NonPeriodicIcma {
rule: DateGenerationRule,
},
#[non_exhaustive]
InvalidIcmaFrequency {
frequency: Frequency,
},
#[non_exhaustive]
IcmaFrequencyMismatch {
tenor: Period,
frequency: Frequency,
expected_tenor: Period,
},
#[non_exhaustive]
IcmaPeriodLimitExceeded {
maximum: u32,
required_at_least: u32,
},
#[non_exhaustive]
Icma {
cause: IcmaError,
},
#[non_exhaustive]
StubRequired {
rule: DateGenerationRule,
tenor: Period,
start: Date,
end: Date,
regular_boundary: Date,
},
#[non_exhaustive]
LongStubWithoutRegularPeriod {
rule: DateGenerationRule,
tenor: Period,
start: Date,
end: Date,
},
#[non_exhaustive]
BoundaryLimitExceeded {
maximum: u32,
required_at_least: u32,
},
#[non_exhaustive]
AllocationFailed {
phase: &'static str,
requested: usize,
},
#[non_exhaustive]
Date {
index: usize,
phase: &'static str,
cause: DateError,
},
#[non_exhaustive]
Calendar {
index: Option<usize>,
phase: &'static str,
cause: CalendarError,
},
#[non_exhaustive]
AdjustedOrder {
index: usize,
previous_contractual: Date,
contractual: Date,
previous_adjusted: Date,
adjusted: Date,
},
}
impl ScheduleError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::InvalidCdsTenor { .. } => "schedule.invalid_cds_tenor",
Self::CdsMaturityDate { .. } => "schedule.cds_maturity_date",
Self::InvalidCdsMaturity { .. } => "schedule.invalid_cds_maturity",
Self::InvalidCdsCouponLimit { .. } => "schedule.invalid_cds_coupon_limit",
Self::CdsCouponLimitExceeded { .. } => "schedule.cds_coupon_limit_exceeded",
Self::NoEligibleCdsPayment { .. } => "schedule.no_eligible_cds_payment",
Self::CdsDate { .. } => "schedule.cds_date",
Self::CdsCalendar { .. } => "schedule.cds_calendar",
Self::CdsAdjustedOrder { .. } => "schedule.cds_adjusted_order",
Self::InvalidRollTenor { .. } => "schedule.invalid_roll_tenor",
Self::ResolvedRollRange { .. } => "schedule.resolved_roll_range",
Self::RollDate { .. } => "schedule.roll_date",
Self::InvalidOldCdsTenor { .. } => "schedule.invalid_old_cds_tenor",
Self::OldCdsDate { .. } => "schedule.old_cds_date",
Self::InvalidRange { .. } => "schedule.invalid_range",
Self::InvalidBoundaryLimit { .. } => "schedule.invalid_boundary_limit",
Self::InvalidRule { .. } => "schedule.invalid_rule",
Self::InvalidTenor { .. } => "schedule.invalid_tenor",
Self::IncompatibleEndOfMonth { .. } => "schedule.incompatible_end_of_month",
Self::EmptyExplicitStubDates => "schedule.empty_explicit_stub_dates",
Self::ReversedExplicitStubDates { .. } => "schedule.reversed_explicit_stub_dates",
Self::ExplicitStubOutsideRange { .. } => "schedule.explicit_stub_outside_range",
Self::IrregularExplicitMiddle { .. } => "schedule.irregular_explicit_middle",
Self::InvalidIcmaPeriodLimit { .. } => "schedule.invalid_icma_period_limit",
Self::NonPeriodicIcma { .. } => "schedule.non_periodic_icma",
Self::InvalidIcmaFrequency { .. } => "schedule.invalid_icma_frequency",
Self::IcmaFrequencyMismatch { .. } => "schedule.icma_frequency_mismatch",
Self::IcmaPeriodLimitExceeded { .. } => "schedule.icma_period_limit_exceeded",
Self::Icma { .. } => "schedule.icma",
Self::StubRequired { .. } => "schedule.stub_required",
Self::LongStubWithoutRegularPeriod { .. } => {
"schedule.long_stub_without_regular_period"
}
Self::BoundaryLimitExceeded { .. } => "schedule.boundary_limit_exceeded",
Self::AllocationFailed { .. } => "schedule.allocation_failed",
Self::Date { .. } => "schedule.date",
Self::Calendar { .. } => "schedule.calendar",
Self::AdjustedOrder { .. } => "schedule.adjusted_order",
}
}
}
impl fmt::Display for ScheduleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidCdsTenor {
trade_date,
tenor,
convention,
} => write!(
f,
"CDS {convention} trade {trade_date} rejects stored quote {tenor}; use nonnegative Years or quarter-multiple Months"
),
Self::CdsMaturityDate {
trade_date,
tenor,
convention,
cause,
} => write!(
f,
"CDS {convention} trade {trade_date} quote {tenor} required maturity: {cause}"
),
Self::InvalidCdsMaturity {
maturity,
convention,
} => write!(
f,
"CDS {convention} maturity {maturity} must be a quarterly twentieth"
),
Self::InvalidCdsCouponLimit {
requested,
minimum,
maximum,
} => write!(
f,
"CDS coupon cap {requested} must be in {minimum}..={maximum}"
),
Self::CdsCouponLimitExceeded { maximum, required } => {
write!(f, "CDS needs {required} coupons, exceeding cap {maximum}")
}
Self::NoEligibleCdsPayment {
trade_date,
payment_selection_cutoff,
unadjusted_maturity,
final_payment,
} => write!(
f,
"CDS trade {trade_date} maturity {unadjusted_maturity} final payment {final_payment} is not after cutoff {payment_selection_cutoff}"
),
Self::CdsDate {
trade_date,
unadjusted_maturity,
role,
input,
cause,
} => write!(
f,
"CDS trade {trade_date} maturity {unadjusted_maturity} required {role} from {input}: {cause}"
),
Self::CdsCalendar {
trade_date,
unadjusted_maturity,
phase,
index,
start,
end,
cause,
} => write!(
f,
"CDS trade {trade_date} maturity {unadjusted_maturity} {phase} coupon {index:?} over {start} to {end}: {cause}"
),
Self::CdsAdjustedOrder {
index,
previous_contractual,
contractual,
previous_adjusted,
adjusted,
} => write!(
f,
"CDS adjustment conflicts at coupon {index}: raw {previous_contractual}, {contractual}; adjusted {previous_adjusted}, {adjusted}"
),
Self::InvalidRollTenor { rule, tenor } => write!(
f,
"schedule roll {rule} does not accept stored tenor {tenor}; use positive Months/Years and a quarter-multiple stride for TwentiethIMM"
),
Self::ResolvedRollRange {
rule,
tenor,
requested_start,
requested_end,
resolved_start,
resolved_end,
} => write!(
f,
"schedule roll {rule} {tenor} resolves {requested_start} to {requested_end} into non-increasing {resolved_start} to {resolved_end}"
),
Self::RollDate {
rule,
tenor,
role,
input,
cause,
} => write!(
f,
"schedule roll {rule} {tenor} required {role} from {input}: {cause}"
),
Self::InvalidOldCdsTenor {
generation_start,
requested_end,
tenor,
} => write!(
f,
"schedule OldCDS from {generation_start} to {requested_end} does not accept stored tenor {tenor}; use positive Months/Years"
),
Self::OldCdsDate {
generation_start,
requested_end,
tenor,
role,
input,
cause,
} => write!(
f,
"schedule OldCDS {tenor} from {generation_start} to {requested_end} required {role} from {input}: {cause}"
),
Self::InvalidRange { start, end } => {
write!(f, "schedule range must increase: {start} to {end}")
}
Self::InvalidBoundaryLimit {
requested,
minimum,
maximum,
} => write!(
f,
"schedule cap {requested} must be in {minimum}..={maximum}"
),
Self::InvalidRule { rule } => write!(
f,
"schedule recurrence does not accept {rule}; use the constructor for its rule family"
),
Self::InvalidTenor { tenor } => write!(
f,
"schedule tenor {tenor} must have a positive stored length"
),
Self::IncompatibleEndOfMonth {
tenor,
end_of_month,
} => write!(
f,
"schedule EOM {end_of_month} is incompatible with {tenor}"
),
Self::EmptyExplicitStubDates => {
f.write_str("schedule needs a first or penultimate coupon date")
}
Self::ReversedExplicitStubDates { first, penultimate } => write!(
f,
"schedule first coupon {first} follows penultimate coupon {penultimate}"
),
Self::ExplicitStubOutsideRange {
role,
date,
start,
end,
} => write!(
f,
"schedule {role} coupon {date} must be strictly inside {start} to {end}"
),
Self::IrregularExplicitMiddle {
rule,
tenor,
anchor,
endpoint,
regular_boundary,
} => write!(
f,
"schedule explicit {rule} {tenor} middle from {anchor} to {endpoint} is off lattice after {regular_boundary}"
),
Self::InvalidIcmaPeriodLimit {
requested,
minimum,
maximum,
} => write!(
f,
"schedule ICMA period cap {requested} must be in {minimum}..={maximum}"
),
Self::NonPeriodicIcma { rule } => {
write!(f, "schedule {rule} has no recurring ICMA reference lattice")
}
Self::InvalidIcmaFrequency { frequency } => {
write!(f, "schedule ICMA frequency {frequency} must be periodic")
}
Self::IcmaFrequencyMismatch {
tenor,
frequency,
expected_tenor,
} => write!(
f,
"schedule tenor {tenor} does not match ICMA frequency {frequency} ({expected_tenor})"
),
Self::IcmaPeriodLimitExceeded {
maximum,
required_at_least,
} => write!(
f,
"schedule ICMA needs at least {required_at_least} periods, exceeding cap {maximum}"
),
Self::Icma { cause } => write!(f, "schedule ICMA reference context: {cause}"),
Self::StubRequired {
rule,
tenor,
start,
end,
regular_boundary,
} => write!(
f,
"schedule {rule} {tenor} from {start} to {end} needs a stub after directional boundary {regular_boundary}"
),
Self::LongStubWithoutRegularPeriod {
rule,
tenor,
start,
end,
} => write!(
f,
"schedule {rule} {tenor} from {start} to {end} has no complete regular interval to merge into Long"
),
Self::BoundaryLimitExceeded {
maximum,
required_at_least,
} => write!(
f,
"schedule needs at least {required_at_least} final boundaries, exceeding cap {maximum}"
),
Self::AllocationFailed { phase, requested } => {
write!(f, "schedule {phase} cannot reserve {requested} elements")
}
Self::Date {
index,
phase,
cause,
} => write!(f, "schedule {phase} step {index}: {cause}"),
Self::Calendar {
index,
phase,
cause,
} => write!(f, "schedule {phase} boundary {index:?}: {cause}"),
Self::AdjustedOrder {
index,
previous_contractual,
contractual,
previous_adjusted,
adjusted,
} => write!(
f,
"schedule adjustment conflicts at boundary {index}: raw {previous_contractual}, {contractual}; adjusted {previous_adjusted}, {adjusted}"
),
}
}
}
impl StdError for ScheduleError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Date { cause, .. }
| Self::RollDate { cause, .. }
| Self::OldCdsDate { cause, .. }
| Self::CdsDate { cause, .. }
| Self::CdsMaturityDate { cause, .. } => Some(cause),
Self::Calendar { cause, .. } | Self::CdsCalendar { cause, .. } => Some(cause),
Self::Icma { cause } => Some(cause),
_ => None,
}
}
}
fn validate_limit(requested: u32) -> Result<(), ScheduleError> {
if !(ScheduleBuilder::MIN_BOUNDARIES..=ScheduleBuilder::MAX_BOUNDARIES).contains(&requested) {
return Err(ScheduleError::InvalidBoundaryLimit {
requested,
minimum: ScheduleBuilder::MIN_BOUNDARIES,
maximum: ScheduleBuilder::MAX_BOUNDARIES,
});
}
Ok(())
}
fn outer_regularity(adjacent: Option<Date>, endpoint: Date) -> PeriodRegularity {
if adjacent == Some(endpoint) {
PeriodRegularity::Regular
} else {
PeriodRegularity::Stub
}
}
#[derive(Clone, Copy)]
enum IcmaLattice {
Anchored { anchor: Date, preserve: bool },
Roll(RollProvenance),
}
impl IcmaLattice {
fn anchor(self) -> Result<Date, ScheduleError> {
match self {
Self::Anchored { anchor, .. } => Ok(anchor),
Self::Roll(provenance) => {
provenance
.reference_anchor()
.map_err(|cause| ScheduleError::Date {
index: 0,
phase: "ICMA reference anchor",
cause,
})
}
}
}
fn date(
self,
anchor: Date,
tenor: Period,
step: u32,
forward: bool,
index: usize,
) -> Result<Date, ScheduleError> {
match self {
Self::Anchored { preserve, .. } => checked_anchored_date(
anchor,
tenor,
step,
forward,
preserve,
index,
"ICMA reference extension",
),
Self::Roll(provenance) => {
provenance
.reference_date(anchor, step, forward)
.map_err(|cause| ScheduleError::Date {
index,
phase: "ICMA reference extension",
cause,
})
}
}
}
}
fn check_icma_count(required: u32, maximum: u32) -> Result<(), ScheduleError> {
if required > maximum {
Err(ScheduleError::IcmaPeriodLimitExceeded {
maximum,
required_at_least: required,
})
} else {
Ok(())
}
}
struct Rolling {
dates: Vec<Date>,
regularity: Vec<PeriodRegularity>,
count: u32,
}
impl Rolling {
fn new(anchor: Date) -> Result<Self, ScheduleError> {
let mut dates = Vec::new();
reserve_one(&mut dates, "contractual boundaries")?;
dates.push(anchor);
Ok(Self {
dates,
regularity: Vec::new(),
count: 1,
})
}
fn push(&mut self, date: Date, flag: PeriodRegularity) -> Result<(), ScheduleError> {
reserve_one(&mut self.dates, "contractual boundaries")?;
reserve_one(&mut self.regularity, "contractual regularity")?;
self.dates.push(date);
self.regularity.push(flag);
self.count += 1;
Ok(())
}
fn push_limited(
&mut self,
date: Date,
flag: PeriodRegularity,
maximum: u32,
) -> Result<(), ScheduleError> {
if self.count + 1 > maximum {
return Err(ScheduleError::BoundaryLimitExceeded {
maximum,
required_at_least: self.count + 1,
});
}
self.push(date, flag)
}
}
fn reserve_one<T>(values: &mut Vec<T>, phase: &'static str) -> Result<(), ScheduleError> {
if values.len() == values.capacity() {
values
.try_reserve(1)
.map_err(|_| ScheduleError::AllocationFailed {
phase,
requested: values.len() + 1,
})?;
}
Ok(())
}
fn anchored_candidate(
anchor: Date,
tenor: Period,
step: u32,
forward: bool,
preserve: bool,
index: usize,
) -> Result<Option<Date>, ScheduleError> {
let magnitude = i64::from(tenor.length()) * i64::from(step);
let days_available = if forward {
Date::MAX.days_since(anchor)
} else {
anchor.days_since(Date::MIN)
};
let months_available = if forward {
i64::from(9999 - anchor.year()) * 12 + (12 - i64::from(anchor.month()))
} else {
i64::from(anchor.year() - 1) * 12 + (i64::from(anchor.month()) - 1)
};
let years_available = if forward {
9999 - anchor.year()
} else {
anchor.year() - 1
};
let outside = match tenor.unit() {
TimeUnit::Days => magnitude > i64::from(days_available),
TimeUnit::Weeks => magnitude > i64::from(days_available) / 7,
TimeUnit::Months => magnitude > months_available,
TimeUnit::Years => magnitude > i64::from(years_available),
};
if outside {
return Ok(None);
}
checked_anchored_date(anchor, tenor, step, forward, preserve, index, "generation").map(Some)
}
fn checked_anchored_date(
anchor: Date,
tenor: Period,
step: u32,
forward: bool,
preserve: bool,
index: usize,
phase: &'static str,
) -> Result<Date, ScheduleError> {
let magnitude = i64::from(tenor.length()) * i64::from(step);
let offset = if forward { magnitude } else { -magnitude };
let date = match tenor.unit() {
TimeUnit::Days => anchor.checked_add_days(offset),
TimeUnit::Weeks => anchor.checked_add_weeks(offset),
TimeUnit::Months => anchor.checked_add_months(offset),
TimeUnit::Years => anchor.checked_add_years(offset),
}
.map_err(|cause| ScheduleError::Date {
index,
phase,
cause,
})?;
Ok(if preserve {
date.last_day_of_month()
} else {
date
})
}