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).expect("valid date/time components"),
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)
380 .expect("valid date/time components");
381 self.next_business_day(first, true)
382 }
383
384 pub fn adjust_for_business_day(&self, date: NaiveDate) -> NaiveDate {
388 if self.is_business_day(date) {
389 return date;
390 }
391
392 match self.month_end_convention {
393 MonthEndConvention::Following => self.next_business_day(date, false),
394 MonthEndConvention::Preceding => self.prev_business_day(date, false),
395 MonthEndConvention::ModifiedFollowing => {
396 let following = self.next_business_day(date, false);
397 if following.month() != date.month() {
398 self.prev_business_day(date, false)
399 } else {
400 following
401 }
402 }
403 MonthEndConvention::EndOfMonth => self.last_business_day_of_month(date),
404 }
405 }
406
407 pub fn settlement_rules(&self) -> &SettlementRules {
409 &self.settlement_rules
410 }
411
412 fn last_day_of_month(&self, date: NaiveDate) -> NaiveDate {
414 let year = date.year();
415 let month = date.month();
416
417 if month == 12 {
418 NaiveDate::from_ymd_opt(year + 1, 1, 1).expect("valid date/time components")
419 - Duration::days(1)
420 } else {
421 NaiveDate::from_ymd_opt(year, month + 1, 1).expect("valid date/time components")
422 - Duration::days(1)
423 }
424 }
425
426 pub fn business_days_in_month(&self, year: i32, month: u32) -> Vec<NaiveDate> {
428 let first = NaiveDate::from_ymd_opt(year, month, 1).expect("valid date/time components");
429 let last = self.last_day_of_month(first);
430
431 let mut business_days = Vec::new();
432 let mut current = first;
433
434 while current <= last {
435 if self.is_business_day(current) {
436 business_days.push(current);
437 }
438 current += Duration::days(1);
439 }
440
441 business_days
442 }
443
444 pub fn count_business_days_in_month(&self, year: i32, month: u32) -> usize {
446 self.business_days_in_month(year, month).len()
447 }
448}
449
450pub struct BusinessDayCalculatorBuilder {
452 calendar: HolidayCalendar,
453 weekend_days: Option<HashSet<Weekday>>,
454 half_day_policy: HalfDayPolicy,
455 half_days: HashMap<NaiveDate, NaiveTime>,
456 settlement_rules: SettlementRules,
457 month_end_convention: MonthEndConvention,
458}
459
460impl BusinessDayCalculatorBuilder {
461 pub fn new(calendar: HolidayCalendar) -> Self {
463 Self {
464 calendar,
465 weekend_days: None,
466 half_day_policy: HalfDayPolicy::default(),
467 half_days: HashMap::new(),
468 settlement_rules: SettlementRules::default(),
469 month_end_convention: MonthEndConvention::default(),
470 }
471 }
472
473 pub fn weekend_days(mut self, days: HashSet<Weekday>) -> Self {
475 self.weekend_days = Some(days);
476 self
477 }
478
479 pub fn middle_east_weekend(mut self) -> Self {
481 let mut days = HashSet::new();
482 days.insert(Weekday::Fri);
483 days.insert(Weekday::Sat);
484 self.weekend_days = Some(days);
485 self
486 }
487
488 pub fn half_day_policy(mut self, policy: HalfDayPolicy) -> Self {
490 self.half_day_policy = policy;
491 self
492 }
493
494 pub fn add_half_day(mut self, date: NaiveDate, close_time: NaiveTime) -> Self {
496 self.half_days.insert(date, close_time);
497 self
498 }
499
500 pub fn add_us_market_half_days(mut self, year: i32) -> Self {
504 let close_time = NaiveTime::from_hms_opt(13, 0, 0).expect("valid date/time components");
505
506 let july_3 = NaiveDate::from_ymd_opt(year, 7, 3).expect("valid date/time components");
508 if !matches!(july_3.weekday(), Weekday::Sat | Weekday::Sun) {
509 self.half_days.insert(july_3, close_time);
510 }
511
512 let first_nov = NaiveDate::from_ymd_opt(year, 11, 1).expect("valid date/time components");
514 let days_until_thu = (Weekday::Thu.num_days_from_monday() as i32
515 - first_nov.weekday().num_days_from_monday() as i32
516 + 7)
517 % 7;
518 let thanksgiving = first_nov + Duration::days(days_until_thu as i64 + 21); let black_friday = thanksgiving + Duration::days(1);
520 self.half_days.insert(black_friday, close_time);
521
522 let christmas_eve =
524 NaiveDate::from_ymd_opt(year, 12, 24).expect("valid date/time components");
525 if !matches!(christmas_eve.weekday(), Weekday::Sat | Weekday::Sun) {
526 self.half_days.insert(christmas_eve, close_time);
527 }
528
529 self
530 }
531
532 pub fn settlement_rules(mut self, rules: SettlementRules) -> Self {
534 self.settlement_rules = rules;
535 self
536 }
537
538 pub fn month_end_convention(mut self, convention: MonthEndConvention) -> Self {
540 self.month_end_convention = convention;
541 self
542 }
543
544 pub fn build(self) -> BusinessDayCalculator {
546 let mut calc = BusinessDayCalculator::new(self.calendar);
547
548 if let Some(weekend_days) = self.weekend_days {
549 calc.weekend_days = weekend_days;
550 }
551
552 calc.half_day_policy = self.half_day_policy;
553 calc.half_days = self.half_days;
554 calc.settlement_rules = self.settlement_rules;
555 calc.month_end_convention = self.month_end_convention;
556
557 calc
558 }
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct BusinessDayConfig {
564 #[serde(default = "default_true")]
566 pub enabled: bool,
567 #[serde(default)]
569 pub half_day_policy: HalfDayPolicy,
570 #[serde(default)]
572 pub settlement_rules: SettlementRulesConfig,
573 #[serde(default)]
575 pub month_end_convention: MonthEndConvention,
576 #[serde(default)]
578 pub weekend_days: Option<Vec<String>>,
579}
580
581fn default_true() -> bool {
582 true
583}
584
585impl Default for BusinessDayConfig {
586 fn default() -> Self {
587 Self {
588 enabled: true,
589 half_day_policy: HalfDayPolicy::default(),
590 settlement_rules: SettlementRulesConfig::default(),
591 month_end_convention: MonthEndConvention::default(),
592 weekend_days: None,
593 }
594 }
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize)]
599pub struct SettlementRulesConfig {
600 #[serde(default = "default_settlement_2")]
602 pub equity_days: i32,
603 #[serde(default = "default_settlement_1")]
605 pub government_bonds_days: i32,
606 #[serde(default = "default_settlement_2")]
608 pub fx_spot_days: i32,
609 #[serde(default = "default_settlement_2")]
611 pub corporate_bonds_days: i32,
612 #[serde(default = "default_wire_cutoff")]
614 pub wire_cutoff_time: String,
615 #[serde(default = "default_settlement_1")]
617 pub wire_international_days: i32,
618 #[serde(default = "default_settlement_1")]
620 pub ach_days: i32,
621}
622
623fn default_settlement_1() -> i32 {
624 1
625}
626
627fn default_settlement_2() -> i32 {
628 2
629}
630
631fn default_wire_cutoff() -> String {
632 "14:00".to_string()
633}
634
635impl Default for SettlementRulesConfig {
636 fn default() -> Self {
637 Self {
638 equity_days: 2,
639 government_bonds_days: 1,
640 fx_spot_days: 2,
641 corporate_bonds_days: 2,
642 wire_cutoff_time: "14:00".to_string(),
643 wire_international_days: 1,
644 ach_days: 1,
645 }
646 }
647}
648
649impl SettlementRulesConfig {
650 pub fn to_settlement_rules(&self) -> SettlementRules {
652 let cutoff_time = NaiveTime::parse_from_str(&self.wire_cutoff_time, "%H:%M")
653 .unwrap_or_else(|_| {
654 NaiveTime::from_hms_opt(14, 0, 0).expect("valid date/time components")
655 });
656
657 SettlementRules {
658 equity: SettlementType::TPlus(self.equity_days),
659 government_bonds: SettlementType::TPlus(self.government_bonds_days),
660 fx_spot: SettlementType::TPlus(self.fx_spot_days),
661 fx_forward: SettlementType::TPlus(self.fx_spot_days),
662 corporate_bonds: SettlementType::TPlus(self.corporate_bonds_days),
663 wire_domestic: WireSettlementConfig {
664 cutoff_time,
665 before_cutoff: SettlementType::SameDay,
666 after_cutoff: SettlementType::NextBusinessDay,
667 },
668 wire_international: SettlementType::TPlus(self.wire_international_days),
669 ach: SettlementType::TPlus(self.ach_days),
670 }
671 }
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used)]
676mod tests {
677 use super::*;
678 use crate::distributions::holidays::Region;
679
680 fn test_calendar() -> HolidayCalendar {
681 HolidayCalendar::for_region(Region::US, 2024)
682 }
683
684 #[test]
685 fn test_is_business_day() {
686 let calc = BusinessDayCalculator::new(test_calendar());
687
688 let wednesday = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap();
690 assert!(calc.is_business_day(wednesday));
691
692 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
694 assert!(!calc.is_business_day(saturday));
695
696 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
698 assert!(!calc.is_business_day(christmas));
699 }
700
701 #[test]
702 fn test_add_business_days() {
703 let calc = BusinessDayCalculator::new(test_calendar());
704
705 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
707 let next = calc.add_business_days(friday, 1);
708 assert_eq!(next.weekday(), Weekday::Mon);
709 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
710
711 let next_week = calc.add_business_days(friday, 5);
714 assert_eq!(next_week.weekday(), Weekday::Mon);
715 assert_eq!(next_week, NaiveDate::from_ymd_opt(2024, 6, 24).unwrap());
716 }
717
718 #[test]
719 fn test_sub_business_days() {
720 let calc = BusinessDayCalculator::new(test_calendar());
721
722 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
724 let prev = calc.sub_business_days(monday, 1);
725 assert_eq!(prev.weekday(), Weekday::Fri);
726 assert_eq!(prev, NaiveDate::from_ymd_opt(2024, 6, 14).unwrap());
727 }
728
729 #[test]
730 fn test_business_days_between() {
731 let calc = BusinessDayCalculator::new(test_calendar());
732
733 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
736 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
737 assert_eq!(calc.business_days_between(monday, friday), 3);
738
739 assert_eq!(calc.business_days_between(monday, monday), 0);
741
742 assert_eq!(calc.business_days_between(friday, monday), -3);
744
745 let next_monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
748 assert_eq!(calc.business_days_between(monday, next_monday), 4);
749 }
750
751 #[test]
752 fn test_settlement_t_plus_2() {
753 let calc = BusinessDayCalculator::new(test_calendar());
754
755 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
757 let settlement = calc.settlement_date(monday, SettlementType::TPlus(2));
758 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 12).unwrap());
759
760 let thursday = NaiveDate::from_ymd_opt(2024, 6, 13).unwrap();
762 let settlement = calc.settlement_date(thursday, SettlementType::TPlus(2));
763 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
764 }
765
766 #[test]
767 fn test_settlement_same_day() {
768 let calc = BusinessDayCalculator::new(test_calendar());
769
770 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
772 assert_eq!(
773 calc.settlement_date(monday, SettlementType::SameDay),
774 monday
775 );
776
777 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
779 let settlement = calc.settlement_date(saturday, SettlementType::SameDay);
780 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
781 }
782
783 #[test]
784 fn test_next_business_day() {
785 let calc = BusinessDayCalculator::new(test_calendar());
786
787 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
789 let next = calc.next_business_day(saturday, true);
790 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
791
792 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
794 assert_eq!(calc.next_business_day(monday, true), monday);
795
796 assert_eq!(
798 calc.next_business_day(monday, false),
799 NaiveDate::from_ymd_opt(2024, 6, 18).unwrap()
800 );
801 }
802
803 #[test]
804 fn test_prev_business_day() {
805 let calc = BusinessDayCalculator::new(test_calendar());
806
807 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
809 let prev = calc.prev_business_day(sunday, true);
810 assert_eq!(prev, NaiveDate::from_ymd_opt(2024, 6, 14).unwrap());
811 }
812
813 #[test]
814 fn test_last_business_day_of_month() {
815 let calc = BusinessDayCalculator::new(test_calendar());
816
817 let june = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
819 let last = calc.last_business_day_of_month(june);
820 assert_eq!(last, NaiveDate::from_ymd_opt(2024, 6, 28).unwrap());
821 }
822
823 #[test]
824 fn test_modified_following_convention() {
825 let calc = BusinessDayCalculator::new(test_calendar())
826 .with_month_end_convention(MonthEndConvention::ModifiedFollowing);
827
828 let june_30 = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap();
832 let adjusted = calc.adjust_for_business_day(june_30);
833 assert_eq!(adjusted, NaiveDate::from_ymd_opt(2024, 6, 28).unwrap());
834 }
835
836 #[test]
837 fn test_middle_east_weekend() {
838 let calc = BusinessDayCalculatorBuilder::new(test_calendar())
839 .middle_east_weekend()
840 .build();
841
842 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
844 assert!(!calc.is_business_day(friday));
845
846 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
848 assert!(calc.is_business_day(sunday));
849 }
850
851 #[test]
852 fn test_half_day_policy() {
853 let half_day = NaiveDate::from_ymd_opt(2024, 7, 3).unwrap();
855 let close_time = NaiveTime::from_hms_opt(13, 0, 0).unwrap();
856
857 let calc_half = BusinessDayCalculatorBuilder::new(test_calendar())
859 .half_day_policy(HalfDayPolicy::HalfDay)
860 .add_half_day(half_day, close_time)
861 .build();
862 assert!(calc_half.is_business_day(half_day));
863 assert_eq!(calc_half.get_half_day_close(half_day), Some(close_time));
864
865 let calc_non = BusinessDayCalculatorBuilder::new(test_calendar())
867 .half_day_policy(HalfDayPolicy::NonBusinessDay)
868 .add_half_day(half_day, close_time)
869 .build();
870 assert!(!calc_non.is_business_day(half_day));
871 }
872
873 #[test]
874 fn test_business_days_in_month() {
875 let calc = BusinessDayCalculator::new(test_calendar());
876
877 let days = calc.business_days_in_month(2024, 6);
880 assert!(
881 days.len() >= 18 && days.len() <= 22,
882 "Expected 18-22 business days in June 2024, got {}",
883 days.len()
884 );
885
886 for day in &days {
888 assert!(
889 calc.is_business_day(*day),
890 "{} should be a business day",
891 day
892 );
893 }
894 }
895
896 #[test]
897 fn test_wire_settlement() {
898 let calc = BusinessDayCalculator::new(test_calendar());
899 let config = WireSettlementConfig::default();
900
901 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
902
903 let morning = NaiveTime::from_hms_opt(10, 0, 0).unwrap();
905 assert_eq!(calc.wire_settlement_date(monday, morning, &config), monday);
906
907 let evening = NaiveTime::from_hms_opt(16, 0, 0).unwrap();
909 let next = calc.wire_settlement_date(monday, evening, &config);
910 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 11).unwrap());
911 }
912
913 #[test]
914 fn test_settlement_rules_config() {
915 let config = SettlementRulesConfig {
916 equity_days: 3,
917 wire_cutoff_time: "15:30".to_string(),
918 ..Default::default()
919 };
920
921 let rules = config.to_settlement_rules();
922 assert_eq!(rules.equity, SettlementType::TPlus(3));
923 assert_eq!(
924 rules.wire_domestic.cutoff_time,
925 NaiveTime::from_hms_opt(15, 30, 0).unwrap()
926 );
927 }
928}