Shared Rust kernels

ported/odd_bond_family

All modules · quoted at commit 9e1884369a67 of DnaCalc/DnaCalc. Comments are the source's own.

parse_basis · 13 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn parse_basis(value: f64) -> Result<DayCountBasis, OddBondEvalError> {
    if !value.is_finite() {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
    }
    match value.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(OddBondEvalError::Domain(WorksheetErrorCode::Num)),
    }
}

Used by 4 functions.

parse_frequency · 9 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn parse_frequency(value: f64) -> Result<i64, OddBondEvalError> {
    if !value.is_finite() {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
    }
    match value.trunc() as i64 {
        1 | 2 | 4 => Ok(value.trunc() as i64),
        _ => Err(OddBondEvalError::Domain(WorksheetErrorCode::Num)),
    }
}

Used by 4 functions.

max_excel_serial · 3 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn max_excel_serial() -> i64 {
    excel_serial_from_ymd(WorkbookDateSystem::System1900, 9999, 12, 31).unwrap() as i64
}

Used by 4 functions.

parse_date_serial · 12 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn parse_date_serial(value: f64) -> Result<i64, OddBondEvalError> {
    if !value.is_finite() {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Value));
    }
    let serial = value.trunc() as i64;
    if serial < 1 || serial > max_excel_serial() {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Value));
    }
    ymd_from_excel_serial(WorkbookDateSystem::System1900, serial as f64)
        .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Value))?;
    Ok(serial)
}

Used by 4 functions.

days_in_year · 7 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn days_in_year(year: i64) -> f64 {
    if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
        366.0
    } else {
        365.0
    }
}

Used by 4 functions.

days_in_month · 14 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn days_in_month(year: i64, month: i64) -> i64 {
    match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => {
            if days_in_year(year) == 366.0 {
                29
            } else {
                28
            }
        }
        _ => 30,
    }
}

Used by 4 functions.

add_months_clamped · 22 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn add_months_clamped(serial: i64, months: i64) -> Option<i64> {
    let (year, month, day) = ymd_from_excel_serial(WorkbookDateSystem::System1900, serial as f64)?;
    let month_index = year
        .checked_mul(12)?
        .checked_add(month - 1)?
        .checked_add(months)?;
    let target_year = month_index.div_euclid(12);
    let target_month = month_index.rem_euclid(12) + 1;
    let source_is_month_end = day == days_in_month(year, month);
    let target_day = if source_is_month_end {
        days_in_month(target_year, target_month)
    } else {
        day.min(days_in_month(target_year, target_month))
    };
    excel_serial_from_ymd(
        WorkbookDateSystem::System1900,
        target_year,
        target_month,
        target_day,
    )
    .map(|v| v as i64)
}

Used by 2 functions.

days360_us · 3 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn days360_us(start: i64, end: i64) -> Result<f64, OddBondEvalError> {
    crate::ported::day_count_common::us_30_360(start, end).map_err(OddBondEvalError::Domain)
}

Used by 4 functions.

days360_us_both · 23 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn days360_us_both(start: i64, end: i64) -> Result<f64, OddBondEvalError> {
    let (sy, sm, mut sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, start as f64)
        .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Value))?;
    let (ey, em, mut ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, end as f64)
        .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Value))?;

    let start_last_feb = sm == 2 && sd == days_in_month(sy, sm);
    let end_last_feb = em == 2 && ed == days_in_month(ey, em);
    if end_last_feb {
        ed = 30;
    }
    if ed == 31 {
        ed = 30;
    }
    if sd == 31 {
        sd = 30;
    }
    if start_last_feb {
        sd = 30;
    }

    Ok(((ey - sy) * 360 + (em - sm) * 30 + (ed - sd)) as f64)
}

Used by 2 functions.

