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 GitHub
fn 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 GitHub
fn derr(c: WorksheetErrorCode) -> BondCoreEvalError {
    BondCoreEvalError::Domain(c)
}

Used by 9 functions.

dyear · 7 lines · calc-func/src/ported/bond_core_family.rs on GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
fn 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 GitHub
pub 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 GitHub
fn 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 GitHub
pub fn map_bond_core_error_to_ws(e: &BondCoreEvalError) -> WorksheetErrorCode {
    match e {
        BondCoreEvalError::ArityMismatch { .. } => WorksheetErrorCode::Value,
        BondCoreEvalError::Domain(c) => *c,
    }
}

Used by 9 functions.