use std::error::Error as StdError;
use std::fmt;
use crate::cashflows::{
FixedCouponRate, FixedRateCoupon, Leg, PaymentDateInclusion, SimpleCashFlow, product,
query_work_units,
};
use crate::error::{Interval, ValueError};
use crate::math::{SumError, exact_sum};
use crate::numeric;
use crate::rates::{AccrualConvention, Compounding, RateError, present_value_at, validate_rate};
use crate::schedule::Schedule;
use crate::time::Date;
mod quote;
mod yield_risk;
pub use yield_risk::{BondRisk, BondYieldError, BondYieldReport, BondYieldSensitivity};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "bullet bond core has exactly two coupon structures"
)]
pub enum BondKind {
ZeroCoupon,
FixedRate,
}
impl BondKind {
pub const ALL: [Self; 2] = [Self::ZeroCoupon, Self::FixedRate];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "cutoff equality has exactly two boundary conventions"
)]
pub enum ExCouponBoundary {
OnOrAfter,
After,
}
impl ExCouponBoundary {
pub const ALL: [Self; 2] = [Self::OnOrAfter, Self::After];
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ExCouponTerms {
cutoff_date: Date,
boundary: ExCouponBoundary,
}
impl ExCouponTerms {
#[must_use]
pub const fn new(cutoff_date: Date, boundary: ExCouponBoundary) -> Self {
Self {
cutoff_date,
boundary,
}
}
#[must_use]
pub const fn cutoff_date(self) -> Date {
self.cutoff_date
}
#[must_use]
pub const fn boundary(self) -> ExCouponBoundary {
self.boundary
}
#[must_use]
pub fn is_ex_coupon(self, settlement_date: Date) -> bool {
match self.boundary {
ExCouponBoundary::OnOrAfter => settlement_date >= self.cutoff_date,
ExCouponBoundary::After => settlement_date > self.cutoff_date,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BondSettlement {
date: Date,
payment_date_inclusion: PaymentDateInclusion,
}
impl BondSettlement {
#[must_use]
pub const fn new(date: Date, payment_date_inclusion: PaymentDateInclusion) -> Self {
Self {
date,
payment_date_inclusion,
}
}
#[must_use]
pub const fn date(self) -> Date {
self.date
}
#[must_use]
pub const fn payment_date_inclusion(self) -> PaymentDateInclusion {
self.payment_date_inclusion
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BondYieldConvention {
accrual: AccrualConvention,
compounding: Compounding,
}
impl BondYieldConvention {
#[must_use]
pub fn new(accrual: impl Into<AccrualConvention>, compounding: Compounding) -> Self {
Self {
accrual: accrual.into(),
compounding,
}
}
#[must_use]
pub const fn accrual(&self) -> &AccrualConvention {
&self.accrual
}
#[must_use]
pub const fn compounding(&self) -> Compounding {
self.compounding
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "quoted bond prices are either clean or dirty"
)]
pub enum BondPriceKind {
Clean,
Dirty,
}
impl BondPriceKind {
pub const ALL: [Self; 2] = [Self::Clean, Self::Dirty];
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BondPrice {
value_per_100_face: f64,
kind: BondPriceKind,
}
impl BondPrice {
pub fn new(value_per_100_face: f64, kind: BondPriceKind) -> Result<Self, BondError> {
let value = numeric::finite("price per 100", value_per_100_face)
.map_err(|cause| value_error(None, "bond price", cause))?;
Ok(Self {
value_per_100_face: canonical_zero(value),
kind,
})
}
pub fn from_amount(amount: f64, face: f64, kind: BondPriceKind) -> Result<Self, BondError> {
let value_per_100_face = price_per_100_from_amount(amount, face)?;
Ok(Self {
value_per_100_face,
kind,
})
}
#[must_use]
pub const fn value_per_100_face(self) -> f64 {
self.value_per_100_face
}
#[must_use]
pub const fn kind(self) -> BondPriceKind {
self.kind
}
pub fn to_amount(self, face: f64) -> Result<f64, BondError> {
amount_from_price_per_100(self.value_per_100_face, face)
}
}
pub fn price_per_100_from_amount(amount: f64, face: f64) -> Result<f64, BondError> {
let face = validate_face(face)?;
let amount = numeric::finite("amount", amount)
.map_err(|cause| value_error(None, "price per 100", cause))?;
quote::per_100_from_amount(amount, face)
.map_err(|cause| value_error(None, "price per 100", cause))
}
pub fn amount_from_price_per_100(value_per_100_face: f64, face: f64) -> Result<f64, BondError> {
let face = validate_face(face)?;
let price = numeric::finite("price per 100", value_per_100_face)
.map_err(|cause| value_error(None, "bond price", cause))?;
quote::amount_from_per_100(price, face).map_err(|cause| value_error(None, "bond amount", cause))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[expect(
clippy::exhaustive_enums,
reason = "a coupon at settlement is ceased, buyer-entitled or ex-coupon detached"
)]
pub enum CouponEntitlement {
Ceased,
BuyerEntitled,
ExCouponDetached,
}
impl CouponEntitlement {
pub const ALL: [Self; 3] = [Self::Ceased, Self::BuyerEntitled, Self::ExCouponDetached];
}
#[derive(Clone, Debug, PartialEq)]
pub struct Bond {
kind: BondKind,
issue_date: Date,
contractual_maturity: Date,
face: f64,
redemption: SimpleCashFlow,
schedule: Option<Schedule>,
coupon_rate: Option<FixedCouponRate>,
coupons: Vec<FixedRateCoupon>,
ex_coupon: Vec<Option<ExCouponTerms>>,
}
impl Bond {
pub const MAX_FLOWS: u32 = Leg::MAX_FLOWS;
pub fn zero_coupon(
issue_date: Date,
contractual_maturity: Date,
face: f64,
redemption: SimpleCashFlow,
) -> Result<Self, BondError> {
if issue_date > contractual_maturity {
return Err(BondError::InvalidIssueMaturity {
issue_date,
contractual_maturity,
});
}
if issue_date > redemption.payment_date() {
return Err(BondError::RedemptionBeforeIssue {
issue_date,
redemption_payment: redemption.payment_date(),
});
}
let face = validate_face(face)?;
validate_redemption_amount(redemption)?;
Ok(Self {
kind: BondKind::ZeroCoupon,
issue_date,
contractual_maturity,
face,
redemption,
schedule: None,
coupon_rate: None,
coupons: Vec::new(),
ex_coupon: Vec::new(),
})
}
pub fn fixed_rate(
issue_date: Date,
schedule: Schedule,
face: f64,
coupon_rate: FixedCouponRate,
ex_coupon: Vec<Option<ExCouponTerms>>,
redemption: SimpleCashFlow,
max_flows: u32,
) -> Result<Self, BondError> {
if max_flows > Self::MAX_FLOWS {
return Err(BondError::InvalidFlowLimit {
requested: max_flows,
maximum: Self::MAX_FLOWS,
});
}
let raw = schedule.contractual_dates();
let adjusted = schedule.adjusted_dates();
let coupon_count = raw.len() - 1;
let total_flows = coupon_count + 1;
if total_flows > max_flows as usize {
return Err(BondError::FlowLimitExceeded {
maximum: max_flows,
required: total_flows,
});
}
if ex_coupon.len() != coupon_count {
return Err(BondError::ExCouponCountMismatch {
expected: coupon_count,
actual: ex_coupon.len(),
});
}
let contractual_maturity = raw[coupon_count];
if issue_date > contractual_maturity {
return Err(BondError::InvalidIssueMaturity {
issue_date,
contractual_maturity,
});
}
if issue_date > redemption.payment_date() {
return Err(BondError::RedemptionBeforeIssue {
issue_date,
redemption_payment: redemption.payment_date(),
});
}
let last_coupon_payment = adjusted[coupon_count];
if redemption.payment_date() < last_coupon_payment {
return Err(BondError::RedemptionBeforeLastCoupon {
last_coupon_payment,
redemption_payment: redemption.payment_date(),
});
}
let face = validate_face(face)?;
validate_redemption_amount(redemption)?;
for (index, terms) in ex_coupon.iter().copied().enumerate() {
if let Some(terms) = terms {
let payment_date = adjusted[index + 1];
if terms.cutoff_date() > payment_date {
return Err(BondError::ExCouponAfterPayment {
index,
cutoff_date: terms.cutoff_date(),
payment_date,
});
}
}
}
let mut coupons = reserved(coupon_count, "fixed-rate bond coupons")?;
for (index, pair) in raw.windows(2).enumerate() {
let accrual_fraction = checked_horizon(
coupon_rate.accrual(),
pair[0],
pair[1],
Some(index),
"coupon accrual",
)?;
let amount =
product::product("coupon amount", face, coupon_rate.rate(), accrual_fraction)
.map_err(|cause| value_error(Some(index), "coupon amount", cause))?;
coupons.push(FixedRateCoupon::from_parts(
pair[0],
pair[1],
adjusted[index + 1],
face,
coupon_rate.clone(),
accrual_fraction,
amount,
));
}
Ok(Self {
kind: BondKind::FixedRate,
issue_date,
contractual_maturity,
face,
redemption,
schedule: Some(schedule),
coupon_rate: Some(coupon_rate),
coupons,
ex_coupon,
})
}
#[must_use]
pub const fn kind(&self) -> BondKind {
self.kind
}
#[must_use]
pub const fn issue_date(&self) -> Date {
self.issue_date
}
#[must_use]
pub const fn contractual_maturity(&self) -> Date {
self.contractual_maturity
}
#[must_use]
pub const fn face(&self) -> f64 {
self.face
}
#[must_use]
pub const fn redemption(&self) -> SimpleCashFlow {
self.redemption
}
#[must_use]
pub const fn schedule(&self) -> Option<&Schedule> {
self.schedule.as_ref()
}
#[must_use]
pub const fn coupon_rate(&self) -> Option<&FixedCouponRate> {
self.coupon_rate.as_ref()
}
#[must_use]
pub fn coupons(&self) -> &[FixedRateCoupon] {
&self.coupons
}
#[must_use]
pub fn ex_coupon_terms(&self) -> &[Option<ExCouponTerms>] {
&self.ex_coupon
}
pub fn price_per_100_from_amount(&self, amount: f64) -> Result<f64, BondError> {
price_per_100_from_amount(amount, self.face)
}
pub fn amount_from_price_per_100(&self, price: BondPrice) -> Result<f64, BondError> {
price.to_amount(self.face)
}
pub fn evaluate(
&self,
settlement: BondSettlement,
yield_convention: &BondYieldConvention,
yield_rate: f64,
max_accrual_work: u64,
) -> Result<BondValuation, BondError> {
let plan = BondPricingPlan::new(self, settlement, yield_convention, max_accrual_work)?;
plan.evaluate(yield_rate)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct PreparedDiscountFlow {
index: usize,
amount: f64,
horizon: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BondPricingPlan<'a> {
bond: &'a Bond,
settlement: BondSettlement,
yield_convention: &'a BondYieldConvention,
coupon_entitlements: Vec<CouponEntitlement>,
redemption_entitled: bool,
discount_flows: Vec<PreparedDiscountFlow>,
accrued_amount: f64,
accrual_work_used: u64,
}
impl<'a> BondPricingPlan<'a> {
pub fn new(
bond: &'a Bond,
settlement: BondSettlement,
yield_convention: &'a BondYieldConvention,
max_accrual_work: u64,
) -> Result<Self, BondError> {
let settlement_date = settlement.date();
if settlement_date < bond.issue_date() {
return Err(BondError::SettlementBeforeIssue {
settlement_date,
issue_date: bond.issue_date(),
});
}
let mut work = AccrualWork {
maximum: max_accrual_work,
used: 0,
};
work.charge(
yield_convention.accrual(),
settlement_date,
settlement_date,
None,
"discount anchor",
)?;
let _ = checked_horizon(
yield_convention.accrual(),
settlement_date,
settlement_date,
None,
"discount anchor",
)?;
let coupon_count = bond.coupons().len();
let mut coupon_entitlements = reserved(coupon_count, "pricing plan entitlements")?;
let mut accrued_terms = reserved(coupon_count, "pricing plan accrued terms")?;
let mut discount_flows = reserved(coupon_count + 1, "pricing plan discount flows")?;
let inclusion = settlement.payment_date_inclusion();
for (index, coupon) in bond.coupons().iter().enumerate() {
if !inclusion.eligible(coupon.payment_date(), settlement_date) {
coupon_entitlements.push(CouponEntitlement::Ceased);
continue;
}
let detached = bond.ex_coupon_terms()[index]
.is_some_and(|terms| terms.is_ex_coupon(settlement_date));
let earned = earned_coupon_accrual(coupon, settlement_date, index, &mut work)?;
if detached {
coupon_entitlements.push(CouponEntitlement::ExCouponDetached);
let ex_term =
exact_sum(&[earned, -coupon.amount()]).map_err(|cause| BondError::Sum {
phase: "ex-coupon accrual",
cause,
})?;
accrued_terms.push(ex_term);
} else {
coupon_entitlements.push(CouponEntitlement::BuyerEntitled);
accrued_terms.push(earned);
work.charge(
yield_convention.accrual(),
settlement_date,
coupon.payment_date(),
Some(index),
"discount horizon",
)?;
let horizon = checked_horizon(
yield_convention.accrual(),
settlement_date,
coupon.payment_date(),
Some(index),
"discount horizon",
)?;
discount_flows.push(PreparedDiscountFlow {
index,
amount: coupon.amount(),
horizon,
});
}
}
let redemption = bond.redemption();
let redemption_entitled = inclusion.eligible(redemption.payment_date(), settlement_date);
if redemption_entitled {
work.charge(
yield_convention.accrual(),
settlement_date,
redemption.payment_date(),
Some(coupon_count),
"redemption discount horizon",
)?;
let horizon = checked_horizon(
yield_convention.accrual(),
settlement_date,
redemption.payment_date(),
Some(coupon_count),
"redemption discount horizon",
)?;
discount_flows.push(PreparedDiscountFlow {
index: coupon_count,
amount: redemption.amount(),
horizon,
});
}
let accrued_amount = exact_sum(&accrued_terms).map_err(|cause| BondError::Sum {
phase: "accrued reduction",
cause,
})?;
Ok(Self {
bond,
settlement,
yield_convention,
coupon_entitlements,
redemption_entitled,
discount_flows,
accrued_amount: canonical_zero(accrued_amount),
accrual_work_used: work.used,
})
}
#[must_use]
pub const fn bond(&self) -> &'a Bond {
self.bond
}
#[must_use]
pub const fn settlement(&self) -> BondSettlement {
self.settlement
}
#[must_use]
pub const fn yield_convention(&self) -> &'a BondYieldConvention {
self.yield_convention
}
#[must_use]
pub const fn accrued_amount(&self) -> f64 {
self.accrued_amount
}
pub fn accrued_per_100(&self) -> Result<f64, BondError> {
price_per_100_from_amount(self.accrued_amount, self.bond.face)
}
#[must_use]
pub fn coupon_entitlements(&self) -> &[CouponEntitlement] {
&self.coupon_entitlements
}
#[must_use]
pub const fn redemption_entitled(&self) -> bool {
self.redemption_entitled
}
#[must_use]
pub const fn entitled_flow_count(&self) -> usize {
self.discount_flows.len()
}
#[must_use]
pub const fn accrual_work_used(&self) -> u64 {
self.accrual_work_used
}
pub fn evaluate(&self, yield_rate: f64) -> Result<BondValuation, BondError> {
let compounding = self.yield_convention.compounding();
let rate = validate_rate(yield_rate, compounding)
.map_err(|cause| rate_error(None, "yield quote", cause))?;
let mut pv_terms = reserved(self.discount_flows.len(), "present value terms")?;
let dirty_amount = self.evaluate_dirty_with_validated_rate(rate, &mut pv_terms)?;
let clean_amount =
exact_sum(&[dirty_amount, -self.accrued_amount]).map_err(|cause| BondError::Sum {
phase: "clean price reduction",
cause,
})?;
Ok(BondValuation {
settlement: self.settlement,
face: self.bond.face,
yield_rate: rate,
dirty_amount,
accrued_amount: self.accrued_amount,
clean_amount: canonical_zero(clean_amount),
})
}
pub(super) fn evaluate_dirty_into(
&self,
yield_rate: f64,
pv_terms: &mut Vec<f64>,
) -> Result<(f64, f64), BondError> {
let compounding = self.yield_convention.compounding();
let rate = validate_rate(yield_rate, compounding)
.map_err(|cause| rate_error(None, "yield quote", cause))?;
let dirty_amount = self.evaluate_dirty_with_validated_rate(rate, pv_terms)?;
Ok((rate, dirty_amount))
}
fn evaluate_dirty_with_validated_rate(
&self,
rate: f64,
pv_terms: &mut Vec<f64>,
) -> Result<f64, BondError> {
let compounding = self.yield_convention.compounding();
pv_terms.clear();
for flow in &self.discount_flows {
let pv = present_value_at(rate, compounding, flow.amount, flow.horizon)
.map_err(|cause| rate_error(Some(flow.index), "discount factor", cause))?;
pv_terms.push(pv);
}
let dirty_amount = exact_sum(pv_terms).map_err(|cause| BondError::Sum {
phase: "dirty price reduction",
cause,
})?;
Ok(canonical_zero(dirty_amount))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BondValuation {
settlement: BondSettlement,
face: f64,
yield_rate: f64,
dirty_amount: f64,
accrued_amount: f64,
clean_amount: f64,
}
impl BondValuation {
#[must_use]
pub const fn settlement(self) -> BondSettlement {
self.settlement
}
#[must_use]
pub const fn face(self) -> f64 {
self.face
}
#[must_use]
pub const fn yield_rate(self) -> f64 {
self.yield_rate
}
#[must_use]
pub const fn dirty_amount(self) -> f64 {
self.dirty_amount
}
#[must_use]
pub const fn accrued_amount(self) -> f64 {
self.accrued_amount
}
#[must_use]
pub const fn clean_amount(self) -> f64 {
self.clean_amount
}
pub fn dirty_price_per_100(self) -> Result<f64, BondError> {
price_per_100_from_amount(self.dirty_amount, self.face)
}
pub fn accrued_per_100(self) -> Result<f64, BondError> {
price_per_100_from_amount(self.accrued_amount, self.face)
}
pub fn clean_price_per_100(self) -> Result<f64, BondError> {
price_per_100_from_amount(self.clean_amount, self.face)
}
pub fn dirty_price(self) -> Result<BondPrice, BondError> {
BondPrice::from_amount(self.dirty_amount, self.face, BondPriceKind::Dirty)
}
pub fn clean_price(self) -> Result<BondPrice, BondError> {
BondPrice::from_amount(self.clean_amount, self.face, BondPriceKind::Clean)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum BondError {
#[non_exhaustive]
InvalidFlowLimit {
requested: u32,
maximum: u32,
},
#[non_exhaustive]
FlowLimitExceeded {
maximum: u32,
required: usize,
},
#[non_exhaustive]
ExCouponCountMismatch {
expected: usize,
actual: usize,
},
#[non_exhaustive]
InvalidIssueMaturity {
issue_date: Date,
contractual_maturity: Date,
},
#[non_exhaustive]
RedemptionBeforeIssue {
issue_date: Date,
redemption_payment: Date,
},
#[non_exhaustive]
RedemptionBeforeLastCoupon {
last_coupon_payment: Date,
redemption_payment: Date,
},
#[non_exhaustive]
ExCouponAfterPayment {
index: usize,
cutoff_date: Date,
payment_date: Date,
},
#[non_exhaustive]
SettlementBeforeIssue {
settlement_date: Date,
issue_date: Date,
},
#[non_exhaustive]
AllocationFailed {
phase: &'static str,
requested: usize,
},
#[non_exhaustive]
AccrualWorkLimitExceeded {
index: Option<usize>,
phase: &'static str,
maximum: u64,
used: u64,
required_at_least: u64,
},
#[non_exhaustive]
Value {
index: Option<usize>,
phase: &'static str,
cause: ValueError,
},
#[non_exhaustive]
Rate {
index: Option<usize>,
phase: &'static str,
cause: RateError,
},
#[non_exhaustive]
Sum {
phase: &'static str,
cause: SumError,
},
#[non_exhaustive]
ZeroDirtyPrice {
measure: &'static str,
yield_rate: f64,
},
#[non_exhaustive]
InternalCapacity {
index: Option<usize>,
phase: &'static str,
operation: &'static str,
},
}
impl BondError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::InvalidFlowLimit { .. } => "bond.invalid_flow_limit",
Self::FlowLimitExceeded { .. } => "bond.flow_limit_exceeded",
Self::ExCouponCountMismatch { .. } => "bond.ex_coupon_count_mismatch",
Self::InvalidIssueMaturity { .. } => "bond.invalid_issue_maturity",
Self::RedemptionBeforeIssue { .. } => "bond.redemption_before_issue",
Self::RedemptionBeforeLastCoupon { .. } => "bond.redemption_before_last_coupon",
Self::ExCouponAfterPayment { .. } => "bond.ex_coupon_after_payment",
Self::SettlementBeforeIssue { .. } => "bond.settlement_before_issue",
Self::AllocationFailed { .. } => "bond.allocation_failed",
Self::AccrualWorkLimitExceeded { .. } => "bond.accrual_work_limit_exceeded",
Self::Value { .. } => "bond.value",
Self::Rate { .. } => "bond.rate",
Self::Sum { .. } => "bond.sum",
Self::ZeroDirtyPrice { .. } => "bond.zero_dirty_price",
Self::InternalCapacity { .. } => "bond.internal_capacity",
}
}
}
impl fmt::Display for BondError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidFlowLimit { requested, maximum } => write!(
f,
"bond flow limit {requested} exceeds supported maximum {maximum}"
),
Self::FlowLimitExceeded { maximum, required } => write!(
f,
"bond flow count {required} exceeds caller limit {maximum}"
),
Self::ExCouponCountMismatch { expected, actual } => write!(
f,
"ex-coupon count {actual} does not match schedule coupon count {expected}"
),
Self::InvalidIssueMaturity {
issue_date,
contractual_maturity,
} => write!(
f,
"bond issue date {issue_date} follows contractual maturity {contractual_maturity}"
),
Self::RedemptionBeforeIssue {
issue_date,
redemption_payment,
} => write!(
f,
"redemption payment {redemption_payment} precedes issue date {issue_date}"
),
Self::RedemptionBeforeLastCoupon {
last_coupon_payment,
redemption_payment,
} => write!(
f,
"redemption payment {redemption_payment} precedes final coupon payment {last_coupon_payment}"
),
Self::ExCouponAfterPayment {
index,
cutoff_date,
payment_date,
} => write!(
f,
"coupon {index} ex-coupon cutoff {cutoff_date} follows payment date {payment_date}"
),
Self::SettlementBeforeIssue {
settlement_date,
issue_date,
} => write!(
f,
"settlement date {settlement_date} precedes bond issue date {issue_date}"
),
Self::AllocationFailed { phase, requested } => {
write!(f, "{phase}: cannot reserve {requested} elements")
}
Self::AccrualWorkLimitExceeded {
index,
phase,
maximum,
used,
required_at_least,
} => write!(
f,
"{phase} at flow {index:?}: work limit {maximum}, used {used}, requires at least {required_at_least}"
),
Self::Value {
index,
phase,
cause,
} => write!(f, "{phase} at flow {index:?}: {cause}"),
Self::Rate {
index,
phase,
cause,
} => write!(f, "{phase} at flow {index:?}: {cause}"),
Self::Sum { phase, cause } => write!(f, "{phase}: {cause}"),
Self::ZeroDirtyPrice {
measure,
yield_rate,
} => write!(
f,
"cannot normalize {measure} at yield {yield_rate:?}: dirty price is zero"
),
Self::InternalCapacity {
index,
phase,
operation,
} => write!(
f,
"{phase} at flow {index:?}: internal capacity invariant failed during {operation}"
),
}
}
}
impl StdError for BondError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Value { cause, .. } => Some(cause),
Self::Rate { cause, .. } => Some(cause),
Self::Sum { cause, .. } => Some(cause),
_ => None,
}
}
}
fn validate_face(face: f64) -> Result<f64, BondError> {
numeric::within("bond face", face, Interval::greater_than(0.0))
.map_err(|cause| value_error(None, "bond face", cause))
}
fn validate_redemption_amount(redemption: SimpleCashFlow) -> Result<(), BondError> {
let _ = numeric::within(
"redemption amount",
redemption.amount(),
Interval::greater_than(0.0),
)
.map_err(|cause| value_error(None, "redemption amount", cause))?;
Ok(())
}
fn earned_coupon_accrual(
coupon: &FixedRateCoupon,
settlement_date: Date,
index: usize,
work: &mut AccrualWork,
) -> Result<f64, BondError> {
if settlement_date <= coupon.start() {
return Ok(0.0);
}
if settlement_date >= coupon.end() {
return Ok(coupon.amount());
}
work.charge(
coupon.coupon_rate().accrual(),
coupon.start(),
settlement_date,
Some(index),
"partial accrual",
)?;
let fraction = checked_horizon(
coupon.coupon_rate().accrual(),
coupon.start(),
settlement_date,
Some(index),
"partial accrual",
)?;
product::product(
"accrued amount",
coupon.notional(),
coupon.coupon_rate().rate(),
fraction,
)
.map_err(|cause| value_error(Some(index), "accrued amount", cause))
}
fn checked_horizon(
convention: &AccrualConvention,
start: Date,
end: Date,
index: Option<usize>,
phase: &'static str,
) -> Result<f64, BondError> {
let fraction = convention
.year_fraction(start, end)
.map_err(|cause| rate_error(index, phase, cause))?;
numeric::result_within("accrual fraction", fraction, Interval::at_least(0.0))
.map(canonical_zero)
.map_err(|cause| value_error(index, phase, cause))
}
fn reserved<T>(count: usize, phase: &'static str) -> Result<Vec<T>, BondError> {
let mut values = Vec::new();
values
.try_reserve_exact(count)
.map_err(|_| BondError::AllocationFailed {
phase,
requested: count,
})?;
Ok(values)
}
fn canonical_zero(value: f64) -> f64 {
if value == 0.0 { 0.0 } else { value }
}
fn value_error(index: Option<usize>, phase: &'static str, cause: ValueError) -> BondError {
BondError::Value {
index,
phase,
cause,
}
}
fn rate_error(index: Option<usize>, phase: &'static str, cause: RateError) -> BondError {
BondError::Rate {
index,
phase,
cause,
}
}
struct AccrualWork {
maximum: u64,
used: u64,
}
impl AccrualWork {
fn charge(
&mut self,
convention: &AccrualConvention,
start: Date,
end: Date,
index: Option<usize>,
phase: &'static str,
) -> Result<(), BondError> {
let required_at_least = self.used + query_work_units(convention, start, end);
if required_at_least > self.maximum {
return Err(BondError::AccrualWorkLimitExceeded {
index,
phase,
maximum: self.maximum,
used: self.used,
required_at_least,
});
}
self.used = required_at_least;
Ok(())
}
}