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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubfn 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 GitHubpub 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 GitHubpub fn map_odd_bond_error_to_ws(error: &OddBondEvalError) -> WorksheetErrorCode {
match error {
OddBondEvalError::ArityMismatch { .. } => WorksheetErrorCode::Value,
OddBondEvalError::Domain(code) => *code,
}
}
Used by 4 functions.