convex_bonds/traits/extensions.rs
1//! Extension traits for specialized bond types.
2//!
3//! These traits extend the base `Bond` trait with specialized functionality
4//! for different bond categories.
5
6use convex_core::Date;
7use rust_decimal::Decimal;
8
9use super::Bond;
10use crate::types::{
11 AmortizationSchedule, CallSchedule, InflationIndexType, PutSchedule, RateIndex,
12};
13
14/// Extension trait for fixed coupon bonds.
15///
16/// Provides access to fixed coupon characteristics including rate and frequency.
17///
18/// # Example
19///
20/// ```rust,ignore
21/// fn analyze_fixed_bond<B: FixedCouponBond>(bond: &B) {
22/// println!("Coupon rate: {}%", bond.coupon_rate());
23/// println!("Frequency: {} payments/year", bond.coupon_frequency());
24/// }
25/// ```
26pub trait FixedCouponBond: Bond {
27 /// Returns the annual coupon rate as a decimal (e.g., 0.05 for 5%).
28 fn coupon_rate(&self) -> Decimal;
29
30 /// Returns the number of coupon payments per year.
31 fn coupon_frequency(&self) -> u32;
32
33 /// Returns the coupon amount per period per unit of face.
34 fn coupon_amount(&self) -> Decimal {
35 let face = self.face_value();
36 let rate = self.coupon_rate();
37 let freq = Decimal::from(self.coupon_frequency());
38 face * rate / freq
39 }
40
41 /// Returns the first coupon date.
42 fn first_coupon_date(&self) -> Option<Date>;
43
44 /// Returns the last coupon date before maturity.
45 fn last_coupon_date(&self) -> Option<Date>;
46
47 /// Returns true if this is an ex-dividend date (for markets with record dates).
48 fn is_ex_dividend(&self, _settlement: Date) -> bool {
49 false // Default implementation
50 }
51}
52
53/// Extension trait for floating rate notes.
54///
55/// Provides access to floating rate characteristics including index and spread.
56pub trait FloatingCouponBond: Bond {
57 /// Returns the reference rate index.
58 fn rate_index(&self) -> &RateIndex;
59
60 /// Returns the spread over the reference rate in basis points.
61 fn spread_bps(&self) -> Decimal;
62
63 /// Returns the spread as a decimal (e.g., 0.0050 for 50 bps).
64 fn spread(&self) -> Decimal {
65 self.spread_bps() / Decimal::from(10000)
66 }
67
68 /// Returns the reset frequency (payments per year).
69 fn reset_frequency(&self) -> u32;
70
71 /// Returns the number of lookback days for the rate fixing.
72 fn lookback_days(&self) -> u32 {
73 0 // Default: no lookback
74 }
75
76 /// Returns the floor rate if any (as decimal).
77 fn floor(&self) -> Option<Decimal> {
78 None
79 }
80
81 /// Returns the cap rate if any (as decimal).
82 fn cap(&self) -> Option<Decimal> {
83 None
84 }
85
86 /// Calculates the coupon for the current period given the reference rate.
87 fn current_coupon(&self, reference_rate: Decimal) -> Decimal {
88 let mut rate = reference_rate + self.spread();
89
90 // Apply floor
91 if let Some(floor) = self.floor() {
92 if rate < floor {
93 rate = floor;
94 }
95 }
96
97 // Apply cap
98 if let Some(cap) = self.cap() {
99 if rate > cap {
100 rate = cap;
101 }
102 }
103
104 let face = self.face_value();
105 let freq = Decimal::from(self.reset_frequency());
106 face * rate / freq
107 }
108
109 /// Returns the next reset date after the given date.
110 fn next_reset_date(&self, after: Date) -> Option<Date>;
111
112 /// Returns the fixing date for a given reset date.
113 fn fixing_date(&self, reset_date: Date) -> Date;
114}
115
116/// Extension trait for bonds with embedded options (callable/puttable).
117///
118/// Provides access to call and put schedules and option characteristics.
119pub trait EmbeddedOptionBond: Bond {
120 /// Returns the call schedule if the bond is callable.
121 fn call_schedule(&self) -> Option<&CallSchedule>;
122
123 /// Returns the put schedule if the bond is puttable.
124 fn put_schedule(&self) -> Option<&PutSchedule>;
125
126 /// Returns true if the bond is currently callable.
127 fn is_callable_on(&self, date: Date) -> bool {
128 self.call_schedule().is_some_and(|s| s.is_callable_on(date))
129 }
130
131 /// Returns true if the bond is currently puttable.
132 fn is_puttable_on(&self, date: Date) -> bool {
133 self.put_schedule().is_some_and(|s| s.is_puttable_on(date))
134 }
135
136 /// Returns the call price on the given date if callable.
137 fn call_price_on(&self, date: Date) -> Option<f64> {
138 self.call_schedule().and_then(|s| s.call_price_on(date))
139 }
140
141 /// Returns the put price on the given date if puttable.
142 fn put_price_on(&self, date: Date) -> Option<f64> {
143 self.put_schedule().and_then(|s| s.put_price_on(date))
144 }
145
146 /// Returns the first call date.
147 fn first_call_date(&self) -> Option<Date> {
148 self.call_schedule()
149 .and_then(crate::types::CallSchedule::first_call_date)
150 }
151
152 /// Returns the first put date.
153 fn first_put_date(&self) -> Option<Date> {
154 self.put_schedule()
155 .and_then(crate::types::PutSchedule::first_put_date)
156 }
157
158 /// Returns true if the bond has any optionality.
159 fn has_optionality(&self) -> bool {
160 self.call_schedule().is_some() || self.put_schedule().is_some()
161 }
162
163 /// Calculates yield-to-call for a given price.
164 ///
165 /// This is a placeholder; actual implementation requires numerical methods.
166 fn yield_to_call(&self, _price: Decimal, _settlement: Date) -> Option<Decimal> {
167 None // To be implemented by concrete types
168 }
169
170 /// Calculates yield-to-put for a given price.
171 fn yield_to_put(&self, _price: Decimal, _settlement: Date) -> Option<Decimal> {
172 None // To be implemented by concrete types
173 }
174
175 /// Calculates yield-to-worst (minimum of YTM, YTC, YTP).
176 fn yield_to_worst(&self, _price: Decimal, _settlement: Date) -> Option<Decimal> {
177 None // To be implemented by concrete types
178 }
179}
180
181/// Extension trait for amortizing bonds.
182///
183/// Provides access to amortization schedules and factor calculations.
184pub trait AmortizingBond: Bond {
185 /// Returns the amortization schedule.
186 fn amortization_schedule(&self) -> &AmortizationSchedule;
187
188 /// Returns the current factor (remaining principal / original face).
189 fn factor(&self, as_of: Date) -> f64 {
190 self.amortization_schedule().factor_as_of(as_of)
191 }
192
193 /// Returns the outstanding principal as of the given date.
194 fn outstanding_principal(&self, as_of: Date) -> Decimal {
195 let factor = Decimal::try_from(self.factor(as_of)).unwrap_or(Decimal::ONE);
196 self.face_value() * factor
197 }
198
199 /// Returns the next principal payment date.
200 fn next_principal_date(&self, after: Date) -> Option<Date> {
201 self.amortization_schedule().next_payment_date(after)
202 }
203
204 /// Returns the principal payment amount for a specific date.
205 fn principal_payment(&self, date: Date) -> Option<Decimal> {
206 self.amortization_schedule().principal_on(date).map(|pct| {
207 let pct_dec = Decimal::try_from(pct / 100.0).unwrap_or(Decimal::ZERO);
208 self.face_value() * pct_dec
209 })
210 }
211
212 /// Returns the weighted average life (WAL) from the given date.
213 fn weighted_average_life(&self, from: Date) -> f64;
214}
215
216/// Extension trait for inflation-linked bonds.
217///
218/// Provides access to inflation adjustment and index ratio calculations.
219pub trait InflationLinkedBond: Bond {
220 /// Returns the inflation index type.
221 fn inflation_index(&self) -> InflationIndexType;
222
223 /// Returns the base index value at issue.
224 fn base_index_value(&self) -> Decimal;
225
226 /// Returns the index ratio for the given settlement date.
227 ///
228 /// The index ratio = settlement index / base index.
229 fn index_ratio(&self, _settlement: Date, settlement_index: Decimal) -> Decimal {
230 if self.base_index_value().is_zero() {
231 Decimal::ONE
232 } else {
233 settlement_index / self.base_index_value()
234 }
235 }
236
237 /// Returns the inflation-adjusted principal for settlement.
238 fn inflation_adjusted_principal(&self, settlement: Date, settlement_index: Decimal) -> Decimal {
239 let ratio = self.index_ratio(settlement, settlement_index);
240 self.face_value() * ratio
241 }
242
243 /// Returns the inflation-adjusted coupon for a period.
244 ///
245 /// For TIPS-style bonds, coupons are calculated on the adjusted principal.
246 fn inflation_adjusted_coupon(
247 &self,
248 coupon_date: Date,
249 coupon_index: Decimal,
250 real_coupon: Decimal,
251 ) -> Decimal {
252 let ratio = self.index_ratio(coupon_date, coupon_index);
253 real_coupon * ratio
254 }
255
256 /// Returns true if principal is protected from deflation (floor at par).
257 fn has_deflation_floor(&self) -> bool {
258 true // Default: most inflation bonds have deflation floor
259 }
260
261 /// Applies deflation floor to index ratio if applicable.
262 fn apply_deflation_floor(&self, ratio: Decimal) -> Decimal {
263 if self.has_deflation_floor() && ratio < Decimal::ONE {
264 Decimal::ONE
265 } else {
266 ratio
267 }
268 }
269
270 /// Returns the reference index for a specific settlement date.
271 ///
272 /// This interpolates between monthly index values per the bond's convention.
273 fn reference_index(
274 &self,
275 settlement: Date,
276 monthly_indices: &[(Date, Decimal)],
277 ) -> Option<Decimal>;
278
279 /// Returns the real yield given price and settlement info.
280 fn real_yield(&self, _price: Decimal, _settlement: Date) -> Option<Decimal> {
281 None // To be implemented by concrete types
282 }
283
284 /// Returns the breakeven inflation rate.
285 fn breakeven_inflation(&self, _nominal_yield: Decimal, _real_yield: Decimal) -> Decimal {
286 Decimal::ZERO // To be implemented
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 // These tests would require mock implementations of the traits.
295 // For now, we test the default method implementations where possible.
296
297 #[test]
298 fn test_spread_conversion() {
299 // Test that 50 bps = 0.0050
300 let spread_bps = Decimal::from(50);
301 let spread = spread_bps / Decimal::from(10000);
302 assert_eq!(spread, Decimal::new(5, 3));
303 }
304}