days360_eu · 13 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn days360_eu(start: i64, end: i64) -> Result<f64, OddBondEvalError> {
    let (sy, sm, mut sd) = ymd_from_excel_serial(WorkbookDateSystem::System1900, start as f64)
        .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Value))?;
    let (ey, em, mut ed) = ymd_from_excel_serial(WorkbookDateSystem::System1900, end as f64)
        .ok_or(OddBondEvalError::Domain(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 4 functions.

day_count · 9 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn day_count(start: i64, end: i64, basis: DayCountBasis) -> Result<f64, OddBondEvalError> {
    match basis {
        DayCountBasis::Us30_360 => days360_us(start, end),
        DayCountBasis::ActualActual | DayCountBasis::Actual360 | DayCountBasis::Actual365 => {
            Ok((end - start) as f64)
        }
        DayCountBasis::European30_360 => days360_eu(start, end),
    }
}

Used by 4 functions.

day_count_non_negative · 7 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn day_count_non_negative(
    start: i64,
    end: i64,
    basis: DayCountBasis,
) -> Result<f64, OddBondEvalError> {
    Ok(day_count(start, end, basis)?.max(0.0))
}

Used by 4 functions.

day_count_non_negative_with_us_hack · 11 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn day_count_non_negative_with_us_hack(
    start: i64,
    end: i64,
    basis: DayCountBasis,
) -> Result<f64, OddBondEvalError> {
    let result = match basis {
        DayCountBasis::Us30_360 => days360_us_both(start, end)?,
        _ => day_count(start, end, basis)?,
    };
    Ok(result.max(0.0))
}

Used by 2 functions.

ymd · 4 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn ymd(serial: i64) -> Result<(i64, i64, i64), OddBondEvalError> {
    ymd_from_excel_serial(WorkbookDateSystem::System1900, serial as f64)
        .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Num))
}

Used by 2 functions.

last_day_of_month · 3 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn last_day_of_month(year: i64, month: i64, day: i64) -> bool {
    day == days_in_month(year, month)
}

Used by 2 functions.

change_month · 32 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `changeMonth` (daycountbasis.fs): shift the month and either clamp the day
/// to the target month length (.NET `DateTime.AddMonths` semantics — no
/// end-of-month snapping) or force the last day of the target month when
/// `return_last_day` is set. `basis` is intentionally not consulted (the F#
/// `isLastDay` it computes is dead code).
fn change_month(
    serial: i64,
    num_months: i64,
    return_last_day: bool,
) -> Result<i64, OddBondEvalError> {
    let (year, month, day) = ymd(serial)?;
    let month_index = year
        .checked_mul(12)
        .and_then(|v| v.checked_add(month - 1))
        .and_then(|v| v.checked_add(num_months))
        .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Num))?;
    let target_year = month_index.div_euclid(12);
    let target_month = month_index.rem_euclid(12) + 1;
    let target_day = if return_last_day {
        days_in_month(target_year, target_month)
    } else {
        day.min(days_in_month(target_year, target_month))
    };
    excel_serial_from_ymd(
        WorkbookDateSystem::System1900,
        target_year,
        target_month,
        target_day,
    )
    .map(|v| v as i64)
    .ok_or(OddBondEvalError::Domain(WorksheetErrorCode::Num))
}

Used by 2 functions.

find_pcd_ncd · 24 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `datesAggregate1`/`findPcdNcd`: walk `start` toward `end` by `num_months`
/// steps, returning the bounding pair `(front, trailing)` where `front` is the
/// first position at/past `end` and `trailing` the one before it.
fn find_pcd_ncd(
    start: i64,
    end: i64,
    num_months: i64,
    return_last_month: bool,
) -> Result<(i64, i64), OddBondEvalError> {
    let mut front = start;
    let mut trailing = end;
    loop {
        let stop = if num_months > 0 {
            front >= end
        } else {
            front <= end
        };
        if stop {
            return Ok((front, trailing));
        }
        trailing = front;
        front = change_month(front, num_months, return_last_month)?;
    }
}

Used by 2 functions.

find_coupon_dates · 8 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `findCouponDates`: the (previous, next) quasi-coupon dates bounding
/// `settlement`, anchored on `maturity` and stepping back one period at a time.
fn find_coupon_dates(settl: i64, mat: i64, frequency: i64) -> Result<(i64, i64), OddBondEvalError> {
    let (my, mm, md) = ymd(mat)?;
    let end_month = last_day_of_month(my, mm, md);
    let num_months = -(12 / frequency);
    find_pcd_ncd(mat, settl, num_months, end_month)
}

Used by 2 functions.

previous_coupon_date · 3 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn previous_coupon_date(settl: i64, mat: i64, frequency: i64) -> Result<i64, OddBondEvalError> {
    Ok(find_coupon_dates(settl, mat, frequency)?.0)
}

Used by 2 functions.

next_coupon_date · 3 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn next_coupon_date(settl: i64, mat: i64, frequency: i64) -> Result<i64, OddBondEvalError> {
    Ok(find_coupon_dates(settl, mat, frequency)?.1)
}

Used by 2 functions.

