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}