From e1902111d85cd32d7f081630e1ce021c1c0b7839 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:29:44 +0000 Subject: [PATCH] Read securities function dates in the workbook's date system Fixes #1457 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Opus 5.5 --- crates/functions/src/financial.rs | 286 ++++++++++++++++-------------- 1 file changed, 154 insertions(+), 132 deletions(-) diff --git a/crates/functions/src/financial.rs b/crates/functions/src/financial.rs index 2758246ce..af28326e7 100644 --- a/crates/functions/src/financial.rs +++ b/crates/functions/src/financial.rs @@ -1,9 +1,9 @@ //! Financial functions: time value of money, cash-flow analysis, depreciation and securities. //! //! Sign convention follows Excel: money paid out is negative, money received is positive. -//! Date arguments are serial numbers (truncated) in the 1900 date system. +//! Date arguments are serial numbers (truncated) in the workbook's date system. -use gridcraft_core::date::{datetime_from_serial, days_in_month, serial_from_ymd}; +use gridcraft_core::date::{datetime_from_serial, days_in_month, is_leap, max_serial, serial_from_ymd}; 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}; @@ -526,13 +526,12 @@ fn vdb(a: &[Arg], _c: &mut dyn Ctx) -> R { 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)?); - let _ = c; 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(purchased, first, basis)?).min(limit); + let dep0 = (full * year_frac(c.date_system(), purchased, first, basis)?).min(limit); if period == 0.0 { return num_val(dep0); } @@ -543,7 +542,6 @@ fn amorlinc(a: &[Arg], c: &mut dyn Ctx) -> R { 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)?); - let _ = c; if cost < 0.0 || salvage < 0.0 || salvage > cost || period < 0.0 || rate <= 0.0 || first < purchased || period > 1e6 { return Err(CellError::Num); } @@ -559,7 +557,7 @@ fn amordegrc(a: &[Arg], c: &mut dyn Ctx) -> R { }; let r = rate * coef; let mut book = cost; - let mut dep = (year_frac(purchased, first, basis)? * r * cost).round(); + let mut dep = (year_frac(c.date_system(), purchased, first, basis)? * r * cost).round(); if period == 0.0 { return num_val(dep); } @@ -592,13 +590,13 @@ fn date(a: &[Arg], i: usize) -> R { Ok(d) } -fn ymd(s: f64) -> R<(i32, u32, u32)> { - let dt = datetime_from_serial(DateSystem::D1900, s.trunc()).ok_or(CellError::Num)?; +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(y: i32, m: u32, d: u32) -> R { - serial_from_ymd(DateSystem::D1900, y as i64, m as i64, d as i64).ok_or(CellError::Num) +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 { @@ -617,14 +615,10 @@ fn freq_arg(a: &[Arg], i: usize) -> R { Ok(f) } -fn is_leap(y: i32) -> bool { - (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 -} - /// 30/360 day count (US/NASD when `us`, European otherwise). -fn days360(s1: f64, s2: f64, us: bool) -> R { - let (y1, m1, mut d1) = ymd(s1)?; - let (y2, m2, mut d2) = ymd(s2)?; +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); @@ -648,25 +642,25 @@ fn days360(s1: f64, s2: f64, us: bool) -> R { } /// Days between two dates under a basis. -fn day_count(s1: f64, s2: f64, basis: u32) -> R { +fn day_count(sys: DateSystem, s1: f64, s2: f64, basis: u32) -> R { match basis { - 0 => days360(s1, s2, true), - 4 => days360(s1, s2, false), + 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(s1: f64, s2: f64, basis: u32) -> R { +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(s1, s2, true)? / 360.0), + 0 => Ok(days360(sys, s1, s2, true)? / 360.0), 2 => Ok((s2 - s1) / 360.0), 3 => Ok((s2 - s1) / 365.0), - 4 => Ok(days360(s1, s2, false)? / 360.0), + 4 => Ok(days360(sys, s1, s2, false)? / 360.0), _ => { - let (y1, m1, d1) = ymd(s1)?; - let (y2, m2, d2) = ymd(s2)?; + 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 }); @@ -678,8 +672,8 @@ fn year_frac(s1: f64, s2: f64, basis: u32) -> R { let denom = if feb29_1 || feb29_2 { 366.0 } else { 365.0 }; return Ok(days / denom); } - let start = serial(y1, 1, 1)?; - let end = serial(y2 + 1, 1, 1).unwrap_or(2_958_466.0); + 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) } @@ -687,12 +681,12 @@ fn year_frac(s1: f64, s2: f64, basis: u32) -> R { } /// Days in a year for discount securities. -fn year_basis(basis: u32, s: f64) -> R { +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(s)?.0) { + if is_leap(ymd(sys, s)?.0) { 366.0 } else { 365.0 @@ -702,7 +696,7 @@ fn year_basis(basis: u32, s: f64) -> R { } /// Date `months` months before/after an anchor date, keeping end-of-month alignment. -fn shift_months(anchor: (i32, u32, u32), months: i64, eom: bool) -> R { +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); @@ -712,7 +706,7 @@ fn shift_months(anchor: (i32, u32, u32), months: i64, eom: bool) -> R { } let dim = days_in_month(ny as i32, nm); let nd = if eom { dim } else { d.min(dim) }; - serial(ny as i32, nm, nd) + serial(sys, ny as i32, nm, nd) } struct Coupon { @@ -721,27 +715,27 @@ struct Coupon { num: f64, } -fn coupons(settle: f64, mat: f64, freq: f64) -> R { +fn coupons(sys: DateSystem, settle: f64, mat: f64, freq: f64) -> R { if settle >= mat { return Err(CellError::Num); } - let anchor = ymd(mat)?; + 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(anchor, -(k - 1) * step, eom)? <= settle { + while k > 1 && shift_months(sys, anchor, -(k - 1) * step, eom)? <= settle { k -= 1; } let mut guard = 0; - while shift_months(anchor, -k * step, eom)? > settle { + 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(anchor, -k * step, eom)?, ncd: shift_months(anchor, -(k - 1) * step, eom)?, num: k as f64 }) + 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 { @@ -752,56 +746,57 @@ fn coup_days(c: &Coupon, freq: f64, basis: u32) -> f64 { } } -fn coup_daybs(c: &Coupon, settle: f64, basis: u32) -> R { - day_count(c.pcd, settle, basis) +fn coup_daybs(sys: DateSystem, c: &Coupon, settle: f64, basis: u32) -> R { + day_count(sys, c.pcd, settle, basis) } -fn coup_daysnc(c: &Coupon, settle: f64, freq: f64, basis: u32) -> R { +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(c, settle, 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]) -> R<(f64, f64, f64, u32, Coupon)> { +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(s, m, f)?; + let c = coupons(sys, s, m, f)?; Ok((s, m, f, b, c)) } -fn coupdaybs(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (s, _, _, b, c) = coup_args(a)?; - num_val(coup_daybs(&c, s, b)?) +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], _c: &mut dyn Ctx) -> R { - let (_, _, f, b, c) = coup_args(a)?; +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], _c: &mut dyn Ctx) -> R { - let (s, _, f, b, c) = coup_args(a)?; - num_val(coup_daysnc(&c, s, 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)?.4.ncd) +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)?.4.pcd) +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)?.4.num) +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 { +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)?; @@ -810,10 +805,10 @@ fn accrint(a: &[Arg], _c: &mut dyn Ctx) -> R { if issue >= settle || rate <= 0.0 || par <= 0.0 { return Err(CellError::Num); } - let anchor = ymd(first)?; + 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(anchor, j * step, eom); + 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); @@ -851,7 +846,7 @@ fn accrint(a: &[Arg], _c: &mut dyn Ctx) -> R { 3 => 365.0 / freq, _ => 360.0 / freq, }; - sum += day_count(from, to, basis)? / nl; + sum += day_count(sys, from, to, basis)? / nl; } j += 1; guard += 1; @@ -862,14 +857,14 @@ fn accrint(a: &[Arg], _c: &mut dyn Ctx) -> R { num_val(par * rate / freq * sum) } -fn accrintm(a: &[Arg], _c: &mut dyn Ctx) -> R { +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(issue, settle, basis)?) + num_val(par * rate * year_frac(c.date_system(), issue, settle, basis)?) } /// settlement, maturity, value, value, [basis] for discount securities. @@ -881,55 +876,55 @@ fn disc_args(a: &[Arg]) -> R<(f64, f64, f64, f64, u32)> { Ok((s, m, x, y, b)) } -fn disc_frac(s: f64, m: f64, b: u32) -> R { - Ok(day_count(s, m, b)? / year_basis(b, s)?) +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 { +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(s, m, b)?) + num_val((1.0 - pr / red) / disc_frac(c.date_system(), s, m, b)?) } -fn pricedisc(a: &[Arg], _c: &mut dyn Ctx) -> R { +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(s, m, b)?) + num_val(red - d * red * disc_frac(c.date_system(), s, m, b)?) } -fn yielddisc(a: &[Arg], _c: &mut dyn Ctx) -> R { +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(s, m, b)?) + num_val((red / pr - 1.0) / disc_frac(c.date_system(), s, m, b)?) } -fn intrate(a: &[Arg], _c: &mut dyn Ctx) -> R { +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(s, m, b)?) + num_val((red - inv) / inv / disc_frac(c.date_system(), s, m, b)?) } -fn received(a: &[Arg], _c: &mut dyn Ctx) -> R { +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(s, m, b)?; + 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]) -> R<(f64, f64)> { +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(s)?; - let limit = shift_months((y, mo, d), 12, false)?; + 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)?; +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); @@ -937,13 +932,13 @@ fn tbillprice(a: &[Arg], _c: &mut dyn Ctx) -> R { num_val(p) } -fn tbillyield(a: &[Arg], _c: &mut dyn Ctx) -> R { - let (dsm, pr) = tbill_args(a)?; +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)?; +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)); } @@ -960,24 +955,24 @@ fn tbilleq(a: &[Arg], _c: &mut dyn Ctx) -> R { } /// settlement, maturity, issue, rate, value, [basis]. -fn mat_terms(a: &[Arg]) -> R<(f64, f64, f64, f64, f64)> { +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(issue, m, b)?; - let dsm = year_frac(s, m, b)?; - let ai = year_frac(issue, s, b)?; + 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)?; +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)?; +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); } @@ -993,11 +988,11 @@ struct Bond { freq: f64, } -fn bond(settle: f64, mat: f64, freq: f64, basis: u32) -> R { - let c = coupons(settle, mat, freq)?; +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(&c, settle, basis)?; - let dsc = coup_daysnc(&c, settle, 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); } @@ -1022,23 +1017,23 @@ fn bond_price(b: &Bond, rate: f64, yld: f64, red: f64) -> f64 { sum - accrued } -fn price(a: &[Arg], _c: &mut dyn Ctx) -> R { +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(s, m, f, basis)?; + 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 { +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(s, m, f, basis)?; + 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); @@ -1065,13 +1060,13 @@ fn yield_(a: &[Arg], _c: &mut dyn Ctx) -> R { num_val(0.5 * (lo + hi)) } -fn duration_calc(a: &[Arg]) -> R<(f64, f64, f64)> { +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(s, m, f, basis)?; + 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; @@ -1091,12 +1086,12 @@ fn duration_calc(a: &[Arg]) -> R<(f64, f64, f64)> { Ok((wsum / psum / f, yld, f)) } -fn duration(a: &[Arg], _c: &mut dyn Ctx) -> R { - num_val(duration_calc(a)?.0) +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)?; +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)) } @@ -1122,8 +1117,8 @@ fn normal_length(start: f64, end: f64, freq: f64, basis: u32) -> f64 { } /// Days of `[from, to]` under a basis, or 0 when the span is empty. -fn span(from: f64, to: f64, basis: u32) -> R { - if to > from { day_count(from, to, basis) } else { Ok(0.0) } +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. @@ -1141,14 +1136,14 @@ struct OddFirst { freq: f64, } -fn odd_first(settle: f64, mat: f64, issue: f64, first: f64, freq: f64, basis: u32) -> R { +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(first)?; + 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(anchor, -k * step, eom); + 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). @@ -1163,11 +1158,11 @@ fn odd_first(settle: f64, mat: f64, issue: f64, first: f64, freq: f64, basis: u3 return Err(CellError::Num); } let from = start.max(issue); - dc += span(from, end, basis)? / nl; - acc += span(from, settle.min(end), basis)? / nl; + 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(settle, end, basis)? / nl; + frac = span(sys, settle, end, basis)? / nl; } if start <= issue { break; @@ -1175,7 +1170,7 @@ fn odd_first(settle: f64, mat: f64, issue: f64, first: f64, freq: f64, basis: u3 k += 1; } let nq = nq.ok_or(CellError::Num)?; - let n = coupons(first, mat, freq)?.num; + let n = coupons(sys, first, mat, freq)?.num; Ok(OddFirst { n, nq, frac, dc, acc, freq }) } @@ -1193,26 +1188,26 @@ fn odd_first_price(t: &OddFirst, rate: f64, yld: f64, red: f64) -> f64 { } /// settlement, maturity, issue, first_coupon, rate, yld/pr, redemption, frequency, [basis]. -fn odd_first_args(a: &[Arg]) -> R<(OddFirst, f64, f64, f64)> { +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(s, m, issue, first, f, basis)?, rate, x, red)) + 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)?; +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)?; +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); } @@ -1240,11 +1235,11 @@ fn oddfyield(a: &[Arg], _c: &mut dyn Ctx) -> R { } /// Σ DCi/NLi, Σ Ai/NLi and Σ DSCi/NLi over an odd last period. -fn odd_last(settle: f64, mat: f64, last: f64, freq: f64, basis: u32) -> R<(f64, f64, f64)> { +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(last)?; + 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); @@ -1255,15 +1250,15 @@ fn odd_last(settle: f64, mat: f64, last: f64, freq: f64, basis: u32) -> R<(f64, if k > MAX_QUASI { return Err(CellError::Num); } - let end = shift_months(anchor, k * step, eom)?; + 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(start, stop, basis)? / nl; - acc += span(start, settle.min(stop), basis)? / nl; - dsc += span(settle.max(start), stop, basis)? / nl; + 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; } @@ -1274,18 +1269,18 @@ fn odd_last(settle: f64, mat: f64, last: f64, freq: f64, basis: u32) -> R<(f64, } /// settlement, maturity, last_interest, rate, yld/pr, redemption, frequency, [basis]. -fn odd_last_args(a: &[Arg]) -> R<((f64, f64, f64), f64, f64, f64, f64)> { +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(s, m, last, f, basis)?, rate, x, red, f)) + 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)?; +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); } @@ -1293,8 +1288,8 @@ fn oddlprice(a: &[Arg], _c: &mut dyn Ctx) -> R { 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)?; +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); } @@ -2019,4 +2014,31 @@ mod tests { is_err(ev("PMT", vec![t("abc"), n(1.0), n(1.0)]), CellError::Value); is_err(ev("PMT", vec![e(CellError::NA), n(1.0), n(1.0)]), CellError::NA); } + + #[test] + fn securities_read_dates_in_the_1904_system() { + let d4 = |y, m, dd| n(serial_from_ymd(DateSystem::D1904, y, m, dd).unwrap()); + let ev4 = |name: &str, args: Vec| { + let mut ctx = TestCtx { sys: DateSystem::D1904, ..TestCtx::default() }; + crate::call(crate::lookup(name).unwrap(), &args, &mut ctx) + }; + let (s, m) = (d4(2011, 3, 15), d4(2011, 11, 30)); + close(ev4("COUPNCD", vec![s.clone(), m.clone(), n(2.0)]), 39232.0); + close(ev4("COUPPCD", vec![s.clone(), m.clone(), n(4.0), n(0.0)]), 39140.0); + close(ev4("COUPDAYBS", vec![s.clone(), m.clone(), n(4.0), n(0.0)]), 15.0); + let accrint = vec![d4(2008, 3, 1), d4(2008, 8, 31), d4(2008, 5, 1), n(0.1), n(1000.0), n(2.0), n(1.0)]; + close_tol(ev4("ACCRINT", accrint), 16.576086956522, 1e-9); + // Results that already matched Excel keep matching. + let semi = |name| ev4(name, vec![s.clone(), m.clone(), n(2.0), n(0.0)]); + close(semi("COUPPCD"), 39050.0); + close(semi("COUPDAYS"), 180.0); + close(semi("COUPDAYBS"), 105.0); + close(semi("COUPDAYSNC"), 75.0); + close(semi("COUPNUM"), 2.0); + let price = vec![s.clone(), m.clone(), n(0.0575), n(0.065), n(100.0), n(2.0), n(0.0)]; + close_tol(ev4("PRICE", price), 99.47768286339101, 1e-9); + close_tol(ev4("DURATION", vec![s, m, n(0.08), n(0.09), n(2.0), n(0.0)]), 0.6890136809022, 1e-9); + let odd = vec![d4(2008, 11, 11), d4(2021, 3, 1), d4(2008, 10, 15), d4(2009, 3, 1), n(0.0785), n(0.0625), n(100.0), n(2.0), n(1.0)]; + close_tol(ev4("ODDFPRICE", odd), 113.597717474079, 1e-9); + } }