mirror of
https://github.com/storytold/gridcraft.git
synced 2026-10-31 22:26:57 -06:00
refactor(functions): move financial.rs items to financial/
This commit is contained in:
+14
-1306
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,214 @@
|
||||
use super::*;
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Dates and day counts
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
pub(super) fn date(a: &[Arg], i: usize) -> R<f64> {
|
||||
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<f64> {
|
||||
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<u32> {
|
||||
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<f64> {
|
||||
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<f64> {
|
||||
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<f64> {
|
||||
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<f64> {
|
||||
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<f64> {
|
||||
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<f64> {
|
||||
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<Coupon> {
|
||||
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<f64> {
|
||||
day_count(sys, c.pcd, settle, basis)
|
||||
}
|
||||
|
||||
pub(super) fn coup_daysnc(sys: DateSystem, c: &Coupon, settle: f64, freq: f64, basis: u32) -> R<f64> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
num_val(coup_args(a, c.date_system())?.4.ncd)
|
||||
}
|
||||
|
||||
pub(super) fn couppcd(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(coup_args(a, c.date_system())?.4.pcd)
|
||||
}
|
||||
|
||||
pub(super) fn coupnum(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
num_val(coup_args(a, c.date_system())?.4.num)
|
||||
}
|
||||
@@ -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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<f64> {
|
||||
let mut out = Vec::with_capacity(periods);
|
||||
let mut book = cost;
|
||||
let mut sl: Option<f64> = 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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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)
|
||||
}
|
||||
@@ -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<Value> {
|
||||
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<i64> {
|
||||
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<Value> {
|
||||
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<f64> {
|
||||
Ok(day_count(sys, s, m, b)? / year_basis(sys, b, s)?)
|
||||
}
|
||||
|
||||
pub(super) fn disc(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Bond> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
num_val(duration_calc(a, c.date_system())?.0)
|
||||
}
|
||||
|
||||
pub(super) fn mduration(a: &[Arg], c: &mut dyn Ctx) -> R<Value> {
|
||||
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<f64> {
|
||||
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<OddFirst> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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)
|
||||
}
|
||||
@@ -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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
cumulative(a, false)
|
||||
}
|
||||
|
||||
pub(super) fn cumprinc(a: &[Arg], _c: &mut dyn Ctx) -> R<Value> {
|
||||
cumulative(a, true)
|
||||
}
|
||||
|
||||
/// Newton iteration with a numeric derivative, keeping the rate above -1.
|
||||
fn newton(f: impl Fn(f64) -> f64, guess: f64) -> Option<f64> {
|
||||
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<f64> {
|
||||
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<f64> = [-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<Value> {
|
||||
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<Value> {
|
||||
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<f64>, Vec<f64>)> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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<f64> {
|
||||
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<Value> {
|
||||
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<Value> {
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user