number_of_coupons · 9 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `numberOfCoupons` (`CoupNum`): whole months from settlement's previous
/// coupon date to maturity, scaled to coupon periods.
fn number_of_coupons(settl: i64, mat: i64, frequency: i64) -> Result<f64, OddBondEvalError> {
    let pcd = previous_coupon_date(settl, mat, frequency)?;
    let (my, mm, _) = ymd(mat)?;
    let (pcy, pcm, _) = ymd(pcd)?;
    let months = ((my - pcy) * 12 + (mm - pcm)) as f64;
    Ok(months * frequency as f64 / 12.0)
}

Used by 2 functions.

coup_days · 18 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `CoupDays`: the normal length of the coupon period that contains
/// settlement. 30/360 and Actual/360 use `360/freq`, Actual/365 `365/freq`, and
/// Actual/Actual the actual days between the bounding coupon dates.
fn coup_days(
    settl: i64,
    mat: i64,
    frequency: i64,
    basis: DayCountBasis,
) -> Result<f64, OddBondEvalError> {
    match basis {
        DayCountBasis::ActualActual => {
            let (pcd, ncd) = find_coupon_dates(settl, mat, frequency)?;
            Ok((ncd - pcd) as f64)
        }
        DayCountBasis::Actual365 => Ok(365.0 / frequency as f64),
        _ => Ok(360.0 / frequency as f64),
    }
}

Used by 2 functions.

whole_coupon_count · 24 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `coupNumber` with `isWholeNumber=true`: the count of whole quasi-coupon
/// periods between settlement and the first coupon (used as `Nq`).
fn whole_coupon_count(
    mat: i64,
    settl: i64,
    months_per_coupon: i64,
) -> Result<f64, OddBondEvalError> {
    let (my, mm, md) = ymd(mat)?;
    let (sy, sm, sd) = ymd(settl)?;
    let end_of_month_temp = last_day_of_month(my, mm, md);
    let end_of_month = if !end_of_month_temp && mm != 2 && md > 28 && md < days_in_month(my, mm) {
        last_day_of_month(sy, sm, sd)
    } else {
        end_of_month_temp
    };
    let start_date = change_month(settl, 0, end_of_month)?;
    let mut coupons = if settl < start_date { 1.0 } else { 0.0 };
    let mut front = change_month(start_date, months_per_coupon, end_of_month)?;
    while front < mat {
        front = change_month(front, months_per_coupon, end_of_month)?;
        coupons += 1.0;
    }
    Ok(coupons)
}

Used by 2 functions.

aggr_indices · 9 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// F# `aggrBetween` index sequence: ascending when `start <= end`, otherwise
/// descending. The fold order is observable in floating-point accumulation.
fn aggr_indices(start: i64, end: i64) -> Vec<i64> {
    if start <= end {
        (start..=end).collect()
    } else {
        (end..=start).rev().collect()
    }
}

Used by 2 functions.

validate_positive_inputs · 7 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
fn validate_positive_inputs(values: &[f64]) -> Result<(), OddBondEvalError> {
    if values.iter().all(|v| v.is_finite()) {
        Ok(())
    } else {
        Err(OddBondEvalError::Domain(WorksheetErrorCode::Value))
    }
}

Used by 4 functions.

excel_bond_pow · 24 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
/// Excel's bond discount factor uses the C-runtime `pow` integer special case: binary
/// exponentiation for non-negative integer exponents, `powf` otherwise. Identical to
/// `bond_core_family::excel_bond_pow` (W109, held-out validated 25/25 on PRICE). For
/// fractional exponents this equals `base.powf(exp)`, so only integer-exponent (e.g.
/// on-coupon / whole-quasi-period) odd bonds change.
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 {
        crt_pow(base, exp)
    }
}

Used by 2 functions.

