1use chrono::prelude::*;
2use chrono::Months;
3use pyo3::exceptions::PyValueError;
4use pyo3::prelude::*;
5use serde::{Deserialize, Serialize};
6use std::cmp::PartialEq;
7
8use crate::scheduling::{
9 ndt, Adjuster, Adjustment, Calendar, DateRoll, Frequency, Imm, RollDay, Scheduling,
10};
11
12#[pyclass(module = "rateslib.rs", eq, eq_int, hash, frozen)]
14#[derive(Debug, Hash, Copy, Clone, Serialize, Deserialize, PartialEq)]
15pub enum Convention {
16 Act365F = 0,
18 Act360 = 1,
20 Thirty360 = 2,
25 ThirtyU360 = 3,
33 ThirtyE360 = 4,
38 ThirtyE360ISDA = 5,
45 YearsAct365F = 6,
47 YearsAct360 = 7,
49 YearsMonths = 8,
51 One = 9,
53 ActActISDA = 10,
55 ActActICMA = 11,
61 Bus252 = 12,
66 ActActICMAStubAct365F = 13,
70 Act365_25 = 14,
72 Act364 = 15,
74}
75
76impl Convention {
77 pub fn dcf(
78 &self,
79 start: &NaiveDateTime,
80 end: &NaiveDateTime,
81 termination: Option<&NaiveDateTime>,
82 frequency: Option<&Frequency>,
83 stub: Option<bool>,
84 calendar: Option<&Calendar>,
85 adjuster: Option<&Adjuster>,
86 ) -> Result<f64, PyErr> {
87 match self {
88 Convention::Act360 => Ok(dcf_act_numeric(360.0, start, end)),
89 Convention::Act365F => Ok(dcf_act_numeric(365.0, start, end)),
90 Convention::Act365_25 => Ok(dcf_act_numeric(365.25, start, end)),
91 Convention::Act364 => Ok(dcf_act_numeric(364.0, start, end)),
92 Convention::YearsAct365F => Ok(dcf_years_and_act_numeric(365.0, start, end)),
93 Convention::YearsAct360 => Ok(dcf_years_and_act_numeric(360.0, start, end)),
94 Convention::YearsMonths => Ok(dcf_years_and_months(start, end)),
95 Convention::Thirty360 => Ok(dcf_30360(start, end)),
96 Convention::ThirtyU360 => dcf_30u360(start, end, frequency),
97 Convention::ThirtyE360 => Ok(dcf_30e360(start, end)),
98 Convention::ThirtyE360ISDA => dcf_30e360_isda(start, end, termination),
99 Convention::One => Ok(1.0),
100 Convention::ActActISDA => Ok(dcf_act_isda(start, end)),
101 Convention::ActActICMA => {
102 if frequency.is_none() {
103 Err(PyValueError::new_err(
104 "`frequency` must be supplied for 'ActActICMA' type convention.",
105 ))
106 } else if stub.is_none() {
107 Err(PyValueError::new_err(
108 "`stub` must be supplied for 'ActActICMA' type convention.",
109 ))
110 } else {
111 dcf_act_icma(
112 start,
113 end,
114 termination,
115 frequency.unwrap(),
116 stub.unwrap(),
117 calendar,
118 adjuster,
119 )
120 }
121 }
122 Convention::Bus252 => {
123 let calendar_: &Calendar;
124 if calendar.is_none() {
125 match frequency {
126 Some(Frequency::BusDays {
127 number: _,
128 calendar: c,
129 }) => calendar_ = c,
130 _ => {
131 return Err(PyValueError::new_err(
132 "`calendar` must be supplied for 'Bus252' type convention.",
133 ));
134 }
135 }
136 } else {
137 calendar_ = calendar.unwrap();
138 }
139 Ok(dcf_bus252(start, end, calendar_))
140 }
141 Convention::ActActICMAStubAct365F => {
142 if frequency.is_none() {
143 Err(PyValueError::new_err(
144 "`frequency` must be supplied for 'ActActICMA' type convention.",
145 ))
146 } else if stub.is_none() {
147 Err(PyValueError::new_err(
148 "`stub` must be supplied for 'ActActICMA' type convention.",
149 ))
150 } else {
151 dcf_act_icma_stub_365f(
152 start,
153 end,
154 termination,
155 frequency.unwrap(),
156 stub.unwrap(),
157 calendar,
158 adjuster,
159 )
160 }
161 }
162 }
163 }
164}
165
166fn dcf_act_numeric(denominator: f64, start: &NaiveDateTime, end: &NaiveDateTime) -> f64 {
167 (*end - *start).num_days() as f64 / denominator
168}
169
170fn dcf_years_and_act_numeric(denominator: f64, start: &NaiveDateTime, end: &NaiveDateTime) -> f64 {
171 if *end <= (*start + Months::new(12)) {
172 dcf_act_numeric(denominator, start, end)
173 } else {
174 let intermediate = RollDay::Day(start.day())
175 .try_from_ym(end.year(), start.month())
176 .expect("Dates are out of bounds");
177 if intermediate <= *end {
178 let years: f64 = (end.year() - start.year()) as f64;
179 years + dcf_act_numeric(denominator, &intermediate, end)
180 } else {
181 let years: f64 = (end.year() - start.year()) as f64 - 1.0;
182 years + dcf_act_numeric(denominator, &(intermediate - Months::new(12)), end)
183 }
184 }
185}
186
187fn dcf_years_and_months(start: &NaiveDateTime, end: &NaiveDateTime) -> f64 {
188 let start_ = ndt(start.year(), start.month(), 1);
189 let end_ = ndt(end.year(), end.month(), 1);
190 let mut count_date = ndt(end.year(), start.month(), 1);
191 if count_date > end_ {
192 count_date = count_date - Months::new(12)
193 };
194 let years = count_date.year() - start_.year();
195 let mut counter = 0;
196 while count_date < end_ {
197 count_date = count_date + Months::new(1);
198 counter += 1;
199 }
200 years as f64 + counter as f64 / 12.0
201}
202
203fn dcf_30360_unadjusted(ys: i32, ms: u32, ds: u32, ye: i32, me: u32, de: u32) -> f64 {
205 (ye - ys) as f64 + (me as f64 - ms as f64) / 12.0 + (de as f64 - ds as f64) / 360.0
206}
207
208fn dcf_30360(start: &NaiveDateTime, end: &NaiveDateTime) -> f64 {
214 let ds = u32::min(30_u32, start.day());
215 let de = if ds == 30 {
216 u32::min(30_u32, end.day())
217 } else {
218 end.day()
219 };
220 dcf_30360_unadjusted(start.year(), start.month(), ds, end.year(), end.month(), de)
221}
222
223fn dcf_30e360(start: &NaiveDateTime, end: &NaiveDateTime) -> f64 {
229 let ds = u32::min(30_u32, start.day());
230 let de = u32::min(30_u32, end.day());
231 dcf_30360_unadjusted(start.year(), start.month(), ds, end.year(), end.month(), de)
232}
233
234fn dcf_30u360(
244 start: &NaiveDateTime,
245 end: &NaiveDateTime,
246 frequency: Option<&Frequency>,
247) -> Result<f64, PyErr> {
248 let mut ds = start.day();
249 let mut de = end.day();
250
251 if Imm::Eom.validate(start) && start.month() == 2 {
253 let roll: RollDay = match frequency {
254 Some(Frequency::Months {
255 number: _,
256 roll: Some(r),
257 }) => *r,
258 _ => {
259 return Err(PyValueError::new_err(
260 "`frequency` must be provided or has no `roll`. A roll-day must be supplied for '30u360' convention to detect February EoM rolls.\n`start` is detected as end of February, otherwise use '30360' which will leave this date unadjusted.",
261 ));
262 }
263 };
264 if roll == RollDay::Day(31) {
265 ds = 30;
266 if Imm::Eom.validate(end) && end.month() == 2 {
267 de = 30;
268 }
269 }
270 }
271
272 ds = u32::min(30_u32, ds);
274 if de == 31 && ds == 30 {
275 de = 30;
276 }
277 Ok(dcf_30360_unadjusted(
278 start.year(),
279 start.month(),
280 ds,
281 end.year(),
282 end.month(),
283 de,
284 ))
285}
286
287fn dcf_30e360_isda(
292 start: &NaiveDateTime,
293 end: &NaiveDateTime,
294 termination: Option<&NaiveDateTime>,
295) -> Result<f64, PyErr> {
296 let mut ds = u32::min(30_u32, start.day());
297
298 if Imm::Eom.validate(start) && start.month() == 2 {
300 ds = 30;
301 }
302 let mut de = u32::min(30_u32, end.day());
303 if Imm::Eom.validate(end) && end.month() == 2 {
304 if termination.is_none() {
305 return Err(PyValueError::new_err(
306 "`termination` must be provided for '30e360ISDA' convention to detect end of February.\n`end` is detected as end of February, otherwise use '30e360' which will leave this date unadjusted.",
307 ));
308 } else if *end != *(termination.unwrap()) {
309 de = 30;
310 }
311 }
312
313 Ok(dcf_30360_unadjusted(
314 start.year(),
315 start.month(),
316 ds,
317 end.year(),
318 end.month(),
319 de,
320 ))
321}
322
323fn dcf_act_isda(start: &NaiveDateTime, end: &NaiveDateTime) -> f64 {
324 if start == end {
325 return 0.0;
326 };
327
328 let is_start_leap = NaiveDate::from_ymd_opt(start.year(), 2, 29).is_some();
329 let is_end_leap = NaiveDate::from_ymd_opt(end.year(), 2, 29).is_some();
330
331 let year_1_diff = if is_start_leap { 366.0 } else { 365.0 };
332 let year_2_diff = if is_end_leap { 366.0 } else { 365.0 };
333
334 let mut total_sum: f64 = (end.year() - start.year()) as f64 - 1.0;
335 total_sum += (ndt(start.year() + 1, 1, 1) - *start).num_days() as f64 / year_1_diff;
336 total_sum += (*end - ndt(end.year(), 1, 1)).num_days() as f64 / year_2_diff;
337 total_sum
338}
339
340fn dcf_act_icma(
341 start: &NaiveDateTime,
342 end: &NaiveDateTime,
343 termination: Option<&NaiveDateTime>,
344 frequency: &Frequency,
345 stub: bool,
346 calendar: Option<&Calendar>,
347 adjuster: Option<&Adjuster>,
348) -> Result<f64, PyErr> {
349 let freq = actacticma_frequency_conversion(frequency);
350 let ppa = freq.periods_per_annum();
351
352 if !stub {
353 Ok(1.0 / ppa)
354 } else {
355 if termination.is_none() || adjuster.is_none() || calendar.is_none() {
356 return Err(PyValueError::new_err(
357 "Stub periods under ActActICMA require `termination`, `adjuster` and `calendar` arguments to determine appropriate fractions."
358 ));
359 }
360 let is_back_stub = end == termination.unwrap();
361 let mut fraction = -1.0;
362 if is_back_stub {
363 let mut qe0 = *start;
364 let mut qe1 = *start;
365 while *end > qe1 {
366 fraction += 1.0;
367 qe0 = qe1;
368 qe1 = (*(adjuster.unwrap())).adjust(&freq.next(&qe0), calendar.unwrap());
369 }
370 fraction =
371 fraction + ((*end - qe0).num_days() as f64) / ((qe1 - qe0).num_days() as f64);
372 Ok(fraction / ppa)
373 } else {
374 let mut qs0 = *end;
375 let mut qs1 = *end;
376 while *start < qs1 {
377 fraction += 1.0;
378 qs0 = qs1;
379 qs1 = (*(adjuster.unwrap())).adjust(&freq.previous(&qs0), calendar.unwrap());
380 }
381 fraction =
382 fraction + ((qs0 - *start).num_days() as f64) / ((qs0 - qs1).num_days() as f64);
383 Ok(fraction / ppa)
384 }
385 }
386}
387
388fn dcf_act_icma_stub_365f(
389 start: &NaiveDateTime,
390 end: &NaiveDateTime,
391 termination: Option<&NaiveDateTime>,
392 frequency: &Frequency,
393 stub: bool,
394 calendar: Option<&Calendar>,
395 adjuster: Option<&Adjuster>,
396) -> Result<f64, PyErr> {
397 let freq = actacticma_frequency_conversion(frequency);
398 let ppa = freq.periods_per_annum();
399
400 if !stub {
401 Ok(1.0 / ppa)
402 } else {
403 if termination.is_none() || adjuster.is_none() || calendar.is_none() {
404 return Err(PyValueError::new_err(
405 "Stub periods under ActActICMA require `termination`, `adjuster` and `calendar` arguments to determine appropriate fractions."
406 ));
407 }
408 let is_back_stub = end == termination.unwrap();
409 let mut fraction = -1.0;
410 if is_back_stub {
411 let mut qe0 = *start;
412 let mut qe1 = *start;
413 while *end > qe1 {
414 fraction += 1.0;
415 qe0 = qe1;
416 qe1 = (*(adjuster.unwrap())).adjust(&freq.next(&qe0), calendar.unwrap());
417 }
418 fraction = fraction + ppa * (*end - qe0).num_days() as f64 / 365.0;
419 Ok(fraction / ppa)
420 } else {
421 let mut qs0 = *end;
422 let mut qs1 = *end;
423 while *start < qs1 {
424 fraction += 1.0;
425 qs0 = qs1;
426 qs1 = (*(adjuster.unwrap())).adjust(&freq.previous(&qs0), calendar.unwrap());
427 }
428 fraction = fraction + ppa * (qs0 - *start).num_days() as f64 / 365.0;
429 Ok(fraction / ppa)
430 }
431 }
432}
433
434fn actacticma_frequency_conversion(frequency: &Frequency) -> Frequency {
435 match frequency {
436 Frequency::Zero {} => Frequency::Months {
437 number: 12,
438 roll: None,
439 },
440 _ => frequency.clone(),
441 }
442}
443
444fn dcf_bus252(start: &NaiveDateTime, end: &NaiveDateTime, calendar: &Calendar) -> f64 {
445 if end < start {
446 panic!("Given end is greater than start");
447 } else if start == end {
448 return 0.0;
449 }
450 let start_bd = Adjuster::Following {}.adjust(start, calendar);
451 let end_bd = Adjuster::Previous {}.adjust(end, calendar);
452 let subtract = if end_bd == *end { -1.0 } else { 0.0 };
453 if start_bd == end_bd {
454 if start_bd > *start && end_bd < *end {
455 1.0 / 252.0
457 } else if end_bd < *end {
458 1.0 / 252.0
460 } else {
461 0.0
465 }
466 } else if start_bd > end_bd {
467 0.0
469 } else {
470 (calendar.bus_date_range(&start_bd, &end_bd).unwrap().len() as f64 + subtract) / 252.0
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use crate::scheduling::{ndt, Cal};
478
479 #[test]
480 fn test_act_numeric() {
481 let result = dcf_act_numeric(10.0, &ndt(2000, 1, 1), &ndt(2000, 1, 21));
482 assert_eq!(result, 2.0)
483 }
484
485 #[test]
486 fn test_act_plus() {
487 let options: Vec<(NaiveDateTime, NaiveDateTime, f64)> = vec![
488 (ndt(2000, 1, 1), ndt(2002, 1, 21), 2.0 + 20.0 / 365.0),
489 (ndt(2000, 12, 31), ndt(2002, 1, 1), 1.0 + 1.0 / 365.0),
490 (ndt(2000, 12, 31), ndt(2002, 12, 31), 2.0),
491 (ndt(2024, 2, 29), ndt(2025, 2, 28), 1.0),
492 (ndt(2000, 12, 15), ndt(2003, 1, 15), 2.0 + 31.0 / 365.0),
493 ];
494 for option in options {
495 let result = dcf_years_and_act_numeric(365.0, &option.0, &option.1);
496 assert_eq!(result, option.2)
497 }
498 }
499
500 #[test]
501 fn test_30360() {
502 let options: Vec<(NaiveDateTime, NaiveDateTime, f64)> = vec![
503 (ndt(2000, 1, 1), ndt(2000, 1, 21), 20.0 / 360.0),
504 (
505 ndt(2000, 1, 1),
506 ndt(2001, 3, 21),
507 1.0 + 2.0 / 12.0 + 20.0 / 360.0,
508 ),
509 ];
510 for option in options {
511 let result = dcf_30360(&option.0, &option.1);
512 assert_eq!(result, option.2)
513 }
514 }
515
516 #[test]
517 fn test_30u360() {
518 let options: Vec<(NaiveDateTime, NaiveDateTime, Frequency, f64)> = vec![
519 (
520 ndt(2000, 1, 1),
521 ndt(2000, 1, 21),
522 Frequency::Months {
523 number: 1,
524 roll: Some(RollDay::Day(1)),
525 },
526 20.0 / 360.0,
527 ),
528 (
529 ndt(2000, 1, 1),
530 ndt(2001, 3, 21),
531 Frequency::CalDays { number: 20 },
532 1.0 + 2.0 / 12.0 + 20.0 / 360.0,
533 ),
534 (
535 ndt(2024, 2, 29),
536 ndt(2025, 2, 28),
537 Frequency::Months {
538 number: 12,
539 roll: Some(RollDay::Day(29)),
540 },
541 1.0 - 1.0 / 360.0,
542 ),
543 (
544 ndt(2024, 2, 29),
545 ndt(2025, 2, 28),
546 Frequency::Months {
547 number: 12,
548 roll: Some(RollDay::Day(31)),
549 },
550 1.0,
551 ),
552 ];
553 for option in options {
554 let result = dcf_30u360(&option.0, &option.1, Some(&option.2)).unwrap();
555 assert_eq!(result, option.3);
556 }
557 }
558
559 #[test]
560 fn test_years_and_months() {
561 let options: Vec<(NaiveDateTime, NaiveDateTime, f64)> = vec![
562 (ndt(2000, 1, 1), ndt(2000, 1, 21), 0.0),
563 (ndt(2000, 1, 1), ndt(2001, 3, 21), 1.0 + 2.0 / 12.0),
564 (ndt(2024, 2, 29), ndt(2025, 2, 28), 1.0),
565 (ndt(2024, 2, 29), ndt(2025, 2, 28), 1.0),
566 (ndt(2000, 12, 29), ndt(2025, 1, 12), 24.0 + 1.0 / 12.0),
567 ];
568 for option in options {
569 let result = dcf_years_and_months(&option.0, &option.1);
570 assert_eq!(result, option.2)
571 }
572 }
573
574 #[test]
575 fn test_actacticma() {
576 let options: Vec<(NaiveDateTime, NaiveDateTime, Frequency, f64)> = vec![
577 (
578 ndt(1999, 2, 1),
579 ndt(1999, 7, 1),
580 Frequency::Months {
581 number: 12,
582 roll: None,
583 },
584 150.0 / 365.0,
585 ),
586 (
587 ndt(2002, 8, 15),
588 ndt(2003, 7, 15),
589 Frequency::Months {
590 number: 6,
591 roll: None,
592 },
593 0.5 + 153.0 / 368.0,
594 ),
595 ];
596 for option in options {
597 let result = dcf_act_icma(
598 &option.0,
599 &option.1,
600 Some(&ndt(2099, 1, 1)),
601 &option.2,
602 true,
603 Some(&Cal::new(vec![], vec![]).into()),
604 Some(&Adjuster::Actual {}),
605 )
606 .unwrap();
607 assert_eq!(result, option.3)
608 }
609 }
610}