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)]
675mod tests {
676 use super::*;
677 use crate::distributions::holidays::Region;
678
679 fn test_calendar() -> HolidayCalendar {
680 HolidayCalendar::for_region(Region::US, 2024)
681 }
682
683 #[test]
684 fn test_is_business_day() {
685 let calc = BusinessDayCalculator::new(test_calendar());
686
687 let wednesday = NaiveDate::from_ymd_opt(2024, 6, 12).unwrap();
689 assert!(calc.is_business_day(wednesday));
690
691 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
693 assert!(!calc.is_business_day(saturday));
694
695 let christmas = NaiveDate::from_ymd_opt(2024, 12, 25).unwrap();
697 assert!(!calc.is_business_day(christmas));
698 }
699
700 #[test]
701 fn test_add_business_days() {
702 let calc = BusinessDayCalculator::new(test_calendar());
703
704 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
706 let next = calc.add_business_days(friday, 1);
707 assert_eq!(next.weekday(), Weekday::Mon);
708 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
709
710 let next_week = calc.add_business_days(friday, 5);
713 assert_eq!(next_week.weekday(), Weekday::Mon);
714 assert_eq!(next_week, NaiveDate::from_ymd_opt(2024, 6, 24).unwrap());
715 }
716
717 #[test]
718 fn test_sub_business_days() {
719 let calc = BusinessDayCalculator::new(test_calendar());
720
721 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
723 let prev = calc.sub_business_days(monday, 1);
724 assert_eq!(prev.weekday(), Weekday::Fri);
725 assert_eq!(prev, NaiveDate::from_ymd_opt(2024, 6, 14).unwrap());
726 }
727
728 #[test]
729 fn test_business_days_between() {
730 let calc = BusinessDayCalculator::new(test_calendar());
731
732 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
735 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
736 assert_eq!(calc.business_days_between(monday, friday), 3);
737
738 assert_eq!(calc.business_days_between(monday, monday), 0);
740
741 assert_eq!(calc.business_days_between(friday, monday), -3);
743
744 let next_monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
747 assert_eq!(calc.business_days_between(monday, next_monday), 4);
748 }
749
750 #[test]
751 fn test_settlement_t_plus_2() {
752 let calc = BusinessDayCalculator::new(test_calendar());
753
754 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
756 let settlement = calc.settlement_date(monday, SettlementType::TPlus(2));
757 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 12).unwrap());
758
759 let thursday = NaiveDate::from_ymd_opt(2024, 6, 13).unwrap();
761 let settlement = calc.settlement_date(thursday, SettlementType::TPlus(2));
762 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
763 }
764
765 #[test]
766 fn test_settlement_same_day() {
767 let calc = BusinessDayCalculator::new(test_calendar());
768
769 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
771 assert_eq!(
772 calc.settlement_date(monday, SettlementType::SameDay),
773 monday
774 );
775
776 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
778 let settlement = calc.settlement_date(saturday, SettlementType::SameDay);
779 assert_eq!(settlement, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
780 }
781
782 #[test]
783 fn test_next_business_day() {
784 let calc = BusinessDayCalculator::new(test_calendar());
785
786 let saturday = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
788 let next = calc.next_business_day(saturday, true);
789 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 17).unwrap());
790
791 let monday = NaiveDate::from_ymd_opt(2024, 6, 17).unwrap();
793 assert_eq!(calc.next_business_day(monday, true), monday);
794
795 assert_eq!(
797 calc.next_business_day(monday, false),
798 NaiveDate::from_ymd_opt(2024, 6, 18).unwrap()
799 );
800 }
801
802 #[test]
803 fn test_prev_business_day() {
804 let calc = BusinessDayCalculator::new(test_calendar());
805
806 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
808 let prev = calc.prev_business_day(sunday, true);
809 assert_eq!(prev, NaiveDate::from_ymd_opt(2024, 6, 14).unwrap());
810 }
811
812 #[test]
813 fn test_last_business_day_of_month() {
814 let calc = BusinessDayCalculator::new(test_calendar());
815
816 let june = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
818 let last = calc.last_business_day_of_month(june);
819 assert_eq!(last, NaiveDate::from_ymd_opt(2024, 6, 28).unwrap());
820 }
821
822 #[test]
823 fn test_modified_following_convention() {
824 let calc = BusinessDayCalculator::new(test_calendar())
825 .with_month_end_convention(MonthEndConvention::ModifiedFollowing);
826
827 let june_30 = NaiveDate::from_ymd_opt(2024, 6, 30).unwrap();
831 let adjusted = calc.adjust_for_business_day(june_30);
832 assert_eq!(adjusted, NaiveDate::from_ymd_opt(2024, 6, 28).unwrap());
833 }
834
835 #[test]
836 fn test_middle_east_weekend() {
837 let calc = BusinessDayCalculatorBuilder::new(test_calendar())
838 .middle_east_weekend()
839 .build();
840
841 let friday = NaiveDate::from_ymd_opt(2024, 6, 14).unwrap();
843 assert!(!calc.is_business_day(friday));
844
845 let sunday = NaiveDate::from_ymd_opt(2024, 6, 16).unwrap();
847 assert!(calc.is_business_day(sunday));
848 }
849
850 #[test]
851 fn test_half_day_policy() {
852 let half_day = NaiveDate::from_ymd_opt(2024, 7, 3).unwrap();
854 let close_time = NaiveTime::from_hms_opt(13, 0, 0).unwrap();
855
856 let calc_half = BusinessDayCalculatorBuilder::new(test_calendar())
858 .half_day_policy(HalfDayPolicy::HalfDay)
859 .add_half_day(half_day, close_time)
860 .build();
861 assert!(calc_half.is_business_day(half_day));
862 assert_eq!(calc_half.get_half_day_close(half_day), Some(close_time));
863
864 let calc_non = BusinessDayCalculatorBuilder::new(test_calendar())
866 .half_day_policy(HalfDayPolicy::NonBusinessDay)
867 .add_half_day(half_day, close_time)
868 .build();
869 assert!(!calc_non.is_business_day(half_day));
870 }
871
872 #[test]
873 fn test_business_days_in_month() {
874 let calc = BusinessDayCalculator::new(test_calendar());
875
876 let days = calc.business_days_in_month(2024, 6);
879 assert!(
880 days.len() >= 18 && days.len() <= 22,
881 "Expected 18-22 business days in June 2024, got {}",
882 days.len()
883 );
884
885 for day in &days {
887 assert!(
888 calc.is_business_day(*day),
889 "{} should be a business day",
890 day
891 );
892 }
893 }
894
895 #[test]
896 fn test_wire_settlement() {
897 let calc = BusinessDayCalculator::new(test_calendar());
898 let config = WireSettlementConfig::default();
899
900 let monday = NaiveDate::from_ymd_opt(2024, 6, 10).unwrap();
901
902 let morning = NaiveTime::from_hms_opt(10, 0, 0).unwrap();
904 assert_eq!(calc.wire_settlement_date(monday, morning, &config), monday);
905
906 let evening = NaiveTime::from_hms_opt(16, 0, 0).unwrap();
908 let next = calc.wire_settlement_date(monday, evening, &config);
909 assert_eq!(next, NaiveDate::from_ymd_opt(2024, 6, 11).unwrap());
910 }
911
912 #[test]
913 fn test_settlement_rules_config() {
914 let config = SettlementRulesConfig {
915 equity_days: 3,
916 wire_cutoff_time: "15:30".to_string(),
917 ..Default::default()
918 };
919
920 let rules = config.to_settlement_rules();
921 assert_eq!(rules.equity, SettlementType::TPlus(3));
922 assert_eq!(
923 rules.wire_domestic.cutoff_time,
924 NaiveTime::from_hms_opt(15, 30, 0).unwrap()
925 );
926 }
927}