oddfprice_kernel · 126 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
pub fn oddfprice_kernel(
    settlement: f64,
    maturity: f64,
    issue: f64,
    first_coupon: f64,
    rate: f64,
    yld: f64,
    redemption: f64,
    frequency: f64,
    basis: Option<f64>,
) -> Result<f64, OddBondEvalError> {
    validate_positive_inputs(&[
        settlement,
        maturity,
        issue,
        first_coupon,
        rate,
        yld,
        redemption,
        frequency,
    ])?;
    if rate < 0.0 || yld < 0.0 || redemption <= 0.0 {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
    }
    let settlement = parse_date_serial(settlement)?;
    let maturity = parse_date_serial(maturity)?;
    let issue = parse_date_serial(issue)?;
    let first_coupon = parse_date_serial(first_coupon)?;
    let frequency = parse_frequency(frequency)?;
    let basis = parse_basis(basis.unwrap_or(0.0))?;
    if !(issue < settlement && settlement < first_coupon && first_coupon <= maturity) {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
    }

    // Faithful port of ExcelFinancialFunctions `oddFPrice` (oddbonds.fs), which is
    // bit-exact with live Excel across every day-count basis (verified by the G6
    // three-way ledger). Two branches: a short odd first coupon (DFC < E) and a
    // long one spanning multiple quasi-coupon periods (DFC >= E). The long branch
    // sums per-period day-count fractions (`dci/nl`) — this is exactly what the
    // earlier single-period-length closed form got materially wrong for the
    // actual-day bases (where quasi periods have unequal actual lengths).
    let m = frequency as f64;
    let months_per_coupon = 12 / frequency;
    let e = coup_days(settlement, first_coupon, frequency, basis)?;
    if e <= 0.0 {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
    }
    let dfc = day_count_non_negative(issue, first_coupon, basis)?;
    let x = yld / m + 1.0;
    if x <= 0.0 {
        return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
    }

    let price = if dfc < e {
        // Short odd first coupon.
        let n = number_of_coupons(settlement, maturity, frequency)?;
        let dsc = day_count_non_negative(settlement, first_coupon, basis)?;
        let a = day_count_non_negative(issue, settlement, basis)?;
        let y = dsc / e;
        let term1 = redemption / excel_bond_pow(x, n - 1.0 + y);
        let term2 = 100.0 * rate / m * dfc / e / excel_bond_pow(x, y);
        let mut term3 = 0.0;
        for index in aggr_indices(2, n as i64) {
            term3 += 100.0 * rate / m / excel_bond_pow(x, index as f64 - 1.0 + y);
        }
        let term4 = a / e * (rate / m) * 100.0;
        term1 + term2 + term3 - term4
    } else {
        // Long odd first coupon: accumulate day-count fractions over each quasi
        // period (walking back from first_coupon), then discount.
        let nc = number_of_coupons(issue, first_coupon, frequency)? as i64;
        let mut late_coupon = first_coupon;
        let mut dcnl = 0.0;
        let mut anl = 0.0;
        for index in aggr_indices(nc, 1) {
            let early_coupon = change_month(late_coupon, -months_per_coupon, false)?;
            let nl = if basis == DayCountBasis::ActualActual {
                day_count_non_negative(early_coupon, late_coupon, basis)?
            } else {
                e
            };
            if nl <= 0.0 {
                return Err(OddBondEvalError::Domain(WorksheetErrorCode::Num));
            }
            let dci = if index > 1 {
                nl
            } else {
                day_count_non_negative(issue, late_coupon, basis)?
            };
            let start_date = issue.max(early_coupon);
            let end_date = settlement.min(late_coupon);
            let a = day_count_non_negative(start_date, end_date, basis)?;
            late_coupon = early_coupon;
            dcnl += dci / nl;
            anl += a / nl;
        }
        let dsc = match basis {
            DayCountBasis::Actual360 | DayCountBasis::Actual365 => {
                let date = next_coupon_date(settlement, first_coupon, frequency)?;
                day_count_non_negative(settlement, date, basis)?
            }
            _ => {
                let date = previous_coupon_date(settlement, first_coupon, frequency)?;
                let a = day_count(date, settlement, basis)?;
                e - a
            }
        };
        let nq = whole_coupon_count(first_coupon, settlement, months_per_coupon)?;
        let n = number_of_coupons(first_coupon, maturity, frequency)?;
        let y = dsc / e;
        let term1 = redemption / excel_bond_pow(x, y + nq + n);
        let term2 = 100.0 * rate / m * dcnl / excel_bond_pow(x, nq + y);
        let mut term3 = 0.0;
        for index in aggr_indices(1, n as i64) {
            term3 += 100.0 * rate / m / excel_bond_pow(x, index as f64 + nq + y);
        }
        let term4 = 100.0 * rate / m * anl;
        term1 + term2 + term3 - term4
    };

    if price.is_finite() {
        Ok(price)
    } else {
        Err(OddBondEvalError::Domain(WorksheetErrorCode::Num))
    }
}

Used by 2 functions.

map_odd_bond_error_to_ws · 6 lines · calc-func/src/ported/odd_bond_family.rs on GitHub
pub fn map_odd_bond_error_to_ws(error: &OddBondEvalError) -> WorksheetErrorCode {
    match error {
        OddBondEvalError::ArityMismatch { .. } => WorksheetErrorCode::Value,
        OddBondEvalError::Domain(code) => *code,
    }
}

Used by 4 functions.