1use chrono::{Datelike, Duration, NaiveDate, NaiveTime, Weekday};
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10use super::holidays::HolidayCalendar;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum HalfDayPolicy {
16 #[default]
18 FullDay,
19 HalfDay,
21 NonBusinessDay,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
27#[serde(rename_all = "snake_case")]
28pub enum MonthEndConvention {
29 #[default]
31 ModifiedFollowing,
32 Preceding,
34 Following,
36 EndOfMonth,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum SettlementType {
44 TPlus(i32),
46 SameDay,
48 NextBusinessDay,
50 MonthEnd,
52}
53
54impl Default for SettlementType {
55 fn default() -> Self {
56 Self::TPlus(2)
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct WireSettlementConfig {
63 pub cutoff_time: NaiveTime,
65 pub before_cutoff: SettlementType,
67 pub after_cutoff: SettlementType,
69}
70
71impl Default for WireSettlementConfig {
72 fn default() -> Self {
73 Self {
74 cutoff_time: NaiveTime::from_hms_opt(14, 0, 0).unwrap(),
75 before_cutoff: SettlementType::SameDay,
76 after_cutoff: SettlementType::NextBusinessDay,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SettlementRules {
84 pub equity: SettlementType,
86 pub government_bonds: SettlementType,
88 pub fx_spot: SettlementType,
90 pub fx_forward: SettlementType,
92 pub corporate_bonds: SettlementType,
94 pub wire_domestic: WireSettlementConfig,
96 pub wire_international: SettlementType,
98 pub ach: SettlementType,
100}
101
102impl Default for SettlementRules {
103 fn default() -> Self {
104 Self {
105 equity: SettlementType::TPlus(2),
106 government_bonds: SettlementType::TPlus(1),
107 fx_spot: SettlementType::TPlus(2),
108 fx_forward: SettlementType::TPlus(2),
109 corporate_bonds: SettlementType::TPlus(2),
110 wire_domestic: WireSettlementConfig::default(),
111 wire_international: SettlementType::TPlus(1),
112 ach: SettlementType::TPlus(1),
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
122pub struct BusinessDayCalculator {
123 calendar: HolidayCalendar,
125 weekend_days: HashSet<Weekday>,
127 half_day_policy: HalfDayPolicy,
129 half_days: HashMap<NaiveDate, NaiveTime>,
131 settlement_rules: SettlementRules,
133 month_end_convention: MonthEndConvention,
135}
136
137impl BusinessDayCalculator {
138 pub fn new(calendar: HolidayCalendar) -> Self {
140 let mut weekend_days = HashSet::new();
141 weekend_days.insert(Weekday::Sat);
142 weekend_days.insert(Weekday::Sun);
143
144 Self {
145 calendar,
146 weekend_days,
147 half_day_policy: HalfDayPolicy::default(),
148 half_days: HashMap::new(),
149 settlement_rules: SettlementRules::default(),
150 month_end_convention: MonthEndConvention::default(),
151 }
152 }
153
154 pub fn with_weekend_days(mut self, weekend_days: HashSet<Weekday>) -> Self {
156 self.weekend_days = weekend_days;
157 self
158 }
159
160 pub fn with_half_day_policy(mut self, policy: HalfDayPolicy) -> Self {
162 self.half_day_policy = policy;
163 self
164 }
165
166 pub fn add_half_day(&mut self, date: NaiveDate, close_time: NaiveTime) {
168 self.half_days.insert(date, close_time);
169 }
170
171 pub fn with_settlement_rules(mut self, rules: SettlementRules) -> Self {
173 self.settlement_rules = rules;
174 self
175 }
176
177 pub fn with_month_end_convention(mut self, convention: MonthEndConvention) -> Self {
179 self.month_end_convention = convention;
180 self
181 }
182
183 pub fn is_weekend(&self, date: NaiveDate) -> bool {
185 self.weekend_days.contains(&date.weekday())
186 }
187
188 pub fn is_holiday(&self, date: NaiveDate) -> bool {
190 self.calendar.is_holiday(date)
191 }
192
193 pub fn is_half_day(&self, date: NaiveDate) -> bool {
195 self.half_days.contains_key(&date)
196 }
197
198 pub fn get_half_day_close(&self, date: NaiveDate) -> Option<NaiveTime> {
200 self.half_days.get(&date).copied()
201 }
202
203 pub fn is_business_day(&self, date: NaiveDate) -> bool {
210 if self.is_weekend(date) {
212 return false;
213 }
214
215 if self.calendar.is_holiday(date) {
218 let holidays = self.calendar.get_holidays(date);
219 if holidays.iter().any(|h| h.is_bank_holiday) {
221 return false;
222 }
223 let mult = self.calendar.get_multiplier(date);
225 if mult < 0.1 {
226 return false;
227 }
228 }
229
230 if self.is_half_day(date) && self.half_day_policy == HalfDayPolicy::NonBusinessDay {
232 return false;
233 }
234
235 true
236 }
237
238 pub fn add_business_days(&self, date: NaiveDate, days: i32) -> NaiveDate {
242 if days == 0 {
243 return date;
244 }
245
246 let direction = if days > 0 { 1 } else { -1 };
247 let mut remaining = days.abs();
248 let mut current = date;
249
250 while remaining > 0 {
251 current += Duration::days(direction as i64);
252 if self.is_business_day(current) {
253 remaining -= 1;
254 }
255 }
256
257 current
258 }
259
260 pub fn sub_business_days(&self, date: NaiveDate, days: i32) -> NaiveDate {
262 self.add_business_days(date, -days)
263 }
264
265 pub fn next_business_day(&self, date: NaiveDate, inclusive: bool) -> NaiveDate {
270 let mut current = date;
271
272 if inclusive && self.is_business_day(current) {
273 return current;
274 }
275
276 loop {
277 current += Duration::days(1);
278 if self.is_business_day(current) {
279 return current;
280 }
281 }
282 }
283
284 pub fn prev_business_day(&self, date: NaiveDate, inclusive: bool) -> NaiveDate {
289 let mut current = date;
290
291 if inclusive && self.is_business_day(current) {
292 return current;
293 }
294
295 loop {
296 current -= Duration::days(1);
297 if self.is_business_day(current) {
298 return current;
299 }
300 }
301 }
302
303 pub fn business_days_between(&self, start: NaiveDate, end: NaiveDate) -> i32 {
307 if start == end {
308 return 0;
309 }
310
311 let (earlier, later, sign) = if start < end {
312 (start, end, 1)
313 } else {
314 (end, start, -1)
315 };
316
317 let mut count = 0;
318 let mut current = earlier + Duration::days(1);
319
320 while current < later {
321 if self.is_business_day(current) {
322 count += 1;
323 }
324 current += Duration::days(1);
325 }
326
327 count * sign
328 }
329
330 pub fn settlement_date(&self, trade_date: NaiveDate, settlement: SettlementType) -> NaiveDate {
335 match settlement {
336 SettlementType::TPlus(days) => {
337 self.add_business_days(trade_date, days)
339 }
340 SettlementType::SameDay => {
341 self.next_business_day(trade_date, true)
343 }
344 SettlementType::NextBusinessDay => {
345 self.next_business_day(trade_date, false)
347 }
348 SettlementType::MonthEnd => {
349 self.last_business_day_of_month(trade_date)
351 }
352 }
353 }
354
355 pub fn wire_settlement_date(
357 &self,
358 trade_date: NaiveDate,
359 trade_time: NaiveTime,
360 config: &WireSettlementConfig,
361 ) -> NaiveDate {
362 let settlement_type = if trade_time <= config.cutoff_time {
363 config.before_cutoff
364 } else {
365 config.after_cutoff
366 };
367
368 self.settlement_date(trade_date, settlement_type)
369 }
370
371 pub fn last_business_day_of_month(&self, date: NaiveDate) -> NaiveDate {
373 let last_calendar_day = self.last_day_of_month(date);
374 self.prev_business_day(last_calendar_day, true)
375 }
376
377 pub fn first_business_day_of_month(&self, date: NaiveDate) -> NaiveDate {
379 let first = NaiveDate::from_ymd_opt(date.year(), date.month(), 1).unwrap();
380 self.next_business_day(first, true)
381 }
382
383 pub fn adjust_for_business_day(&self, date: NaiveDate) -> NaiveDate {
387 if self.is_business_day(date) {
388 return date;
389 }
390
391 match self.month_end_convention {
392 MonthEndConvention::Following => self.next_business_day(date, false),
393 MonthEndConvention::Preceding => self.prev_business_day(date, false),
394 MonthEndConvention::ModifiedFollowing => {
395 let following = self.next_business_day(date, false);
396 if following.month() != date.month() {
397 self.prev_business_day(date, false)
398 } else {
399 following
400 }
401 }
402 MonthEndConvention::EndOfMonth => self.last_business_day_of_month(date),
403 }
404 }
405
406 pub fn settlement_rules(&self) -> &SettlementRules {
408 &self.settlement_rules
409 }
410
411 fn last_day_of_month(&self, date: NaiveDate) -> NaiveDate {
413 let year = date.year();
414 let month = date.month();
415
416 if month == 12 {
417 NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap() - Duration::days(1)
418 } else {
419 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap() - Duration::days(1)
420 }
421 }
422
423 pub fn business_days_in_month(&self, year: i32, month: u32) -> Vec<NaiveDate> {
425 let first = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
426 let last = self.last_day_of_month(first);
427
428 let mut business_days = Vec::new();
429 let mut current = first;
430
431 while current <= last {
432 if self.is_business_day(current) {
433 business_days.push(current);
434 }
435 current += Duration::days(1);
436 }
437
438 business_days
439 }
440
441 pub fn count_business_days_in_month(&self, year: i32, month: u32) -> usize {
443 self.business_days_in_month(year, month).len()
444 }
445}
446
447pub struct BusinessDayCalculatorBuilder {
449 calendar: HolidayCalendar,
450 weekend_days: Option<HashSet<Weekday>>,
451 half_day_policy: HalfDayPolicy,
452 half_days: HashMap<NaiveDate, NaiveTime>,
453 settlement_rules: SettlementRules,
454 month_end_convention: MonthEndConvention,
455}
456
457impl BusinessDayCalculatorBuilder {
458 pub fn new(calendar: HolidayCalendar) -> Self {
460 Self {
461 calendar,
462 weekend_days: None,
463 half_day_policy: HalfDayPolicy::default(),
464 half_days: HashMap::new(),
465 settlement_rules: SettlementRules::default(),
466 month_end_convention: MonthEndConvention::default(),
467 }
468 }
469
470 pub fn weekend_days(mut self, days: HashSet<Weekday>) -> Self {
472 self.weekend_days = Some(days);
473 self
474 }
475
476 pub fn middle_east_weekend(mut self) -> Self {
478 let mut days = HashSet::new();
479 days.insert(Weekday::Fri);
480 days.insert(Weekday::Sat);
481 self.weekend_days = Some(days);
482 self
483 }
484
485 pub fn half_day_policy(mut self, policy: HalfDayPolicy) -> Self {
487 self.half_day_policy = policy;
488 self
489 }
490
491 pub fn add_half_day(mut self, date: NaiveDate, close_time: NaiveTime) -> Self {
493 self.half_days.insert(date, close_time);
494 self
495 }
496
497 pub fn add_us_market_half_days(mut self, year: i32) -> Self {
501 let close_time = NaiveTime::from_hms_opt(13, 0, 0).unwrap();
502
503 let july_3 = NaiveDate::from_ymd_opt(year, 7, 3).unwrap();
505 if !matches!(july_3.weekday(), Weekday::Sat | Weekday::Sun) {
506 self.half_days.insert(july_3, close_time);
507 }
508
509 let first_nov = NaiveDate::from_ymd_opt(year, 11, 1).unwrap();
511 let days_until_thu = (Weekday::Thu.num_days_from_monday() as i32
512 - first_nov.weekday().num_days_from_monday() as i32
513 + 7)
514 % 7;
515 let thanksgiving = first_nov + Duration::days(days_until_thu as i64 + 21); let black_friday = thanksgiving + Duration::days(1);
517 self.half_days.insert(black_friday, close_time);
518
519 let christmas_eve = NaiveDate::from_ymd_opt(year, 12, 24).unwrap();
521 if !matches!(christmas_eve.weekday(), Weekday::Sat | Weekday::Sun) {
522 self.half_days.insert(christmas_eve, close_time);
523 }
524
525 self
526 }
527
528 pub fn settlement_rules(mut self, rules: SettlementRules) -> Self {
530 self.settlement_rules = rules;
531 self
532 }
533
534 pub fn month_end_convention(mut self, convention: MonthEndConvention) -> Self {
536 self.month_end_convention = convention;
537 self
538 }
539
540 pub fn build(self) -> BusinessDayCalculator {
542 let mut calc = BusinessDayCalculator::new(self.calendar);
543
544 if let Some(weekend_days) = self.weekend_days {
545 calc.weekend_days = weekend_days;
546 }
547
548 calc.half_day_policy = self.half_day_policy;
549 calc.half_days = self.half_days;
550 calc.settlement_rules = self.settlement_rules;
551 calc.month_end_convention = self.month_end_convention;
552
553 calc
554 }
555}
556
557#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct BusinessDayConfig {
560 #[serde(default = "default_true")]
562 pub enabled: bool,
563 #[serde(default)]
565 pub half_day_policy: HalfDayPolicy,
566 #[serde(default)]
568 pub settlement_rules: SettlementRulesConfig,
569 #[serde(default)]
571 pub month_end_convention: MonthEndConvention,
572 #[serde(default)]
574 pub weekend_days: Option<Vec<String>>,
575}
576
577fn default_true() -> bool {
578 true
579}
580
581impl Default for BusinessDayConfig {
582 fn default() -> Self {
583 Self {
584 enabled: true,
585 half_day_policy: HalfDayPolicy::default(),
586 settlement_rules: SettlementRulesConfig::default(),
587 month_end_convention: MonthEndConvention::default(),
588 weekend_days: None,
589 }
590 }
591}
592
593#[derive(Debug, Clone, Serialize, Deserialize)]
595pub struct SettlementRulesConfig {
596 #[serde(default = "default_settlement_2")]
598 pub equity_days: i32,
599 #[serde(default = "default_settlement_1")]
601 pub government_bonds_days: i32,
602 #[serde(default = "default_settlement_2")]
604 pub fx_spot_days: i32,
605 #[serde(default = "default_settlement_2")]
607 pub corporate_bonds_days: i32,
608 #[serde(default = "default_wire_cutoff")]
610 pub wire_cutoff_time: String,
611 #[serde(default = "default_settlement_1")]
613 pub wire_international_days: i32,
614 #[serde(default = "default_settlement_1")]
616 pub ach_days: i32,
617}
618
619fn default_settlement_1() -> i32 {
620 1
621}
622
623fn default_settlement_2() -> i32 {
624 2
625}
626
627fn default_wire_cutoff() -> String {
628 "14:00".to_string()
629}
630
631impl Default for SettlementRulesConfig {
632 fn default() -> Self {
633 Self {
634 equity_days: 2,
635 government_bonds_days: 1,
636 fx_spot_days: 2,
637 corporate_bonds_days: 2,
638 wire_cutoff_time: "14:00".to_string(),
639 wire_international_days: 1,
640 ach_days: 1,
641 }
642 }
643}
644
645impl SettlementRulesConfig {
646 pub fn to_settlement_rules(&self) -> SettlementRules {
648 let cutoff_time = NaiveTime::parse_from_str(&self.wire_cutoff_time, "%H:%M")
649 .unwrap_or_else(|_| NaiveTime::from_hms_opt(14, 0, 0).unwrap());
650
651 SettlementRules {
652 equity: SettlementType::TPlus(self.equity_days),
653 government_bonds: SettlementType::TPlus(self.government_bonds_days),
654 fx_spot: SettlementType::TPlus(self.fx_spot_days),
655 fx_forward: SettlementType::TPlus(self.fx_spot_days),
656 corporate_bonds: SettlementType::TPlus(self.corporate_bonds_days),
657 wire_domestic: WireSettlementConfig {
658 cutoff_time,
659 before_cutoff: SettlementType::SameDay,
660 after_cutoff: SettlementType::NextBusinessDay,
661 },
662 wire_international: SettlementType::TPlus(self.wire_international_days),
663 ach: SettlementType::TPlus(self.ach_days),
664 }
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671 use crate::distributions::holidays::Region;
672
673 fn test_calendar() -> HolidayCalendar {
674 HolidayCalendar::for_region(Region::US, 2024)
675 }
676
677 #[test]
678 fn test_is_business_day() {
679 let calc = BusinessDayCalculator::new(test_calendar());
680
681 let wednesday = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap();
683 assert!(calc.is_business_day(wednesday));
684
685 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
687 assert!(!calc.is_business_day(saturday));
688
689 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
691 assert!(!calc.is_business_day(christmas));
692 }
693
694 #[test]
695 fn test_add_business_days() {
696 let calc = BusinessDayCalculator::new(test_calendar());
697
698 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
700 let next = calc.add_business_days(friday, 1);
701 assert_eq!(next.weekday(), Weekday::Mon);
702 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
703
704 let next_week = calc.add_business_days(friday, 5);
707 assert_eq!(next_week.weekday(), Weekday::Mon);
708 assert_eq!(next_week, NaiveDate::from_ymd_opt(2024, 6, 24).unwrap());
709 }
710
711 #[test]
712 fn test_sub_business_days() {
713 let calc = BusinessDayCalculator::new(test_calendar());
714
715 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
717 let prev = calc.sub_business_days(monday, 1);
718 assert_eq!(prev.weekday(), Weekday::Fri);
719 assert_eq!(prev, NaiveDate::from_ymd_opt(2024, 6, 14).unwrap());
720 }
721
722 #[test]
723 fn test_business_days_between() {
724 let calc = BusinessDayCalculator::new(test_calendar());
725
726 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
729 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
730 assert_eq!(calc.business_days_between(monday, friday), 3);
731
732 assert_eq!(calc.business_days_between(monday, monday), 0);
734
735 assert_eq!(calc.business_days_between(friday, monday), -3);
737
738 let next_monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
741 assert_eq!(calc.business_days_between(monday, next_monday), 4);
742 }
743
744 #[test]
745 fn test_settlement_t_plus_2() {
746 let calc = BusinessDayCalculator::new(test_calendar());
747
748 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
750 let settlement = calc.settlement_date(monday, SettlementType::TPlus(2));
751 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 12).unwrap());
752
753 let thursday = NaiveDate::from_ymd_opt(2024, 6, 13).unwrap();
755 let settlement = calc.settlement_date(thursday, SettlementType::TPlus(2));
756 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
757 }
758
759 #[test]
760 fn test_settlement_same_day() {
761 let calc = BusinessDayCalculator::new(test_calendar());
762
763 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
765 assert_eq!(
766 calc.settlement_date(monday, SettlementType::SameDay),
767 monday
768 );
769
770 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
772 let settlement = calc.settlement_date(saturday, SettlementType::SameDay);
773 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
774 }
775
776 #[test]
777 fn test_next_business_day() {
778 let calc = BusinessDayCalculator::new(test_calendar());
779
780 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
782 let next = calc.next_business_day(saturday, true);
783 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
784
785 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
787 assert_eq!(calc.next_business_day(monday, true), monday);
788
789 assert_eq!(
791 calc.next_business_day(monday, false),
792 NaiveDate::from_ymd_opt(2024, 6, 18).unwrap()
793 );
794 }
795
796 #[test]
797 fn test_prev_business_day() {
798 let calc = BusinessDayCalculator::new(test_calendar());
799
800 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
802 let prev = calc.prev_business_day(sunday, true);
803 assert_eq!(prev, NaiveDate::from_ymd_opt(2024, 6, 14).unwrap());
804 }
805
806 #[test]
807 fn test_last_business_day_of_month() {
808 let calc = BusinessDayCalculator::new(test_calendar());
809
810 let june = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
812 let last = calc.last_business_day_of_month(june);
813 assert_eq!(last, NaiveDate::from_ymd_opt(2024, 6, 28).unwrap());
814 }
815
816 #[test]
817 fn test_modified_following_convention() {
818 let calc = BusinessDayCalculator::new(test_calendar())
819 .with_month_end_convention(MonthEndConvention::ModifiedFollowing);
820
821 let june_30 = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap();
825 let adjusted = calc.adjust_for_business_day(june_30);
826 assert_eq!(adjusted, NaiveDate::from_ymd_opt(2024, 6, 28).unwrap());
827 }
828
829 #[test]
830 fn test_middle_east_weekend() {
831 let calc = BusinessDayCalculatorBuilder::new(test_calendar())
832 .middle_east_weekend()
833 .build();
834
835 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
837 assert!(!calc.is_business_day(friday));
838
839 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
841 assert!(calc.is_business_day(sunday));
842 }
843
844 #[test]
845 fn test_half_day_policy() {
846 let half_day = NaiveDate::from_ymd_opt(2024, 7, 3).unwrap();
848 let close_time = NaiveTime::from_hms_opt(13, 0, 0).unwrap();
849
850 let calc_half = BusinessDayCalculatorBuilder::new(test_calendar())
852 .half_day_policy(HalfDayPolicy::HalfDay)
853 .add_half_day(half_day, close_time)
854 .build();
855 assert!(calc_half.is_business_day(half_day));
856 assert_eq!(calc_half.get_half_day_close(half_day), Some(close_time));
857
858 let calc_non = BusinessDayCalculatorBuilder::new(test_calendar())
860 .half_day_policy(HalfDayPolicy::NonBusinessDay)
861 .add_half_day(half_day, close_time)
862 .build();
863 assert!(!calc_non.is_business_day(half_day));
864 }
865
866 #[test]
867 fn test_business_days_in_month() {
868 let calc = BusinessDayCalculator::new(test_calendar());
869
870 let days = calc.business_days_in_month(2024, 6);
873 assert!(
874 days.len() >= 18 && days.len() <= 22,
875 "Expected 18-22 business days in June 2024, got {}",
876 days.len()
877 );
878
879 for day in &days {
881 assert!(
882 calc.is_business_day(*day),
883 "{} should be a business day",
884 day
885 );
886 }
887 }
888
889 #[test]
890 fn test_wire_settlement() {
891 let calc = BusinessDayCalculator::new(test_calendar());
892 let config = WireSettlementConfig::default();
893
894 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
895
896 let morning = NaiveTime::from_hms_opt(10, 0, 0).unwrap();
898 assert_eq!(calc.wire_settlement_date(monday, morning, &config), monday);
899
900 let evening = NaiveTime::from_hms_opt(16, 0, 0).unwrap();
902 let next = calc.wire_settlement_date(monday, evening, &config);
903 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 11).unwrap());
904 }
905
906 #[test]
907 fn test_settlement_rules_config() {
908 let config = SettlementRulesConfig {
909 equity_days: 3,
910 wire_cutoff_time: "15:30".to_string(),
911 ..Default::default()
912 };
913
914 let rules = config.to_settlement_rules();
915 assert_eq!(rules.equity, SettlementType::TPlus(3));
916 assert_eq!(
917 rules.wire_domestic.cutoff_time,
918 NaiveTime::from_hms_opt(15, 30, 0).unwrap()
919 );
920 }
921}