Shared Rust kernels
ported/bond_core_family
All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.
max_serial · 3 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn max_serial() -> i64 {
excel_serial_from_ymd(WorkbookDateSystem::System1900, 9999, 12, 31).unwrap() as i64
}
Used by 9 functions.
derr · 3 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn derr(c: WorksheetErrorCode) -> BondCoreEvalError {
BondCoreEvalError::Domain(c)
}
Used by 9 functions.
dyear · 7 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn dyear(y: i64) -> f64 {
if (y % 4 == 0 && y % 100 != 0) || y % 400 == 0 {
366.0
} else {
365.0
}
}
Used by 8 functions.
dim · 14 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn dim(y: i64, m: i64) -> i64 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if dyear(y) == 366.0 {
29
} else {
28
}
}
_ => 30,
}
}
Used by 8 functions.
act · 3 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn act(s: i64, e: i64) -> f64 {
(e - s) as f64
}
Used by 9 functions.
basis · 13 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn basis(v: f64) -> Result<DayCountBasis, BondCoreEvalError> {
if !v.is_finite() {
return Err(derr(WorksheetErrorCode::Num));
}
match v.trunc() as i64 {
0 => Ok(DayCountBasis::Us30_360),
1 => Ok(DayCountBasis::ActualActual),
2 => Ok(DayCountBasis::Actual360),
3 => Ok(DayCountBasis::Actual365),
4 => Ok(DayCountBasis::European30_360),
_ => Err(derr(WorksheetErrorCode::Num)),
}
}
Used by 9 functions.
freq · 9 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn freq(v: f64) -> Result<i64, BondCoreEvalError> {
if !v.is_finite() {
return Err(derr(WorksheetErrorCode::Num));
}
match v.trunc() as i64 {
1 | 2 | 4 => Ok(v.trunc() as i64),
_ => Err(derr(WorksheetErrorCode::Num)),
}
}
Used by 5 functions.
dser · 12 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn dser(v: f64) -> Result<i64, BondCoreEvalError> {
if !v.is_finite() {
return Err(derr(WorksheetErrorCode::Value));
}
let s = v.trunc() as i64;
if s < 1 || s > max_serial() {
return Err(derr(WorksheetErrorCode::Value));
}
ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
Ok(s)
}
Used by 9 functions.
pos · 9 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn pos(v: f64) -> Result<f64, BondCoreEvalError> {
if !v.is_finite() {
Err(derr(WorksheetErrorCode::Value))
} else if v <= 0.0 {
Err(derr(WorksheetErrorCode::Num))
} else {
Ok(v)
}
}
Used by 6 functions.
rate · 9 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn rate(v: f64) -> Result<f64, BondCoreEvalError> {
if !v.is_finite() {
Err(derr(WorksheetErrorCode::Value))
} else if v < 0.0 {
Err(derr(WorksheetErrorCode::Num))
} else {
Ok(v)
}
}
Used by 7 functions.
addm · 9 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn addm(s: i64, m: i64) -> Option<i64> {
let (y, mo, d) = ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)?;
let idx = y.checked_mul(12)?.checked_add(mo - 1)?.checked_add(m)?;
let ty = idx.div_euclid(12);
let tm = idx.rem_euclid(12) + 1;
let end = d == dim(y, mo);
let td = if end { dim(ty, tm) } else { d.min(dim(ty, tm)) };
excel_serial_from_ymd(WorkbookDateSystem::System1900, ty, tm, td).map(|v| v as i64)
}
Used by 4 functions.
d360us · 3 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn d360us(s: i64, e: i64) -> Result<f64, BondCoreEvalError> {
crate::ported::day_count_common::us_30_360(s, e).map_err(derr)
}
Used by 5 functions.
d360eu · 13 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn d360eu(s: i64, e: i64) -> Result<f64, BondCoreEvalError> {
let (sy, sm, mut sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
let (ey, em, mut ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, e as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
if sd == 31 {
sd = 30;
}
if ed == 31 {
ed = 30;
}
Ok(((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) as f64)
}
Used by 9 functions.
less_or_equal_to_a_year_apart · 7 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn less_or_equal_to_a_year_apart(s: i64, e: i64) -> Result<bool, BondCoreEvalError> {
let (sy, sm, sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
let (ey, em, ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, e as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
Ok((ey - sy) < 1 || ((ey - sy) == 1 && (em < sm || (em == sm && ed <= sd))))
}
Used by 3 functions.
consider_as_bisestile · 22 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn consider_as_bisestile(s: i64, e: i64) -> Result<bool, BondCoreEvalError> {
let (sy, _sm, _sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
let (ey, em, ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, e as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
if sy == ey {
return Ok(dyear(sy) == 366.0);
}
if em == 2 && ed == 29 {
return Ok(true);
}
for year in sy..=ey {
if dyear(year) == 366.0 {
let feb29 = excel_serial_from_ymd(WorkbookDateSystem::System1900, year, 2, 29)
.ok_or(derr(WorksheetErrorCode::Value))? as i64;
if s <= feb29 && feb29 <= e {
return Ok(true);
}
}
}
Ok(false)
}
Used by 3 functions.
days_in_year_for_mat · 28 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn days_in_year_for_mat(s: i64, e: i64, b: DayCountBasis) -> Result<f64, BondCoreEvalError> {
match b {
DayCountBasis::Us30_360 | DayCountBasis::Actual360 | DayCountBasis::European30_360 => {
Ok(360.0)
}
DayCountBasis::Actual365 => Ok(365.0),
DayCountBasis::ActualActual => {
if !less_or_equal_to_a_year_apart(s, e)? {
let (sy, _, _) = ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
let (ey, _, _) = ymd_from_excel_serial(WorkbookDateSystem::System1900, e as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
let tot_years = (ey - sy) + 1;
let start_of_issue_year =
excel_serial_from_ymd(WorkbookDateSystem::System1900, sy, 1, 1)
.ok_or(derr(WorksheetErrorCode::Value))? as i64;
let start_after_end_year =
excel_serial_from_ymd(WorkbookDateSystem::System1900, ey + 1, 1, 1)
.ok_or(derr(WorksheetErrorCode::Value))? as i64;
Ok(act(start_of_issue_year, start_after_end_year) / tot_years as f64)
} else if consider_as_bisestile(s, e)? {
Ok(366.0)
} else {
Ok(365.0)
}
}
}
}
Used by 3 functions.
dc · 9 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn dc(s: i64, e: i64, b: DayCountBasis, f: i64) -> Result<f64, BondCoreEvalError> {
match b {
DayCountBasis::Us30_360 => Ok(360.0 / f as f64),
DayCountBasis::ActualActual => Ok(act(s, e)),
DayCountBasis::Actual360 => Ok(360.0 / f as f64),
DayCountBasis::Actual365 => Ok(365.0 / f as f64),
DayCountBasis::European30_360 => Ok(360.0 / f as f64),
}
}
Used by 4 functions.
dd · 9 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn dd(s: i64, e: i64, b: DayCountBasis) -> Result<f64, BondCoreEvalError> {
match b {
DayCountBasis::Us30_360 => d360us(s, e),
DayCountBasis::ActualActual | DayCountBasis::Actual360 | DayCountBasis::Actual365 => {
Ok(act(s, e))
}
DayCountBasis::European30_360 => d360eu(s, e),
}
}
Used by 4 functions.
ctx · 20 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn ctx(
settlement: f64,
maturity: f64,
frequency: f64,
b: Option<f64>,
) -> Result<Ctx, BondCoreEvalError> {
let settlement = dser(settlement)?;
let maturity = dser(maturity)?;
let frequency = freq(frequency)?;
let basis = basis(b.unwrap_or(0.0))?;
if settlement >= maturity {
return Err(derr(WorksheetErrorCode::Num));
}
Ok(Ctx {
settlement,
maturity,
frequency,
basis,
})
}
Used by 4 functions.
period · 13 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn period(c: Ctx) -> Result<Period, BondCoreEvalError> {
let mpc = 12 / c.frequency;
let mut next = c.maturity;
let mut n = 1i64;
loop {
let prev = addm(next, -mpc).ok_or(derr(WorksheetErrorCode::Num))?;
if prev <= c.settlement {
return Ok(Period { prev, next, n });
}
next = prev;
n += 1;
}
}
Used by 4 functions.
excel_bond_pow · 38 lines · calc-func/src/ported/bond_core_family.rs on GitHub/// Excel's bond discount factor `base^(off+k)` is computed with the C-runtime `pow`.
///
/// Two staged cases, both live-Excel identified:
/// * **Non-negative integer exponent** (on-coupon settlement, where `off = dsc/e = 1`,
/// so every `off+k` is an integer) → the C-runtime `pow` integer special case =
/// **binary exponentiation** (square-and-multiply, plain `f64` multiply). Rust's
/// `f64::powf` uses `exp·ln` even for integers, so on-coupon PRICE drifts 1–2 ULP
/// without this. Held-out validated 25/25 across 5 live-Excel bonds (build 20131).
/// * **Fractional exponent** (off-coupon) → the `pow` chain
/// `exp(RN53(RN64(exp·ln base)))` via the x87 `ln`/`mul`/`exp`, i.e.
/// [`crate::ported::excel_numeric::excel_pow_chain`] — NOT the platform `f64::powf`. The two
/// agree on the vast majority of inputs but diverge ±1 ULP on the Actual/360 (basis 2)
/// and Actual/365 (basis 3) fractional discount ladders at higher yields; the x87
/// chain reproduces live Excel where `powf` does not (W109 G6-03d, b37 3474→3656 / 3446→3658
/// on bases 2/3, b38 held-out 315/315 + 315/315). This is the SAME distribution-substrate
/// pow identified independently in the G3 lane-1 work — a cross-lane confirmation.
///
/// See `work/w109/G6-solvers/YIELD_PRICE_FORWARD_KERNEL.md` and
/// `work/w109/G6-b2b3/agentV_results.md`.
fn excel_bond_pow(base: f64, exp: f64) -> f64 {
if exp >= 0.0 && exp < 1024.0 && exp.fract() == 0.0 {
let mut n = exp as u64;
let mut r = 1.0_f64;
let mut b = base;
while n > 0 {
if n & 1 == 1 {
r *= b;
}
n >>= 1;
if n > 0 {
b *= b;
}
}
r
} else {
crate::ported::excel_numeric::excel_pow_chain(base, exp)
}
}
Used by 4 functions.
pcomp_disc · 51 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn pcomp_disc(
rate: f64,
yld: f64,
red: f64,
c: Ctx,
p: Period,
binexp: bool,
) -> Result<(f64, f64, f64, f64, f64), BondCoreEvalError> {
let coup = 100.0 * rate / c.frequency as f64;
let e = dc(p.prev, p.next, c.basis, c.frequency)?;
let a = dd(p.prev, c.settlement, c.basis)?;
// Excel's internal discount/settlement fraction is the DERIVED complement
// `dsc = E - A` (universal across bases), NOT the direct day-count span
// settlement→next-coupon. For bases 0/1/4 the two coincide except on
// settlement-on-31st 30/360 rows (which `E - A` also fixes); for Actual/360
// (basis 2) and Actual/365 (basis 3) they differ materially (the ~cents
// PRICE error, G6-03d). Excel's own COUPDAYSNC still PUBLISHES the actual
// days at bases 2/3 — PRICE's internal DSC deliberately diverges from the
// published COUP* function. (W109 G6-03d; b37/b38 live-Excel, build 20131.)
let dsc = e - a;
if yld <= -(c.frequency as f64) {
return Err(derr(WorksheetErrorCode::Num));
}
let pw = |b: f64, ex: f64| {
if binexp {
excel_bond_pow(b, ex)
} else {
crt_pow(b, ex)
}
};
let dirty = if p.n == 1 {
let den = 1.0 + (yld / c.frequency as f64) * (dsc / e);
if den <= 0.0 {
return Err(derr(WorksheetErrorCode::Num));
}
(red + coup) / den
} else {
let base = 1.0 + yld / c.frequency as f64;
if base <= 0.0 {
return Err(derr(WorksheetErrorCode::Num));
}
let off = dsc / e;
let mut pv = 0.0;
for k in 0..p.n {
pv += coup / pw(base, off + k as f64);
}
pv + red / pw(base, off + (p.n - 1) as f64)
};
let accr = coup * a / e;
Ok((dirty - accr, dirty, coup, a, e))
}
Used by 2 functions.
duration_kernel · 56 lines · calc-func/src/ported/bond_core_family.rs on GitHubpub fn duration_kernel(
settlement: f64,
maturity: f64,
coupon: f64,
yld: f64,
frequency: f64,
basis_: Option<f64>,
) -> Result<f64, BondCoreEvalError> {
let c = ctx(settlement, maturity, frequency, basis_)?;
let p = period(c)?;
let coupon = rate(coupon)?;
let yld = rate(yld)?;
let f = c.frequency as f64;
// W109 G6-03c identification (live Excel build 20131, b44 6,360 witnesses).
// The weighting exponent shares PRICE's schedule quantities: the internal
// settlement fraction is the DERIVED complement `dsc = E - A`, so `off =
// (E - A)/E` (NOT the direct settlement->next day-count span the docs-era
// kernel used — that made bases 2/3 off by cents, 0/1272 exact). The
// discount factor is Excel's C-runtime `pow` staging (`excel_bond_pow`:
// binary exponentiation on the on-coupon integer exponents, the legacy x87
// `exp(RN53(RN64(exp*ln)))` chain off-coupon) — the SAME substrate PRICE
// uses. The Macaulay body op-graph is a plain-`f64` (SSE2) accrual with the
// redemption as a SEPARATE term and the numerator weight grouped
// `(diff*cash)/disc` (left-assoc), NOT `diff*(cash/disc)`; the final
// `num/den/f` association. This is the one internally-consistent kernel:
// numerator and denominator discount the SAME cashflows with the SAME `off`.
let coup = 100.0 * coupon / f;
let e = dc(p.prev, p.next, c.basis, c.frequency)?;
// The accrued span A is `CoupDaysBS` (F# `IDayCount.DaysBetween`, numerator
// position): for US 30/360 this is `dateDiff360US ... ModifyStartDate`
// (`diff360_us(_, _, false)`), NOT the plain `us_30_360` (`dd`). The two
// diverge only on 31st/month-end settlements — e.g. a Feb-end previous coupon
// with a 31st settlement: plain `us_30_360` collapses the end 31->30 AFTER
// the Feb-end start adjustment (giving 30), while `ModifyStartDate` checks
// `end==31` BEFORE adjusting the start (giving 31, matching Excel). This is
// the W109 G6-03c b45 month-end break (the ~2.5e13-ULP FC-45747-b0 explosion).
let a = days_between_num(p.prev, c.settlement, c.basis)?;
let off = (e - a) / e;
if p.n == 1 {
return Ok((off / f).max(0.0));
}
let base = 1.0 + yld / f;
let mut num = 0.0;
let mut den = 0.0;
for k in 0..p.n {
let diff = off + k as f64;
let disc = excel_bond_pow(base, diff);
num += diff * coup / disc;
den += coup / disc;
if k + 1 == p.n {
num += diff * 100.0 / disc;
den += 100.0 / disc;
}
}
Ok(num / den / f)
}
Used by 2 functions.
last_day_of_feb · 5 lines · calc-func/src/ported/bond_core_family.rs on GitHubfn last_day_of_feb(s: i64) -> bool {
ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.map(|(y, mo, d)| mo == 2 && d == dim(y, mo))
.unwrap_or(false)
}
Used by 4 functions.
diff360_us · 20 lines · calc-func/src/ported/bond_core_family.rs on GitHub/// F# `dateDiff360Us` (US 30/360) with the `ModifyStartDate`/`ModifyBothDates` modes.
fn diff360_us(s: i64, e: i64, modify_both: bool) -> Result<f64, BondCoreEvalError> {
let (sy, sm, mut sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, s as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
let (ey, em, mut ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, e as f64)
.ok_or(derr(WorksheetErrorCode::Value))?;
if last_day_of_feb(e) && (last_day_of_feb(s) || modify_both) {
ed = 30;
}
if ed == 31 && (sd >= 30 || modify_both) {
ed = 30;
}
if sd == 31 {
sd = 30;
}
if last_day_of_feb(s) {
sd = 30;
}
Ok(((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) as f64)
}
Used by 4 functions.
days_between_num · 10 lines · calc-func/src/ported/bond_core_family.rs on GitHub/// F# `IDayCount.DaysBetween` (numerator position) per basis.
fn days_between_num(s: i64, e: i64, b: DayCountBasis) -> Result<f64, BondCoreEvalError> {
match b {
DayCountBasis::Us30_360 => diff360_us(s, e, false),
DayCountBasis::ActualActual | DayCountBasis::Actual360 | DayCountBasis::Actual365 => {
Ok(act(s, e))
}
DayCountBasis::European30_360 => d360eu(s, e),
}
}
Used by 4 functions.
map_bond_core_error_to_ws · 6 lines · calc-func/src/ported/bond_core_family.rs on GitHubpub fn map_bond_core_error_to_ws(e: &BondCoreEvalError) -> WorksheetErrorCode {
match e {
BondCoreEvalError::ArityMismatch { .. } => WorksheetErrorCode::Value,
BondCoreEvalError::Domain(c) => *c,
}
}
Used by 9 functions.