1use crate::args::ArgSchema;
4use crate::builtins::datetime::serial_to_date;
5use crate::function::Function;
6use crate::traits::{ArgumentHandle, CalcValue, FunctionContext};
7use chrono::{Datelike, NaiveDate};
8use formualizer_common::{ExcelError, LiteralValue};
9use formualizer_macros::func_caps;
10
11fn coerce_num(arg: &ArgumentHandle) -> Result<f64, ExcelError> {
12 let v = arg.value()?.into_literal();
13 coerce_literal_num(&v)
14}
15
16fn coerce_literal_num(v: &LiteralValue) -> Result<f64, ExcelError> {
17 match v {
18 LiteralValue::Number(f) => Ok(*f),
19 LiteralValue::Int(i) => Ok(*i as f64),
20 LiteralValue::Boolean(b) => Ok(if *b { 1.0 } else { 0.0 }),
21 LiteralValue::Empty => Ok(0.0),
22 LiteralValue::Error(e) => Err(e.clone()),
23 _ => Err(ExcelError::new_value()),
24 }
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
30enum DayCountBasis {
31 UsNasd30360 = 0, ActualActual = 1, Actual360 = 2, Actual365 = 3, European30360 = 4, }
37
38impl DayCountBasis {
39 fn from_int(basis: i32) -> Result<Self, ExcelError> {
40 match basis {
41 0 => Ok(DayCountBasis::UsNasd30360),
42 1 => Ok(DayCountBasis::ActualActual),
43 2 => Ok(DayCountBasis::Actual360),
44 3 => Ok(DayCountBasis::Actual365),
45 4 => Ok(DayCountBasis::European30360),
46 _ => Err(ExcelError::new_num()),
47 }
48 }
49}
50
51fn is_leap_year(year: i32) -> bool {
53 (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
54}
55
56fn is_last_day_of_month(date: &NaiveDate) -> bool {
58 let next_day = *date + chrono::Duration::days(1);
59 next_day.month() != date.month()
60}
61
62fn days_between(start: &NaiveDate, end: &NaiveDate, basis: DayCountBasis) -> i32 {
64 match basis {
65 DayCountBasis::UsNasd30360 => days_30_360_us(start, end),
66 DayCountBasis::ActualActual | DayCountBasis::Actual360 | DayCountBasis::Actual365 => {
67 (*end - *start).num_days() as i32
68 }
69 DayCountBasis::European30360 => days_30_360_eu(start, end),
70 }
71}
72
73fn days_30_360_us(start: &NaiveDate, end: &NaiveDate) -> i32 {
75 let mut sd = start.day() as i32;
76 let sm = start.month() as i32;
77 let sy = start.year();
78
79 let mut ed = end.day() as i32;
80 let em = end.month() as i32;
81 let ey = end.year();
82
83 let start_is_last_feb = sm == 2 && is_last_day_of_month(start);
85 let end_is_last_feb = em == 2 && is_last_day_of_month(end);
86
87 if start_is_last_feb && end_is_last_feb {
88 ed = 30;
89 }
90 if start_is_last_feb {
91 sd = 30;
92 }
93 if ed == 31 && sd >= 30 {
94 ed = 30;
95 }
96 if sd == 31 {
97 sd = 30;
98 }
99
100 (ey - sy) * 360 + (em - sm) * 30 + (ed - sd)
101}
102
103fn days_30_360_eu(start: &NaiveDate, end: &NaiveDate) -> i32 {
105 let mut sd = start.day() as i32;
106 let sm = start.month() as i32;
107 let sy = start.year();
108
109 let mut ed = end.day() as i32;
110 let em = end.month() as i32;
111 let ey = end.year();
112
113 if sd == 31 {
114 sd = 30;
115 }
116 if ed == 31 {
117 ed = 30;
118 }
119
120 (ey - sy) * 360 + (em - sm) * 30 + (ed - sd)
121}
122
123fn annual_basis(basis: DayCountBasis, start: &NaiveDate, end: &NaiveDate) -> f64 {
125 match basis {
126 DayCountBasis::UsNasd30360 | DayCountBasis::European30360 => 360.0,
127 DayCountBasis::Actual360 => 360.0,
128 DayCountBasis::Actual365 => 365.0,
129 DayCountBasis::ActualActual => {
130 let sy = start.year();
132 let ey = end.year();
133 if sy == ey {
134 if is_leap_year(sy) { 366.0 } else { 365.0 }
135 } else {
136 let mut total_days = 0.0;
138 let mut years = 0;
139 for y in sy..=ey {
140 total_days += if is_leap_year(y) { 366.0 } else { 365.0 };
141 years += 1;
142 }
143 total_days / years as f64
144 }
145 }
146 }
147}
148
149fn year_fraction(start: &NaiveDate, end: &NaiveDate, basis: DayCountBasis) -> f64 {
151 let days = days_between(start, end, basis) as f64;
152 let annual = annual_basis(basis, start, end);
153 days / annual
154}
155
156fn coupon_date_before(settlement: &NaiveDate, maturity: &NaiveDate, frequency: i32) -> NaiveDate {
158 let months_between_coupons = 12 / frequency;
159 let mut coupon_date = *maturity;
160
161 while coupon_date >= *settlement {
163 coupon_date = add_months(&coupon_date, -months_between_coupons);
164 }
165 coupon_date
166}
167
168fn coupon_date_after(settlement: &NaiveDate, maturity: &NaiveDate, frequency: i32) -> NaiveDate {
170 let months_between_coupons = 12 / frequency;
171 let prev_coupon = coupon_date_before(settlement, maturity, frequency);
172 add_months(&prev_coupon, months_between_coupons)
173}
174
175fn add_months(date: &NaiveDate, months: i32) -> NaiveDate {
177 let total_months = date.year() * 12 + date.month() as i32 - 1 + months;
178 let new_year = total_months / 12;
179 let new_month = (total_months % 12 + 1) as u32;
180
181 let mut new_day = date.day();
183 loop {
184 if let Some(d) = NaiveDate::from_ymd_opt(new_year, new_month, new_day) {
185 return d;
186 }
187 new_day -= 1;
188 if new_day == 0 {
189 return NaiveDate::from_ymd_opt(new_year, new_month, 1).unwrap();
191 }
192 }
193}
194
195fn coupons_remaining(settlement: &NaiveDate, maturity: &NaiveDate, frequency: i32) -> i32 {
197 let months_between_coupons = 12 / frequency;
198 let mut count = 0;
199 let mut coupon_date = coupon_date_after(settlement, maturity, frequency);
200
201 while coupon_date <= *maturity {
202 count += 1;
203 coupon_date = add_months(&coupon_date, months_between_coupons);
204 }
205 count
206}
207
208#[derive(Debug)]
211pub struct AccrintFn;
212
213impl Function for AccrintFn {
214 func_caps!(PURE);
215 fn name(&self) -> &'static str {
216 "ACCRINT"
217 }
218 fn min_args(&self) -> usize {
219 6
220 }
221 fn variadic(&self) -> bool {
222 true
223 }
224 fn arg_schema(&self) -> &'static [ArgSchema] {
225 use std::sync::LazyLock;
226 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
227 vec![
228 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ]
237 });
238 &SCHEMA[..]
239 }
240 fn eval<'a, 'b, 'c>(
241 &self,
242 args: &'c [ArgumentHandle<'a, 'b>],
243 _ctx: &dyn FunctionContext<'b>,
244 ) -> Result<CalcValue<'b>, ExcelError> {
245 if args.len() < 6 {
247 return Ok(CalcValue::Scalar(LiteralValue::Error(
248 ExcelError::new_value(),
249 )));
250 }
251
252 let issue_serial = coerce_num(&args[0])?;
253 let first_interest_serial = coerce_num(&args[1])?;
254 let settlement_serial = coerce_num(&args[2])?;
255 let rate = coerce_num(&args[3])?;
256 let par = coerce_num(&args[4])?;
257 let frequency = coerce_num(&args[5])?.trunc() as i32;
258 let basis_int = if args.len() > 6 {
259 coerce_num(&args[6])?.trunc() as i32
260 } else {
261 0
262 };
263 let calc_method = if args.len() > 7 {
264 coerce_num(&args[7])?.trunc() as i32
265 } else {
266 1
267 };
268
269 if rate <= 0.0 || par <= 0.0 {
271 return Ok(CalcValue::Scalar(
272 LiteralValue::Error(ExcelError::new_num()),
273 ));
274 }
275 if frequency != 1 && frequency != 2 && frequency != 4 {
276 return Ok(CalcValue::Scalar(
277 LiteralValue::Error(ExcelError::new_num()),
278 ));
279 }
280
281 let basis = DayCountBasis::from_int(basis_int)?;
282
283 let issue = serial_to_date(issue_serial)?;
284 let first_interest = serial_to_date(first_interest_serial)?;
285 let settlement = serial_to_date(settlement_serial)?;
286
287 if settlement <= issue {
289 return Ok(CalcValue::Scalar(
290 LiteralValue::Error(ExcelError::new_num()),
291 ));
292 }
293
294 let accrued_interest = if calc_method != 0 {
298 let yf = year_fraction(&issue, &settlement, basis);
301 par * rate * yf
302 } else {
303 let prev_coupon = coupon_date_before(&settlement, &first_interest, frequency);
305 let start_date = if prev_coupon < issue {
306 issue
307 } else {
308 prev_coupon
309 };
310 let yf = year_fraction(&start_date, &settlement, basis);
311 par * rate * yf
312 };
313
314 Ok(CalcValue::Scalar(LiteralValue::Number(accrued_interest)))
315 }
316}
317
318#[derive(Debug)]
321pub struct AccrintmFn;
322
323impl Function for AccrintmFn {
324 func_caps!(PURE);
325 fn name(&self) -> &'static str {
326 "ACCRINTM"
327 }
328 fn min_args(&self) -> usize {
329 4
330 }
331 fn variadic(&self) -> bool {
332 true
333 }
334 fn arg_schema(&self) -> &'static [ArgSchema] {
335 use std::sync::LazyLock;
336 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
337 vec![
338 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ]
344 });
345 &SCHEMA[..]
346 }
347 fn eval<'a, 'b, 'c>(
348 &self,
349 args: &'c [ArgumentHandle<'a, 'b>],
350 _ctx: &dyn FunctionContext<'b>,
351 ) -> Result<CalcValue<'b>, ExcelError> {
352 if args.len() < 4 {
354 return Ok(CalcValue::Scalar(LiteralValue::Error(
355 ExcelError::new_value(),
356 )));
357 }
358
359 let issue_serial = coerce_num(&args[0])?;
360 let settlement_serial = coerce_num(&args[1])?;
361 let rate = coerce_num(&args[2])?;
362 let par = coerce_num(&args[3])?;
363 let basis_int = if args.len() > 4 {
364 coerce_num(&args[4])?.trunc() as i32
365 } else {
366 0
367 };
368
369 if rate <= 0.0 || par <= 0.0 {
371 return Ok(CalcValue::Scalar(
372 LiteralValue::Error(ExcelError::new_num()),
373 ));
374 }
375
376 let basis = DayCountBasis::from_int(basis_int)?;
377
378 let issue = serial_to_date(issue_serial)?;
379 let settlement = serial_to_date(settlement_serial)?;
380
381 if settlement <= issue {
383 return Ok(CalcValue::Scalar(
384 LiteralValue::Error(ExcelError::new_num()),
385 ));
386 }
387
388 let yf = year_fraction(&issue, &settlement, basis);
390 let accrued_interest = par * rate * yf;
391
392 Ok(CalcValue::Scalar(LiteralValue::Number(accrued_interest)))
393 }
394}
395
396#[derive(Debug)]
399pub struct PriceFn;
400
401impl Function for PriceFn {
402 func_caps!(PURE);
403 fn name(&self) -> &'static str {
404 "PRICE"
405 }
406 fn min_args(&self) -> usize {
407 6
408 }
409 fn variadic(&self) -> bool {
410 true
411 }
412 fn arg_schema(&self) -> &'static [ArgSchema] {
413 use std::sync::LazyLock;
414 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
415 vec![
416 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ]
424 });
425 &SCHEMA[..]
426 }
427 fn eval<'a, 'b, 'c>(
428 &self,
429 args: &'c [ArgumentHandle<'a, 'b>],
430 _ctx: &dyn FunctionContext<'b>,
431 ) -> Result<CalcValue<'b>, ExcelError> {
432 if args.len() < 6 {
434 return Ok(CalcValue::Scalar(LiteralValue::Error(
435 ExcelError::new_value(),
436 )));
437 }
438
439 let settlement_serial = coerce_num(&args[0])?;
440 let maturity_serial = coerce_num(&args[1])?;
441 let rate = coerce_num(&args[2])?;
442 let yld = coerce_num(&args[3])?;
443 let redemption = coerce_num(&args[4])?;
444 let frequency = coerce_num(&args[5])?.trunc() as i32;
445 let basis_int = if args.len() > 6 {
446 coerce_num(&args[6])?.trunc() as i32
447 } else {
448 0
449 };
450
451 if rate < 0.0 || yld < 0.0 || redemption <= 0.0 {
453 return Ok(CalcValue::Scalar(
454 LiteralValue::Error(ExcelError::new_num()),
455 ));
456 }
457 if frequency != 1 && frequency != 2 && frequency != 4 {
458 return Ok(CalcValue::Scalar(
459 LiteralValue::Error(ExcelError::new_num()),
460 ));
461 }
462
463 let basis = DayCountBasis::from_int(basis_int)?;
464
465 let settlement = serial_to_date(settlement_serial)?;
466 let maturity = serial_to_date(maturity_serial)?;
467
468 if maturity <= settlement {
470 return Ok(CalcValue::Scalar(
471 LiteralValue::Error(ExcelError::new_num()),
472 ));
473 }
474
475 let price = calculate_price(
476 &settlement,
477 &maturity,
478 rate,
479 yld,
480 redemption,
481 frequency,
482 basis,
483 );
484 Ok(CalcValue::Scalar(LiteralValue::Number(price)))
485 }
486}
487
488fn calculate_price(
490 settlement: &NaiveDate,
491 maturity: &NaiveDate,
492 rate: f64,
493 yld: f64,
494 redemption: f64,
495 frequency: i32,
496 basis: DayCountBasis,
497) -> f64 {
498 let n = coupons_remaining(settlement, maturity, frequency);
499 let coupon = 100.0 * rate / frequency as f64;
500
501 let next_coupon = coupon_date_after(settlement, maturity, frequency);
503 let prev_coupon = coupon_date_before(settlement, maturity, frequency);
504
505 let days_to_next = days_between(settlement, &next_coupon, basis) as f64;
507 let days_in_period = days_between(&prev_coupon, &next_coupon, basis) as f64;
508
509 let dsn = if days_in_period > 0.0 {
510 days_to_next / days_in_period
511 } else {
512 0.0
513 };
514
515 let yld_per_period = yld / frequency as f64;
516
517 if n == 1 {
518 (redemption + coupon) / (1.0 + dsn * yld_per_period) - (1.0 - dsn) * coupon
521 } else {
522 let discount_factor = 1.0 + yld_per_period;
525
526 let first_discount = discount_factor.powf(dsn);
528
529 let mut pv_coupons = 0.0;
531 for k in 0..n {
532 let discount = first_discount * discount_factor.powi(k);
533 pv_coupons += coupon / discount;
534 }
535
536 let pv_redemption = redemption / (first_discount * discount_factor.powi(n - 1));
538
539 let accrued = (1.0 - dsn) * coupon;
541
542 pv_coupons + pv_redemption - accrued
543 }
544}
545
546#[derive(Debug)]
549pub struct YieldFn;
550
551impl Function for YieldFn {
552 func_caps!(PURE);
553 fn name(&self) -> &'static str {
554 "YIELD"
555 }
556 fn min_args(&self) -> usize {
557 6
558 }
559 fn variadic(&self) -> bool {
560 true
561 }
562 fn arg_schema(&self) -> &'static [ArgSchema] {
563 use std::sync::LazyLock;
564 static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
565 vec![
566 ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ArgSchema::number_lenient_scalar(), ]
574 });
575 &SCHEMA[..]
576 }
577 fn eval<'a, 'b, 'c>(
578 &self,
579 args: &'c [ArgumentHandle<'a, 'b>],
580 _ctx: &dyn FunctionContext<'b>,
581 ) -> Result<CalcValue<'b>, ExcelError> {
582 if args.len() < 6 {
584 return Ok(CalcValue::Scalar(LiteralValue::Error(
585 ExcelError::new_value(),
586 )));
587 }
588
589 let settlement_serial = coerce_num(&args[0])?;
590 let maturity_serial = coerce_num(&args[1])?;
591 let rate = coerce_num(&args[2])?;
592 let pr = coerce_num(&args[3])?;
593 let redemption = coerce_num(&args[4])?;
594 let frequency = coerce_num(&args[5])?.trunc() as i32;
595 let basis_int = if args.len() > 6 {
596 coerce_num(&args[6])?.trunc() as i32
597 } else {
598 0
599 };
600
601 if rate < 0.0 || pr <= 0.0 || redemption <= 0.0 {
603 return Ok(CalcValue::Scalar(
604 LiteralValue::Error(ExcelError::new_num()),
605 ));
606 }
607 if frequency != 1 && frequency != 2 && frequency != 4 {
608 return Ok(CalcValue::Scalar(
609 LiteralValue::Error(ExcelError::new_num()),
610 ));
611 }
612
613 let basis = DayCountBasis::from_int(basis_int)?;
614
615 let settlement = serial_to_date(settlement_serial)?;
616 let maturity = serial_to_date(maturity_serial)?;
617
618 if maturity <= settlement {
620 return Ok(CalcValue::Scalar(
621 LiteralValue::Error(ExcelError::new_num()),
622 ));
623 }
624
625 let yld = calculate_yield(
627 &settlement,
628 &maturity,
629 rate,
630 pr,
631 redemption,
632 frequency,
633 basis,
634 );
635
636 match yld {
637 Some(y) => Ok(CalcValue::Scalar(LiteralValue::Number(y))),
638 None => Ok(CalcValue::Scalar(
639 LiteralValue::Error(ExcelError::new_num()),
640 )),
641 }
642 }
643}
644
645fn calculate_yield(
647 settlement: &NaiveDate,
648 maturity: &NaiveDate,
649 rate: f64,
650 target_price: f64,
651 redemption: f64,
652 frequency: i32,
653 basis: DayCountBasis,
654) -> Option<f64> {
655 const MAX_ITER: i32 = 100;
656 const EPSILON: f64 = 1e-10;
657
658 let mut yld = rate;
660 if yld == 0.0 {
661 yld = 0.05; }
663
664 for _ in 0..MAX_ITER {
665 let price = calculate_price(
666 settlement, maturity, rate, yld, redemption, frequency, basis,
667 );
668 let diff = price - target_price;
669
670 if diff.abs() < EPSILON {
671 return Some(yld);
672 }
673
674 let delta = 0.0001;
676 let price_up = calculate_price(
677 settlement,
678 maturity,
679 rate,
680 yld + delta,
681 redemption,
682 frequency,
683 basis,
684 );
685 let derivative = (price_up - price) / delta;
686
687 if derivative.abs() < EPSILON {
688 return None;
689 }
690
691 let new_yld = yld - diff / derivative;
692
693 if new_yld < -0.99 {
695 yld = -0.99;
696 } else {
697 yld = new_yld;
698 }
699
700 if yld > 10.0 {
702 yld = 10.0;
703 }
704 }
705
706 let final_price = calculate_price(
708 settlement, maturity, rate, yld, redemption, frequency, basis,
709 );
710 if (final_price - target_price).abs() < 0.01 {
711 Some(yld)
712 } else {
713 None
714 }
715}
716
717pub fn register_builtins() {
718 use std::sync::Arc;
719 crate::function_registry::register_function(Arc::new(AccrintFn));
720 crate::function_registry::register_function(Arc::new(AccrintmFn));
721 crate::function_registry::register_function(Arc::new(PriceFn));
722 crate::function_registry::register_function(Arc::new(YieldFn));
723}