convex_bonds/traits/
bond.rs

1//! Core Bond trait definition.
2//!
3//! The `Bond` trait defines the common interface for all bond types.
4
5use convex_core::types::Frequency;
6use convex_core::{Currency, Date};
7use rust_decimal::Decimal;
8
9use crate::types::{BondIdentifiers, BondType, CalendarId};
10
11/// Type of cash flow.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CashFlowType {
14    /// Interest/coupon payment
15    Coupon,
16    /// Principal payment (redemption or amortization)
17    Principal,
18    /// Combined coupon and principal payment
19    CouponAndPrincipal,
20    /// Fee payment
21    Fee,
22}
23
24/// A single bond cash flow.
25///
26/// Represents a payment from the bond, including both the amount and timing.
27#[derive(Debug, Clone)]
28pub struct BondCashFlow {
29    /// Payment date
30    pub date: Date,
31    /// Cash flow amount (absolute value)
32    pub amount: Decimal,
33    /// Type of cash flow
34    pub flow_type: CashFlowType,
35    /// Accrual start date (for coupons)
36    pub accrual_start: Option<Date>,
37    /// Accrual end date (for coupons)
38    pub accrual_end: Option<Date>,
39    /// Remaining factor (for amortizing bonds)
40    pub factor: Decimal,
41    /// Reference rate for floating rate instruments (projected or actual)
42    pub reference_rate: Option<Decimal>,
43}
44
45impl BondCashFlow {
46    /// Creates a new coupon cash flow.
47    #[must_use]
48    pub fn coupon(date: Date, amount: Decimal) -> Self {
49        Self {
50            date,
51            amount,
52            flow_type: CashFlowType::Coupon,
53            accrual_start: None,
54            accrual_end: None,
55            factor: Decimal::ONE,
56            reference_rate: None,
57        }
58    }
59
60    /// Creates a new principal cash flow.
61    #[must_use]
62    pub fn principal(date: Date, amount: Decimal) -> Self {
63        Self {
64            date,
65            amount,
66            flow_type: CashFlowType::Principal,
67            accrual_start: None,
68            accrual_end: None,
69            factor: Decimal::ONE,
70            reference_rate: None,
71        }
72    }
73
74    /// Creates a combined coupon and principal cash flow.
75    #[must_use]
76    pub fn coupon_and_principal(date: Date, coupon: Decimal, principal: Decimal) -> Self {
77        Self {
78            date,
79            amount: coupon + principal,
80            flow_type: CashFlowType::CouponAndPrincipal,
81            accrual_start: None,
82            accrual_end: None,
83            factor: Decimal::ONE,
84            reference_rate: None,
85        }
86    }
87
88    /// Sets the accrual period.
89    #[must_use]
90    pub fn with_accrual(mut self, start: Date, end: Date) -> Self {
91        self.accrual_start = Some(start);
92        self.accrual_end = Some(end);
93        self
94    }
95
96    /// Sets the remaining factor.
97    #[must_use]
98    pub fn with_factor(mut self, factor: Decimal) -> Self {
99        self.factor = factor;
100        self
101    }
102
103    /// Sets the reference rate (for floating rate instruments).
104    #[must_use]
105    pub fn with_reference_rate(mut self, rate: Decimal) -> Self {
106        self.reference_rate = Some(rate);
107        self
108    }
109
110    /// Returns the factored amount (amount * factor).
111    #[must_use]
112    pub fn factored_amount(&self) -> Decimal {
113        self.amount * self.factor
114    }
115
116    /// Returns true if this is a coupon payment.
117    #[must_use]
118    pub fn is_coupon(&self) -> bool {
119        matches!(
120            self.flow_type,
121            CashFlowType::Coupon | CashFlowType::CouponAndPrincipal
122        )
123    }
124
125    /// Returns true if this is a principal payment.
126    #[must_use]
127    pub fn is_principal(&self) -> bool {
128        matches!(
129            self.flow_type,
130            CashFlowType::Principal | CashFlowType::CouponAndPrincipal
131        )
132    }
133}
134
135/// Core bond trait.
136///
137/// This trait defines the common interface that all bond types must implement.
138/// It provides methods for accessing bond characteristics, generating cash flows,
139/// and basic pricing.
140///
141/// # Design Principles
142///
143/// - **Interface Segregation**: Small, focused methods that can be implemented
144///   efficiently for all bond types
145/// - **Composability**: Extension traits add specialized behavior
146/// - **Flexibility**: Works with both object-safe dyn dispatch and static dispatch
147///
148/// # Example
149///
150/// ```rust,ignore
151/// use convex_bonds::traits::Bond;
152///
153/// fn print_bond_info(bond: &dyn Bond) {
154///     println!("Bond: {:?}", bond.identifiers().primary_id());
155///     println!("Maturity: {}", bond.maturity());
156///     println!("Currency: {}", bond.currency());
157/// }
158/// ```
159pub trait Bond {
160    // ==================== Identity ====================
161
162    /// Returns the bond's identifiers (ISIN, CUSIP, etc.).
163    fn identifiers(&self) -> &BondIdentifiers;
164
165    /// Returns the bond type classification.
166    fn bond_type(&self) -> BondType;
167
168    // ==================== Basic Terms ====================
169
170    /// Returns the bond's currency.
171    fn currency(&self) -> Currency;
172
173    /// Returns the maturity date.
174    ///
175    /// For perpetual bonds, this returns None.
176    fn maturity(&self) -> Option<Date>;
177
178    /// Returns the issue date.
179    fn issue_date(&self) -> Date;
180
181    /// Returns the first settlement date.
182    fn first_settlement_date(&self) -> Date;
183
184    /// Returns the dated date (when interest starts accruing).
185    ///
186    /// This is typically the issue date but can differ.
187    fn dated_date(&self) -> Date;
188
189    /// Returns the face/par value per unit.
190    fn face_value(&self) -> Decimal;
191
192    /// Returns the coupon payment frequency.
193    fn frequency(&self) -> Frequency;
194
195    // ==================== Cash Flow Generation ====================
196
197    /// Generates all cash flows from the given date forward.
198    ///
199    /// Returns a vector of cash flows sorted by payment date.
200    fn cash_flows(&self, from: Date) -> Vec<BondCashFlow>;
201
202    /// Returns the next coupon date after the given date.
203    fn next_coupon_date(&self, after: Date) -> Option<Date>;
204
205    /// Returns the previous coupon date before the given date.
206    fn previous_coupon_date(&self, before: Date) -> Option<Date>;
207
208    // ==================== Accrued Interest ====================
209
210    /// Calculates accrued interest as of the settlement date.
211    ///
212    /// Returns the accrued interest per unit of face value.
213    fn accrued_interest(&self, settlement: Date) -> Decimal;
214
215    /// Returns the day count convention for accrual calculations.
216    fn day_count_convention(&self) -> &str;
217
218    // ==================== Calendar ====================
219
220    /// Returns the payment calendar.
221    fn calendar(&self) -> &CalendarId;
222
223    // ==================== Redemption ====================
224
225    /// Returns the redemption value per unit at maturity (typically 100).
226    fn redemption_value(&self) -> Decimal {
227        Decimal::ONE_HUNDRED
228    }
229
230    // ==================== Convenience ====================
231
232    /// Returns true if the bond has matured as of the given date.
233    fn has_matured(&self, as_of: Date) -> bool {
234        match self.maturity() {
235            Some(maturity) => as_of >= maturity,
236            None => false, // Perpetual bonds never mature
237        }
238    }
239
240    /// Returns the years to maturity from the given date.
241    fn years_to_maturity(&self, from: Date) -> Option<f64> {
242        self.maturity().map(|mat| {
243            let days = mat.days_between(&from);
244            days as f64 / 365.0
245        })
246    }
247
248    /// Returns true if this bond type requires a pricing model.
249    fn requires_model(&self) -> bool {
250        self.bond_type().requires_model()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn date(y: i32, m: u32, d: u32) -> Date {
259        Date::from_ymd(y, m, d).unwrap()
260    }
261
262    #[test]
263    fn test_cash_flow_type() {
264        let coupon = BondCashFlow::coupon(date(2025, 6, 15), Decimal::new(25, 1));
265        assert!(coupon.is_coupon());
266        assert!(!coupon.is_principal());
267
268        let principal = BondCashFlow::principal(date(2025, 6, 15), Decimal::ONE_HUNDRED);
269        assert!(!principal.is_coupon());
270        assert!(principal.is_principal());
271
272        let combined = BondCashFlow::coupon_and_principal(
273            date(2025, 6, 15),
274            Decimal::new(25, 1),
275            Decimal::ONE_HUNDRED,
276        );
277        assert!(combined.is_coupon());
278        assert!(combined.is_principal());
279    }
280
281    #[test]
282    fn test_factored_amount() {
283        let cf = BondCashFlow::coupon(date(2025, 6, 15), Decimal::ONE_HUNDRED)
284            .with_factor(Decimal::new(5, 1)); // 0.5 factor
285
286        assert_eq!(cf.factored_amount(), Decimal::new(50, 0));
287    }
288}