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        (redemption + coupon) / (1.0 + dsn * yld_per_period) - (1.0 - dsn) * coupon
521    } else {
522        // Multiple coupons remaining
523        // Price = sum of discounted coupons + discounted redemption - accrued interest
524        let discount_factor = 1.0 + yld_per_period;
525
526        // Discount factor for first coupon (fractional period)
527        let first_discount = discount_factor.powf(dsn);
528
529        // Present value of coupon payments
530        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        // Present value of redemption
537        let pv_redemption = redemption / (first_discount * discount_factor.powi(n - 1));
538
539        // Accrued interest (negative because we subtract it)
540        let accrued = (1.0 - dsn) * coupon;
541
542        pv_coupons + pv_redemption - accrued
543    }
544}
545
546/// YIELD(settlement, maturity, rate, pr, redemption, frequency, [basis])
547/// Returns yield of a security that pays periodic interest
548#[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(), // settlement
567                ArgSchema::number_lenient_scalar(), // maturity
568                ArgSchema::number_lenient_scalar(), // rate (coupon rate)
569                ArgSchema::number_lenient_scalar(), // pr (price)
570                ArgSchema::number_lenient_scalar(), // redemption
571                ArgSchema::number_lenient_scalar(), // frequency
572                ArgSchema::number_lenient_scalar(), // basis (optional)
573            ]
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        // Check minimum required arguments
583        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        // Validate inputs
602        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        // maturity must be after settlement
619        if maturity <= settlement {
620            return Ok(CalcValue::Scalar(
621                LiteralValue::Error(ExcelError::new_num()),
622            ));
623        }
624
625        // Use Newton-Raphson to find yield where price = target price
626        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
645/// Calculate yield using Newton-Raphson iteration
646fn 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    // Initial guess based on coupon rate
659    let mut yld = rate;
660    if yld == 0.0 {
661        yld = 0.05; // Default guess if rate is 0
662    }
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        // Calculate derivative numerically
675        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        // Prevent yield from going too negative
694        if new_yld < -0.99 {
695            yld = -0.99;
696        } else {
697            yld = new_yld;
698        }
699
700        // Prevent yield from going too high
701        if yld > 10.0 {
702            yld = 10.0;
703        }
704    }
705
706    // If close enough after max iterations, return the result
707    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}