1use rust_decimal::Decimal;
32use serde::{Deserialize, Serialize};
33
34use convex_core::daycounts::DayCountConvention;
35use convex_core::types::{Currency, Date, Frequency};
36
37use crate::cashflows::{Schedule, ScheduleConfig};
38use crate::error::{BondError, BondResult};
39use crate::traits::{Bond, BondCashFlow, FloatingCouponBond};
40use crate::types::{BondIdentifiers, BondType, CalendarId, Cusip, Isin, RateIndex, SOFRConvention};
41
42#[derive(Debug, Clone)]
55pub struct FloatingRateNote {
56 identifiers: BondIdentifiers,
58
59 index: RateIndex,
61
62 sofr_convention: Option<SOFRConvention>,
64
65 spread_bps: Decimal,
67
68 maturity: Date,
70
71 issue_date: Date,
73
74 frequency: Frequency,
76
77 day_count: DayCountConvention,
79
80 reset_lag: i32,
82
83 payment_delay: u32,
85
86 cap: Option<Decimal>,
88
89 floor: Option<Decimal>,
91
92 settlement_days: u32,
94
95 calendar: CalendarId,
97
98 currency: Currency,
100
101 face_value: Decimal,
103
104 current_rate: Option<Decimal>,
106}
107
108impl FloatingRateNote {
109 #[must_use]
111 pub fn builder() -> FloatingRateNoteBuilder {
112 FloatingRateNoteBuilder::default()
113 }
114
115 #[must_use]
117 pub fn index(&self) -> &RateIndex {
118 &self.index
119 }
120
121 #[must_use]
123 pub fn sofr_convention(&self) -> Option<&SOFRConvention> {
124 self.sofr_convention.as_ref()
125 }
126
127 #[must_use]
129 pub fn spread_bps(&self) -> Decimal {
130 self.spread_bps
131 }
132
133 #[must_use]
135 pub fn spread_decimal(&self) -> Decimal {
136 self.spread_bps / Decimal::from(10000)
137 }
138
139 #[must_use]
141 pub fn maturity_date(&self) -> Date {
142 self.maturity
143 }
144
145 #[must_use]
147 pub fn get_issue_date(&self) -> Date {
148 self.issue_date
149 }
150
151 #[must_use]
153 pub fn frequency(&self) -> Frequency {
154 self.frequency
155 }
156
157 #[must_use]
159 pub fn day_count(&self) -> DayCountConvention {
160 self.day_count
161 }
162
163 #[must_use]
165 pub fn cap(&self) -> Option<Decimal> {
166 self.cap
167 }
168
169 #[must_use]
171 pub fn floor(&self) -> Option<Decimal> {
172 self.floor
173 }
174
175 #[must_use]
177 pub fn settlement_days(&self) -> u32 {
178 self.settlement_days
179 }
180
181 #[must_use]
183 pub fn reset_lag(&self) -> i32 {
184 self.reset_lag
185 }
186
187 pub fn set_current_rate(&mut self, rate: Decimal) {
189 self.current_rate = Some(rate);
190 }
191
192 #[must_use]
194 pub fn current_rate(&self) -> Option<Decimal> {
195 self.current_rate
196 }
197
198 #[must_use]
200 pub fn effective_rate(&self, index_rate: Decimal) -> Decimal {
201 let mut rate = index_rate + self.spread_decimal();
202
203 if let Some(floor) = self.floor {
205 if rate < floor {
206 rate = floor;
207 }
208 }
209
210 if let Some(cap) = self.cap {
212 if rate > cap {
213 rate = cap;
214 }
215 }
216
217 rate
218 }
219
220 #[must_use]
222 pub fn period_coupon(
223 &self,
224 period_start: Date,
225 period_end: Date,
226 index_rate: Decimal,
227 ) -> Decimal {
228 let dc = self.day_count.to_day_count();
229 let year_frac = dc.year_fraction(period_start, period_end);
230 let effective_rate = self.effective_rate(index_rate);
231
232 self.face_value * effective_rate * Decimal::try_from(year_frac).unwrap_or(Decimal::ZERO)
233 }
234
235 #[must_use]
237 pub fn accrued_interest_with_rate(&self, settlement: Date, index_rate: Decimal) -> Decimal {
238 if settlement <= self.issue_date {
239 return Decimal::ZERO;
240 }
241
242 let Some(last_coupon) = self.previous_coupon_date(settlement) else {
243 return Decimal::ZERO;
244 };
245
246 let Some(next_coupon) = self.next_coupon_date(settlement) else {
247 return Decimal::ZERO;
248 };
249
250 if settlement >= next_coupon {
251 return Decimal::ZERO;
252 }
253
254 let dc = self.day_count.to_day_count();
255 let accrued_days = dc.day_count(last_coupon, settlement);
256 let period_days = dc.day_count(last_coupon, next_coupon);
257
258 if period_days == 0 {
259 return Decimal::ZERO;
260 }
261
262 let effective_rate = self.effective_rate(index_rate);
263 let periods_per_year = Decimal::from(self.frequency.periods_per_year());
264 let period_coupon = self.face_value * effective_rate / periods_per_year;
265
266 period_coupon * Decimal::from(accrued_days) / Decimal::from(period_days)
267 }
268
269 #[must_use]
284 pub fn sofr_compounded_in_arrears(
285 &self,
286 daily_rates: &[(Date, Decimal)],
287 period_start: Date,
288 period_end: Date,
289 ) -> Decimal {
290 let Some(SOFRConvention::CompoundedInArrears {
291 lookback_days,
292 observation_shift,
293 lockout_days,
294 }) = &self.sofr_convention
295 else {
296 return Decimal::ZERO;
297 };
298
299 let calendar = self.calendar.to_calendar();
300 let mut compounded = 1.0_f64;
301 let mut current = period_start;
302 let mut days_count = 0_i64;
303
304 while current < period_end {
305 let next = calendar.add_business_days(current, 1);
306 let weight_days = current.days_between(&next);
307
308 let observation_date = if *observation_shift {
310 calendar.add_business_days(current, -(*lookback_days as i32))
311 } else {
312 current
313 };
314
315 let rate_date = if let Some(lock) = lockout_days {
317 let lock_start = calendar.add_business_days(period_end, -(*lock as i32));
318 if current >= lock_start {
319 lock_start
320 } else {
321 observation_date
322 }
323 } else {
324 observation_date
325 };
326
327 let rate = daily_rates
329 .iter()
330 .find(|(d, _)| *d == rate_date)
331 .map_or(0.0, |(_, r)| r.to_string().parse::<f64>().unwrap_or(0.0));
332
333 compounded *= 1.0 + rate * weight_days as f64 / 360.0;
335 days_count += weight_days;
336 current = next;
337 }
338
339 if days_count == 0 {
340 return Decimal::ZERO;
341 }
342
343 let annualized = (compounded - 1.0) * 360.0 / days_count as f64;
345 Decimal::try_from(annualized).unwrap_or(Decimal::ZERO)
346 }
347
348 #[must_use]
350 pub fn sofr_simple_average(
351 &self,
352 daily_rates: &[(Date, Decimal)],
353 period_start: Date,
354 period_end: Date,
355 ) -> Decimal {
356 let Some(SOFRConvention::SimpleAverage { lookback_days }) = &self.sofr_convention else {
357 return Decimal::ZERO;
358 };
359
360 let calendar = self.calendar.to_calendar();
361 let mut sum = 0.0_f64;
362 let mut count = 0_i32;
363 let mut current = period_start;
364
365 while current < period_end {
366 let observation_date = calendar.add_business_days(current, -(*lookback_days as i32));
367
368 let rate = daily_rates
369 .iter()
370 .find(|(d, _)| *d == observation_date)
371 .map_or(0.0, |(_, r)| r.to_string().parse::<f64>().unwrap_or(0.0));
372
373 sum += rate;
374 count += 1;
375 current = calendar.add_business_days(current, 1);
376 }
377
378 if count == 0 {
379 return Decimal::ZERO;
380 }
381
382 Decimal::try_from(sum / f64::from(count)).unwrap_or(Decimal::ZERO)
383 }
384
385 #[must_use]
387 pub fn identifier(&self) -> String {
388 if let Some(cusip) = self.identifiers.cusip() {
389 return cusip.to_string();
390 }
391 if let Some(isin) = self.identifiers.isin() {
392 return isin.to_string();
393 }
394 if let Some(ticker) = self.identifiers.ticker() {
395 return ticker.to_string();
396 }
397 "UNKNOWN".to_string()
398 }
399
400 pub fn cash_flows_projected<C>(&self, from: Date, forward_curve: &C) -> Vec<BondCashFlow>
423 where
424 C: convex_curves::RateCurveDyn + ?Sized,
425 {
426 if from >= self.maturity {
427 return Vec::new();
428 }
429
430 let Ok(schedule) = self.schedule() else {
431 return Vec::new();
432 };
433
434 let mut flows = Vec::new();
435 let ref_date = forward_curve.reference_date();
436
437 for (start, end) in schedule.unadjusted_periods() {
438 if end <= from {
439 continue;
440 }
441
442 let t1 = start.days_between(&ref_date) as f64 / 365.0;
444 let t2 = end.days_between(&ref_date) as f64 / 365.0;
445
446 let fwd_rate = if t1 < 0.0 && t2 > 0.0 {
448 forward_curve.forward_rate(0.0, t2.abs()).unwrap_or(0.0)
450 } else if t1 >= 0.0 && t2 > 0.0 {
451 forward_curve.forward_rate(t1, t2).unwrap_or(0.0)
452 } else {
453 forward_curve
455 .zero_rate(t2.abs(), convex_curves::Compounding::Continuous)
456 .unwrap_or(0.0)
457 };
458
459 let projected_rate = Decimal::try_from(fwd_rate).unwrap_or(Decimal::ZERO);
461 let effective_rate = self.effective_rate(projected_rate);
462
463 let dc = self.day_count.to_day_count();
465 let year_frac = dc.year_fraction(start, end);
466 let coupon_amount = self.face_value
467 * effective_rate
468 * Decimal::try_from(year_frac).unwrap_or(Decimal::ZERO);
469
470 if end == self.maturity {
471 flows.push(
472 BondCashFlow::coupon_and_principal(end, coupon_amount, self.face_value)
473 .with_accrual(start, end)
474 .with_reference_rate(projected_rate),
475 );
476 } else {
477 flows.push(
478 BondCashFlow::coupon(end, coupon_amount)
479 .with_accrual(start, end)
480 .with_reference_rate(projected_rate),
481 );
482 }
483 }
484
485 flows
486 }
487
488 #[must_use]
502 pub fn required_fixing_dates(&self, from: Date) -> Vec<Date> {
503 let Ok(schedule) = self.schedule() else {
504 return Vec::new();
505 };
506
507 let calendar = self.calendar.to_calendar();
508 let mut dates = Vec::new();
509
510 let is_overnight = matches!(
512 self.index,
513 RateIndex::Sofr
514 | RateIndex::Sonia
515 | RateIndex::Estr
516 | RateIndex::Tonar
517 | RateIndex::Saron
518 | RateIndex::Corra
519 | RateIndex::Aonia
520 | RateIndex::Honia
521 );
522
523 for (start, end) in schedule.unadjusted_periods() {
524 if end <= from {
525 continue;
526 }
527
528 if is_overnight {
529 if let Some(conv) = &self.sofr_convention {
531 let lookback = conv.lookback_days().unwrap_or(0);
532 let obs_shift = conv.is_in_arrears();
533
534 let mut current = start;
535 while current < end {
536 let obs_date = if obs_shift {
537 calendar.add_business_days(current, -(lookback as i32))
538 } else {
539 current
540 };
541 dates.push(obs_date);
542 current = calendar.add_business_days(current, 1);
543 }
544 }
545 } else {
546 let fixing_date = calendar.add_business_days(start, self.reset_lag);
548 dates.push(fixing_date);
549 }
550 }
551
552 dates.sort();
554 dates.dedup();
555 dates
556 }
557
558 #[must_use]
573 pub fn accrued_interest_from_store(
574 &self,
575 settlement: Date,
576 store: &crate::indices::IndexFixingStore,
577 ) -> Decimal {
578 if settlement <= self.issue_date {
579 return Decimal::ZERO;
580 }
581
582 let Some(last_coupon) = self.previous_coupon_date(settlement) else {
583 return Decimal::ZERO;
584 };
585
586 if let Some(conv) = &self.sofr_convention {
588 if conv.is_in_arrears() {
589 let calendar = self.calendar.to_calendar();
590 let rate = crate::indices::OvernightCompounding::compounded_rate(
591 store,
592 &self.index,
593 last_coupon,
594 settlement,
595 conv,
596 calendar.as_ref(),
597 );
598
599 if let Some(r) = rate {
600 let dc = self.day_count.to_day_count();
601 let year_frac = dc.year_fraction(last_coupon, settlement);
602 let effective = self.effective_rate(r);
603 return self.face_value
604 * effective
605 * Decimal::try_from(year_frac).unwrap_or(Decimal::ZERO);
606 }
607 }
608 }
609
610 let calendar = self.calendar.to_calendar();
612 let fixing_date = calendar.add_business_days(last_coupon, self.reset_lag);
613
614 if let Some(rate) = store.get_fixing(&self.index, fixing_date) {
615 self.accrued_interest_with_rate(settlement, rate)
616 } else {
617 Decimal::ZERO
618 }
619 }
620
621 fn schedule(&self) -> BondResult<Schedule> {
623 let config = ScheduleConfig::new(self.issue_date, self.maturity, self.frequency)
624 .with_calendar(self.calendar.clone());
625 Schedule::generate(config)
626 }
627}
628
629impl Bond for FloatingRateNote {
632 fn identifiers(&self) -> &BondIdentifiers {
633 &self.identifiers
634 }
635
636 fn bond_type(&self) -> BondType {
637 match (&self.cap, &self.floor) {
638 (Some(_), Some(_)) => BondType::CollaredFRN,
639 (Some(_), None) => BondType::CappedFRN,
640 (None, Some(_)) => BondType::FlooredFRN,
641 (None, None) => BondType::FloatingRateNote,
642 }
643 }
644
645 fn currency(&self) -> Currency {
646 self.currency
647 }
648
649 fn maturity(&self) -> Option<Date> {
650 Some(self.maturity)
651 }
652
653 fn issue_date(&self) -> Date {
654 self.issue_date
655 }
656
657 fn first_settlement_date(&self) -> Date {
658 let calendar = self.calendar.to_calendar();
659 calendar.add_business_days(self.issue_date, self.settlement_days as i32)
660 }
661
662 fn dated_date(&self) -> Date {
663 self.issue_date
664 }
665
666 fn face_value(&self) -> Decimal {
667 self.face_value
668 }
669
670 fn frequency(&self) -> Frequency {
671 self.frequency
672 }
673
674 fn cash_flows(&self, from: Date) -> Vec<BondCashFlow> {
675 if from >= self.maturity {
676 return Vec::new();
677 }
678
679 let Ok(schedule) = self.schedule() else {
680 return Vec::new();
681 };
682
683 let mut flows = Vec::new();
684
685 for (start, end) in schedule.unadjusted_periods() {
686 if end <= from {
687 continue;
688 }
689
690 let rate = self.current_rate.unwrap_or(Decimal::ZERO);
693 let coupon_amount = self.period_coupon(start, end, rate);
694
695 if end == self.maturity {
696 flows.push(
698 BondCashFlow::coupon_and_principal(end, coupon_amount, self.face_value)
699 .with_accrual(start, end),
700 );
701 } else {
702 flows.push(BondCashFlow::coupon(end, coupon_amount).with_accrual(start, end));
703 }
704 }
705
706 flows
707 }
708
709 fn next_coupon_date(&self, after: Date) -> Option<Date> {
710 let schedule = self.schedule().ok()?;
711 schedule.dates().iter().find(|&&d| d > after).copied()
712 }
713
714 fn previous_coupon_date(&self, before: Date) -> Option<Date> {
715 let schedule = self.schedule().ok()?;
716 schedule.dates().iter().rfind(|&&d| d < before).copied()
717 }
718
719 fn accrued_interest(&self, settlement: Date) -> Decimal {
720 let rate = self.current_rate.unwrap_or(Decimal::ZERO);
722 self.accrued_interest_with_rate(settlement, rate)
723 }
724
725 fn day_count_convention(&self) -> &str {
726 match self.day_count {
727 DayCountConvention::Act360 => "ACT/360",
728 DayCountConvention::Act365Fixed => "ACT/365F",
729 DayCountConvention::Act365Leap => "ACT/365L",
730 DayCountConvention::ActActIsda => "ACT/ACT ISDA",
731 DayCountConvention::ActActIcma => "ACT/ACT ICMA",
732 DayCountConvention::ActActAfb => "ACT/ACT AFB",
733 DayCountConvention::Thirty360US => "30/360 US",
734 DayCountConvention::Thirty360E => "30E/360",
735 DayCountConvention::Thirty360EIsda => "30E/360 ISDA",
736 DayCountConvention::Thirty360German => "30/360 German",
737 }
738 }
739
740 fn calendar(&self) -> &CalendarId {
741 &self.calendar
742 }
743
744 fn redemption_value(&self) -> Decimal {
745 self.face_value
746 }
747}
748
749impl FloatingCouponBond for FloatingRateNote {
752 fn rate_index(&self) -> &RateIndex {
753 &self.index
754 }
755
756 fn spread_bps(&self) -> Decimal {
757 self.spread_bps
758 }
759
760 fn reset_frequency(&self) -> u32 {
761 self.frequency.periods_per_year()
762 }
763
764 fn lookback_days(&self) -> u32 {
765 self.sofr_convention
766 .as_ref()
767 .and_then(crate::types::SOFRConvention::lookback_days)
768 .unwrap_or(0)
769 }
770
771 fn floor(&self) -> Option<Decimal> {
772 self.floor
773 }
774
775 fn cap(&self) -> Option<Decimal> {
776 self.cap
777 }
778
779 fn next_reset_date(&self, after: Date) -> Option<Date> {
780 self.next_coupon_date(after)
781 }
782
783 fn fixing_date(&self, reset_date: Date) -> Date {
784 let calendar = self.calendar.to_calendar();
785 calendar.add_business_days(reset_date, self.reset_lag)
786 }
787}
788
789#[derive(Debug, Clone, Default)]
793pub struct FloatingRateNoteBuilder {
794 identifiers: Option<BondIdentifiers>,
795 index: Option<RateIndex>,
796 sofr_convention: Option<SOFRConvention>,
797 spread_bps: Option<Decimal>,
798 maturity: Option<Date>,
799 issue_date: Option<Date>,
800 frequency: Option<Frequency>,
801 day_count: Option<DayCountConvention>,
802 reset_lag: Option<i32>,
803 payment_delay: Option<u32>,
804 cap: Option<Decimal>,
805 floor: Option<Decimal>,
806 settlement_days: Option<u32>,
807 calendar: Option<CalendarId>,
808 currency: Option<Currency>,
809 face_value: Option<Decimal>,
810}
811
812impl FloatingRateNoteBuilder {
813 #[must_use]
815 pub fn new() -> Self {
816 Self::default()
817 }
818
819 #[must_use]
821 pub fn identifiers(mut self, ids: BondIdentifiers) -> Self {
822 self.identifiers = Some(ids);
823 self
824 }
825
826 pub fn cusip(mut self, cusip: &str) -> Result<Self, crate::error::IdentifierError> {
828 let cusip = Cusip::new(cusip)?;
829 self.identifiers = Some(BondIdentifiers::new().with_cusip(cusip));
830 Ok(self)
831 }
832
833 #[must_use]
835 pub fn cusip_unchecked(mut self, cusip: &str) -> Self {
836 self.identifiers = Some(BondIdentifiers::new().with_cusip(Cusip::new_unchecked(cusip)));
837 self
838 }
839
840 #[must_use]
842 pub fn isin_unchecked(mut self, isin: &str) -> Self {
843 self.identifiers = Some(BondIdentifiers::new().with_isin(Isin::new_unchecked(isin)));
844 self
845 }
846
847 #[must_use]
849 pub fn index(mut self, index: RateIndex) -> Self {
850 self.index = Some(index);
851 self
852 }
853
854 #[must_use]
856 pub fn sofr_convention(mut self, convention: SOFRConvention) -> Self {
857 self.sofr_convention = Some(convention);
858 self
859 }
860
861 #[must_use]
863 pub fn spread_bps(mut self, bps: i32) -> Self {
864 self.spread_bps = Some(Decimal::from(bps));
865 self
866 }
867
868 #[must_use]
870 pub fn spread_decimal(mut self, spread: Decimal) -> Self {
871 self.spread_bps = Some(spread * Decimal::from(10000));
872 self
873 }
874
875 #[must_use]
877 pub fn maturity(mut self, date: Date) -> Self {
878 self.maturity = Some(date);
879 self
880 }
881
882 #[must_use]
884 pub fn issue_date(mut self, date: Date) -> Self {
885 self.issue_date = Some(date);
886 self
887 }
888
889 #[must_use]
891 pub fn frequency(mut self, freq: Frequency) -> Self {
892 self.frequency = Some(freq);
893 self
894 }
895
896 #[must_use]
898 pub fn day_count(mut self, dc: DayCountConvention) -> Self {
899 self.day_count = Some(dc);
900 self
901 }
902
903 #[must_use]
905 pub fn reset_lag(mut self, days: i32) -> Self {
906 self.reset_lag = Some(days);
907 self
908 }
909
910 #[must_use]
912 pub fn payment_delay(mut self, days: u32) -> Self {
913 self.payment_delay = Some(days);
914 self
915 }
916
917 #[must_use]
919 pub fn cap(mut self, rate: Decimal) -> Self {
920 self.cap = Some(rate);
921 self
922 }
923
924 #[must_use]
926 pub fn floor(mut self, rate: Decimal) -> Self {
927 self.floor = Some(rate);
928 self
929 }
930
931 #[must_use]
933 pub fn settlement_days(mut self, days: u32) -> Self {
934 self.settlement_days = Some(days);
935 self
936 }
937
938 #[must_use]
940 pub fn calendar(mut self, calendar: CalendarId) -> Self {
941 self.calendar = Some(calendar);
942 self
943 }
944
945 #[must_use]
947 pub fn currency(mut self, currency: Currency) -> Self {
948 self.currency = Some(currency);
949 self
950 }
951
952 #[must_use]
954 pub fn face_value(mut self, value: Decimal) -> Self {
955 self.face_value = Some(value);
956 self
957 }
958
959 #[must_use]
969 pub fn us_treasury_frn(mut self) -> Self {
970 self.index = Some(RateIndex::Sofr);
971 self.sofr_convention = Some(SOFRConvention::SimpleAverage { lookback_days: 2 });
972 self.day_count = Some(DayCountConvention::Act360);
973 self.frequency = Some(Frequency::Quarterly);
974 self.settlement_days = Some(1);
975 self.calendar = Some(CalendarId::us_government());
976 self.currency = Some(Currency::USD);
977 self.reset_lag = Some(-2);
978 self
979 }
980
981 #[must_use]
989 pub fn corporate_sofr(mut self) -> Self {
990 self.index = Some(RateIndex::Sofr);
991 self.sofr_convention = Some(SOFRConvention::arrc_standard());
992 self.day_count = Some(DayCountConvention::Act360);
993 self.frequency = Some(Frequency::Quarterly);
994 self.settlement_days = Some(2);
995 self.calendar = Some(CalendarId::us_government());
996 self.currency = Some(Currency::USD);
997 self.reset_lag = Some(-2);
998 self
999 }
1000
1001 #[must_use]
1008 pub fn uk_sonia_frn(mut self) -> Self {
1009 self.index = Some(RateIndex::Sonia);
1010 self.day_count = Some(DayCountConvention::Act365Fixed);
1011 self.frequency = Some(Frequency::Quarterly);
1012 self.settlement_days = Some(1);
1013 self.calendar = Some(CalendarId::uk());
1014 self.currency = Some(Currency::GBP);
1015 self.reset_lag = Some(-5);
1016 self
1017 }
1018
1019 #[must_use]
1026 pub fn estr_frn(mut self) -> Self {
1027 self.index = Some(RateIndex::Estr);
1028 self.day_count = Some(DayCountConvention::Act360);
1029 self.frequency = Some(Frequency::Quarterly);
1030 self.settlement_days = Some(2);
1031 self.calendar = Some(CalendarId::target2());
1032 self.currency = Some(Currency::EUR);
1033 self.reset_lag = Some(-2);
1034 self
1035 }
1036
1037 #[must_use]
1043 pub fn euribor_frn(mut self, tenor: crate::types::Tenor) -> Self {
1044 use crate::types::Tenor;
1045 let index = match tenor {
1046 Tenor::M1 => RateIndex::Euribor1M,
1047 Tenor::M3 => RateIndex::Euribor3M,
1048 Tenor::M6 => RateIndex::Euribor6M,
1049 Tenor::M12 | Tenor::Y1 => RateIndex::Euribor12M,
1050 _ => RateIndex::Euribor3M, };
1052 let frequency = match tenor {
1053 Tenor::M1 => Frequency::Monthly,
1054 Tenor::M3 => Frequency::Quarterly,
1055 Tenor::M6 => Frequency::SemiAnnual,
1056 Tenor::M12 | Tenor::Y1 => Frequency::Annual,
1057 _ => Frequency::Quarterly,
1058 };
1059 self.index = Some(index);
1060 self.day_count = Some(DayCountConvention::Act360);
1061 self.frequency = Some(frequency);
1062 self.settlement_days = Some(2);
1063 self.calendar = Some(CalendarId::target2());
1064 self.currency = Some(Currency::EUR);
1065 self.reset_lag = Some(-2);
1066 self
1067 }
1068
1069 pub fn build(self) -> BondResult<FloatingRateNote> {
1071 let identifiers = self.identifiers.unwrap_or_default();
1072 let index = self.index.ok_or(BondError::MissingField {
1073 field: "index".to_string(),
1074 })?;
1075 let maturity = self.maturity.ok_or(BondError::MissingField {
1076 field: "maturity".to_string(),
1077 })?;
1078 let issue_date = self.issue_date.ok_or(BondError::MissingField {
1079 field: "issue_date".to_string(),
1080 })?;
1081
1082 if maturity <= issue_date {
1083 return Err(BondError::InvalidSpec {
1084 reason: "Maturity must be after issue date".to_string(),
1085 });
1086 }
1087
1088 Ok(FloatingRateNote {
1089 identifiers,
1090 index,
1091 sofr_convention: self.sofr_convention,
1092 spread_bps: self.spread_bps.unwrap_or(Decimal::ZERO),
1093 maturity,
1094 issue_date,
1095 frequency: self.frequency.unwrap_or(Frequency::Quarterly),
1096 day_count: self.day_count.unwrap_or(DayCountConvention::Act360),
1097 reset_lag: self.reset_lag.unwrap_or(-2),
1098 payment_delay: self.payment_delay.unwrap_or(0),
1099 cap: self.cap,
1100 floor: self.floor,
1101 settlement_days: self.settlement_days.unwrap_or(2),
1102 calendar: self.calendar.unwrap_or_else(CalendarId::weekend_only),
1103 currency: self.currency.unwrap_or(Currency::USD),
1104 face_value: self.face_value.unwrap_or(Decimal::ONE_HUNDRED),
1105 current_rate: None,
1106 })
1107 }
1108}
1109
1110fn day_count_to_string(dc: &DayCountConvention) -> &'static str {
1114 match dc {
1115 DayCountConvention::Act360 => "Act360",
1116 DayCountConvention::Act365Fixed => "Act365Fixed",
1117 DayCountConvention::Act365Leap => "Act365Leap",
1118 DayCountConvention::ActActIsda => "ActActIsda",
1119 DayCountConvention::ActActIcma => "ActActIcma",
1120 DayCountConvention::ActActAfb => "ActActAfb",
1121 DayCountConvention::Thirty360US => "Thirty360US",
1122 DayCountConvention::Thirty360E => "Thirty360E",
1123 DayCountConvention::Thirty360EIsda => "Thirty360EIsda",
1124 DayCountConvention::Thirty360German => "Thirty360German",
1125 }
1126}
1127
1128fn string_to_day_count(s: &str) -> DayCountConvention {
1129 match s {
1130 "Act360" => DayCountConvention::Act360,
1131 "Act365Fixed" => DayCountConvention::Act365Fixed,
1132 "Act365Leap" => DayCountConvention::Act365Leap,
1133 "ActActIsda" => DayCountConvention::ActActIsda,
1134 "ActActIcma" => DayCountConvention::ActActIcma,
1135 "ActActAfb" => DayCountConvention::ActActAfb,
1136 "Thirty360US" => DayCountConvention::Thirty360US,
1137 "Thirty360E" => DayCountConvention::Thirty360E,
1138 "Thirty360EIsda" => DayCountConvention::Thirty360EIsda,
1139 "Thirty360German" => DayCountConvention::Thirty360German,
1140 _ => DayCountConvention::Act360, }
1142}
1143
1144impl Serialize for FloatingRateNote {
1145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1146 where
1147 S: serde::Serializer,
1148 {
1149 use serde::ser::SerializeStruct;
1150 let mut state = serializer.serialize_struct("FloatingRateNote", 16)?;
1151 state.serialize_field("identifiers", &self.identifiers)?;
1152 state.serialize_field("index", &self.index)?;
1153 state.serialize_field("sofr_convention", &self.sofr_convention)?;
1154 state.serialize_field("spread_bps", &self.spread_bps)?;
1155 state.serialize_field("maturity", &self.maturity)?;
1156 state.serialize_field("issue_date", &self.issue_date)?;
1157 state.serialize_field("frequency", &self.frequency)?;
1158 state.serialize_field("day_count", &day_count_to_string(&self.day_count))?;
1159 state.serialize_field("reset_lag", &self.reset_lag)?;
1160 state.serialize_field("payment_delay", &self.payment_delay)?;
1161 state.serialize_field("cap", &self.cap)?;
1162 state.serialize_field("floor", &self.floor)?;
1163 state.serialize_field("settlement_days", &self.settlement_days)?;
1164 state.serialize_field("calendar", &self.calendar)?;
1165 state.serialize_field("currency", &self.currency)?;
1166 state.serialize_field("face_value", &self.face_value)?;
1167 state.end()
1168 }
1169}
1170
1171impl<'de> Deserialize<'de> for FloatingRateNote {
1172 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1173 where
1174 D: serde::Deserializer<'de>,
1175 {
1176 #[derive(Deserialize)]
1177 struct FloatingRateNoteData {
1178 identifiers: BondIdentifiers,
1179 index: RateIndex,
1180 sofr_convention: Option<SOFRConvention>,
1181 spread_bps: Decimal,
1182 maturity: Date,
1183 issue_date: Date,
1184 frequency: Frequency,
1185 day_count: String,
1186 reset_lag: i32,
1187 payment_delay: u32,
1188 cap: Option<Decimal>,
1189 floor: Option<Decimal>,
1190 settlement_days: u32,
1191 calendar: CalendarId,
1192 currency: Currency,
1193 face_value: Decimal,
1194 }
1195
1196 let data = FloatingRateNoteData::deserialize(deserializer)?;
1197 Ok(FloatingRateNote {
1198 identifiers: data.identifiers,
1199 index: data.index,
1200 sofr_convention: data.sofr_convention,
1201 spread_bps: data.spread_bps,
1202 maturity: data.maturity,
1203 issue_date: data.issue_date,
1204 frequency: data.frequency,
1205 day_count: string_to_day_count(&data.day_count),
1206 reset_lag: data.reset_lag,
1207 payment_delay: data.payment_delay,
1208 cap: data.cap,
1209 floor: data.floor,
1210 settlement_days: data.settlement_days,
1211 calendar: data.calendar,
1212 currency: data.currency,
1213 face_value: data.face_value,
1214 current_rate: None,
1215 })
1216 }
1217}
1218
1219#[cfg(test)]
1220mod tests {
1221 use super::*;
1222 use rust_decimal_macros::dec;
1223
1224 fn date(y: i32, m: u32, d: u32) -> Date {
1225 Date::from_ymd(y, m, d).unwrap()
1226 }
1227
1228 #[test]
1229 fn test_frn_builder() {
1230 let frn = FloatingRateNote::builder()
1231 .cusip_unchecked("912828ZQ7")
1232 .index(RateIndex::Sofr)
1233 .sofr_convention(SOFRConvention::arrc_standard())
1234 .spread_bps(50)
1235 .maturity(date(2026, 7, 31))
1236 .issue_date(date(2024, 7, 31))
1237 .build()
1238 .unwrap();
1239
1240 assert_eq!(frn.spread_bps(), dec!(50));
1241 assert_eq!(frn.spread_decimal(), dec!(0.0050));
1242 assert!(frn.sofr_convention().is_some());
1243 }
1244
1245 #[test]
1246 fn test_us_treasury_frn() {
1247 let frn = FloatingRateNote::builder()
1248 .cusip_unchecked("912828ZQ7")
1249 .spread_bps(15)
1250 .maturity(date(2026, 7, 31))
1251 .issue_date(date(2024, 7, 31))
1252 .us_treasury_frn()
1253 .build()
1254 .unwrap();
1255
1256 assert_eq!(*frn.index(), RateIndex::Sofr);
1257 assert_eq!(frn.day_count(), DayCountConvention::Act360);
1258 assert_eq!(frn.frequency(), Frequency::Quarterly);
1259 assert_eq!(frn.settlement_days(), 1);
1260 }
1261
1262 #[test]
1263 fn test_corporate_sofr_frn() {
1264 let frn = FloatingRateNote::builder()
1265 .cusip_unchecked("TEST12345")
1266 .spread_bps(150)
1267 .maturity(date(2027, 6, 15))
1268 .issue_date(date(2024, 6, 15))
1269 .corporate_sofr()
1270 .build()
1271 .unwrap();
1272
1273 assert_eq!(*frn.index(), RateIndex::Sofr);
1274 assert!(frn.sofr_convention().unwrap().is_in_arrears());
1275 assert_eq!(frn.settlement_days(), 2);
1276 }
1277
1278 #[test]
1279 fn test_effective_rate_with_floor() {
1280 let frn = FloatingRateNote::builder()
1281 .cusip_unchecked("TEST12345")
1282 .index(RateIndex::Sofr)
1283 .spread_bps(50) .floor(dec!(0.01)) .maturity(date(2026, 6, 15))
1286 .issue_date(date(2024, 6, 15))
1287 .build()
1288 .unwrap();
1289
1290 let effective = frn.effective_rate(dec!(0.003));
1292 assert_eq!(effective, dec!(0.01)); let effective = frn.effective_rate(dec!(0.006));
1296 assert_eq!(effective, dec!(0.011)); }
1298
1299 #[test]
1300 fn test_effective_rate_with_cap() {
1301 let frn = FloatingRateNote::builder()
1302 .cusip_unchecked("TEST12345")
1303 .index(RateIndex::Sofr)
1304 .spread_bps(50)
1305 .cap(dec!(0.08)) .maturity(date(2026, 6, 15))
1307 .issue_date(date(2024, 6, 15))
1308 .build()
1309 .unwrap();
1310
1311 let effective = frn.effective_rate(dec!(0.08));
1313 assert_eq!(effective, dec!(0.08)); let effective = frn.effective_rate(dec!(0.05));
1317 assert_eq!(effective, dec!(0.055)); }
1319
1320 #[test]
1321 fn test_effective_rate_collar() {
1322 let frn = FloatingRateNote::builder()
1323 .cusip_unchecked("TEST12345")
1324 .index(RateIndex::Sofr)
1325 .spread_bps(50)
1326 .floor(dec!(0.02)) .cap(dec!(0.06)) .maturity(date(2026, 6, 15))
1329 .issue_date(date(2024, 6, 15))
1330 .build()
1331 .unwrap();
1332
1333 assert_eq!(frn.effective_rate(dec!(0.01)), dec!(0.02));
1335
1336 assert_eq!(frn.effective_rate(dec!(0.04)), dec!(0.045));
1338
1339 assert_eq!(frn.effective_rate(dec!(0.06)), dec!(0.06));
1341 }
1342
1343 #[test]
1344 fn test_period_coupon() {
1345 let frn = FloatingRateNote::builder()
1346 .cusip_unchecked("TEST12345")
1347 .index(RateIndex::Sofr)
1348 .spread_bps(50)
1349 .face_value(dec!(100))
1350 .maturity(date(2026, 6, 15))
1351 .issue_date(date(2024, 6, 15))
1352 .day_count(DayCountConvention::Act360)
1353 .build()
1354 .unwrap();
1355
1356 let coupon = frn.period_coupon(date(2025, 1, 15), date(2025, 4, 15), dec!(0.05));
1358
1359 assert!(coupon > dec!(1.37) && coupon < dec!(1.38));
1361 }
1362
1363 #[test]
1364 fn test_bond_type_classification() {
1365 let frn = FloatingRateNote::builder()
1367 .cusip_unchecked("TEST12345")
1368 .index(RateIndex::Sofr)
1369 .maturity(date(2026, 6, 15))
1370 .issue_date(date(2024, 6, 15))
1371 .build()
1372 .unwrap();
1373 assert_eq!(frn.bond_type(), BondType::FloatingRateNote);
1374
1375 let frn = FloatingRateNote::builder()
1377 .cusip_unchecked("TEST12345")
1378 .index(RateIndex::Sofr)
1379 .cap(dec!(0.08))
1380 .maturity(date(2026, 6, 15))
1381 .issue_date(date(2024, 6, 15))
1382 .build()
1383 .unwrap();
1384 assert_eq!(frn.bond_type(), BondType::CappedFRN);
1385
1386 let frn = FloatingRateNote::builder()
1388 .cusip_unchecked("TEST12345")
1389 .index(RateIndex::Sofr)
1390 .floor(dec!(0.02))
1391 .maturity(date(2026, 6, 15))
1392 .issue_date(date(2024, 6, 15))
1393 .build()
1394 .unwrap();
1395 assert_eq!(frn.bond_type(), BondType::FlooredFRN);
1396
1397 let frn = FloatingRateNote::builder()
1399 .cusip_unchecked("TEST12345")
1400 .index(RateIndex::Sofr)
1401 .cap(dec!(0.08))
1402 .floor(dec!(0.02))
1403 .maturity(date(2026, 6, 15))
1404 .issue_date(date(2024, 6, 15))
1405 .build()
1406 .unwrap();
1407 assert_eq!(frn.bond_type(), BondType::CollaredFRN);
1408 }
1409
1410 #[test]
1411 fn test_accrued_interest() {
1412 let mut frn = FloatingRateNote::builder()
1413 .cusip_unchecked("TEST12345")
1414 .index(RateIndex::Sofr)
1415 .spread_bps(50)
1416 .face_value(dec!(100))
1417 .frequency(Frequency::Quarterly)
1418 .day_count(DayCountConvention::Act360)
1419 .maturity(date(2026, 6, 15))
1420 .issue_date(date(2024, 6, 15))
1421 .build()
1422 .unwrap();
1423
1424 frn.set_current_rate(dec!(0.05)); let settlement = date(2025, 2, 15);
1429 let accrued = frn.accrued_interest(settlement);
1430
1431 assert!(accrued > Decimal::ZERO);
1433 }
1434
1435 #[test]
1436 fn test_cash_flows() {
1437 let mut frn = FloatingRateNote::builder()
1438 .cusip_unchecked("TEST12345")
1439 .index(RateIndex::Sofr)
1440 .spread_bps(50)
1441 .frequency(Frequency::Quarterly)
1442 .maturity(date(2025, 6, 15))
1443 .issue_date(date(2024, 6, 15))
1444 .build()
1445 .unwrap();
1446
1447 frn.set_current_rate(dec!(0.05));
1448
1449 let flows = frn.cash_flows(date(2024, 6, 15));
1450
1451 assert_eq!(flows.len(), 4);
1453
1454 assert!(flows.last().unwrap().is_principal());
1456 }
1457
1458 #[test]
1459 fn test_sonia_frn() {
1460 let frn = FloatingRateNote::builder()
1461 .cusip_unchecked("GBTEST001")
1462 .spread_bps(25)
1463 .maturity(date(2026, 9, 30))
1464 .issue_date(date(2024, 9, 30))
1465 .uk_sonia_frn()
1466 .build()
1467 .unwrap();
1468
1469 assert_eq!(*frn.index(), RateIndex::Sonia);
1470 assert_eq!(frn.day_count(), DayCountConvention::Act365Fixed);
1471 assert_eq!(frn.currency(), Currency::GBP);
1472 }
1473
1474 #[test]
1475 fn test_estr_frn() {
1476 let frn = FloatingRateNote::builder()
1477 .cusip_unchecked("EUTEST001")
1478 .spread_bps(30)
1479 .maturity(date(2026, 12, 15))
1480 .issue_date(date(2024, 12, 15))
1481 .estr_frn()
1482 .build()
1483 .unwrap();
1484
1485 assert_eq!(*frn.index(), RateIndex::Estr);
1486 assert_eq!(frn.day_count(), DayCountConvention::Act360);
1487 assert_eq!(frn.currency(), Currency::EUR);
1488 }
1489
1490 #[test]
1491 fn test_sofr_convention_display() {
1492 let conv = SOFRConvention::arrc_standard();
1493 let display = format!("{}", conv);
1494 assert!(display.contains("5D lookback"));
1495 assert!(display.contains("observation shift"));
1496 }
1497
1498 #[test]
1499 fn test_missing_required_fields() {
1500 let result = FloatingRateNote::builder()
1502 .cusip_unchecked("TEST12345")
1503 .maturity(date(2026, 6, 15))
1504 .issue_date(date(2024, 6, 15))
1505 .build();
1506 assert!(result.is_err());
1507
1508 let result = FloatingRateNote::builder()
1510 .cusip_unchecked("TEST12345")
1511 .index(RateIndex::Sofr)
1512 .issue_date(date(2024, 6, 15))
1513 .build();
1514 assert!(result.is_err());
1515 }
1516
1517 #[test]
1518 fn test_invalid_dates() {
1519 let result = FloatingRateNote::builder()
1520 .cusip_unchecked("TEST12345")
1521 .index(RateIndex::Sofr)
1522 .maturity(date(2024, 6, 15))
1523 .issue_date(date(2026, 6, 15)) .build();
1525 assert!(result.is_err());
1526 }
1527}