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}