From 17b29b891d1bc99d168e1ae87960a9cb83b7cbeb Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 19:37:47 +0000 Subject: [PATCH] refactor(functions): move financial.rs items to financial/ --- crates/functions/src/financial.rs | 1320 +---------------- crates/functions/src/financial/day_count.rs | 214 +++ .../functions/src/financial/depreciation.rs | 173 +++ crates/functions/src/financial/securities.rs | 512 +++++++ crates/functions/src/financial/tvm.rs | 415 ++++++ 5 files changed, 1328 insertions(+), 1306 deletions(-) create mode 100644 crates/functions/src/financial/day_count.rs create mode 100644 crates/functions/src/financial/depreciation.rs create mode 100644 crates/functions/src/financial/securities.rs create mode 100644 crates/functions/src/financial/tvm.rs diff --git a/crates/functions/src/financial.rs b/crates/functions/src/financial.rs index 22c926185..b569c1617 100644 --- a/crates/functions/src/financial.rs +++ b/crates/functions/src/financial.rs @@ -9,1318 +9,26 @@ use gridcraft_core::{CellError, DateSystem, Value}; use crate::util::{R, S, array_numbers, as_array, flatten, has, num, num_val, numbers, opt_bool, opt_num, round_half_away, text, to_sig_digits}; use crate::{Arg, Ctx, FnSpec, VAR}; -// --------------------------------------------------------------------------------------------- -// Time value of money -// --------------------------------------------------------------------------------------------- +mod day_count; +mod depreciation; +mod securities; +mod tvm; -/// The payment per period. `(1 + rate)^nper` comes from `nper * ln_1p(rate)` and `p - 1` from -/// `exp_m1`: rounding `1 + rate` first throws away the low bits of a small rate, which left PMT up to -/// about 1000 ULPs from Excel (observed over 400 synthetic probes; this form is within 4). A loan -/// schedule's last `IF(p - SUM(principal) > 0, ...)` turns on those bits. Excel gives #NUM! for a -/// rate of -1 or less, and -pv*rate (no divide) once `(1 + rate)^nper` overflows. -fn pmt_calc(rate: f64, nper: f64, pv: f64, fv: f64, typ: f64) -> f64 { - if rate == 0.0 { - return -(pv + fv) / nper; - } - if rate <= -1.0 { - return f64::NAN; - } - let l = nper * rate.ln_1p(); - let m = l.exp_m1(); - let k = 1.0 + rate * typ; - if m > 0.0 { - // (fv + pv*p) / (p - 1) = pv + (pv + fv) / (p - 1), without the cancellation in p - 1. - // rate / m (about 1 / nper for a tiny rate) is taken first so (pv + fv) / m can't overflow. - -(pv * rate + (pv + fv) * (rate / m)) / k - } else if m > -0.5 { - // p near 1 from below: fv + pv*p = (pv + fv) + pv*m keeps the small terms that rounding p would drop. - -(rate * (pv + fv + pv * m)) / (k * m) - } else { - // p well below 1: m nears -1 and pv*m would swallow pv + fv, so use p itself. - -(rate * (fv + pv * l.exp())) / (k * m) - } -} - -fn fv_calc(rate: f64, nper: f64, pmt: f64, pv: f64, typ: f64) -> f64 { - if rate == 0.0 { - return -(pv + pmt * nper); - } - let p = (1.0 + rate).powf(nper); - -(pv * p + pmt * (1.0 + rate * typ) * (p - 1.0) / rate) -} - -fn pv_calc(rate: f64, nper: f64, pmt: f64, fv: f64, typ: f64) -> f64 { - if rate == 0.0 { - return -(fv + pmt * nper); - } - let p = (1.0 + rate).powf(nper); - -(fv + pmt * (1.0 + rate * typ) * (p - 1.0) / rate) / p -} +use day_count::{coupdaybs, coupdays, coupdaysnc, coupncd, coupnum, couppcd}; +use depreciation::{amordegrc, amorlinc, db, ddb, sln, syd, vdb}; +use securities::{ + accrint, accrintm, disc, duration, intrate, mduration, oddfprice, oddfyield, oddlprice, oddlyield, price, pricedisc, pricemat, received, tbilleq, + tbillprice, tbillyield, yield_, yielddisc, yieldmat, +}; +use tvm::{ + cumipmt, cumprinc, dollarde, dollarfr, effect, fv, fvschedule, ipmt, irr, ispmt, mirr, nominal, nper, npv, pduration, pmt, ppmt, pv, rate, rri, + xirr, xnpv, +}; fn type_arg(args: &[Arg], i: usize) -> R { Ok(if opt_num(args, i, 0.0)? != 0.0 { 1.0 } else { 0.0 }) } -fn pmt(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, nper, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - let (fv, typ) = (opt_num(a, 3, 0.0)?, type_arg(a, 4)?); - if nper == 0.0 { - return Err(CellError::Num); - } - num_val(pmt_calc(rate, nper, pv, fv, typ)) -} - -fn fv(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, nper, pmt) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - num_val(fv_calc(rate, nper, pmt, opt_num(a, 3, 0.0)?, type_arg(a, 4)?)) -} - -fn pv(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, nper, pmt) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - num_val(pv_calc(rate, nper, pmt, opt_num(a, 3, 0.0)?, type_arg(a, 4)?)) -} - -fn nper(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, pmt, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - let (fv, typ) = (opt_num(a, 3, 0.0)?, type_arg(a, 4)?); - if rate == 0.0 { - if pmt == 0.0 { - return Err(CellError::Num); - } - return num_val(-(pv + fv) / pmt); - } - let numer = pmt * (1.0 + rate * typ) - fv * rate; - let denom = pv * rate + pmt * (1.0 + rate * typ); - if denom == 0.0 { - return Err(CellError::Num); - } - let q = numer / denom; - if q <= 0.0 || rate <= -1.0 { - return Err(CellError::Num); - } - num_val(q.ln() / (1.0 + rate).ln()) -} - -fn ipmt_calc(rate: f64, per: f64, nper: f64, pv: f64, fv: f64, typ: f64) -> f64 { - let p = pmt_calc(rate, nper, pv, fv, typ); - if typ == 1.0 && per == 1.0 { - return 0.0; - } - let bal = fv_calc(rate, per - 1.0, p, pv, typ); - let i = bal * rate; - if typ == 1.0 { i / (1.0 + rate) } else { i } -} - -fn ipmt(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, per, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); - let (fv, typ) = (opt_num(a, 4, 0.0)?, type_arg(a, 5)?); - if per < 1.0 || per > np || np == 0.0 { - return Err(CellError::Num); - } - num_val(ipmt_calc(rate, per, np, pv, fv, typ)) -} - -fn ppmt(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, per, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); - let (fv, typ) = (opt_num(a, 4, 0.0)?, type_arg(a, 5)?); - if per < 1.0 || per > np || np == 0.0 { - return Err(CellError::Num); - } - num_val(pmt_calc(rate, np, pv, fv, typ) - ipmt_calc(rate, per, np, pv, fv, typ)) -} - -fn cumulative(a: &[Arg], principal: bool) -> R { - let (rate, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - let (start, end, typ) = (num(a, 3)?.trunc(), num(a, 4)?.trunc(), num(a, 5)?); - let np_i = np.trunc(); - if rate <= 0.0 || np_i <= 0.0 || pv <= 0.0 || start < 1.0 || end < start || end > np_i || (typ != 0.0 && typ != 1.0) { - return Err(CellError::Num); - } - if end - start > 1e7 || np_i > 1e15 { - return Err(CellError::Num); - } - let p = pmt_calc(rate, np_i, pv, 0.0, typ); - let mut sum = 0.0; - let mut per = start; - while per <= end { - let i = ipmt_calc(rate, per, np_i, pv, 0.0, typ); - sum += if principal { p - i } else { i }; - per += 1.0; - } - num_val(sum) -} - -fn cumipmt(a: &[Arg], _c: &mut dyn Ctx) -> R { - cumulative(a, false) -} - -fn cumprinc(a: &[Arg], _c: &mut dyn Ctx) -> R { - cumulative(a, true) -} - -/// Newton iteration with a numeric derivative, keeping the rate above -1. -fn newton(f: impl Fn(f64) -> f64, guess: f64) -> Option { - let mut x = guess; - for _ in 0..200 { - let y = f(x); - if !y.is_finite() { - return None; - } - let h = 1e-6 * x.abs().max(1e-3); - let d = (f(x + h) - f(x - h)) / (2.0 * h); - if d == 0.0 || !d.is_finite() { - return None; - } - let mut nx = x - y / d; - if nx <= -1.0 { - nx = (x - 1.0) / 2.0; - if nx <= -1.0 { - nx = -0.999_999_999; - } - } - if (nx - x).abs() < 1e-12 * nx.abs().max(1.0) { - return Some(nx); - } - x = nx; - } - None -} - -/// Tries Newton from the guess and a few fallback starting points; then bisection over a -/// bracket if one can be found. -fn solve_rate(f: impl Fn(f64) -> f64, guess: f64) -> R { - let check = |r: f64| f(r).is_finite() && f(r).abs() < 1e-7 * (1.0 + f(guess).abs().min(1e12)); - for g in [guess, 0.1, 0.01, 0.5, -0.5, 1.0, 0.001, 5.0, -0.9, 10.0] { - if let Some(r) = newton(&f, g) - && r > -1.0 - && check(r) - { - return Ok(r); - } - } - // Scan for a sign change and bisect. - let grid: Vec = [-0.999_999, -0.99, -0.9, -0.5, -0.2, 0.0, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 100.0, 1000.0].to_vec(); - for w in grid.windows(2) { - let (mut lo, mut hi) = match w { - [a, b] => (*a, *b), - _ => continue, - }; - let (flo, fhi) = (f(lo), f(hi)); - if !(flo.is_finite() && fhi.is_finite()) || flo * fhi > 0.0 { - continue; - } - let lo_neg = flo < 0.0; - for _ in 0..200 { - let mid = 0.5 * (lo + hi); - let fm = f(mid); - if (fm < 0.0) == lo_neg { - lo = mid; - } else { - hi = mid; - } - } - return Ok(0.5 * (lo + hi)); - } - Err(CellError::Num) -} - -fn rate(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (np, pmt, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - let (fv, typ, guess) = (opt_num(a, 3, 0.0)?, type_arg(a, 4)?, opt_num(a, 5, 0.1)?); - if np <= 0.0 { - return Err(CellError::Num); - } - let f = |r: f64| { - if r.abs() < 1e-12 { - pv + pmt * np + fv - } else { - let p = (1.0 + r).powf(np); - pv * p + pmt * (1.0 + r * typ) * (p - 1.0) / r + fv - } - }; - num_val(solve_rate(f, guess)?) -} - -fn npv(a: &[Arg], _c: &mut dyn Ctx) -> R { - let rate = num(a, 0)?; - if rate == -1.0 { - return Err(CellError::Div0); - } - let vals = numbers(a.get(1..).unwrap_or(&[]))?; - let mut sum = 0.0; - let mut d = 1.0; - for v in vals { - d *= 1.0 + rate; - sum += v / d; - } - num_val(sum) -} - -/// Values and dates for XNPV/XIRR: both must be numeric and of equal length. -fn dated_flows(values: &Value, dates: &Value) -> R<(Vec, Vec)> { - let va = as_array(values); - let da = as_array(dates); - if va.data.len() != da.data.len() { - return Err(CellError::Num); - } - let mut v = Vec::with_capacity(va.data.len()); - let mut d = Vec::with_capacity(da.data.len()); - for (x, y) in va.data.iter().zip(da.data.iter()) { - match x { - Value::Number(n) => v.push(*n), - Value::Error(e) => return Err(*e), - _ => return Err(CellError::Value), - } - match y { - Value::Number(n) => d.push(n.trunc()), - Value::Error(e) => return Err(*e), - Value::Text(_) => d.push(y.to_number()?.trunc()), - _ => return Err(CellError::Value), - } - } - let d0 = *d.first().ok_or(CellError::Num)?; - if d.iter().any(|&x| x < d0 || x < 0.0) { - return Err(CellError::Num); - } - Ok((v, d)) -} - -fn xnpv_calc(rate: f64, v: &[f64], d: &[f64]) -> f64 { - let d0 = d.first().copied().unwrap_or(0.0); - v.iter().zip(d).map(|(x, t)| x / (1.0 + rate).powf((t - d0) / 365.0)).sum() -} - -fn xnpv(a: &[Arg], _c: &mut dyn Ctx) -> R { - let rate = num(a, 0)?; - if rate <= -1.0 { - return Err(CellError::Num); - } - let (v, d) = dated_flows(&a.get(1).ok_or(CellError::Value)?.value, &a.get(2).ok_or(CellError::Value)?.value)?; - num_val(xnpv_calc(rate, &v, &d)) -} - -fn xirr(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (v, d) = dated_flows(&a.first().ok_or(CellError::Value)?.value, &a.get(1).ok_or(CellError::Value)?.value)?; - let guess = opt_num(a, 2, 0.1)?; - if !v.iter().any(|x| *x > 0.0) || !v.iter().any(|x| *x < 0.0) { - return Err(CellError::Num); - } - num_val(solve_rate(|r| xnpv_calc(r, &v, &d), guess)?) -} - -fn irr(a: &[Arg], _c: &mut dyn Ctx) -> R { - let v = array_numbers(&a.first().ok_or(CellError::Value)?.value)?; - let guess = opt_num(a, 1, 0.1)?; - if !v.iter().any(|x| *x > 0.0) || !v.iter().any(|x| *x < 0.0) { - return Err(CellError::Num); - } - let f = |r: f64| { - let mut s = 0.0; - let mut d = 1.0; - for x in &v { - s += x / d; - d *= 1.0 + r; - } - s - }; - num_val(solve_rate(f, guess)?) -} - -fn mirr(a: &[Arg], _c: &mut dyn Ctx) -> R { - let v = array_numbers(&a.first().ok_or(CellError::Value)?.value)?; - let (fr, rr) = (num(a, 1)?, num(a, 2)?); - let n = v.len() as f64; - if v.len() < 2 { - return Err(CellError::Div0); - } - let (mut pos, mut neg) = (0.0, 0.0); - for (i, x) in v.iter().enumerate() { - if *x > 0.0 { - pos += x / (1.0 + rr).powi(i as i32); - } else if *x < 0.0 { - neg += x / (1.0 + fr).powi(i as i32); - } - } - // No positive flow is not an error in Excel: the formula below gives -1. - if neg == 0.0 { - return Err(CellError::Div0); - } - let r = (-pos * (1.0 + rr).powf(n - 1.0) / neg).powf(1.0 / (n - 1.0)) - 1.0; - num_val(r) -} - -fn fvschedule(a: &[Arg], _c: &mut dyn Ctx) -> R { - let mut p = num(a, 0)?; - for v in flatten(a.get(1..2).unwrap_or(&[])) { - let r = match v { - Value::Number(n) => n, - Value::Empty => 0.0, - Value::Error(e) => return Err(e), - _ => return Err(CellError::Value), - }; - p *= 1.0 + r; - } - num_val(p) -} - -fn effect(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (nom, n) = (num(a, 0)?, num(a, 1)?.trunc()); - if nom <= 0.0 || n < 1.0 { - return Err(CellError::Num); - } - num_val((1.0 + nom / n).powf(n) - 1.0) -} - -fn nominal(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (eff, n) = (num(a, 0)?, num(a, 1)?.trunc()); - if eff <= 0.0 || n < 1.0 { - return Err(CellError::Num); - } - num_val(n * ((1.0 + eff).powf(1.0 / n) - 1.0)) -} - -fn ispmt(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, per, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); - if np == 0.0 { - return Err(CellError::Div0); - } - num_val(pv * rate * (per / np - 1.0)) -} - -fn pduration(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (rate, pv, fv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - if rate <= 0.0 || pv <= 0.0 || fv <= 0.0 { - return Err(CellError::Num); - } - num_val((fv.ln() - pv.ln()) / (1.0 + rate).ln()) -} - -fn rri(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (np, pv, fv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - if np <= 0.0 || pv == 0.0 { - return Err(CellError::Num); - } - num_val((fv / pv).powf(1.0 / np) - 1.0) -} - -fn dollar_digits(fraction: f64) -> R { - if fraction < 0.0 { - return Err(CellError::Num); - } - if fraction < 1.0 { - return Err(CellError::Div0); - } - Ok(10f64.powf(fraction.log10().ceil())) -} - -fn dollarde(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (x, f) = (num(a, 0)?, num(a, 1)?.trunc()); - let p = dollar_digits(f)?; - let int = x.trunc(); - num_val(int + (x - int) * p / f) -} - -fn dollarfr(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (x, f) = (num(a, 0)?, num(a, 1)?.trunc()); - let p = dollar_digits(f)?; - let int = x.trunc(); - num_val(int + (x - int) * f / p) -} - -// --------------------------------------------------------------------------------------------- -// Depreciation -// --------------------------------------------------------------------------------------------- - -fn sln(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (cost, salvage, life) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); - if life == 0.0 { - return Err(CellError::Div0); - } - num_val((cost - salvage) / life) -} - -fn syd(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (cost, salvage, life, per) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); - if life <= 0.0 || per <= 0.0 || per > life || salvage < 0.0 { - return Err(CellError::Num); - } - num_val((cost - salvage) * (life - per + 1.0) * 2.0 / (life * (life + 1.0))) -} - -fn db(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (cost, salvage, life, period) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?.trunc()); - let month = opt_num(a, 4, 12.0)?.trunc(); - if cost < 0.0 || salvage < 0.0 || life <= 0.0 || period <= 0.0 || !(1.0..=12.0).contains(&month) { - return Err(CellError::Num); - } - let max_period = if month == 12.0 { life.trunc() } else { life.trunc() + 1.0 }; - if period > max_period.max(1.0) || period > 1e7 { - return Err(CellError::Num); - } - if cost == 0.0 { - return num_val(0.0); - } - let rate = crate::util::round_half_away(1.0 - (salvage / cost).powf(1.0 / life), 3); - let mut total = 0.0; - let mut dep = 0.0; - let mut p = 1.0; - while p <= period { - dep = if p == 1.0 { - cost * rate * month / 12.0 - } else if p == life.trunc() + 1.0 { - (cost - total) * rate * (12.0 - month) / 12.0 - } else { - (cost - total) * rate - }; - total += dep; - p += 1.0; - } - num_val(dep) -} - -fn ddb(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (cost, salvage, life, period) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); - let factor = opt_num(a, 4, 2.0)?; - if cost < 0.0 || salvage < 0.0 || life <= 0.0 || period <= 0.0 || factor <= 0.0 || period > life { - return Err(CellError::Num); - } - let mut rate = factor / life; - let old; - if rate >= 1.0 { - rate = 1.0; - old = if period == 1.0 { cost } else { 0.0 }; - } else { - old = cost * (1.0 - rate).powf(period - 1.0); - } - let new = cost * (1.0 - rate).powf(period); - let dep = if new < salvage { old - salvage } else { old - new }; - num_val(dep.max(0.0)) -} - -/// Per-period declining-balance depreciation with optional switch to straight line. -fn vdb_schedule(cost: f64, salvage: f64, life: f64, factor: f64, no_switch: bool, periods: usize) -> Vec { - let mut out = Vec::with_capacity(periods); - let mut book = cost; - let mut sl: Option = None; - for i in 0..periods { - let dep = if let Some(s) = sl { - s.min((book - salvage).max(0.0)) - } else { - let ddb = (book * factor / life).min((book - salvage).max(0.0)); - let remaining = life - i as f64; - let slv = if remaining > 0.0 { (book - salvage) / remaining.max(1.0) } else { 0.0 }; - if !no_switch && slv > ddb { - sl = Some(slv); - slv.min((book - salvage).max(0.0)) - } else { - ddb - } - }; - out.push(dep); - book -= dep; - } - out -} - -fn vdb(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (cost, salvage, life, start, end) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?, num(a, 4)?); - let factor = opt_num(a, 5, 2.0)?; - let no_switch = opt_bool(a, 6, false)?; - if cost < 0.0 || salvage < 0.0 || life <= 0.0 || start < 0.0 || end < start || end > life || factor <= 0.0 { - return Err(CellError::Num); - } - let periods = end.ceil(); - if periods > 1e7 { - return Err(CellError::Num); - } - let sched = vdb_schedule(cost, salvage, life, factor, no_switch, periods as usize); - let mut total = 0.0; - for (i, d) in sched.iter().enumerate() { - let (p0, p1) = (i as f64, i as f64 + 1.0); - let overlap = (p1.min(end) - p0.max(start)).max(0.0); - total += d * overlap; - } - num_val(total) -} - -fn amorlinc(a: &[Arg], c: &mut dyn Ctx) -> R { - let (cost, purchased, first, salvage) = (num(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?); - let (period, rate, basis) = (num(a, 4)?.trunc(), num(a, 5)?, basis_arg(a, 6)?); - if cost < 0.0 || salvage < 0.0 || salvage > cost || period < 0.0 || rate <= 0.0 || first < purchased { - return Err(CellError::Num); - } - let full = cost * rate; - let limit = cost - salvage; - let dep0 = (full * year_frac(c.date_system(), purchased, first, basis)?).min(limit); - if period == 0.0 { - return num_val(dep0); - } - let remaining = limit - dep0 - (period - 1.0) * full; - num_val(full.min(remaining.max(0.0))) -} - -fn amordegrc(a: &[Arg], c: &mut dyn Ctx) -> R { - let (cost, purchased, first, salvage) = (num(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?); - let (period, rate, basis) = (num(a, 4)?.trunc(), num(a, 5)?, basis_arg(a, 6)?); - if cost < 0.0 || salvage < 0.0 || salvage > cost || period < 0.0 || rate <= 0.0 || first < purchased || period > 1e6 { - return Err(CellError::Num); - } - let life = 1.0 / rate; - let coef = if life < 3.0 { - 1.0 - } else if life < 5.0 { - 1.5 - } else if life <= 6.0 { - 2.0 - } else { - 2.5 - }; - let r = rate * coef; - let mut book = cost; - let mut dep = (year_frac(c.date_system(), purchased, first, basis)? * r * cost).round(); - if period == 0.0 { - return num_val(dep); - } - book -= dep; - let mut rest = book - salvage; - let mut n = 1.0; - while n <= period { - dep = (r * book).round(); - rest -= dep; - if rest < 0.0 { - // The two final periods split what remains; afterwards nothing is left. - let left = period - n; - return num_val(if left <= 1.0 { (book * 0.5).round() } else { 0.0 }); - } - book -= dep; - n += 1.0; - } - num_val(dep) -} - -// --------------------------------------------------------------------------------------------- -// Dates and day counts -// --------------------------------------------------------------------------------------------- - -fn date(a: &[Arg], i: usize) -> R { - let d = num(a, i)?.trunc(); - if !(0.0..=2_958_465.0).contains(&d) { - return Err(CellError::Num); - } - Ok(d) -} - -fn ymd(sys: DateSystem, s: f64) -> R<(i32, u32, u32)> { - let dt = datetime_from_serial(sys, s.trunc()).ok_or(CellError::Num)?; - Ok((dt.year, dt.month, dt.day.max(1))) -} - -fn serial(sys: DateSystem, y: i32, m: u32, d: u32) -> R { - serial_from_ymd(sys, y as i64, m as i64, d as i64).ok_or(CellError::Num) -} - -fn basis_arg(a: &[Arg], i: usize) -> R { - let b = opt_num(a, i, 0.0)?.trunc(); - if !(0.0..=4.0).contains(&b) { - return Err(CellError::Num); - } - Ok(b as u32) -} - -fn freq_arg(a: &[Arg], i: usize) -> R { - let f = num(a, i)?.trunc(); - if f != 1.0 && f != 2.0 && f != 4.0 { - return Err(CellError::Num); - } - Ok(f) -} - -/// 30/360 day count (US/NASD when `us`, European otherwise). -fn days360(sys: DateSystem, s1: f64, s2: f64, us: bool) -> R { - let (y1, m1, mut d1) = ymd(sys, s1)?; - let (y2, m2, mut d2) = ymd(sys, s2)?; - if us { - let feb_end1 = m1 == 2 && d1 >= days_in_month(y1, 2); - let feb_end2 = m2 == 2 && d2 >= days_in_month(y2, 2); - if feb_end1 && feb_end2 { - d2 = 30; - } - if feb_end1 { - d1 = 30; - } - if d2 == 31 && d1 >= 30 { - d2 = 30; - } - if d1 == 31 { - d1 = 30; - } - } else { - d1 = d1.min(30); - d2 = d2.min(30); - } - Ok((y2 - y1) as f64 * 360.0 + (m2 as f64 - m1 as f64) * 30.0 + (d2 as f64 - d1 as f64)) -} - -/// Days between two dates under a basis. -fn day_count(sys: DateSystem, s1: f64, s2: f64, basis: u32) -> R { - match basis { - 0 => days360(sys, s1, s2, true), - 4 => days360(sys, s1, s2, false), - _ => Ok(s2 - s1), - } -} - -/// Year fraction between two dates under a basis (YEARFRAC semantics). -fn year_frac(sys: DateSystem, s1: f64, s2: f64, basis: u32) -> R { - let (s1, s2) = if s1 <= s2 { (s1, s2) } else { (s2, s1) }; - match basis { - 0 => Ok(days360(sys, s1, s2, true)? / 360.0), - 2 => Ok((s2 - s1) / 360.0), - 3 => Ok((s2 - s1) / 365.0), - 4 => Ok(days360(sys, s1, s2, false)? / 360.0), - _ => { - let (y1, m1, d1) = ymd(sys, s1)?; - let (y2, m2, d2) = ymd(sys, s2)?; - let days = s2 - s1; - if y1 == y2 { - return Ok(days / if is_leap(y1) { 366.0 } else { 365.0 }); - } - let within_year = y2 == y1 + 1 && (m2 < m1 || (m2 == m1 && d2 <= d1)); - if within_year { - let feb29_1 = is_leap(y1) && (m1 < 2 || (m1 == 2 && d1 <= 29)); - let feb29_2 = is_leap(y2) && (m2 > 2 || (m2 == 2 && d2 == 29)); - let denom = if feb29_1 || feb29_2 { 366.0 } else { 365.0 }; - return Ok(days / denom); - } - let start = serial(sys, y1, 1, 1)?; - let end = serial(sys, y2 + 1, 1, 1).unwrap_or(max_serial(sys) as f64 + 1.0); - let avg = (end - start) / (y2 - y1 + 1) as f64; - Ok(days / avg) - } - } -} - -/// Days in a year for discount securities. -fn year_basis(sys: DateSystem, basis: u32, s: f64) -> R { - Ok(match basis { - 0 | 2 | 4 => 360.0, - 3 => 365.0, - _ => { - if is_leap(ymd(sys, s)?.0) { - 366.0 - } else { - 365.0 - } - } - }) -} - -/// Date `months` months before/after an anchor date, keeping end-of-month alignment. -fn shift_months(sys: DateSystem, anchor: (i32, u32, u32), months: i64, eom: bool) -> R { - let (y, m, d) = anchor; - let total = y as i64 * 12 + (m as i64 - 1) + months; - let ny = total.div_euclid(12); - let nm = (total.rem_euclid(12) + 1) as u32; - if !(1900..=9999).contains(&ny) { - return Err(CellError::Num); - } - let dim = days_in_month(ny as i32, nm); - let nd = if eom { dim } else { d.min(dim) }; - serial(sys, ny as i32, nm, nd) -} - -struct Coupon { - pcd: f64, - ncd: f64, - num: f64, -} - -fn coupons(sys: DateSystem, settle: f64, mat: f64, freq: f64) -> R { - if settle >= mat { - return Err(CellError::Num); - } - let anchor = ymd(sys, mat)?; - let eom = anchor.2 == days_in_month(anchor.0, anchor.1); - let step = (12.0 / freq) as i64; - // Estimate the count from the span, then adjust. - let mut k = (((mat - settle) / 365.25 * freq).floor() as i64).max(1); - while k > 1 && shift_months(sys, anchor, -(k - 1) * step, eom)? <= settle { - k -= 1; - } - let mut guard = 0; - while shift_months(sys, anchor, -k * step, eom)? > settle { - k += 1; - guard += 1; - if guard > 100_000 { - return Err(CellError::Num); - } - } - Ok(Coupon { pcd: shift_months(sys, anchor, -k * step, eom)?, ncd: shift_months(sys, anchor, -(k - 1) * step, eom)?, num: k as f64 }) -} - -fn coup_days(c: &Coupon, freq: f64, basis: u32) -> f64 { - match basis { - 1 => c.ncd - c.pcd, - 3 => 365.0 / freq, - _ => 360.0 / freq, - } -} - -fn coup_daybs(sys: DateSystem, c: &Coupon, settle: f64, basis: u32) -> R { - day_count(sys, c.pcd, settle, basis) -} - -fn coup_daysnc(sys: DateSystem, c: &Coupon, settle: f64, freq: f64, basis: u32) -> R { - match basis { - 0 | 4 => Ok(coup_days(c, freq, basis) - coup_daybs(sys, c, settle, basis)?), - _ => Ok(c.ncd - settle), - } -} - -/// settlement, maturity, frequency, basis at positions 0..=3 for the COUP* functions. -fn coup_args(a: &[Arg], sys: DateSystem) -> R<(f64, f64, f64, u32, Coupon)> { - let (s, m, f, b) = (date(a, 0)?, date(a, 1)?, freq_arg(a, 2)?, basis_arg(a, 3)?); - let c = coupons(sys, s, m, f)?; - Ok((s, m, f, b, c)) -} - -fn coupdaybs(a: &[Arg], cx: &mut dyn Ctx) -> R { - let (s, _, _, b, c) = coup_args(a, cx.date_system())?; - num_val(coup_daybs(cx.date_system(), &c, s, b)?) -} - -fn coupdays(a: &[Arg], cx: &mut dyn Ctx) -> R { - let (_, _, f, b, c) = coup_args(a, cx.date_system())?; - num_val(coup_days(&c, f, b)) -} - -fn coupdaysnc(a: &[Arg], cx: &mut dyn Ctx) -> R { - let (s, _, f, b, c) = coup_args(a, cx.date_system())?; - num_val(coup_daysnc(cx.date_system(), &c, s, f, b)?) -} - -fn coupncd(a: &[Arg], c: &mut dyn Ctx) -> R { - num_val(coup_args(a, c.date_system())?.4.ncd) -} - -fn couppcd(a: &[Arg], c: &mut dyn Ctx) -> R { - num_val(coup_args(a, c.date_system())?.4.pcd) -} - -fn coupnum(a: &[Arg], c: &mut dyn Ctx) -> R { - num_val(coup_args(a, c.date_system())?.4.num) -} - -// --------------------------------------------------------------------------------------------- -// Securities -// --------------------------------------------------------------------------------------------- - -fn accrint(a: &[Arg], c: &mut dyn Ctx) -> R { - let sys = c.date_system(); - let (issue, first, settle, rate) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?); - let par = if has(a, 4) { num(a, 4)? } else { 1000.0 }; - let freq = freq_arg(a, 5)?; - let basis = basis_arg(a, 6)?; - let calc_method = opt_bool(a, 7, true)?; - if issue >= settle || rate <= 0.0 || par <= 0.0 { - return Err(CellError::Num); - } - let anchor = ymd(sys, first)?; - let eom = anchor.2 == days_in_month(anchor.0, anchor.1); - let step = (12.0 / freq) as i64; - let cd = |j: i64| shift_months(sys, anchor, j * step, eom); - // Coupon index of the period containing a date: cd(j) <= date < cd(j+1). - let index_of = |d: f64| -> R { - let mut j = (((d - first) / 365.25 * freq).floor() as i64).clamp(-200_000, 200_000); - let mut guard = 0; - while cd(j)? > d { - j -= 1; - guard += 1; - if guard > 400_000 { - return Err(CellError::Num); - } - } - while cd(j + 1)? <= d { - j += 1; - guard += 1; - if guard > 400_000 { - return Err(CellError::Num); - } - } - Ok(j) - }; - let start = if calc_method || settle <= first { issue } else { cd(index_of(settle)?)? }; - let mut j = index_of(start)?; - let mut sum = 0.0; - let mut guard = 0; - loop { - let (p0, p1) = (cd(j)?, cd(j + 1)?); - if p0 >= settle { - break; - } - let from = p0.max(start); - let to = p1.min(settle); - if to > from { - let nl = match basis { - 1 => p1 - p0, - 3 => 365.0 / freq, - _ => 360.0 / freq, - }; - sum += day_count(sys, from, to, basis)? / nl; - } - j += 1; - guard += 1; - if guard > 100_000 { - return Err(CellError::Num); - } - } - num_val(par * rate / freq * sum) -} - -fn accrintm(a: &[Arg], c: &mut dyn Ctx) -> R { - let (issue, settle, rate) = (date(a, 0)?, date(a, 1)?, num(a, 2)?); - let par = if has(a, 3) { num(a, 3)? } else { 1000.0 }; - let basis = basis_arg(a, 4)?; - if issue >= settle || rate <= 0.0 || par <= 0.0 { - return Err(CellError::Num); - } - num_val(par * rate * year_frac(c.date_system(), issue, settle, basis)?) -} - -/// settlement, maturity, value, value, [basis] for discount securities. -fn disc_args(a: &[Arg]) -> R<(f64, f64, f64, f64, u32)> { - let (s, m, x, y, b) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?, basis_arg(a, 4)?); - if s >= m || x <= 0.0 || y <= 0.0 { - return Err(CellError::Num); - } - Ok((s, m, x, y, b)) -} - -fn disc_frac(sys: DateSystem, s: f64, m: f64, b: u32) -> R { - Ok(day_count(sys, s, m, b)? / year_basis(sys, b, s)?) -} - -fn disc(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, pr, red, b) = disc_args(a)?; - num_val((1.0 - pr / red) / disc_frac(c.date_system(), s, m, b)?) -} - -fn pricedisc(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, d, red, b) = disc_args(a)?; - num_val(red - d * red * disc_frac(c.date_system(), s, m, b)?) -} - -fn yielddisc(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, pr, red, b) = disc_args(a)?; - num_val((red / pr - 1.0) / disc_frac(c.date_system(), s, m, b)?) -} - -fn intrate(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, inv, red, b) = disc_args(a)?; - num_val((red - inv) / inv / disc_frac(c.date_system(), s, m, b)?) -} - -fn received(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, inv, d, b) = disc_args(a)?; - let denom = 1.0 - d * disc_frac(c.date_system(), s, m, b)?; - if denom <= 0.0 { - return Err(CellError::Num); - } - num_val(inv / denom) -} - -fn tbill_args(a: &[Arg], sys: DateSystem) -> R<(f64, f64)> { - let (s, m, x) = (date(a, 0)?, date(a, 1)?, num(a, 2)?); - if s >= m || x <= 0.0 { - return Err(CellError::Num); - } - // At most one year after settlement. - let (y, mo, d) = ymd(sys, s)?; - let limit = shift_months(sys, (y, mo, d), 12, false)?; - if m > limit { - return Err(CellError::Num); - } - Ok((m - s, x)) -} - -fn tbillprice(a: &[Arg], c: &mut dyn Ctx) -> R { - let (dsm, d) = tbill_args(a, c.date_system())?; - let p = 100.0 * (1.0 - d * dsm / 360.0); - if p <= 0.0 { - return Err(CellError::Num); - } - num_val(p) -} - -fn tbillyield(a: &[Arg], c: &mut dyn Ctx) -> R { - let (dsm, pr) = tbill_args(a, c.date_system())?; - num_val((100.0 - pr) / pr * 360.0 / dsm) -} - -fn tbilleq(a: &[Arg], c: &mut dyn Ctx) -> R { - let (dsm, d) = tbill_args(a, c.date_system())?; - if dsm <= 182.0 { - return num_val(365.0 * d / (360.0 - d * dsm)); - } - let price = 100.0 * (1.0 - d * dsm / 360.0); - if price <= 0.0 { - return Err(CellError::Num); - } - let t = dsm / 365.0; - let disc = t * t - (2.0 * t - 1.0) * (1.0 - 100.0 / price); - if disc < 0.0 { - return Err(CellError::Num); - } - num_val((-t + disc.sqrt()) / (t - 0.5)) -} - -/// settlement, maturity, issue, rate, value, [basis]. -fn mat_terms(a: &[Arg], sys: DateSystem) -> R<(f64, f64, f64, f64, f64)> { - let (s, m, issue, rate, x, b) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?, num(a, 4)?, basis_arg(a, 5)?); - if s >= m || issue >= s || rate < 0.0 || x < 0.0 { - return Err(CellError::Num); - } - let dim = year_frac(sys, issue, m, b)?; - let dsm = year_frac(sys, s, m, b)?; - let ai = year_frac(sys, issue, s, b)?; - Ok((dim, dsm, ai, rate, x)) -} - -fn pricemat(a: &[Arg], c: &mut dyn Ctx) -> R { - let (dim, dsm, ai, rate, yld) = mat_terms(a, c.date_system())?; - num_val((100.0 + dim * rate * 100.0) / (1.0 + dsm * yld) - ai * rate * 100.0) -} - -fn yieldmat(a: &[Arg], c: &mut dyn Ctx) -> R { - let (dim, dsm, ai, rate, pr) = mat_terms(a, c.date_system())?; - if pr <= 0.0 || dsm == 0.0 { - return Err(CellError::Num); - } - let base = pr / 100.0 + ai * rate; - num_val(((1.0 + dim * rate) - base) / base / dsm) -} - -struct Bond { - n: f64, - e: f64, - a: f64, - dsc: f64, - freq: f64, -} - -fn bond(sys: DateSystem, settle: f64, mat: f64, freq: f64, basis: u32) -> R { - let c = coupons(sys, settle, mat, freq)?; - let e = coup_days(&c, freq, basis); - let a = coup_daybs(sys, &c, settle, basis)?; - let dsc = coup_daysnc(sys, &c, settle, freq, basis)?; - if e <= 0.0 { - return Err(CellError::Num); - } - Ok(Bond { n: c.num, e, a, dsc, freq }) -} - -fn bond_price(b: &Bond, rate: f64, yld: f64, red: f64) -> f64 { - let f = b.freq; - let coupon = 100.0 * rate / f; - let accrued = coupon * b.a / b.e; - if b.n == 1.0 { - return (red + coupon) / (1.0 + b.dsc / b.e * yld / f) - accrued; - } - let base = 1.0 + yld / f; - let frac = b.dsc / b.e; - let mut sum = red / base.powf(b.n - 1.0 + frac); - let mut k = 1.0; - while k <= b.n { - sum += coupon / base.powf(k - 1.0 + frac); - k += 1.0; - } - sum - accrued -} - -fn price(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, rate, yld, red) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?, num(a, 4)?); - let (f, basis) = (freq_arg(a, 5)?, basis_arg(a, 6)?); - if rate < 0.0 || yld < 0.0 || red <= 0.0 { - return Err(CellError::Num); - } - let b = bond(c.date_system(), s, m, f, basis)?; - num_val(bond_price(&b, rate, yld, red)) -} - -fn yield_(a: &[Arg], c: &mut dyn Ctx) -> R { - let (s, m, rate, pr, red) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?, num(a, 4)?); - let (f, basis) = (freq_arg(a, 5)?, basis_arg(a, 6)?); - if rate < 0.0 || pr <= 0.0 || red <= 0.0 { - return Err(CellError::Num); - } - let b = bond(c.date_system(), s, m, f, basis)?; - if b.n == 1.0 { - let base = pr / 100.0 + b.a / b.e * rate / f; - return num_val(((red / 100.0 + rate / f) - base) / base * f * b.e / b.dsc); - } - // Price is decreasing in yield: bisect on [0, hi]. - let g = |y: f64| bond_price(&b, rate, y, red) - pr; - let (mut lo, mut hi) = (-0.999_999 * f, 1.0); - let mut guard = 0; - while g(hi) > 0.0 && guard < 60 { - hi *= 2.0; - guard += 1; - } - if !(g(lo) >= 0.0 && g(hi) <= 0.0) { - return Err(CellError::Num); - } - for _ in 0..200 { - let mid = 0.5 * (lo + hi); - if g(mid) > 0.0 { - lo = mid; - } else { - hi = mid; - } - } - num_val(0.5 * (lo + hi)) -} - -fn duration_calc(a: &[Arg], sys: DateSystem) -> R<(f64, f64, f64)> { - let (s, m, coupon, yld) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?); - let (f, basis) = (freq_arg(a, 4)?, basis_arg(a, 5)?); - if coupon < 0.0 || yld < 0.0 { - return Err(CellError::Num); - } - let b = bond(sys, s, m, f, basis)?; - let c = coupon * 100.0 / f; - let y = 1.0 + yld / f; - let frac = b.dsc / b.e; - let (mut wsum, mut psum) = (0.0, 0.0); - let mut k = 1.0; - while k <= b.n { - let t = k - 1.0 + frac; - let cf = if k == b.n { c + 100.0 } else { c }; - let pv = cf / y.powf(t); - wsum += t * pv; - psum += pv; - k += 1.0; - } - if psum == 0.0 { - return Err(CellError::Num); - } - Ok((wsum / psum / f, yld, f)) -} - -fn duration(a: &[Arg], c: &mut dyn Ctx) -> R { - num_val(duration_calc(a, c.date_system())?.0) -} - -fn mduration(a: &[Arg], c: &mut dyn Ctx) -> R { - let (d, y, f) = duration_calc(a, c.date_system())?; - num_val(d / (1.0 + y / f)) -} - -// --------------------------------------------------------------------------------------------- -// Odd first and last coupon periods -// --------------------------------------------------------------------------------------------- -// -// An odd period is split into quasi-coupon periods: the regular coupon periods the security -// would have had, counted back from the first coupon (ODDF*) or forward from the last interest -// date (ODDL*). For quasi-period i, NL is its normal length, DC the days of it inside the odd -// period, A the accrued days of it before settlement and DSC the days of it from settlement on. - -/// Most quasi-coupon periods an odd period may span (the date range allows ~32,400 quarters). -const MAX_QUASI: i64 = 40_000; - -/// Normal length of the coupon period `[start, end]` under a basis (COUPDAYS semantics). -fn normal_length(start: f64, end: f64, freq: f64, basis: u32) -> f64 { - match basis { - 1 => end - start, - 3 => 365.0 / freq, - _ => 360.0 / freq, - } -} - -/// Days of `[from, to]` under a basis, or 0 when the span is empty. -fn span(sys: DateSystem, from: f64, to: f64, basis: u32) -> R { - if to > from { day_count(sys, from, to, basis) } else { Ok(0.0) } -} - -/// The odd-first-period terms that ODDFPRICE and ODDFYIELD share. -struct OddFirst { - /// Coupons after the first coupon up to maturity. - n: f64, - /// Whole quasi-coupon periods between settlement and the first coupon. - nq: f64, - /// DSC / NL of the quasi-period holding settlement. - frac: f64, - /// Σ DCi / NLi over the odd period. - dc: f64, - /// Σ Ai / NLi over the odd period. - acc: f64, - freq: f64, -} - -fn odd_first(sys: DateSystem, settle: f64, mat: f64, issue: f64, first: f64, freq: f64, basis: u32) -> R { - if !(mat > first && first > settle && settle > issue) { - return Err(CellError::Num); - } - let anchor = ymd(sys, first)?; - let eom = anchor.2 == days_in_month(anchor.0, anchor.1); - let step = (12.0 / freq) as i64; - let quasi = |k: i64| shift_months(sys, anchor, -k * step, eom); - let (mut dc, mut acc) = (0.0, 0.0); - let (mut nq, mut frac) = (None, 0.0); - // Walk back from the first coupon: period k runs from quasi(k + 1) to quasi(k). - let mut k = 0; - loop { - if k >= MAX_QUASI { - return Err(CellError::Num); - } - let (end, start) = (quasi(k)?, quasi(k + 1)?); - let nl = normal_length(start, end, freq, basis); - if nl <= 0.0 { - return Err(CellError::Num); - } - let from = start.max(issue); - dc += span(sys, from, end, basis)? / nl; - acc += span(sys, from, settle.min(end), basis)? / nl; - if start <= settle && settle < end { - nq = Some(k as f64); - frac = span(sys, settle, end, basis)? / nl; - } - if start <= issue { - break; - } - k += 1; - } - let nq = nq.ok_or(CellError::Num)?; - let n = coupons(sys, first, mat, freq)?.num; - Ok(OddFirst { n, nq, frac, dc, acc, freq }) -} - -fn odd_first_price(t: &OddFirst, rate: f64, yld: f64, red: f64) -> f64 { - let coupon = 100.0 * rate / t.freq; - let base = 1.0 + yld / t.freq; - let lead = t.nq + t.frac; - let mut p = red / base.powf(t.n + lead) + coupon * t.dc / base.powf(lead); - let mut k = 1.0; - while k <= t.n { - p += coupon / base.powf(k + lead); - k += 1.0; - } - p - coupon * t.acc -} - -/// settlement, maturity, issue, first_coupon, rate, yld/pr, redemption, frequency, [basis]. -fn odd_first_args(a: &[Arg], sys: DateSystem) -> R<(OddFirst, f64, f64, f64)> { - let (s, m, issue, first) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, date(a, 3)?); - let (rate, x, red) = (num(a, 4)?, num(a, 5)?, num(a, 6)?); - let (f, basis) = (freq_arg(a, 7)?, basis_arg(a, 8)?); - if rate < 0.0 || red <= 0.0 { - return Err(CellError::Num); - } - Ok((odd_first(sys, s, m, issue, first, f, basis)?, rate, x, red)) -} - -fn oddfprice(a: &[Arg], c: &mut dyn Ctx) -> R { - let (t, rate, yld, red) = odd_first_args(a, c.date_system())?; - if yld < 0.0 { - return Err(CellError::Num); - } - num_val(odd_first_price(&t, rate, yld, red)) -} - -fn oddfyield(a: &[Arg], c: &mut dyn Ctx) -> R { - let (t, rate, pr, red) = odd_first_args(a, c.date_system())?; - if pr <= 0.0 { - return Err(CellError::Num); - } - // The price falls as the yield rises: bracket the root, then bisect. - let g = |y: f64| odd_first_price(&t, rate, y, red) - pr; - let (mut lo, mut hi) = (-0.999_999 * t.freq, 1.0); - let mut guard = 0; - while g(hi) > 0.0 && guard < 60 { - hi *= 2.0; - guard += 1; - } - // NaN fails both comparisons. - if !(g(lo) >= 0.0 && g(hi) <= 0.0) { - return Err(CellError::Num); - } - for _ in 0..200 { - let mid = 0.5 * (lo + hi); - if g(mid) > 0.0 { - lo = mid; - } else { - hi = mid; - } - } - num_val(0.5 * (lo + hi)) -} - -/// Σ DCi/NLi, Σ Ai/NLi and Σ DSCi/NLi over an odd last period. -fn odd_last(sys: DateSystem, settle: f64, mat: f64, last: f64, freq: f64, basis: u32) -> R<(f64, f64, f64)> { - if !(mat > settle && settle > last) { - return Err(CellError::Num); - } - let anchor = ymd(sys, last)?; - let eom = anchor.2 == days_in_month(anchor.0, anchor.1); - let step = (12.0 / freq) as i64; - let (mut dc, mut acc, mut dsc) = (0.0, 0.0, 0.0); - // Walk forward from the last interest date: period k runs from quasi(k - 1) to quasi(k). - let mut start = last; - let mut k = 1; - loop { - if k > MAX_QUASI { - return Err(CellError::Num); - } - let end = shift_months(sys, anchor, k * step, eom)?; - let nl = normal_length(start, end, freq, basis); - if nl <= 0.0 { - return Err(CellError::Num); - } - let stop = end.min(mat); - dc += span(sys, start, stop, basis)? / nl; - acc += span(sys, start, settle.min(stop), basis)? / nl; - dsc += span(sys, settle.max(start), stop, basis)? / nl; - if end >= mat { - break; - } - start = end; - k += 1; - } - Ok((dc, acc, dsc)) -} - -/// settlement, maturity, last_interest, rate, yld/pr, redemption, frequency, [basis]. -fn odd_last_args(a: &[Arg], sys: DateSystem) -> R<((f64, f64, f64), f64, f64, f64, f64)> { - let (s, m, last) = (date(a, 0)?, date(a, 1)?, date(a, 2)?); - let (rate, x, red) = (num(a, 3)?, num(a, 4)?, num(a, 5)?); - let (f, basis) = (freq_arg(a, 6)?, basis_arg(a, 7)?); - if rate < 0.0 || red <= 0.0 { - return Err(CellError::Num); - } - Ok((odd_last(sys, s, m, last, f, basis)?, rate, x, red, f)) -} - -fn oddlprice(a: &[Arg], c: &mut dyn Ctx) -> R { - let ((dc, acc, dsc), rate, yld, red, f) = odd_last_args(a, c.date_system())?; - if yld < 0.0 { - return Err(CellError::Num); - } - let coupon = 100.0 * rate / f; - num_val((red + dc * coupon) / (1.0 + dsc * yld / f) - acc * coupon) -} - -fn oddlyield(a: &[Arg], c: &mut dyn Ctx) -> R { - let ((dc, acc, dsc), rate, pr, red, f) = odd_last_args(a, c.date_system())?; - if pr <= 0.0 { - return Err(CellError::Num); - } - let coupon = 100.0 * rate / f; - let paid = pr + acc * coupon; - if paid <= 0.0 || dsc <= 0.0 { - return Err(CellError::Num); - } - num_val((red + dc * coupon - paid) / paid * f / dsc) -} - // --------------------------------------------------------------------------------------------- // Euro conversion // --------------------------------------------------------------------------------------------- diff --git a/crates/functions/src/financial/day_count.rs b/crates/functions/src/financial/day_count.rs new file mode 100644 index 000000000..f91e7968f --- /dev/null +++ b/crates/functions/src/financial/day_count.rs @@ -0,0 +1,214 @@ +use super::*; + +// --------------------------------------------------------------------------------------------- +// Dates and day counts +// --------------------------------------------------------------------------------------------- + +pub(super) fn date(a: &[Arg], i: usize) -> R { + let d = num(a, i)?.trunc(); + if !(0.0..=2_958_465.0).contains(&d) { + return Err(CellError::Num); + } + Ok(d) +} + +pub(super) fn ymd(sys: DateSystem, s: f64) -> R<(i32, u32, u32)> { + let dt = datetime_from_serial(sys, s.trunc()).ok_or(CellError::Num)?; + Ok((dt.year, dt.month, dt.day.max(1))) +} + +fn serial(sys: DateSystem, y: i32, m: u32, d: u32) -> R { + serial_from_ymd(sys, y as i64, m as i64, d as i64).ok_or(CellError::Num) +} + +pub(super) fn basis_arg(a: &[Arg], i: usize) -> R { + let b = opt_num(a, i, 0.0)?.trunc(); + if !(0.0..=4.0).contains(&b) { + return Err(CellError::Num); + } + Ok(b as u32) +} + +pub(super) fn freq_arg(a: &[Arg], i: usize) -> R { + let f = num(a, i)?.trunc(); + if f != 1.0 && f != 2.0 && f != 4.0 { + return Err(CellError::Num); + } + Ok(f) +} + +/// 30/360 day count (US/NASD when `us`, European otherwise). +fn days360(sys: DateSystem, s1: f64, s2: f64, us: bool) -> R { + let (y1, m1, mut d1) = ymd(sys, s1)?; + let (y2, m2, mut d2) = ymd(sys, s2)?; + if us { + let feb_end1 = m1 == 2 && d1 >= days_in_month(y1, 2); + let feb_end2 = m2 == 2 && d2 >= days_in_month(y2, 2); + if feb_end1 && feb_end2 { + d2 = 30; + } + if feb_end1 { + d1 = 30; + } + if d2 == 31 && d1 >= 30 { + d2 = 30; + } + if d1 == 31 { + d1 = 30; + } + } else { + d1 = d1.min(30); + d2 = d2.min(30); + } + Ok((y2 - y1) as f64 * 360.0 + (m2 as f64 - m1 as f64) * 30.0 + (d2 as f64 - d1 as f64)) +} + +/// Days between two dates under a basis. +pub(super) fn day_count(sys: DateSystem, s1: f64, s2: f64, basis: u32) -> R { + match basis { + 0 => days360(sys, s1, s2, true), + 4 => days360(sys, s1, s2, false), + _ => Ok(s2 - s1), + } +} + +/// Year fraction between two dates under a basis (YEARFRAC semantics). +pub(super) fn year_frac(sys: DateSystem, s1: f64, s2: f64, basis: u32) -> R { + let (s1, s2) = if s1 <= s2 { (s1, s2) } else { (s2, s1) }; + match basis { + 0 => Ok(days360(sys, s1, s2, true)? / 360.0), + 2 => Ok((s2 - s1) / 360.0), + 3 => Ok((s2 - s1) / 365.0), + 4 => Ok(days360(sys, s1, s2, false)? / 360.0), + _ => { + let (y1, m1, d1) = ymd(sys, s1)?; + let (y2, m2, d2) = ymd(sys, s2)?; + let days = s2 - s1; + if y1 == y2 { + return Ok(days / if is_leap(y1) { 366.0 } else { 365.0 }); + } + let within_year = y2 == y1 + 1 && (m2 < m1 || (m2 == m1 && d2 <= d1)); + if within_year { + let feb29_1 = is_leap(y1) && (m1 < 2 || (m1 == 2 && d1 <= 29)); + let feb29_2 = is_leap(y2) && (m2 > 2 || (m2 == 2 && d2 == 29)); + let denom = if feb29_1 || feb29_2 { 366.0 } else { 365.0 }; + return Ok(days / denom); + } + let start = serial(sys, y1, 1, 1)?; + let end = serial(sys, y2 + 1, 1, 1).unwrap_or(max_serial(sys) as f64 + 1.0); + let avg = (end - start) / (y2 - y1 + 1) as f64; + Ok(days / avg) + } + } +} + +/// Days in a year for discount securities. +pub(super) fn year_basis(sys: DateSystem, basis: u32, s: f64) -> R { + Ok(match basis { + 0 | 2 | 4 => 360.0, + 3 => 365.0, + _ => { + if is_leap(ymd(sys, s)?.0) { + 366.0 + } else { + 365.0 + } + } + }) +} + +/// Date `months` months before/after an anchor date, keeping end-of-month alignment. +pub(super) fn shift_months(sys: DateSystem, anchor: (i32, u32, u32), months: i64, eom: bool) -> R { + let (y, m, d) = anchor; + let total = y as i64 * 12 + (m as i64 - 1) + months; + let ny = total.div_euclid(12); + let nm = (total.rem_euclid(12) + 1) as u32; + if !(1900..=9999).contains(&ny) { + return Err(CellError::Num); + } + let dim = days_in_month(ny as i32, nm); + let nd = if eom { dim } else { d.min(dim) }; + serial(sys, ny as i32, nm, nd) +} + +pub(super) struct Coupon { + pcd: f64, + ncd: f64, + pub(super) num: f64, +} + +pub(super) fn coupons(sys: DateSystem, settle: f64, mat: f64, freq: f64) -> R { + if settle >= mat { + return Err(CellError::Num); + } + let anchor = ymd(sys, mat)?; + let eom = anchor.2 == days_in_month(anchor.0, anchor.1); + let step = (12.0 / freq) as i64; + // Estimate the count from the span, then adjust. + let mut k = (((mat - settle) / 365.25 * freq).floor() as i64).max(1); + while k > 1 && shift_months(sys, anchor, -(k - 1) * step, eom)? <= settle { + k -= 1; + } + let mut guard = 0; + while shift_months(sys, anchor, -k * step, eom)? > settle { + k += 1; + guard += 1; + if guard > 100_000 { + return Err(CellError::Num); + } + } + Ok(Coupon { pcd: shift_months(sys, anchor, -k * step, eom)?, ncd: shift_months(sys, anchor, -(k - 1) * step, eom)?, num: k as f64 }) +} + +pub(super) fn coup_days(c: &Coupon, freq: f64, basis: u32) -> f64 { + match basis { + 1 => c.ncd - c.pcd, + 3 => 365.0 / freq, + _ => 360.0 / freq, + } +} + +pub(super) fn coup_daybs(sys: DateSystem, c: &Coupon, settle: f64, basis: u32) -> R { + day_count(sys, c.pcd, settle, basis) +} + +pub(super) fn coup_daysnc(sys: DateSystem, c: &Coupon, settle: f64, freq: f64, basis: u32) -> R { + match basis { + 0 | 4 => Ok(coup_days(c, freq, basis) - coup_daybs(sys, c, settle, basis)?), + _ => Ok(c.ncd - settle), + } +} + +/// settlement, maturity, frequency, basis at positions 0..=3 for the COUP* functions. +fn coup_args(a: &[Arg], sys: DateSystem) -> R<(f64, f64, f64, u32, Coupon)> { + let (s, m, f, b) = (date(a, 0)?, date(a, 1)?, freq_arg(a, 2)?, basis_arg(a, 3)?); + let c = coupons(sys, s, m, f)?; + Ok((s, m, f, b, c)) +} + +pub(super) fn coupdaybs(a: &[Arg], cx: &mut dyn Ctx) -> R { + let (s, _, _, b, c) = coup_args(a, cx.date_system())?; + num_val(coup_daybs(cx.date_system(), &c, s, b)?) +} + +pub(super) fn coupdays(a: &[Arg], cx: &mut dyn Ctx) -> R { + let (_, _, f, b, c) = coup_args(a, cx.date_system())?; + num_val(coup_days(&c, f, b)) +} + +pub(super) fn coupdaysnc(a: &[Arg], cx: &mut dyn Ctx) -> R { + let (s, _, f, b, c) = coup_args(a, cx.date_system())?; + num_val(coup_daysnc(cx.date_system(), &c, s, f, b)?) +} + +pub(super) fn coupncd(a: &[Arg], c: &mut dyn Ctx) -> R { + num_val(coup_args(a, c.date_system())?.4.ncd) +} + +pub(super) fn couppcd(a: &[Arg], c: &mut dyn Ctx) -> R { + num_val(coup_args(a, c.date_system())?.4.pcd) +} + +pub(super) fn coupnum(a: &[Arg], c: &mut dyn Ctx) -> R { + num_val(coup_args(a, c.date_system())?.4.num) +} diff --git a/crates/functions/src/financial/depreciation.rs b/crates/functions/src/financial/depreciation.rs new file mode 100644 index 000000000..d656aa76f --- /dev/null +++ b/crates/functions/src/financial/depreciation.rs @@ -0,0 +1,173 @@ +use super::day_count::{basis_arg, date, year_frac}; +use super::*; + +// --------------------------------------------------------------------------------------------- +// Depreciation +// --------------------------------------------------------------------------------------------- + +pub(super) fn sln(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (cost, salvage, life) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + if life == 0.0 { + return Err(CellError::Div0); + } + num_val((cost - salvage) / life) +} + +pub(super) fn syd(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (cost, salvage, life, per) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); + if life <= 0.0 || per <= 0.0 || per > life || salvage < 0.0 { + return Err(CellError::Num); + } + num_val((cost - salvage) * (life - per + 1.0) * 2.0 / (life * (life + 1.0))) +} + +pub(super) fn db(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (cost, salvage, life, period) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?.trunc()); + let month = opt_num(a, 4, 12.0)?.trunc(); + if cost < 0.0 || salvage < 0.0 || life <= 0.0 || period <= 0.0 || !(1.0..=12.0).contains(&month) { + return Err(CellError::Num); + } + let max_period = if month == 12.0 { life.trunc() } else { life.trunc() + 1.0 }; + if period > max_period.max(1.0) || period > 1e7 { + return Err(CellError::Num); + } + if cost == 0.0 { + return num_val(0.0); + } + let rate = crate::util::round_half_away(1.0 - (salvage / cost).powf(1.0 / life), 3); + let mut total = 0.0; + let mut dep = 0.0; + let mut p = 1.0; + while p <= period { + dep = if p == 1.0 { + cost * rate * month / 12.0 + } else if p == life.trunc() + 1.0 { + (cost - total) * rate * (12.0 - month) / 12.0 + } else { + (cost - total) * rate + }; + total += dep; + p += 1.0; + } + num_val(dep) +} + +pub(super) fn ddb(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (cost, salvage, life, period) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); + let factor = opt_num(a, 4, 2.0)?; + if cost < 0.0 || salvage < 0.0 || life <= 0.0 || period <= 0.0 || factor <= 0.0 || period > life { + return Err(CellError::Num); + } + let mut rate = factor / life; + let old; + if rate >= 1.0 { + rate = 1.0; + old = if period == 1.0 { cost } else { 0.0 }; + } else { + old = cost * (1.0 - rate).powf(period - 1.0); + } + let new = cost * (1.0 - rate).powf(period); + let dep = if new < salvage { old - salvage } else { old - new }; + num_val(dep.max(0.0)) +} + +/// Per-period declining-balance depreciation with optional switch to straight line. +fn vdb_schedule(cost: f64, salvage: f64, life: f64, factor: f64, no_switch: bool, periods: usize) -> Vec { + let mut out = Vec::with_capacity(periods); + let mut book = cost; + let mut sl: Option = None; + for i in 0..periods { + let dep = if let Some(s) = sl { + s.min((book - salvage).max(0.0)) + } else { + let ddb = (book * factor / life).min((book - salvage).max(0.0)); + let remaining = life - i as f64; + let slv = if remaining > 0.0 { (book - salvage) / remaining.max(1.0) } else { 0.0 }; + if !no_switch && slv > ddb { + sl = Some(slv); + slv.min((book - salvage).max(0.0)) + } else { + ddb + } + }; + out.push(dep); + book -= dep; + } + out +} + +pub(super) fn vdb(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (cost, salvage, life, start, end) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?, num(a, 4)?); + let factor = opt_num(a, 5, 2.0)?; + let no_switch = opt_bool(a, 6, false)?; + if cost < 0.0 || salvage < 0.0 || life <= 0.0 || start < 0.0 || end < start || end > life || factor <= 0.0 { + return Err(CellError::Num); + } + let periods = end.ceil(); + if periods > 1e7 { + return Err(CellError::Num); + } + let sched = vdb_schedule(cost, salvage, life, factor, no_switch, periods as usize); + let mut total = 0.0; + for (i, d) in sched.iter().enumerate() { + let (p0, p1) = (i as f64, i as f64 + 1.0); + let overlap = (p1.min(end) - p0.max(start)).max(0.0); + total += d * overlap; + } + num_val(total) +} + +pub(super) fn amorlinc(a: &[Arg], c: &mut dyn Ctx) -> R { + let (cost, purchased, first, salvage) = (num(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?); + let (period, rate, basis) = (num(a, 4)?.trunc(), num(a, 5)?, basis_arg(a, 6)?); + if cost < 0.0 || salvage < 0.0 || salvage > cost || period < 0.0 || rate <= 0.0 || first < purchased { + return Err(CellError::Num); + } + let full = cost * rate; + let limit = cost - salvage; + let dep0 = (full * year_frac(c.date_system(), purchased, first, basis)?).min(limit); + if period == 0.0 { + return num_val(dep0); + } + let remaining = limit - dep0 - (period - 1.0) * full; + num_val(full.min(remaining.max(0.0))) +} + +pub(super) fn amordegrc(a: &[Arg], c: &mut dyn Ctx) -> R { + let (cost, purchased, first, salvage) = (num(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?); + let (period, rate, basis) = (num(a, 4)?.trunc(), num(a, 5)?, basis_arg(a, 6)?); + if cost < 0.0 || salvage < 0.0 || salvage > cost || period < 0.0 || rate <= 0.0 || first < purchased || period > 1e6 { + return Err(CellError::Num); + } + let life = 1.0 / rate; + let coef = if life < 3.0 { + 1.0 + } else if life < 5.0 { + 1.5 + } else if life <= 6.0 { + 2.0 + } else { + 2.5 + }; + let r = rate * coef; + let mut book = cost; + let mut dep = (year_frac(c.date_system(), purchased, first, basis)? * r * cost).round(); + if period == 0.0 { + return num_val(dep); + } + book -= dep; + let mut rest = book - salvage; + let mut n = 1.0; + while n <= period { + dep = (r * book).round(); + rest -= dep; + if rest < 0.0 { + // The two final periods split what remains; afterwards nothing is left. + let left = period - n; + return num_val(if left <= 1.0 { (book * 0.5).round() } else { 0.0 }); + } + book -= dep; + n += 1.0; + } + num_val(dep) +} diff --git a/crates/functions/src/financial/securities.rs b/crates/functions/src/financial/securities.rs new file mode 100644 index 000000000..cfecd763a --- /dev/null +++ b/crates/functions/src/financial/securities.rs @@ -0,0 +1,512 @@ +use super::day_count::{basis_arg, coup_daybs, coup_days, coup_daysnc, coupons, date, day_count, freq_arg, shift_months, year_basis, year_frac, ymd}; +use super::*; + +// --------------------------------------------------------------------------------------------- +// Securities +// --------------------------------------------------------------------------------------------- + +pub(super) fn accrint(a: &[Arg], c: &mut dyn Ctx) -> R { + let sys = c.date_system(); + let (issue, first, settle, rate) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?); + let par = if has(a, 4) { num(a, 4)? } else { 1000.0 }; + let freq = freq_arg(a, 5)?; + let basis = basis_arg(a, 6)?; + let calc_method = opt_bool(a, 7, true)?; + if issue >= settle || rate <= 0.0 || par <= 0.0 { + return Err(CellError::Num); + } + let anchor = ymd(sys, first)?; + let eom = anchor.2 == days_in_month(anchor.0, anchor.1); + let step = (12.0 / freq) as i64; + let cd = |j: i64| shift_months(sys, anchor, j * step, eom); + // Coupon index of the period containing a date: cd(j) <= date < cd(j+1). + let index_of = |d: f64| -> R { + let mut j = (((d - first) / 365.25 * freq).floor() as i64).clamp(-200_000, 200_000); + let mut guard = 0; + while cd(j)? > d { + j -= 1; + guard += 1; + if guard > 400_000 { + return Err(CellError::Num); + } + } + while cd(j + 1)? <= d { + j += 1; + guard += 1; + if guard > 400_000 { + return Err(CellError::Num); + } + } + Ok(j) + }; + let start = if calc_method || settle <= first { issue } else { cd(index_of(settle)?)? }; + let mut j = index_of(start)?; + let mut sum = 0.0; + let mut guard = 0; + loop { + let (p0, p1) = (cd(j)?, cd(j + 1)?); + if p0 >= settle { + break; + } + let from = p0.max(start); + let to = p1.min(settle); + if to > from { + let nl = match basis { + 1 => p1 - p0, + 3 => 365.0 / freq, + _ => 360.0 / freq, + }; + sum += day_count(sys, from, to, basis)? / nl; + } + j += 1; + guard += 1; + if guard > 100_000 { + return Err(CellError::Num); + } + } + num_val(par * rate / freq * sum) +} + +pub(super) fn accrintm(a: &[Arg], c: &mut dyn Ctx) -> R { + let (issue, settle, rate) = (date(a, 0)?, date(a, 1)?, num(a, 2)?); + let par = if has(a, 3) { num(a, 3)? } else { 1000.0 }; + let basis = basis_arg(a, 4)?; + if issue >= settle || rate <= 0.0 || par <= 0.0 { + return Err(CellError::Num); + } + num_val(par * rate * year_frac(c.date_system(), issue, settle, basis)?) +} + +/// settlement, maturity, value, value, [basis] for discount securities. +fn disc_args(a: &[Arg]) -> R<(f64, f64, f64, f64, u32)> { + let (s, m, x, y, b) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?, basis_arg(a, 4)?); + if s >= m || x <= 0.0 || y <= 0.0 { + return Err(CellError::Num); + } + Ok((s, m, x, y, b)) +} + +fn disc_frac(sys: DateSystem, s: f64, m: f64, b: u32) -> R { + Ok(day_count(sys, s, m, b)? / year_basis(sys, b, s)?) +} + +pub(super) fn disc(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, pr, red, b) = disc_args(a)?; + num_val((1.0 - pr / red) / disc_frac(c.date_system(), s, m, b)?) +} + +pub(super) fn pricedisc(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, d, red, b) = disc_args(a)?; + num_val(red - d * red * disc_frac(c.date_system(), s, m, b)?) +} + +pub(super) fn yielddisc(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, pr, red, b) = disc_args(a)?; + num_val((red / pr - 1.0) / disc_frac(c.date_system(), s, m, b)?) +} + +pub(super) fn intrate(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, inv, red, b) = disc_args(a)?; + num_val((red - inv) / inv / disc_frac(c.date_system(), s, m, b)?) +} + +pub(super) fn received(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, inv, d, b) = disc_args(a)?; + let denom = 1.0 - d * disc_frac(c.date_system(), s, m, b)?; + if denom <= 0.0 { + return Err(CellError::Num); + } + num_val(inv / denom) +} + +fn tbill_args(a: &[Arg], sys: DateSystem) -> R<(f64, f64)> { + let (s, m, x) = (date(a, 0)?, date(a, 1)?, num(a, 2)?); + if s >= m || x <= 0.0 { + return Err(CellError::Num); + } + // At most one year after settlement. + let (y, mo, d) = ymd(sys, s)?; + let limit = shift_months(sys, (y, mo, d), 12, false)?; + if m > limit { + return Err(CellError::Num); + } + Ok((m - s, x)) +} + +pub(super) fn tbillprice(a: &[Arg], c: &mut dyn Ctx) -> R { + let (dsm, d) = tbill_args(a, c.date_system())?; + let p = 100.0 * (1.0 - d * dsm / 360.0); + if p <= 0.0 { + return Err(CellError::Num); + } + num_val(p) +} + +pub(super) fn tbillyield(a: &[Arg], c: &mut dyn Ctx) -> R { + let (dsm, pr) = tbill_args(a, c.date_system())?; + num_val((100.0 - pr) / pr * 360.0 / dsm) +} + +pub(super) fn tbilleq(a: &[Arg], c: &mut dyn Ctx) -> R { + let (dsm, d) = tbill_args(a, c.date_system())?; + if dsm <= 182.0 { + return num_val(365.0 * d / (360.0 - d * dsm)); + } + let price = 100.0 * (1.0 - d * dsm / 360.0); + if price <= 0.0 { + return Err(CellError::Num); + } + let t = dsm / 365.0; + let disc = t * t - (2.0 * t - 1.0) * (1.0 - 100.0 / price); + if disc < 0.0 { + return Err(CellError::Num); + } + num_val((-t + disc.sqrt()) / (t - 0.5)) +} + +/// settlement, maturity, issue, rate, value, [basis]. +fn mat_terms(a: &[Arg], sys: DateSystem) -> R<(f64, f64, f64, f64, f64)> { + let (s, m, issue, rate, x, b) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, num(a, 3)?, num(a, 4)?, basis_arg(a, 5)?); + if s >= m || issue >= s || rate < 0.0 || x < 0.0 { + return Err(CellError::Num); + } + let dim = year_frac(sys, issue, m, b)?; + let dsm = year_frac(sys, s, m, b)?; + let ai = year_frac(sys, issue, s, b)?; + Ok((dim, dsm, ai, rate, x)) +} + +pub(super) fn pricemat(a: &[Arg], c: &mut dyn Ctx) -> R { + let (dim, dsm, ai, rate, yld) = mat_terms(a, c.date_system())?; + num_val((100.0 + dim * rate * 100.0) / (1.0 + dsm * yld) - ai * rate * 100.0) +} + +pub(super) fn yieldmat(a: &[Arg], c: &mut dyn Ctx) -> R { + let (dim, dsm, ai, rate, pr) = mat_terms(a, c.date_system())?; + if pr <= 0.0 || dsm == 0.0 { + return Err(CellError::Num); + } + let base = pr / 100.0 + ai * rate; + num_val(((1.0 + dim * rate) - base) / base / dsm) +} + +struct Bond { + n: f64, + e: f64, + a: f64, + dsc: f64, + freq: f64, +} + +fn bond(sys: DateSystem, settle: f64, mat: f64, freq: f64, basis: u32) -> R { + let c = coupons(sys, settle, mat, freq)?; + let e = coup_days(&c, freq, basis); + let a = coup_daybs(sys, &c, settle, basis)?; + let dsc = coup_daysnc(sys, &c, settle, freq, basis)?; + if e <= 0.0 { + return Err(CellError::Num); + } + Ok(Bond { n: c.num, e, a, dsc, freq }) +} + +fn bond_price(b: &Bond, rate: f64, yld: f64, red: f64) -> f64 { + let f = b.freq; + let coupon = 100.0 * rate / f; + let accrued = coupon * b.a / b.e; + if b.n == 1.0 { + return (red + coupon) / (1.0 + b.dsc / b.e * yld / f) - accrued; + } + let base = 1.0 + yld / f; + let frac = b.dsc / b.e; + let mut sum = red / base.powf(b.n - 1.0 + frac); + let mut k = 1.0; + while k <= b.n { + sum += coupon / base.powf(k - 1.0 + frac); + k += 1.0; + } + sum - accrued +} + +pub(super) fn price(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, rate, yld, red) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?, num(a, 4)?); + let (f, basis) = (freq_arg(a, 5)?, basis_arg(a, 6)?); + if rate < 0.0 || yld < 0.0 || red <= 0.0 { + return Err(CellError::Num); + } + let b = bond(c.date_system(), s, m, f, basis)?; + num_val(bond_price(&b, rate, yld, red)) +} + +pub(super) fn yield_(a: &[Arg], c: &mut dyn Ctx) -> R { + let (s, m, rate, pr, red) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?, num(a, 4)?); + let (f, basis) = (freq_arg(a, 5)?, basis_arg(a, 6)?); + if rate < 0.0 || pr <= 0.0 || red <= 0.0 { + return Err(CellError::Num); + } + let b = bond(c.date_system(), s, m, f, basis)?; + if b.n == 1.0 { + let base = pr / 100.0 + b.a / b.e * rate / f; + return num_val(((red / 100.0 + rate / f) - base) / base * f * b.e / b.dsc); + } + // Price is decreasing in yield: bisect on [0, hi]. + let g = |y: f64| bond_price(&b, rate, y, red) - pr; + let (mut lo, mut hi) = (-0.999_999 * f, 1.0); + let mut guard = 0; + while g(hi) > 0.0 && guard < 60 { + hi *= 2.0; + guard += 1; + } + if !(g(lo) >= 0.0 && g(hi) <= 0.0) { + return Err(CellError::Num); + } + for _ in 0..200 { + let mid = 0.5 * (lo + hi); + if g(mid) > 0.0 { + lo = mid; + } else { + hi = mid; + } + } + num_val(0.5 * (lo + hi)) +} + +fn duration_calc(a: &[Arg], sys: DateSystem) -> R<(f64, f64, f64)> { + let (s, m, coupon, yld) = (date(a, 0)?, date(a, 1)?, num(a, 2)?, num(a, 3)?); + let (f, basis) = (freq_arg(a, 4)?, basis_arg(a, 5)?); + if coupon < 0.0 || yld < 0.0 { + return Err(CellError::Num); + } + let b = bond(sys, s, m, f, basis)?; + let c = coupon * 100.0 / f; + let y = 1.0 + yld / f; + let frac = b.dsc / b.e; + let (mut wsum, mut psum) = (0.0, 0.0); + let mut k = 1.0; + while k <= b.n { + let t = k - 1.0 + frac; + let cf = if k == b.n { c + 100.0 } else { c }; + let pv = cf / y.powf(t); + wsum += t * pv; + psum += pv; + k += 1.0; + } + if psum == 0.0 { + return Err(CellError::Num); + } + Ok((wsum / psum / f, yld, f)) +} + +pub(super) fn duration(a: &[Arg], c: &mut dyn Ctx) -> R { + num_val(duration_calc(a, c.date_system())?.0) +} + +pub(super) fn mduration(a: &[Arg], c: &mut dyn Ctx) -> R { + let (d, y, f) = duration_calc(a, c.date_system())?; + num_val(d / (1.0 + y / f)) +} + +// --------------------------------------------------------------------------------------------- +// Odd first and last coupon periods +// --------------------------------------------------------------------------------------------- +// +// An odd period is split into quasi-coupon periods: the regular coupon periods the security +// would have had, counted back from the first coupon (ODDF*) or forward from the last interest +// date (ODDL*). For quasi-period i, NL is its normal length, DC the days of it inside the odd +// period, A the accrued days of it before settlement and DSC the days of it from settlement on. + +/// Most quasi-coupon periods an odd period may span (the date range allows ~32,400 quarters). +const MAX_QUASI: i64 = 40_000; + +/// Normal length of the coupon period `[start, end]` under a basis (COUPDAYS semantics). +fn normal_length(start: f64, end: f64, freq: f64, basis: u32) -> f64 { + match basis { + 1 => end - start, + 3 => 365.0 / freq, + _ => 360.0 / freq, + } +} + +/// Days of `[from, to]` under a basis, or 0 when the span is empty. +fn span(sys: DateSystem, from: f64, to: f64, basis: u32) -> R { + if to > from { day_count(sys, from, to, basis) } else { Ok(0.0) } +} + +/// The odd-first-period terms that ODDFPRICE and ODDFYIELD share. +struct OddFirst { + /// Coupons after the first coupon up to maturity. + n: f64, + /// Whole quasi-coupon periods between settlement and the first coupon. + nq: f64, + /// DSC / NL of the quasi-period holding settlement. + frac: f64, + /// Σ DCi / NLi over the odd period. + dc: f64, + /// Σ Ai / NLi over the odd period. + acc: f64, + freq: f64, +} + +fn odd_first(sys: DateSystem, settle: f64, mat: f64, issue: f64, first: f64, freq: f64, basis: u32) -> R { + if !(mat > first && first > settle && settle > issue) { + return Err(CellError::Num); + } + let anchor = ymd(sys, first)?; + let eom = anchor.2 == days_in_month(anchor.0, anchor.1); + let step = (12.0 / freq) as i64; + let quasi = |k: i64| shift_months(sys, anchor, -k * step, eom); + let (mut dc, mut acc) = (0.0, 0.0); + let (mut nq, mut frac) = (None, 0.0); + // Walk back from the first coupon: period k runs from quasi(k + 1) to quasi(k). + let mut k = 0; + loop { + if k >= MAX_QUASI { + return Err(CellError::Num); + } + let (end, start) = (quasi(k)?, quasi(k + 1)?); + let nl = normal_length(start, end, freq, basis); + if nl <= 0.0 { + return Err(CellError::Num); + } + let from = start.max(issue); + dc += span(sys, from, end, basis)? / nl; + acc += span(sys, from, settle.min(end), basis)? / nl; + if start <= settle && settle < end { + nq = Some(k as f64); + frac = span(sys, settle, end, basis)? / nl; + } + if start <= issue { + break; + } + k += 1; + } + let nq = nq.ok_or(CellError::Num)?; + let n = coupons(sys, first, mat, freq)?.num; + Ok(OddFirst { n, nq, frac, dc, acc, freq }) +} + +fn odd_first_price(t: &OddFirst, rate: f64, yld: f64, red: f64) -> f64 { + let coupon = 100.0 * rate / t.freq; + let base = 1.0 + yld / t.freq; + let lead = t.nq + t.frac; + let mut p = red / base.powf(t.n + lead) + coupon * t.dc / base.powf(lead); + let mut k = 1.0; + while k <= t.n { + p += coupon / base.powf(k + lead); + k += 1.0; + } + p - coupon * t.acc +} + +/// settlement, maturity, issue, first_coupon, rate, yld/pr, redemption, frequency, [basis]. +fn odd_first_args(a: &[Arg], sys: DateSystem) -> R<(OddFirst, f64, f64, f64)> { + let (s, m, issue, first) = (date(a, 0)?, date(a, 1)?, date(a, 2)?, date(a, 3)?); + let (rate, x, red) = (num(a, 4)?, num(a, 5)?, num(a, 6)?); + let (f, basis) = (freq_arg(a, 7)?, basis_arg(a, 8)?); + if rate < 0.0 || red <= 0.0 { + return Err(CellError::Num); + } + Ok((odd_first(sys, s, m, issue, first, f, basis)?, rate, x, red)) +} + +pub(super) fn oddfprice(a: &[Arg], c: &mut dyn Ctx) -> R { + let (t, rate, yld, red) = odd_first_args(a, c.date_system())?; + if yld < 0.0 { + return Err(CellError::Num); + } + num_val(odd_first_price(&t, rate, yld, red)) +} + +pub(super) fn oddfyield(a: &[Arg], c: &mut dyn Ctx) -> R { + let (t, rate, pr, red) = odd_first_args(a, c.date_system())?; + if pr <= 0.0 { + return Err(CellError::Num); + } + // The price falls as the yield rises: bracket the root, then bisect. + let g = |y: f64| odd_first_price(&t, rate, y, red) - pr; + let (mut lo, mut hi) = (-0.999_999 * t.freq, 1.0); + let mut guard = 0; + while g(hi) > 0.0 && guard < 60 { + hi *= 2.0; + guard += 1; + } + // NaN fails both comparisons. + if !(g(lo) >= 0.0 && g(hi) <= 0.0) { + return Err(CellError::Num); + } + for _ in 0..200 { + let mid = 0.5 * (lo + hi); + if g(mid) > 0.0 { + lo = mid; + } else { + hi = mid; + } + } + num_val(0.5 * (lo + hi)) +} + +/// Σ DCi/NLi, Σ Ai/NLi and Σ DSCi/NLi over an odd last period. +fn odd_last(sys: DateSystem, settle: f64, mat: f64, last: f64, freq: f64, basis: u32) -> R<(f64, f64, f64)> { + if !(mat > settle && settle > last) { + return Err(CellError::Num); + } + let anchor = ymd(sys, last)?; + let eom = anchor.2 == days_in_month(anchor.0, anchor.1); + let step = (12.0 / freq) as i64; + let (mut dc, mut acc, mut dsc) = (0.0, 0.0, 0.0); + // Walk forward from the last interest date: period k runs from quasi(k - 1) to quasi(k). + let mut start = last; + let mut k = 1; + loop { + if k > MAX_QUASI { + return Err(CellError::Num); + } + let end = shift_months(sys, anchor, k * step, eom)?; + let nl = normal_length(start, end, freq, basis); + if nl <= 0.0 { + return Err(CellError::Num); + } + let stop = end.min(mat); + dc += span(sys, start, stop, basis)? / nl; + acc += span(sys, start, settle.min(stop), basis)? / nl; + dsc += span(sys, settle.max(start), stop, basis)? / nl; + if end >= mat { + break; + } + start = end; + k += 1; + } + Ok((dc, acc, dsc)) +} + +/// settlement, maturity, last_interest, rate, yld/pr, redemption, frequency, [basis]. +fn odd_last_args(a: &[Arg], sys: DateSystem) -> R<((f64, f64, f64), f64, f64, f64, f64)> { + let (s, m, last) = (date(a, 0)?, date(a, 1)?, date(a, 2)?); + let (rate, x, red) = (num(a, 3)?, num(a, 4)?, num(a, 5)?); + let (f, basis) = (freq_arg(a, 6)?, basis_arg(a, 7)?); + if rate < 0.0 || red <= 0.0 { + return Err(CellError::Num); + } + Ok((odd_last(sys, s, m, last, f, basis)?, rate, x, red, f)) +} + +pub(super) fn oddlprice(a: &[Arg], c: &mut dyn Ctx) -> R { + let ((dc, acc, dsc), rate, yld, red, f) = odd_last_args(a, c.date_system())?; + if yld < 0.0 { + return Err(CellError::Num); + } + let coupon = 100.0 * rate / f; + num_val((red + dc * coupon) / (1.0 + dsc * yld / f) - acc * coupon) +} + +pub(super) fn oddlyield(a: &[Arg], c: &mut dyn Ctx) -> R { + let ((dc, acc, dsc), rate, pr, red, f) = odd_last_args(a, c.date_system())?; + if pr <= 0.0 { + return Err(CellError::Num); + } + let coupon = 100.0 * rate / f; + let paid = pr + acc * coupon; + if paid <= 0.0 || dsc <= 0.0 { + return Err(CellError::Num); + } + num_val((red + dc * coupon - paid) / paid * f / dsc) +} diff --git a/crates/functions/src/financial/tvm.rs b/crates/functions/src/financial/tvm.rs new file mode 100644 index 000000000..a62d29bb5 --- /dev/null +++ b/crates/functions/src/financial/tvm.rs @@ -0,0 +1,415 @@ +use super::*; + +// --------------------------------------------------------------------------------------------- +// Time value of money +// --------------------------------------------------------------------------------------------- + +/// The payment per period. `(1 + rate)^nper` comes from `nper * ln_1p(rate)` and `p - 1` from +/// `exp_m1`: rounding `1 + rate` first throws away the low bits of a small rate, which left PMT up to +/// about 1000 ULPs from Excel (observed over 400 synthetic probes; this form is within 4). A loan +/// schedule's last `IF(p - SUM(principal) > 0, ...)` turns on those bits. Excel gives #NUM! for a +/// rate of -1 or less, and -pv*rate (no divide) once `(1 + rate)^nper` overflows. +fn pmt_calc(rate: f64, nper: f64, pv: f64, fv: f64, typ: f64) -> f64 { + if rate == 0.0 { + return -(pv + fv) / nper; + } + if rate <= -1.0 { + return f64::NAN; + } + let l = nper * rate.ln_1p(); + let m = l.exp_m1(); + let k = 1.0 + rate * typ; + if m > 0.0 { + // (fv + pv*p) / (p - 1) = pv + (pv + fv) / (p - 1), without the cancellation in p - 1. + // rate / m (about 1 / nper for a tiny rate) is taken first so (pv + fv) / m can't overflow. + -(pv * rate + (pv + fv) * (rate / m)) / k + } else if m > -0.5 { + // p near 1 from below: fv + pv*p = (pv + fv) + pv*m keeps the small terms that rounding p would drop. + -(rate * (pv + fv + pv * m)) / (k * m) + } else { + // p well below 1: m nears -1 and pv*m would swallow pv + fv, so use p itself. + -(rate * (fv + pv * l.exp())) / (k * m) + } +} + +fn fv_calc(rate: f64, nper: f64, pmt: f64, pv: f64, typ: f64) -> f64 { + if rate == 0.0 { + return -(pv + pmt * nper); + } + let p = (1.0 + rate).powf(nper); + -(pv * p + pmt * (1.0 + rate * typ) * (p - 1.0) / rate) +} + +fn pv_calc(rate: f64, nper: f64, pmt: f64, fv: f64, typ: f64) -> f64 { + if rate == 0.0 { + return -(fv + pmt * nper); + } + let p = (1.0 + rate).powf(nper); + -(fv + pmt * (1.0 + rate * typ) * (p - 1.0) / rate) / p +} + +pub(super) fn pmt(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, nper, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + let (fv, typ) = (opt_num(a, 3, 0.0)?, type_arg(a, 4)?); + if nper == 0.0 { + return Err(CellError::Num); + } + num_val(pmt_calc(rate, nper, pv, fv, typ)) +} + +pub(super) fn fv(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, nper, pmt) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + num_val(fv_calc(rate, nper, pmt, opt_num(a, 3, 0.0)?, type_arg(a, 4)?)) +} + +pub(super) fn pv(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, nper, pmt) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + num_val(pv_calc(rate, nper, pmt, opt_num(a, 3, 0.0)?, type_arg(a, 4)?)) +} + +pub(super) fn nper(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, pmt, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + let (fv, typ) = (opt_num(a, 3, 0.0)?, type_arg(a, 4)?); + if rate == 0.0 { + if pmt == 0.0 { + return Err(CellError::Num); + } + return num_val(-(pv + fv) / pmt); + } + let numer = pmt * (1.0 + rate * typ) - fv * rate; + let denom = pv * rate + pmt * (1.0 + rate * typ); + if denom == 0.0 { + return Err(CellError::Num); + } + let q = numer / denom; + if q <= 0.0 || rate <= -1.0 { + return Err(CellError::Num); + } + num_val(q.ln() / (1.0 + rate).ln()) +} + +fn ipmt_calc(rate: f64, per: f64, nper: f64, pv: f64, fv: f64, typ: f64) -> f64 { + let p = pmt_calc(rate, nper, pv, fv, typ); + if typ == 1.0 && per == 1.0 { + return 0.0; + } + let bal = fv_calc(rate, per - 1.0, p, pv, typ); + let i = bal * rate; + if typ == 1.0 { i / (1.0 + rate) } else { i } +} + +pub(super) fn ipmt(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, per, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); + let (fv, typ) = (opt_num(a, 4, 0.0)?, type_arg(a, 5)?); + if per < 1.0 || per > np || np == 0.0 { + return Err(CellError::Num); + } + num_val(ipmt_calc(rate, per, np, pv, fv, typ)) +} + +pub(super) fn ppmt(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, per, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); + let (fv, typ) = (opt_num(a, 4, 0.0)?, type_arg(a, 5)?); + if per < 1.0 || per > np || np == 0.0 { + return Err(CellError::Num); + } + num_val(pmt_calc(rate, np, pv, fv, typ) - ipmt_calc(rate, per, np, pv, fv, typ)) +} + +fn cumulative(a: &[Arg], principal: bool) -> R { + let (rate, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + let (start, end, typ) = (num(a, 3)?.trunc(), num(a, 4)?.trunc(), num(a, 5)?); + let np_i = np.trunc(); + if rate <= 0.0 || np_i <= 0.0 || pv <= 0.0 || start < 1.0 || end < start || end > np_i || (typ != 0.0 && typ != 1.0) { + return Err(CellError::Num); + } + if end - start > 1e7 || np_i > 1e15 { + return Err(CellError::Num); + } + let p = pmt_calc(rate, np_i, pv, 0.0, typ); + let mut sum = 0.0; + let mut per = start; + while per <= end { + let i = ipmt_calc(rate, per, np_i, pv, 0.0, typ); + sum += if principal { p - i } else { i }; + per += 1.0; + } + num_val(sum) +} + +pub(super) fn cumipmt(a: &[Arg], _c: &mut dyn Ctx) -> R { + cumulative(a, false) +} + +pub(super) fn cumprinc(a: &[Arg], _c: &mut dyn Ctx) -> R { + cumulative(a, true) +} + +/// Newton iteration with a numeric derivative, keeping the rate above -1. +fn newton(f: impl Fn(f64) -> f64, guess: f64) -> Option { + let mut x = guess; + for _ in 0..200 { + let y = f(x); + if !y.is_finite() { + return None; + } + let h = 1e-6 * x.abs().max(1e-3); + let d = (f(x + h) - f(x - h)) / (2.0 * h); + if d == 0.0 || !d.is_finite() { + return None; + } + let mut nx = x - y / d; + if nx <= -1.0 { + nx = (x - 1.0) / 2.0; + if nx <= -1.0 { + nx = -0.999_999_999; + } + } + if (nx - x).abs() < 1e-12 * nx.abs().max(1.0) { + return Some(nx); + } + x = nx; + } + None +} + +/// Tries Newton from the guess and a few fallback starting points; then bisection over a +/// bracket if one can be found. +fn solve_rate(f: impl Fn(f64) -> f64, guess: f64) -> R { + let check = |r: f64| f(r).is_finite() && f(r).abs() < 1e-7 * (1.0 + f(guess).abs().min(1e12)); + for g in [guess, 0.1, 0.01, 0.5, -0.5, 1.0, 0.001, 5.0, -0.9, 10.0] { + if let Some(r) = newton(&f, g) + && r > -1.0 + && check(r) + { + return Ok(r); + } + } + // Scan for a sign change and bisect. + let grid: Vec = [-0.999_999, -0.99, -0.9, -0.5, -0.2, 0.0, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 100.0, 1000.0].to_vec(); + for w in grid.windows(2) { + let (mut lo, mut hi) = match w { + [a, b] => (*a, *b), + _ => continue, + }; + let (flo, fhi) = (f(lo), f(hi)); + if !(flo.is_finite() && fhi.is_finite()) || flo * fhi > 0.0 { + continue; + } + let lo_neg = flo < 0.0; + for _ in 0..200 { + let mid = 0.5 * (lo + hi); + let fm = f(mid); + if (fm < 0.0) == lo_neg { + lo = mid; + } else { + hi = mid; + } + } + return Ok(0.5 * (lo + hi)); + } + Err(CellError::Num) +} + +pub(super) fn rate(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (np, pmt, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + let (fv, typ, guess) = (opt_num(a, 3, 0.0)?, type_arg(a, 4)?, opt_num(a, 5, 0.1)?); + if np <= 0.0 { + return Err(CellError::Num); + } + let f = |r: f64| { + if r.abs() < 1e-12 { + pv + pmt * np + fv + } else { + let p = (1.0 + r).powf(np); + pv * p + pmt * (1.0 + r * typ) * (p - 1.0) / r + fv + } + }; + num_val(solve_rate(f, guess)?) +} + +pub(super) fn npv(a: &[Arg], _c: &mut dyn Ctx) -> R { + let rate = num(a, 0)?; + if rate == -1.0 { + return Err(CellError::Div0); + } + let vals = numbers(a.get(1..).unwrap_or(&[]))?; + let mut sum = 0.0; + let mut d = 1.0; + for v in vals { + d *= 1.0 + rate; + sum += v / d; + } + num_val(sum) +} + +/// Values and dates for XNPV/XIRR: both must be numeric and of equal length. +fn dated_flows(values: &Value, dates: &Value) -> R<(Vec, Vec)> { + let va = as_array(values); + let da = as_array(dates); + if va.data.len() != da.data.len() { + return Err(CellError::Num); + } + let mut v = Vec::with_capacity(va.data.len()); + let mut d = Vec::with_capacity(da.data.len()); + for (x, y) in va.data.iter().zip(da.data.iter()) { + match x { + Value::Number(n) => v.push(*n), + Value::Error(e) => return Err(*e), + _ => return Err(CellError::Value), + } + match y { + Value::Number(n) => d.push(n.trunc()), + Value::Error(e) => return Err(*e), + Value::Text(_) => d.push(y.to_number()?.trunc()), + _ => return Err(CellError::Value), + } + } + let d0 = *d.first().ok_or(CellError::Num)?; + if d.iter().any(|&x| x < d0 || x < 0.0) { + return Err(CellError::Num); + } + Ok((v, d)) +} + +fn xnpv_calc(rate: f64, v: &[f64], d: &[f64]) -> f64 { + let d0 = d.first().copied().unwrap_or(0.0); + v.iter().zip(d).map(|(x, t)| x / (1.0 + rate).powf((t - d0) / 365.0)).sum() +} + +pub(super) fn xnpv(a: &[Arg], _c: &mut dyn Ctx) -> R { + let rate = num(a, 0)?; + if rate <= -1.0 { + return Err(CellError::Num); + } + let (v, d) = dated_flows(&a.get(1).ok_or(CellError::Value)?.value, &a.get(2).ok_or(CellError::Value)?.value)?; + num_val(xnpv_calc(rate, &v, &d)) +} + +pub(super) fn xirr(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (v, d) = dated_flows(&a.first().ok_or(CellError::Value)?.value, &a.get(1).ok_or(CellError::Value)?.value)?; + let guess = opt_num(a, 2, 0.1)?; + if !v.iter().any(|x| *x > 0.0) || !v.iter().any(|x| *x < 0.0) { + return Err(CellError::Num); + } + num_val(solve_rate(|r| xnpv_calc(r, &v, &d), guess)?) +} + +pub(super) fn irr(a: &[Arg], _c: &mut dyn Ctx) -> R { + let v = array_numbers(&a.first().ok_or(CellError::Value)?.value)?; + let guess = opt_num(a, 1, 0.1)?; + if !v.iter().any(|x| *x > 0.0) || !v.iter().any(|x| *x < 0.0) { + return Err(CellError::Num); + } + let f = |r: f64| { + let mut s = 0.0; + let mut d = 1.0; + for x in &v { + s += x / d; + d *= 1.0 + r; + } + s + }; + num_val(solve_rate(f, guess)?) +} + +pub(super) fn mirr(a: &[Arg], _c: &mut dyn Ctx) -> R { + let v = array_numbers(&a.first().ok_or(CellError::Value)?.value)?; + let (fr, rr) = (num(a, 1)?, num(a, 2)?); + let n = v.len() as f64; + if v.len() < 2 { + return Err(CellError::Div0); + } + let (mut pos, mut neg) = (0.0, 0.0); + for (i, x) in v.iter().enumerate() { + if *x > 0.0 { + pos += x / (1.0 + rr).powi(i as i32); + } else if *x < 0.0 { + neg += x / (1.0 + fr).powi(i as i32); + } + } + // No positive flow is not an error in Excel: the formula below gives -1. + if neg == 0.0 { + return Err(CellError::Div0); + } + let r = (-pos * (1.0 + rr).powf(n - 1.0) / neg).powf(1.0 / (n - 1.0)) - 1.0; + num_val(r) +} + +pub(super) fn fvschedule(a: &[Arg], _c: &mut dyn Ctx) -> R { + let mut p = num(a, 0)?; + for v in flatten(a.get(1..2).unwrap_or(&[])) { + let r = match v { + Value::Number(n) => n, + Value::Empty => 0.0, + Value::Error(e) => return Err(e), + _ => return Err(CellError::Value), + }; + p *= 1.0 + r; + } + num_val(p) +} + +pub(super) fn effect(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (nom, n) = (num(a, 0)?, num(a, 1)?.trunc()); + if nom <= 0.0 || n < 1.0 { + return Err(CellError::Num); + } + num_val((1.0 + nom / n).powf(n) - 1.0) +} + +pub(super) fn nominal(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (eff, n) = (num(a, 0)?, num(a, 1)?.trunc()); + if eff <= 0.0 || n < 1.0 { + return Err(CellError::Num); + } + num_val(n * ((1.0 + eff).powf(1.0 / n) - 1.0)) +} + +pub(super) fn ispmt(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, per, np, pv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?, num(a, 3)?); + if np == 0.0 { + return Err(CellError::Div0); + } + num_val(pv * rate * (per / np - 1.0)) +} + +pub(super) fn pduration(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (rate, pv, fv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + if rate <= 0.0 || pv <= 0.0 || fv <= 0.0 { + return Err(CellError::Num); + } + num_val((fv.ln() - pv.ln()) / (1.0 + rate).ln()) +} + +pub(super) fn rri(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (np, pv, fv) = (num(a, 0)?, num(a, 1)?, num(a, 2)?); + if np <= 0.0 || pv == 0.0 { + return Err(CellError::Num); + } + num_val((fv / pv).powf(1.0 / np) - 1.0) +} + +fn dollar_digits(fraction: f64) -> R { + if fraction < 0.0 { + return Err(CellError::Num); + } + if fraction < 1.0 { + return Err(CellError::Div0); + } + Ok(10f64.powf(fraction.log10().ceil())) +} + +pub(super) fn dollarde(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (x, f) = (num(a, 0)?, num(a, 1)?.trunc()); + let p = dollar_digits(f)?; + let int = x.trunc(); + num_val(int + (x - int) * p / f) +} + +pub(super) fn dollarfr(a: &[Arg], _c: &mut dyn Ctx) -> R { + let (x, f) = (num(a, 0)?, num(a, 1)?.trunc()); + let p = dollar_digits(f)?; + let int = x.trunc(); + num_val(int + (x - int) * f / p) +}