1use crate::errors::BootstrapError;
55use crate::types::{Date, Daycount, Frequency};
56
57use super::{CurveSnapshot, InstrumentLike, SwapSchedule};
58
59#[derive(Debug, Clone, PartialEq)]
93pub struct SwapFixedFloat {
94 pub start: Date,
96 pub maturity: Date,
98 pub rate: f64,
100 pub fixed_freq: Frequency,
102 pub fixed_daycount: Daycount,
104 pub float_freq: Frequency,
106 pub float_daycount: Daycount,
109 pub fixed_schedule: SwapSchedule,
111 pub float_schedule: SwapSchedule,
113}
114
115impl SwapFixedFloat {
116 pub fn new(
169 start: Date,
170 maturity: Date,
171 rate: f64,
172 fixed_freq: Frequency,
173 fixed_daycount: Daycount,
174 float_freq: Frequency,
175 float_daycount: Daycount,
176 ) -> Result<Self, BootstrapError> {
177 if !rate.is_finite() {
178 return Err(BootstrapError::InvalidInstrument {
179 at_index: 0,
180 reason: "swap rate must be finite",
181 });
182 }
183 if start.serial() >= maturity.serial() {
184 return Err(BootstrapError::InvalidInstrument {
185 at_index: 0,
186 reason: "swap start must precede maturity",
187 });
188 }
189 let fixed_schedule = SwapSchedule::from_regular(start, maturity, fixed_freq)?;
190 let float_schedule = SwapSchedule::from_regular(start, maturity, float_freq)?;
191 Ok(Self {
192 start,
193 maturity,
194 rate,
195 fixed_freq,
196 fixed_daycount,
197 float_freq,
198 float_daycount,
199 fixed_schedule,
200 float_schedule,
201 })
202 }
203
204 #[allow(clippy::too_many_arguments)]
244 pub fn with_schedules(
245 start: Date,
246 maturity: Date,
247 rate: f64,
248 fixed_freq: Frequency,
249 fixed_daycount: Daycount,
250 float_freq: Frequency,
251 float_daycount: Daycount,
252 fixed_schedule: SwapSchedule,
253 float_schedule: SwapSchedule,
254 ) -> Result<Self, BootstrapError> {
255 if !rate.is_finite() {
256 return Err(BootstrapError::InvalidInstrument {
257 at_index: 0,
258 reason: "swap rate must be finite",
259 });
260 }
261 if start.serial() >= maturity.serial() {
262 return Err(BootstrapError::InvalidInstrument {
263 at_index: 0,
264 reason: "swap start must precede maturity",
265 });
266 }
267 if fixed_schedule.start() != start || fixed_schedule.maturity() != maturity {
268 return Err(BootstrapError::InvalidInstrument {
269 at_index: 0,
270 reason: "fixed schedule does not span [start, maturity]",
271 });
272 }
273 if float_schedule.start() != start || float_schedule.maturity() != maturity {
274 return Err(BootstrapError::InvalidInstrument {
275 at_index: 0,
276 reason: "float schedule does not span [start, maturity]",
277 });
278 }
279 Ok(Self {
280 start,
281 maturity,
282 rate,
283 fixed_freq,
284 fixed_daycount,
285 float_freq,
286 float_daycount,
287 fixed_schedule,
288 float_schedule,
289 })
290 }
291
292 pub(crate) fn fixed_leg_pv(
333 &self,
334 _reference_date: Date,
335 curve: &CurveSnapshot<'_>,
336 ) -> Result<f64, BootstrapError> {
337 let mut annuity = 0.0_f64;
338 for i in 0..self.fixed_schedule.len() {
339 let period_start = self.fixed_schedule.period_start(i);
340 let payment = self.fixed_schedule.period_end(i);
341 let tau_i = self.fixed_daycount.year_fraction(period_start, payment)?;
342 let t_payment = curve
343 .daycount
344 .year_fraction(curve.reference_date, payment)?;
345 let d_payment =
346 curve
347 .discount_at(t_payment)
348 .ok_or(BootstrapError::InvalidInstrument {
349 at_index: 0,
350 reason: "curve snapshot is empty",
351 })?;
352 annuity += tau_i * d_payment;
353 }
354 Ok(self.rate * annuity)
355 }
356
357 pub(crate) fn float_leg_pv_single_curve(
397 &self,
398 _reference_date: Date,
399 curve: &CurveSnapshot<'_>,
400 ) -> Result<f64, BootstrapError> {
401 let t_start = curve
402 .daycount
403 .year_fraction(curve.reference_date, self.start)?;
404 let t_maturity = curve
405 .daycount
406 .year_fraction(curve.reference_date, self.maturity)?;
407 let d_start = curve
408 .discount_at(t_start)
409 .ok_or(BootstrapError::InvalidInstrument {
410 at_index: 0,
411 reason: "curve snapshot is empty",
412 })?;
413 let d_maturity =
414 curve
415 .discount_at(t_maturity)
416 .ok_or(BootstrapError::InvalidInstrument {
417 at_index: 0,
418 reason: "curve snapshot is empty",
419 })?;
420 Ok(d_start - d_maturity)
421 }
422}
423
424impl InstrumentLike for SwapFixedFloat {
425 #[inline]
426 fn pillar(&self) -> Date {
427 self.maturity
428 }
429
430 fn residual(
431 &self,
432 reference_date: Date,
433 curve: &CurveSnapshot<'_>,
434 ) -> Result<f64, BootstrapError> {
435 let pv_fixed = self.fixed_leg_pv(reference_date, curve)?;
436 let pv_float = self.float_leg_pv_single_curve(reference_date, curve)?;
437 Ok(pv_fixed - pv_float)
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::instruments::CurveSnapshot;
445
446 fn d(y: i32, m: u32, day: u32) -> Date {
447 Date::from_ymd(y, m, day).unwrap()
448 }
449
450 #[test]
453 fn new_accepts_valid_2y_sa_q_swap() {
454 let s = d(2024, 1, 2);
455 let m = d(2026, 1, 2);
456 let swap = SwapFixedFloat::new(
457 s,
458 m,
459 0.04,
460 Frequency::SemiAnnual,
461 Daycount::Act360,
462 Frequency::Quarterly,
463 Daycount::Act360,
464 )
465 .unwrap();
466 assert_eq!(swap.start, s);
467 assert_eq!(swap.maturity, m);
468 assert_eq!(swap.fixed_schedule.len(), 4);
469 assert_eq!(swap.float_schedule.len(), 8);
470 assert_eq!(swap.pillar(), m);
471 }
472
473 #[test]
474 fn new_accepts_negative_rate() {
475 let s = d(2024, 1, 2);
476 let m = d(2026, 1, 2);
477 let swap = SwapFixedFloat::new(
478 s,
479 m,
480 -0.005,
481 Frequency::SemiAnnual,
482 Daycount::Act360,
483 Frequency::Quarterly,
484 Daycount::Act360,
485 )
486 .unwrap();
487 assert!(swap.rate < 0.0);
488 }
489
490 #[test]
491 fn new_rejects_nan_rate() {
492 let s = d(2024, 1, 2);
493 let m = d(2026, 1, 2);
494 let err = SwapFixedFloat::new(
495 s,
496 m,
497 f64::NAN,
498 Frequency::SemiAnnual,
499 Daycount::Act360,
500 Frequency::Quarterly,
501 Daycount::Act360,
502 )
503 .unwrap_err();
504 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
505 }
506
507 #[test]
508 fn new_rejects_inf_rate() {
509 let s = d(2024, 1, 2);
510 let m = d(2026, 1, 2);
511 let err = SwapFixedFloat::new(
512 s,
513 m,
514 f64::INFINITY,
515 Frequency::SemiAnnual,
516 Daycount::Act360,
517 Frequency::Quarterly,
518 Daycount::Act360,
519 )
520 .unwrap_err();
521 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
522 }
523
524 #[test]
525 fn new_rejects_inverted_dates() {
526 let s = d(2024, 1, 2);
527 let m = d(2026, 1, 2);
528 let err = SwapFixedFloat::new(
529 m,
530 s,
531 0.04,
532 Frequency::SemiAnnual,
533 Daycount::Act360,
534 Frequency::Quarterly,
535 Daycount::Act360,
536 )
537 .unwrap_err();
538 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
539 }
540
541 #[test]
542 fn new_rejects_equal_dates() {
543 let s = d(2024, 1, 2);
544 let err = SwapFixedFloat::new(
545 s,
546 s,
547 0.04,
548 Frequency::SemiAnnual,
549 Daycount::Act360,
550 Frequency::Quarterly,
551 Daycount::Act360,
552 )
553 .unwrap_err();
554 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
555 }
556
557 #[test]
558 fn new_rejects_irregular_term() {
559 let s = d(2024, 1, 2);
561 let m = d(2025, 2, 2);
562 let err = SwapFixedFloat::new(
563 s,
564 m,
565 0.04,
566 Frequency::SemiAnnual,
567 Daycount::Act360,
568 Frequency::Quarterly,
569 Daycount::Act360,
570 )
571 .unwrap_err();
572 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
573 }
574
575 #[test]
576 fn with_schedules_validates_alignment() {
577 let s = d(2024, 1, 2);
578 let m = d(2026, 1, 2);
579 let fixed = SwapSchedule::from_regular(s, m, Frequency::SemiAnnual).unwrap();
580 let float = SwapSchedule::from_regular(s, m, Frequency::Quarterly).unwrap();
581 let ok = SwapFixedFloat::with_schedules(
582 s,
583 m,
584 0.04,
585 Frequency::SemiAnnual,
586 Daycount::Act360,
587 Frequency::Quarterly,
588 Daycount::Act360,
589 fixed.clone(),
590 float.clone(),
591 );
592 assert!(ok.is_ok());
593
594 let mid = d(2025, 1, 2);
596 let mismatched = SwapSchedule::from_regular(mid, m, Frequency::SemiAnnual).unwrap();
597 let err = SwapFixedFloat::with_schedules(
598 s,
599 m,
600 0.04,
601 Frequency::SemiAnnual,
602 Daycount::Act360,
603 Frequency::Quarterly,
604 Daycount::Act360,
605 mismatched,
606 float.clone(),
607 )
608 .unwrap_err();
609 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
610
611 let mismatched_float = SwapSchedule::from_regular(mid, m, Frequency::Quarterly).unwrap();
613 let err = SwapFixedFloat::with_schedules(
614 s,
615 m,
616 0.04,
617 Frequency::SemiAnnual,
618 Daycount::Act360,
619 Frequency::Quarterly,
620 Daycount::Act360,
621 fixed,
622 mismatched_float,
623 )
624 .unwrap_err();
625 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
626 }
627
628 #[test]
629 fn with_schedules_rejects_nan_rate() {
630 let s = d(2024, 1, 2);
631 let m = d(2026, 1, 2);
632 let fixed = SwapSchedule::from_regular(s, m, Frequency::SemiAnnual).unwrap();
633 let float = SwapSchedule::from_regular(s, m, Frequency::Quarterly).unwrap();
634 let err = SwapFixedFloat::with_schedules(
635 s,
636 m,
637 f64::NAN,
638 Frequency::SemiAnnual,
639 Daycount::Act360,
640 Frequency::Quarterly,
641 Daycount::Act360,
642 fixed,
643 float,
644 )
645 .unwrap_err();
646 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
647 }
648
649 #[test]
650 fn with_schedules_rejects_inverted_dates() {
651 let s = d(2024, 1, 2);
652 let m = d(2026, 1, 2);
653 let fixed = SwapSchedule::from_regular(s, m, Frequency::SemiAnnual).unwrap();
654 let float = SwapSchedule::from_regular(s, m, Frequency::Quarterly).unwrap();
655 let err = SwapFixedFloat::with_schedules(
656 m,
657 s,
658 0.04,
659 Frequency::SemiAnnual,
660 Daycount::Act360,
661 Frequency::Quarterly,
662 Daycount::Act360,
663 fixed,
664 float,
665 )
666 .unwrap_err();
667 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
668 }
669
670 fn flat_curve(reference_date: Date, daycount: Daycount, r: f64) -> (Vec<f64>, Vec<f64>) {
675 let mut times = Vec::new();
676 let mut discounts = Vec::new();
677 for i in 0..=120 {
678 let date = Date::from_serial(reference_date.serial() + i * 91);
679 let t = daycount.year_fraction(reference_date, date).unwrap();
680 times.push(t);
681 discounts.push((-r * t).exp());
682 }
683 (times, discounts)
684 }
685
686 fn par_rate_against_flat(
690 swap_start: Date,
691 swap_maturity: Date,
692 fixed_freq: Frequency,
693 fixed_daycount: Daycount,
694 reference_date: Date,
695 curve_daycount: Daycount,
696 r_c: f64,
697 ) -> f64 {
698 let schedule = SwapSchedule::from_regular(swap_start, swap_maturity, fixed_freq).unwrap();
699 let mut annuity = 0.0_f64;
700 for i in 0..schedule.len() {
701 let p_start = schedule.period_start(i);
702 let p_end = schedule.period_end(i);
703 let tau_i = fixed_daycount.year_fraction(p_start, p_end).unwrap();
704 let t = curve_daycount.year_fraction(reference_date, p_end).unwrap();
705 annuity += tau_i * (-r_c * t).exp();
706 }
707 let t_start = curve_daycount
708 .year_fraction(reference_date, swap_start)
709 .unwrap();
710 let t_mat = curve_daycount
711 .year_fraction(reference_date, swap_maturity)
712 .unwrap();
713 ((-r_c * t_start).exp() - (-r_c * t_mat).exp()) / annuity
714 }
715
716 #[test]
717 fn residual_is_zero_on_flat_curve_with_closed_form_par_rate() {
718 let reference = d(2024, 1, 2);
721 let start = reference;
722 let maturity = d(2026, 1, 2);
723 let dc_curve = Daycount::Act360;
724 let r_c = 0.04_f64;
725 let (times, discounts) = flat_curve(reference, dc_curve, r_c);
726
727 let r_par = par_rate_against_flat(
728 start,
729 maturity,
730 Frequency::SemiAnnual,
731 Daycount::Act360,
732 reference,
733 dc_curve,
734 r_c,
735 );
736 assert!(r_par > 0.03 && r_par < 0.05, "unexpected r_par = {r_par}");
739
740 let swap = SwapFixedFloat::new(
741 start,
742 maturity,
743 r_par,
744 Frequency::SemiAnnual,
745 Daycount::Act360,
746 Frequency::Quarterly,
747 Daycount::Act360,
748 )
749 .unwrap();
750 let snapshot = CurveSnapshot {
751 reference_date: reference,
752 daycount: dc_curve,
753 times: ×,
754 discounts: &discounts,
755 };
756 let residual = swap.residual(reference, &snapshot).unwrap();
757 assert!(
758 residual.abs() < 1e-10,
759 "residual at par should be < 1e-10, got {residual}",
760 );
761 }
762
763 #[test]
764 fn residual_sign_responds_to_rate_perturbation() {
765 let reference = d(2024, 1, 2);
767 let start = reference;
768 let maturity = d(2026, 1, 2);
769 let dc_curve = Daycount::Act360;
770 let r_c = 0.04_f64;
771 let (times, discounts) = flat_curve(reference, dc_curve, r_c);
772
773 let r_par = par_rate_against_flat(
774 start,
775 maturity,
776 Frequency::SemiAnnual,
777 Daycount::Act360,
778 reference,
779 dc_curve,
780 r_c,
781 );
782 let snapshot = CurveSnapshot {
783 reference_date: reference,
784 daycount: dc_curve,
785 times: ×,
786 discounts: &discounts,
787 };
788
789 let high = SwapFixedFloat::new(
790 start,
791 maturity,
792 r_par + 0.005,
793 Frequency::SemiAnnual,
794 Daycount::Act360,
795 Frequency::Quarterly,
796 Daycount::Act360,
797 )
798 .unwrap();
799 let low = SwapFixedFloat::new(
800 start,
801 maturity,
802 r_par - 0.005,
803 Frequency::SemiAnnual,
804 Daycount::Act360,
805 Frequency::Quarterly,
806 Daycount::Act360,
807 )
808 .unwrap();
809 let res_high = high.residual(reference, &snapshot).unwrap();
810 let res_low = low.residual(reference, &snapshot).unwrap();
811 assert!(
812 res_high > 1e-6,
813 "expected positive residual, got {res_high}"
814 );
815 assert!(res_low < -1e-6, "expected negative residual, got {res_low}");
816 }
817
818 #[test]
819 fn fixed_leg_pv_matches_manual_sum() {
820 let reference = d(2024, 1, 2);
822 let start = reference;
823 let maturity = d(2026, 1, 2);
824 let dc_curve = Daycount::Act360;
825 let r_c = 0.04_f64;
826 let (times, discounts) = flat_curve(reference, dc_curve, r_c);
827
828 let swap = SwapFixedFloat::new(
829 start,
830 maturity,
831 0.04,
832 Frequency::SemiAnnual,
833 Daycount::Act360,
834 Frequency::Quarterly,
835 Daycount::Act360,
836 )
837 .unwrap();
838 let snapshot = CurveSnapshot {
839 reference_date: reference,
840 daycount: dc_curve,
841 times: ×,
842 discounts: &discounts,
843 };
844
845 let mut expected = 0.0_f64;
847 for i in 0..swap.fixed_schedule.len() {
848 let p_start = swap.fixed_schedule.period_start(i);
849 let p_end = swap.fixed_schedule.period_end(i);
850 let tau = Daycount::Act360.year_fraction(p_start, p_end).unwrap();
851 let t = dc_curve.year_fraction(reference, p_end).unwrap();
852 expected += tau * (-r_c * t).exp();
853 }
854 expected *= 0.04;
855 let got = swap.fixed_leg_pv(reference, &snapshot).unwrap();
856 assert!(
857 (got - expected).abs() < 1e-12,
858 "fixed_leg_pv mismatch: got {got}, expected {expected}",
859 );
860 }
861
862 #[test]
863 fn float_leg_pv_telescopes_to_two_discounts() {
864 let reference = d(2024, 1, 2);
865 let start = reference;
866 let maturity = d(2026, 1, 2);
867 let dc_curve = Daycount::Act360;
868 let r_c = 0.04_f64;
869 let (times, discounts) = flat_curve(reference, dc_curve, r_c);
870
871 let swap = SwapFixedFloat::new(
872 start,
873 maturity,
874 0.04,
875 Frequency::SemiAnnual,
876 Daycount::Act360,
877 Frequency::Quarterly,
878 Daycount::Act360,
879 )
880 .unwrap();
881 let snapshot = CurveSnapshot {
882 reference_date: reference,
883 daycount: dc_curve,
884 times: ×,
885 discounts: &discounts,
886 };
887
888 let t_start = dc_curve.year_fraction(reference, start).unwrap();
889 let t_mat = dc_curve.year_fraction(reference, maturity).unwrap();
890 let expected = (-r_c * t_start).exp() - (-r_c * t_mat).exp();
891 let got = swap
892 .float_leg_pv_single_curve(reference, &snapshot)
893 .unwrap();
894 assert!((got - expected).abs() < 1e-14);
895 }
896
897 #[test]
898 fn fixed_leg_pv_errors_on_empty_snapshot() {
899 let reference = d(2024, 1, 2);
900 let swap = SwapFixedFloat::new(
901 reference,
902 d(2026, 1, 2),
903 0.04,
904 Frequency::SemiAnnual,
905 Daycount::Act360,
906 Frequency::Quarterly,
907 Daycount::Act360,
908 )
909 .unwrap();
910 let snapshot = CurveSnapshot {
911 reference_date: reference,
912 daycount: Daycount::Act360,
913 times: &[],
914 discounts: &[],
915 };
916 let err = swap.fixed_leg_pv(reference, &snapshot).unwrap_err();
917 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
918 }
919
920 #[test]
921 fn float_leg_pv_errors_on_empty_snapshot() {
922 let reference = d(2024, 1, 2);
923 let swap = SwapFixedFloat::new(
924 reference,
925 d(2026, 1, 2),
926 0.04,
927 Frequency::SemiAnnual,
928 Daycount::Act360,
929 Frequency::Quarterly,
930 Daycount::Act360,
931 )
932 .unwrap();
933 let snapshot = CurveSnapshot {
934 reference_date: reference,
935 daycount: Daycount::Act360,
936 times: &[],
937 discounts: &[],
938 };
939 let err = swap
940 .float_leg_pv_single_curve(reference, &snapshot)
941 .unwrap_err();
942 assert!(matches!(err, BootstrapError::InvalidInstrument { .. }));
943 }
944
945 #[test]
946 fn pillar_is_maturity() {
947 let s = d(2024, 1, 2);
948 let m = d(2029, 1, 2);
949 let swap = SwapFixedFloat::new(
950 s,
951 m,
952 0.04,
953 Frequency::SemiAnnual,
954 Daycount::Act360,
955 Frequency::Quarterly,
956 Daycount::Act360,
957 )
958 .unwrap();
959 assert_eq!(swap.pillar(), m);
960 }
961
962 #[test]
963 fn par_rate_invariant_under_mixed_daycounts() {
964 let reference = d(2024, 1, 2);
967 let start = reference;
968 let maturity = d(2026, 1, 2);
969 let dc_curve = Daycount::Act360;
970 let r_c = 0.04_f64;
971 let (times, discounts) = flat_curve(reference, dc_curve, r_c);
972
973 let r_par = par_rate_against_flat(
974 start,
975 maturity,
976 Frequency::SemiAnnual,
977 Daycount::Thirty360BondBasis,
978 reference,
979 dc_curve,
980 r_c,
981 );
982 let swap = SwapFixedFloat::new(
983 start,
984 maturity,
985 r_par,
986 Frequency::SemiAnnual,
987 Daycount::Thirty360BondBasis,
988 Frequency::Quarterly,
989 Daycount::Act360,
990 )
991 .unwrap();
992 let snapshot = CurveSnapshot {
993 reference_date: reference,
994 daycount: dc_curve,
995 times: ×,
996 discounts: &discounts,
997 };
998 let residual = swap.residual(reference, &snapshot).unwrap();
999 assert!(residual.abs() < 1e-10);
1000 }
1001
1002 #[test]
1003 fn five_year_swap_residual_zero_at_par() {
1004 let reference = d(2024, 1, 2);
1005 let start = reference;
1006 let maturity = d(2029, 1, 2);
1007 let dc_curve = Daycount::Act360;
1008 let r_c = 0.035_f64;
1009 let (times, discounts) = flat_curve(reference, dc_curve, r_c);
1010
1011 let r_par = par_rate_against_flat(
1012 start,
1013 maturity,
1014 Frequency::SemiAnnual,
1015 Daycount::Act360,
1016 reference,
1017 dc_curve,
1018 r_c,
1019 );
1020 let swap = SwapFixedFloat::new(
1021 start,
1022 maturity,
1023 r_par,
1024 Frequency::SemiAnnual,
1025 Daycount::Act360,
1026 Frequency::Quarterly,
1027 Daycount::Act360,
1028 )
1029 .unwrap();
1030 let snapshot = CurveSnapshot {
1031 reference_date: reference,
1032 daycount: dc_curve,
1033 times: ×,
1034 discounts: &discounts,
1035 };
1036 let residual = swap.residual(reference, &snapshot).unwrap();
1037 assert!(
1038 residual.abs() < 1e-10,
1039 "5y residual at par should be < 1e-10, got {residual}",
1040 );
1041 }
1042
1043 #[test]
1044 fn debug_format_contains_struct_name() {
1045 let swap = SwapFixedFloat::new(
1046 d(2024, 1, 2),
1047 d(2026, 1, 2),
1048 0.04,
1049 Frequency::SemiAnnual,
1050 Daycount::Act360,
1051 Frequency::Quarterly,
1052 Daycount::Act360,
1053 )
1054 .unwrap();
1055 let s = format!("{swap:?}");
1056 assert!(s.contains("SwapFixedFloat"));
1057 }
1058
1059 #[test]
1060 fn clone_and_eq_round_trip() {
1061 let swap = SwapFixedFloat::new(
1062 d(2024, 1, 2),
1063 d(2026, 1, 2),
1064 0.04,
1065 Frequency::SemiAnnual,
1066 Daycount::Act360,
1067 Frequency::Quarterly,
1068 Daycount::Act360,
1069 )
1070 .unwrap();
1071 let cloned = swap.clone();
1072 assert_eq!(swap, cloned);
1073 }
1074}