Skip to main content

formualizer_eval/builtins/financial/
bonds.rs

1//! Bond pricing functions: ACCRINT, ACCRINTM, PRICE, YIELD
2
3use 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/// Day count basis calculation
28/// Returns (num_days, year_basis) for the given basis type
29#[derive(Debug, Clone, Copy, PartialEq)]
30enum DayCountBasis {
31    UsNasd30360 = 0,   // US (NASD) 30/360
32    ActualActual = 1,  // Actual/actual
33    Actual360 = 2,     // Actual/360
34    Actual365 = 3,     // Actual/365
35    European30360 = 4, // European 30/360
36}
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
51/// Check if a year is a leap year
52fn is_leap_year(year: i32) -> bool {
53    (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
54}
55
56/// Check if a date is the last day of the month
57fn 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
62/// Calculate days between two dates using the specified basis
63fn 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
73/// Calculate days using US (NASD) 30/360 method
74fn 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    // Adjust for last day of February
84    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
103/// Calculate days using European 30/360 method
104fn 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
123/// Get the annual basis (denominator for year fraction)
124fn 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            // Determine the average year length based on leap years in the period
131            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                // Average across years
137                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
149/// Calculate the year fraction between two dates
150fn 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
156/// Find the coupon date before settlement date
157fn 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    // Work backwards from maturity to find the coupon date just before settlement
162    while coupon_date >= *settlement {
163        coupon_date = add_months(&coupon_date, -months_between_coupons);
164    }
165    coupon_date
166}
167
168/// Find the coupon date after settlement date
169fn 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
175/// Add months to a date, handling end-of-month adjustments
176fn 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    // Try to keep the same day, but cap at month's end
182    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            // Fallback - should never reach here
190            return NaiveDate::from_ymd_opt(new_year, new_month, 1).unwrap();
191        }
192    }
193}
194
195/// Count the number of coupons remaining
196fn 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/// ACCRINT(issue, first_interest, settlement, rate, par, frequency, [basis], [calc_method])
209/// Returns accrued interest for a security that pays periodic interest
210#[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(), // issue
229                ArgSchema::number_lenient_scalar(), // first_interest
230                ArgSchema::number_lenient_scalar(), // settlement
231                ArgSchema::number_lenient_scalar(), // rate
232                ArgSchema::number_lenient_scalar(), // par
233                ArgSchema::number_lenient_scalar(), // frequency
234                ArgSchema::number_lenient_scalar(), // basis (optional)
235                ArgSchema::number_lenient_scalar(), // calc_method (optional)
236            ]
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        // Check minimum required arguments
246        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        // Validate inputs
270        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        // settlement must be after issue
288        if settlement <= issue {
289            return Ok(CalcValue::Scalar(
290                LiteralValue::Error(ExcelError::new_num()),
291            ));
292        }
293
294        // Calculate accrued interest
295        // If calc_method is TRUE (or 1), calculate from issue to settlement
296        // If calc_method is FALSE (or 0), calculate from last coupon to settlement
297        let accrued_interest = if calc_method != 0 {
298            // Calculate from issue date to settlement date
299            // ACCRINT = par * rate * year_fraction(issue, settlement)
300            let yf = year_fraction(&issue, &settlement, basis);
301            par * rate * yf
302        } else {
303            // Calculate from last coupon date to settlement
304            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/// ACCRINTM(issue, settlement, rate, par, [basis])
319/// Returns accrued interest for a security that pays interest at maturity
320#[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(), // issue
339                ArgSchema::number_lenient_scalar(), // settlement
340                ArgSchema::number_lenient_scalar(), // rate
341                ArgSchema::number_lenient_scalar(), // par
342                ArgSchema::number_lenient_scalar(), // basis (optional)
343            ]
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        // Check minimum required arguments
353        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        // Validate inputs
370        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        // settlement must be after issue
382        if settlement <= issue {
383            return Ok(CalcValue::Scalar(
384                LiteralValue::Error(ExcelError::new_num()),
385            ));
386        }
387
388        // ACCRINTM = par * rate * year_fraction(issue, settlement)
389        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/// PRICE(settlement, maturity, rate, yld, redemption, frequency, [basis])
397/// Returns price per $100 face value for a security that pays periodic interest
398#[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(), // settlement
417                ArgSchema::number_lenient_scalar(), // maturity
418                ArgSchema::number_lenient_scalar(), // rate (coupon rate)
419                ArgSchema::number_lenient_scalar(), // yld (yield)
420                ArgSchema::number_lenient_scalar(), // redemption
421                ArgSchema::number_lenient_scalar(), // frequency
422                ArgSchema::number_lenient_scalar(), // basis (optional)
423            ]
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        // Check minimum required arguments
433        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        // Validate inputs
452        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        // maturity must be after settlement
469        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
488/// Calculate bond price using standard bond pricing formula
489fn 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    // Find previous and next coupon dates
502    let next_coupon = coupon_date_after(settlement, maturity, frequency);
503    let prev_coupon = coupon_date_before(settlement, maturity, frequency);
504
505    // Calculate fraction of period from settlement to next coupon
506    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        // Short first period (single coupon remaining)
519        // Price = (redemption + coupon) / (1 + dsn * yld_per_period) - (1 - dsn) * coupon
520        let price = (redemption + coupon) / (1.0 + dsn * yld_per_period) - (1.0 - dsn) * coupon;
521        price
522    } else {
523        // Multiple coupons remaining
524        // Price = sum of discounted coupons + discounted redemption - accrued interest
525        let discount_factor = 1.0 + yld_per_period;
526
527        // Discount factor for first coupon (fractional period)
528        let first_discount = discount_factor.powf(dsn);
529
530        // Present value of coupon payments
531        let mut pv_coupons = 0.0;
532        for k in 0..n {
533            let discount = first_discount * discount_factor.powi(k);
534            pv_coupons += coupon / discount;
535        }
536
537        // Present value of redemption
538        let pv_redemption = redemption / (first_discount * discount_factor.powi(n - 1));
539
540        // Accrued interest (negative because we subtract it)
541        let accrued = (1.0 - dsn) * coupon;
542
543        pv_coupons + pv_redemption - accrued
544    }
545}
546
547/// YIELD(settlement, maturity, rate, pr, redemption, frequency, [basis])
548/// Returns yield of a security that pays periodic interest
549#[derive(Debug)]
550pub struct YieldFn;
551
552impl Function for YieldFn {
553    func_caps!(PURE);
554    fn name(&self) -> &'static str {
555        "YIELD"
556    }
557    fn min_args(&self) -> usize {
558        6
559    }
560    fn variadic(&self) -> bool {
561        true
562    }
563    fn arg_schema(&self) -> &'static [ArgSchema] {
564        use std::sync::LazyLock;
565        static SCHEMA: LazyLock<Vec<ArgSchema>> = LazyLock::new(|| {
566            vec![
567                ArgSchema::number_lenient_scalar(), // settlement
568                ArgSchema::number_lenient_scalar(), // maturity
569                ArgSchema::number_lenient_scalar(), // rate (coupon rate)
570                ArgSchema::number_lenient_scalar(), // pr (price)
571                ArgSchema::number_lenient_scalar(), // redemption
572                ArgSchema::number_lenient_scalar(), // frequency
573                ArgSchema::number_lenient_scalar(), // basis (optional)
574            ]
575        });
576        &SCHEMA[..]
577    }
578    fn eval<'a, 'b, 'c>(
579        &self,
580        args: &'c [ArgumentHandle<'a, 'b>],
581        _ctx: &dyn FunctionContext<'b>,
582    ) -> Result<CalcValue<'b>, ExcelError> {
583        // Check minimum required arguments
584        if args.len() < 6 {
585            return Ok(CalcValue::Scalar(LiteralValue::Error(
586                ExcelError::new_value(),
587            )));
588        }
589
590        let settlement_serial = coerce_num(&args[0])?;
591        let maturity_serial = coerce_num(&args[1])?;
592        let rate = coerce_num(&args[2])?;
593        let pr = coerce_num(&args[3])?;
594        let redemption = coerce_num(&args[4])?;
595        let frequency = coerce_num(&args[5])?.trunc() as i32;
596        let basis_int = if args.len() > 6 {
597            coerce_num(&args[6])?.trunc() as i32
598        } else {
599            0
600        };
601
602        // Validate inputs
603        if rate < 0.0 || pr <= 0.0 || redemption <= 0.0 {
604            return Ok(CalcValue::Scalar(
605                LiteralValue::Error(ExcelError::new_num()),
606            ));
607        }
608        if frequency != 1 && frequency != 2 && frequency != 4 {
609            return Ok(CalcValue::Scalar(
610                LiteralValue::Error(ExcelError::new_num()),
611            ));
612        }
613
614        let basis = DayCountBasis::from_int(basis_int)?;
615
616        let settlement = serial_to_date(settlement_serial)?;
617        let maturity = serial_to_date(maturity_serial)?;
618
619        // maturity must be after settlement
620        if maturity <= settlement {
621            return Ok(CalcValue::Scalar(
622                LiteralValue::Error(ExcelError::new_num()),
623            ));
624        }
625
626        // Use Newton-Raphson to find yield where price = target price
627        let yld = calculate_yield(
628            &settlement,
629            &maturity,
630            rate,
631            pr,
632            redemption,
633            frequency,
634            basis,
635        );
636
637        match yld {
638            Some(y) => Ok(CalcValue::Scalar(LiteralValue::Number(y))),
639            None => Ok(CalcValue::Scalar(
640                LiteralValue::Error(ExcelError::new_num()),
641            )),
642        }
643    }
644}
645
646/// Calculate yield using Newton-Raphson iteration
647fn calculate_yield(
648    settlement: &NaiveDate,
649    maturity: &NaiveDate,
650    rate: f64,
651    target_price: f64,
652    redemption: f64,
653    frequency: i32,
654    basis: DayCountBasis,
655) -> Option<f64> {
656    const MAX_ITER: i32 = 100;
657    const EPSILON: f64 = 1e-10;
658
659    // Initial guess based on coupon rate
660    let mut yld = rate;
661    if yld == 0.0 {
662        yld = 0.05; // Default guess if rate is 0
663    }
664
665    for _ in 0..MAX_ITER {
666        let price = calculate_price(
667            settlement, maturity, rate, yld, redemption, frequency, basis,
668        );
669        let diff = price - target_price;
670
671        if diff.abs() < EPSILON {
672            return Some(yld);
673        }
674
675        // Calculate derivative numerically
676        let delta = 0.0001;
677        let price_up = calculate_price(
678            settlement,
679            maturity,
680            rate,
681            yld + delta,
682            redemption,
683            frequency,
684            basis,
685        );
686        let derivative = (price_up - price) / delta;
687
688        if derivative.abs() < EPSILON {
689            return None;
690        }
691
692        let new_yld = yld - diff / derivative;
693
694        // Prevent yield from going too negative
695        if new_yld < -0.99 {
696            yld = -0.99;
697        } else {
698            yld = new_yld;
699        }
700
701        // Prevent yield from going too high
702        if yld > 10.0 {
703            yld = 10.0;
704        }
705    }
706
707    // If close enough after max iterations, return the result
708    let final_price = calculate_price(
709        settlement, maturity, rate, yld, redemption, frequency, basis,
710    );
711    if (final_price - target_price).abs() < 0.01 {
712        Some(yld)
713    } else {
714        None
715    }
716}
717
718pub fn register_builtins() {
719    use std::sync::Arc;
720    crate::function_registry::register_function(Arc::new(AccrintFn));
721    crate::function_registry::register_function(Arc::new(AccrintmFn));
722    crate::function_registry::register_function(Arc::new(PriceFn));
723    crate::function_registry::register_function(Arc::new(YieldFn));
724}