pub fn pmt(rate: f64, nper: f64, pv: f64, fv: f64, pmt_type: f64) -> f64 {
if rate == 0.0 {
return -(pv + fv) / nper;
}
let pvif = (1.0 + rate).powf(nper);
let mut result = (rate / (pvif - 1.0)) * -(pv * pvif + fv);
if pmt_type == 1.0 {
result /= 1.0 + rate;
}
result
}
pub fn fv(rate: f64, nper: f64, pmt: f64, pv: f64, pmt_type: f64) -> f64 {
if rate == 0.0 {
return -(pv + pmt * nper);
}
let term = (1.0 + rate).powf(nper);
let result = if pmt_type == 1.0 {
pv * term + pmt * (1.0 + rate) * (term - 1.0) / rate
} else {
pv * term + pmt * (term - 1.0) / rate
};
-result
}
pub fn pv(rate: f64, nper: f64, pmt: f64, fv: f64, pmt_type: f64) -> f64 {
if rate == 0.0 {
return -(fv + pmt * nper);
}
let term = (1.0 + rate).powf(nper);
let result = if pmt_type == 1.0 {
(fv + pmt * (1.0 + rate) * (term - 1.0) / rate) / term
} else {
(fv + pmt * (term - 1.0) / rate) / term
};
-result
}
pub fn nper(rate: f64, pmt: f64, pv: f64, fv: f64, pmt_type: f64) -> Option<f64> {
if rate == 0.0 {
if pmt == 0.0 {
return None;
}
return Some(-(pv + fv) / pmt);
}
let num = pmt * (1.0 + rate * pmt_type) - fv * rate;
let den = pv * rate + pmt * (1.0 + rate * pmt_type);
if den == 0.0 || num / den <= 0.0 {
return None;
}
let result = (num / den).ln() / (1.0 + rate).ln();
if result.is_finite() {
Some(result)
} else {
None
}
}
fn newton_raphson(f: impl Fn(f64) -> f64, guess: f64) -> Option<f64> {
newton_raphson_bounded(f, guess, None, true)
}
fn newton_raphson_bounded(
f: impl Fn(f64) -> f64,
guess: f64,
max_step: Option<f64>,
allow_pos_transition: bool,
) -> Option<f64> {
newton_raphson_bounded_df::<_, fn(f64) -> f64>(f, None, guess, max_step, allow_pos_transition)
}
fn newton_raphson_bounded_df<F: Fn(f64) -> f64, DF: Fn(f64) -> f64>(
f: F,
df: Option<DF>,
guess: f64,
max_step: Option<f64>,
allow_pos_transition: bool,
) -> Option<f64> {
let mut r = guess;
const EPS: f64 = 1e-7;
const MAX_ITER: usize = 200;
for step_i in 0..MAX_ITER {
if r <= -0.9999 {
r = -0.999999;
}
let y = f(r);
if abs_val(y) < EPS {
if !allow_pos_transition && r > 0.0 {
return None;
}
return Some(r);
}
let deriv = if let Some(ref deriv_fn) = df {
deriv_fn(r)
} else {
let h = 1e-6 * (1.0 + r.abs());
(f(r + h) - f(r - h)) / (2.0 * h)
};
if deriv == 0.0 || !deriv.is_finite() {
return None;
}
let mut step = -y / deriv;
if step_i == 0 && step.abs() > 4.0 {
return None;
}
if let Some(ms) = max_step {
if step > ms {
step = ms;
} else if step < -ms {
step = -ms;
}
}
let mut halvings = 0;
while r + step <= -0.9999 && halvings < 50 {
step /= 2.0;
halvings += 1;
}
let next = r + step;
if !next.is_finite() || next <= -0.9999 {
return None;
}
if !allow_pos_transition && next > 0.0 {
return None;
}
if (next - r).abs() < EPS {
return Some(next);
}
r = next;
}
None
}
fn abs_val(x: f64) -> f64 {
x.abs()
}
pub fn rate(nper: f64, pmt: f64, pv: f64, fv: f64, pmt_type: f64, guess: f64) -> Option<f64> {
let total_cf = pv + pmt * nper + fv;
if guess < 0.0 && pmt_type == 1.0 && nper >= 36.0 && total_cf > 0.0 {
return None;
}
let f = |r: f64| -> f64 {
if r == 0.0 {
pv + pmt * nper + fv
} else {
let term = (1.0 + r).powf(nper);
pv * term + pmt * (1.0 + r * pmt_type) * (term - 1.0) / r + fv
}
};
let r = newton_raphson(f, guess)?;
if r <= -0.999 { None } else { Some(r) }
}
fn ipmt_ordinary(rate: f64, period: f64, pv: f64, payment: f64) -> f64 {
let mut balance = pv;
let periods = period.round() as i64;
for _ in 1..periods {
balance = balance * (1.0 + rate) + payment;
}
-balance * rate
}
pub fn ipmt(rate: f64, period: f64, nper: f64, pv: f64, fv: f64, pmt_type: f64) -> f64 {
let payment = pmt(rate, nper, pv, fv, pmt_type);
if pmt_type == 1.0 {
if period == 1.0 {
return 0.0;
}
let reduced_pv = pv + payment;
return ipmt_ordinary(rate, period - 1.0, reduced_pv, payment);
}
ipmt_ordinary(rate, period, pv, payment)
}
pub fn ppmt(rate: f64, period: f64, nper: f64, pv: f64, fv: f64, pmt_type: f64) -> f64 {
pmt(rate, nper, pv, fv, pmt_type) - ipmt(rate, period, nper, pv, fv, pmt_type)
}
pub fn cumipmt(
rate: f64,
nper: f64,
pv: f64,
start_period: f64,
end_period: f64,
pmt_type: f64,
) -> f64 {
let mut total = 0.0;
let mut per = start_period;
while per <= end_period + 1e-9 {
total += ipmt(rate, per, nper, pv, 0.0, pmt_type);
per += 1.0;
}
total
}
pub fn cumprinc(
rate: f64,
nper: f64,
pv: f64,
start_period: f64,
end_period: f64,
pmt_type: f64,
) -> f64 {
let mut total = 0.0;
let mut per = start_period;
while per <= end_period + 1e-9 {
total += ppmt(rate, per, nper, pv, 0.0, pmt_type);
per += 1.0;
}
total
}
pub fn npv(rate: f64, values: &[f64]) -> f64 {
values
.iter()
.enumerate()
.map(|(i, v)| v / (1.0 + rate).powi(i as i32 + 1))
.sum()
}
fn npv_from_period_zero(rate: f64, values: &[f64]) -> f64 {
values
.iter()
.enumerate()
.map(|(i, v)| v / (1.0 + rate).powi(i as i32))
.sum()
}
fn newton_raphson_with_zero_fallback(
f: impl Fn(f64) -> f64,
guess: f64,
max_step: Option<f64>,
) -> Option<f64> {
let allow_pos_transition = guess >= 0.0;
if let Some(r) = newton_raphson_bounded(&f, guess, max_step, allow_pos_transition) {
return Some(r);
}
if guess == 0.0 {
return None;
}
newton_raphson_bounded(f, 0.0, max_step, true)
}
pub fn irr(values: &[f64], guess: f64) -> Option<f64> {
if values.is_empty() {
return None;
}
if values[0] < 0.0 && values[1..].iter().all(|&v| v >= 0.0) && values.iter().sum::<f64>() <= 0.0
{
return None;
}
newton_raphson_with_zero_fallback(|r| npv_from_period_zero(r, values), guess, Some(1.0))
}
pub fn mirr(values: &[f64], finance_rate: f64, reinvest_rate: f64) -> Option<f64> {
let n = values.len();
if n < 2 {
return None;
}
let periods = (n - 1) as i32;
let npv_neg: f64 = values
.iter()
.enumerate()
.filter(|(_, v)| **v < 0.0)
.map(|(i, v)| v / (1.0 + finance_rate).powi(i as i32))
.sum();
let fv_pos: f64 = values
.iter()
.enumerate()
.filter(|(_, v)| **v >= 0.0)
.map(|(i, v)| v * (1.0 + reinvest_rate).powi(periods - i as i32))
.sum();
if npv_neg == 0.0 {
return None;
}
let ratio = -fv_pos / npv_neg;
if ratio < 0.0 {
return None;
}
let result = ratio.powf(1.0 / periods as f64) - 1.0;
if result.is_finite() {
Some(result)
} else {
None
}
}
pub fn xnpv(rate: f64, values: &[f64], dates: &[f64]) -> f64 {
let d0 = dates[0];
values
.iter()
.zip(dates.iter())
.map(|(v, d)| v / (1.0 + rate).powf((d - d0) / 365.0))
.sum()
}
fn count_sign_flips(values: &[f64]) -> usize {
let mut flips = 0;
let mut prev_sign = None;
for &v in values {
if v != 0.0 {
let sign = v > 0.0;
if let Some(p) = prev_sign
&& sign != p
{
flips += 1;
}
prev_sign = Some(sign);
}
}
flips
}
fn xirr_asymptotic_guess(values: &[f64], dates: &[f64]) -> Option<f64> {
if values.is_empty() {
return None;
}
let v0 = values[0];
if v0 >= 0.0 {
return None;
}
for i in 1..values.len() {
if values[i] > v0.abs() {
let d1 = dates[i] - dates[0];
if d1 > 0.0 {
let ratio = values[i] / v0.abs();
return Some(ratio.powf(365.0 / d1) - 1.0);
}
}
}
None
}
fn xnpv_prime(rate: f64, values: &[f64], dates: &[f64]) -> f64 {
let d0 = dates[0];
values
.iter()
.zip(dates.iter())
.map(|(v, d)| {
let f = (d - d0) / 365.0;
if f == 0.0 {
0.0
} else {
-f * v / (1.0 + rate).powf(f + 1.0)
}
})
.sum()
}
pub fn xirr(values: &[f64], dates: &[f64], guess: f64) -> Option<f64> {
if values.is_empty() || dates.len() != values.len() {
return None;
}
let flips = count_sign_flips(values);
let v0_pos = values[0] > 0.0;
let sum_v: f64 = values.iter().sum();
if guess < 0.0 && flips >= 2 && !v0_pos {
return None;
}
if v0_pos && guess >= 1.0 {
return None;
}
let mut start_r = guess;
if guess == 0.0
&& sum_v.abs() < 1e-9
&& flips >= 2
&& let Some(asymp_g) = xirr_asymptotic_guess(values, dates)
&& asymp_g > 0.0
{
start_r = asymp_g;
}
let f = |r: f64| xnpv(r, values, dates);
let df = |r: f64| xnpv_prime(r, values, dates);
let res = newton_raphson_bounded_df(f, Some(df), start_r, None, start_r >= 0.0);
let is_neg_when_pos_guess = guess >= 0.0 && res.is_some_and(|r| r < 0.0) && flips >= 2;
let is_trivial_zero = res.is_some_and(|r| r.abs() < 1e-5) && sum_v.abs() < 1e-9;
if (res.is_none() || is_trivial_zero || is_neg_when_pos_guess)
&& guess >= 0.0
&& let Some(asymp_g) = xirr_asymptotic_guess(values, dates)
&& asymp_g > 0.0
{
let res_asymp = newton_raphson_bounded_df(f, Some(df), asymp_g, None, true);
if res_asymp.is_some() {
return res_asymp;
}
}
if guess < 0.0 && res.is_some_and(|r| r > 0.0) {
return None;
}
res
}
pub fn sln(cost: f64, salvage: f64, life: f64) -> f64 {
(cost - salvage) / life
}
pub fn syd(cost: f64, salvage: f64, life: f64, per: f64) -> f64 {
(cost - salvage) * (life - per + 1.0) / (life * (life + 1.0) / 2.0)
}
pub fn db(cost: f64, salvage: f64, life: f64, period: f64, month: f64) -> f64 {
if cost == 0.0 {
return 0.0;
}
let rate = 1.0 - (salvage / cost).powf(1.0 / life);
let rate = (rate * 1000.0).round() / 1000.0;
let mut total_depreciation = 0.0;
let mut p = 1.0;
let last_period = life + 1.0;
let mut this_period_dep = 0.0;
while p <= period {
this_period_dep = if p == 1.0 {
cost * rate * month / 12.0
} else if p == last_period {
(cost - total_depreciation) * rate * (12.0 - month) / 12.0
} else {
(cost - total_depreciation) * rate
};
total_depreciation += this_period_dep;
p += 1.0;
}
this_period_dep
}
pub fn ddb(cost: f64, salvage: f64, life: f64, period: f64, factor: f64) -> f64 {
let rate = factor / life;
let mut book_value = cost;
let mut p = 1.0;
let mut this_period_dep = 0.0;
while p <= period {
let candidate = book_value * rate;
this_period_dep = candidate.min(book_value - salvage).max(0.0);
book_value -= this_period_dep;
p += 1.0;
}
this_period_dep
}
pub fn vdb(
cost: f64,
salvage: f64,
life: f64,
start_period: f64,
end_period: f64,
factor: f64,
no_switch: bool,
) -> Option<f64> {
if cost < 0.0
|| salvage < 0.0
|| life <= 0.0
|| start_period < 0.0
|| end_period < start_period
|| end_period > life
{
return None;
}
let cumulative = |until: f64| -> f64 {
let mut total = 0.0;
let mut book_value = cost;
let mut remaining_life = life;
let mut switched_to_sln = false;
let full_periods = until.floor() as i64;
let step = |book_value: &mut f64,
remaining_life: &mut f64,
switched: &mut bool,
frac: f64|
-> f64 {
let amt = if !*switched {
let rate = (factor / life).min(1.0);
let ddb_amt = *book_value * rate;
let sln_amt = if *remaining_life > 0.0 {
(*book_value - salvage) / *remaining_life
} else {
0.0
};
if !no_switch && sln_amt > ddb_amt {
*switched = true;
sln_amt
} else {
ddb_amt
}
} else {
if *remaining_life > 0.0 {
(*book_value - salvage) / *remaining_life
} else {
0.0
}
};
let amt = (amt * frac).min((*book_value - salvage).max(0.0)).max(0.0);
*book_value -= amt;
*remaining_life -= frac;
amt
};
for _ in 0..full_periods {
total += step(
&mut book_value,
&mut remaining_life,
&mut switched_to_sln,
1.0,
);
}
let frac = until - full_periods as f64;
if frac > 1e-9 {
total += step(
&mut book_value,
&mut remaining_life,
&mut switched_to_sln,
frac,
);
}
total
};
Some(cumulative(end_period) - cumulative(start_period))
}
pub fn effect(nominal_rate: f64, npery: f64) -> f64 {
(1.0 + nominal_rate / npery).powf(npery) - 1.0
}
pub fn nominal(effect_rate: f64, npery: f64) -> f64 {
npery * ((1.0 + effect_rate).powf(1.0 / npery) - 1.0)
}
fn dollar_digits(fraction: f64) -> f64 {
fraction.log10().ceil()
}
pub fn dollarde(fractional_dollar: f64, fraction: f64) -> Option<f64> {
if fraction < 0.0 {
return None;
}
if fraction == 0.0 {
return None;
}
let n = fractional_dollar.trunc();
let frac_part = fractional_dollar - n;
let digits = dollar_digits(fraction);
let factor = 10f64.powf(digits);
Some(n + frac_part * factor / fraction)
}
pub fn dollarfr(decimal_dollar: f64, fraction: f64) -> Option<f64> {
if fraction < 0.0 {
return None;
}
if fraction == 0.0 {
return None;
}
let n = decimal_dollar.trunc();
let frac_part = decimal_dollar - n;
let digits = dollar_digits(fraction);
let factor = 10f64.powf(digits);
Some(n + frac_part * fraction / factor)
}
pub fn fvschedule(principal: f64, schedule: &[f64]) -> f64 {
schedule.iter().fold(principal, |acc, r| acc * (1.0 + r))
}
pub fn rri(nper: f64, pv: f64, fv: f64) -> Option<f64> {
if nper == 0.0 || pv == 0.0 {
return None;
}
let ratio = fv / pv;
if ratio < 0.0 {
return None;
}
Some(ratio.powf(1.0 / nper) - 1.0)
}
pub fn pduration(rate: f64, pv: f64, fv: f64) -> Option<f64> {
if rate <= -1.0 || pv <= 0.0 || fv <= 0.0 {
return None;
}
Some((fv.ln() - pv.ln()) / (1.0 + rate).ln())
}
pub fn ispmt(rate: f64, per: f64, nper: f64, pv: f64) -> f64 {
-pv * rate * (nper - per) / nper
}
use crate::core::date_fn;
fn basis_days_between(start: f64, end: f64, basis: f64) -> f64 {
match basis as i64 {
0 => date_fn::days_30_360_nasd(start, end),
4 => date_fn::days360(start, end, Some(true)).unwrap_or(0.0),
_ => end - start,
}
}
fn basis_days_between_pricemat_leg(
start: f64,
end: f64,
basis: f64,
settlement_is_start: bool,
settlement_is_end: bool,
) -> f64 {
match basis as i64 {
0 => date_fn::days_30_360_bond_ex(start, end, !settlement_is_start, !settlement_is_end),
_ => basis_days_between(start, end, basis),
}
}
fn amort_first_period_frac(date_purchased: f64, first_period: f64, basis: f64) -> f64 {
let diff = basis_days_between(date_purchased, first_period, basis);
let year = basis_year_days(basis, date_purchased, date_purchased);
diff / year
}
fn basis_year_days(basis: f64, start: f64, end: f64) -> f64 {
match basis as i64 {
1 => date_fn::actual_actual_year_days(start, end),
3 => 365.0,
_ => 360.0,
}
}
fn round_half_away_from_zero(x: f64) -> f64 {
if x >= 0.0 {
(x + 0.5).floor()
} else {
(x - 0.5).ceil()
}
}
fn step_months(anchor: f64, months_per_period: f64, k: f64) -> f64 {
let stepped = date_fn::edate(anchor, months_per_period * k).unwrap_or(anchor);
let (ay, am, ad) = date_fn::serial_to_ymd(anchor);
if ad != date_fn::days_in_month(ay, am) {
return stepped;
}
let (sy, sm, _) = date_fn::serial_to_ymd(stepped);
date_fn::ymd_to_serial(sy, sm, date_fn::days_in_month(sy, sm))
}
fn coupon_period_index(settlement: f64, maturity: f64, frequency: f64) -> f64 {
let months = 12.0 / frequency;
let mut k = 0.0;
let mut guard = 0;
while step_months(maturity, -months, k) > settlement && guard < 10_000 {
k += 1.0;
guard += 1;
}
k
}
fn coupon_pcd(settlement: f64, maturity: f64, frequency: f64) -> f64 {
let months = 12.0 / frequency;
let k = coupon_period_index(settlement, maturity, frequency);
step_months(maturity, -months, k)
}
fn coupon_ncd(settlement: f64, maturity: f64, frequency: f64) -> f64 {
let months = 12.0 / frequency;
let k = coupon_period_index(settlement, maturity, frequency);
step_months(maturity, -months, k - 1.0)
}
pub fn coupnum(settlement: f64, maturity: f64, frequency: f64) -> f64 {
coupon_period_index(settlement, maturity, frequency)
}
pub fn couppcd(settlement: f64, maturity: f64, frequency: f64) -> f64 {
coupon_pcd(settlement, maturity, frequency)
}
pub fn coupncd(settlement: f64, maturity: f64, frequency: f64) -> f64 {
coupon_ncd(settlement, maturity, frequency)
}
pub fn coupdays(settlement: f64, maturity: f64, frequency: f64, basis: f64) -> f64 {
match basis as i64 {
1 => {
coupon_ncd(settlement, maturity, frequency)
- coupon_pcd(settlement, maturity, frequency)
}
3 => 365.0 / frequency,
_ => 360.0 / frequency,
}
}
pub fn coupdaybs(settlement: f64, maturity: f64, frequency: f64, basis: f64) -> f64 {
let pcd = coupon_pcd(settlement, maturity, frequency);
basis_days_between(pcd, settlement, basis)
}
pub fn coupdaysnc(settlement: f64, maturity: f64, frequency: f64, basis: f64) -> f64 {
let ncd = coupon_ncd(settlement, maturity, frequency);
coupon_end_days(settlement, ncd, basis)
}
fn bond_price_from_yield(
settlement: f64,
maturity: f64,
rate: f64,
yld: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> f64 {
let n = coupnum(settlement, maturity, frequency);
let e = coupdays(settlement, maturity, frequency, basis);
let a = coupdaybs(settlement, maturity, frequency, basis);
let dsc = e - a;
let coupon = 100.0 * rate / frequency;
if n <= 1.0 {
(redemption + coupon) / (1.0 + (dsc / e) * (yld / frequency)) - coupon * (a / e)
} else {
let mut sum = redemption / (1.0 + yld / frequency).powf(n - 1.0 + dsc / e);
let mut k = 1.0;
while k <= n {
sum += coupon / (1.0 + yld / frequency).powf(k - 1.0 + dsc / e);
k += 1.0;
}
sum - coupon * (a / e)
}
}
pub fn price(
settlement: f64,
maturity: f64,
rate: f64,
yld: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> f64 {
bond_price_from_yield(
settlement, maturity, rate, yld, redemption, frequency, basis,
)
}
pub fn yield_(
settlement: f64,
maturity: f64,
rate: f64,
pr: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> Option<f64> {
let f = |y: f64| {
bond_price_from_yield(settlement, maturity, rate, y, redemption, frequency, basis) - pr
};
bisection(f, -0.99, 10.0)
}
fn bisection(f: impl Fn(f64) -> f64, mut lo: f64, mut hi: f64) -> Option<f64> {
let mut f_lo = f(lo);
let f_hi = f(hi);
if !f_lo.is_finite() || !f_hi.is_finite() {
return None;
}
if f_lo == 0.0 {
return Some(lo);
}
if f_lo.signum() == f_hi.signum() {
return None;
}
for _ in 0..200 {
let mid = (lo + hi) / 2.0;
let f_mid = f(mid);
if !f_mid.is_finite() {
return None;
}
if f_mid.abs() < 1e-10 || (hi - lo).abs() < 1e-12 {
return Some(mid);
}
if f_mid.signum() == f_lo.signum() {
lo = mid;
f_lo = f_mid;
} else {
hi = mid;
}
}
Some((lo + hi) / 2.0)
}
pub fn duration(
settlement: f64,
maturity: f64,
coupon: f64,
yld: f64,
frequency: f64,
basis: f64,
) -> f64 {
let n = coupnum(settlement, maturity, frequency).round() as i64;
let e = coupdays(settlement, maturity, frequency, basis);
let a = coupdaybs(settlement, maturity, frequency, basis);
let dsc = e - a;
let coupon_amt = 100.0 * coupon / frequency;
let mut weighted_sum = 0.0;
let mut price_sum = 0.0;
for k in 1..=n.max(1) {
let t = (k - 1) as f64 + dsc / e;
let cf = if k == n {
coupon_amt + 100.0
} else {
coupon_amt
};
let pv = cf / (1.0 + yld / frequency).powf(t);
weighted_sum += t * pv;
price_sum += pv;
}
weighted_sum / (price_sum * frequency)
}
pub fn mduration(
settlement: f64,
maturity: f64,
coupon: f64,
yld: f64,
frequency: f64,
basis: f64,
) -> f64 {
duration(settlement, maturity, coupon, yld, frequency, basis) / (1.0 + yld / frequency)
}
pub fn disc(settlement: f64, maturity: f64, pr: f64, redemption: f64, basis: f64) -> f64 {
let dsm = basis_days_between(settlement, maturity, basis);
let year = basis_year_days(basis, settlement, maturity);
(redemption - pr) / redemption * (year / dsm)
}
pub fn pricedisc(
settlement: f64,
maturity: f64,
discount: f64,
redemption: f64,
basis: f64,
) -> f64 {
let dsm = basis_days_between(settlement, maturity, basis);
let year = basis_year_days(basis, settlement, maturity);
redemption * (1.0 - discount * dsm / year)
}
pub fn yielddisc(settlement: f64, maturity: f64, pr: f64, redemption: f64, basis: f64) -> f64 {
let dsm = basis_days_between(settlement, maturity, basis);
let year = basis_year_days(basis, settlement, maturity);
(redemption - pr) / pr * (year / dsm)
}
pub fn pricemat(
settlement: f64,
maturity: f64,
issue: f64,
rate: f64,
yld: f64,
basis: f64,
) -> f64 {
let dim = basis_days_between_pricemat_leg(issue, maturity, basis, false, false);
let a = basis_days_between_pricemat_leg(issue, settlement, basis, false, true);
let dsm = basis_days_between_pricemat_leg(settlement, maturity, basis, true, false);
let year = basis_year_days(basis, issue, settlement);
let num = 100.0 + (dim / year) * rate * 100.0;
num / (1.0 + (dsm / year) * yld) - (a / year) * rate * 100.0
}
pub fn yieldmat(settlement: f64, maturity: f64, issue: f64, rate: f64, pr: f64, basis: f64) -> f64 {
let dim = basis_days_between_pricemat_leg(issue, maturity, basis, false, false);
let a = basis_days_between_pricemat_leg(issue, settlement, basis, false, true);
let dsm = basis_days_between_pricemat_leg(settlement, maturity, basis, true, false);
let year = basis_year_days(basis, issue, settlement);
let numerator = 100.0 + (dim / year) * rate * 100.0;
let denominator = pr + (a / year) * rate * 100.0;
(numerator / denominator - 1.0) * (year / dsm)
}
pub fn received(settlement: f64, maturity: f64, investment: f64, discount: f64, basis: f64) -> f64 {
let dsm = basis_days_between(settlement, maturity, basis);
let year = basis_year_days(basis, settlement, maturity);
investment / (1.0 - discount * dsm / year)
}
pub fn intrate(
settlement: f64,
maturity: f64,
investment: f64,
redemption: f64,
basis: f64,
) -> f64 {
let dsm = basis_days_between(settlement, maturity, basis);
let year = basis_year_days(basis, settlement, maturity);
(redemption - investment) / investment * (year / dsm)
}
pub fn tbillprice(settlement: f64, maturity: f64, discount: f64) -> f64 {
let dsm = maturity - settlement;
100.0 * (1.0 - discount * dsm / 360.0)
}
pub fn tbillyield(settlement: f64, maturity: f64, pr: f64) -> f64 {
let dsm = maturity - settlement;
(100.0 - pr) / pr * (360.0 / dsm)
}
pub fn tbilleq(settlement: f64, maturity: f64, discount: f64) -> Option<f64> {
let dsm = maturity - settlement;
if dsm <= 182.0 {
Some((365.0 * discount) / (360.0 - discount * dsm))
} else {
let term1 = dsm / 365.0;
let term2 = term1.powi(2) - (2.0 * term1 - 1.0) * (discount * dsm) / 360.0;
if term2 < 0.0 {
return None;
}
let term3 = -term1 - term2.sqrt();
Some((2.0 * term3) / (term1 - 2.0))
}
}
pub fn accrintm(
issue: f64,
settlement: f64,
rate: f64,
par: f64,
basis: f64,
) -> Result<f64, String> {
let frac = date_fn::yearfrac(issue, settlement, Some(basis))?;
Ok(par * rate * frac)
}
fn quasi_coupon_schedule(anchor: f64, lo: f64, hi: f64, frequency: f64) -> Vec<f64> {
let months = 12.0 / frequency;
let mut dates = vec![anchor];
let mut d = anchor;
let mut k = 0.0;
let mut guard = 0;
while d > lo && guard < 10_000 {
k += 1.0;
d = step_months(anchor, -months, k);
dates.push(d);
guard += 1;
}
let mut d = anchor;
let mut k = 0.0;
guard = 0;
while d < hi && guard < 10_000 {
k += 1.0;
d = step_months(anchor, months, k);
dates.push(d);
guard += 1;
}
dates.sort_by(|a, b| a.partial_cmp(b).unwrap());
dates.dedup();
dates
}
#[allow(clippy::too_many_arguments)]
pub fn accrint(
issue: f64,
first_interest: f64,
settlement: f64,
rate: f64,
par: f64,
frequency: f64,
basis: f64,
_calc_method: bool,
) -> f64 {
let schedule = quasi_coupon_schedule(
first_interest,
issue,
settlement.max(first_interest),
frequency,
);
let coupon_amt = par * rate / frequency;
let mut total = 0.0;
for w in schedule.windows(2) {
let (p_start, p_end) = (w[0], w[1]);
let seg_start = p_start.max(issue);
let seg_end = p_end.min(settlement);
if seg_end <= seg_start {
continue;
}
let a = basis_days_between(seg_start, seg_end, basis);
let e = match basis as i64 {
1 => p_end - p_start,
3 => 365.0 / frequency,
_ => 360.0 / frequency,
};
total += coupon_amt * a / e;
}
total
}
pub fn amorlinc(
cost: f64,
date_purchased: f64,
first_period: f64,
salvage: f64,
period: f64,
rate: f64,
basis: f64,
) -> Result<f64, String> {
if basis as i64 == 2 {
return Err("#NUM!".to_string());
}
let one_rate = cost * rate;
let cost_delta = cost - salvage;
let frac = amort_first_period_frac(date_purchased, first_period, basis);
let first_period_amort = rate * cost * frac;
if period == 0.0 {
return Ok(first_period_amort.min(cost_delta));
}
let n_periods = ((cost_delta - first_period_amort) / one_rate).trunc();
if period <= n_periods {
Ok(one_rate)
} else if period == n_periods + 1.0 {
Ok(cost_delta - one_rate * n_periods - first_period_amort)
} else {
Ok(0.0)
}
}
pub fn amordegrc(
cost: f64,
date_purchased: f64,
first_period: f64,
salvage: f64,
period: f64,
rate: f64,
basis: f64,
) -> Result<f64, String> {
if basis as i64 == 2 {
return Err("#NUM!".to_string());
}
let life = 1.0 / rate;
if life <= 2.0 {
return Err("#NUM!".to_string());
}
let coeff = if life < 5.0 {
1.5
} else if life < 6.0 {
2.0
} else {
2.5
};
let rate_d = rate * coeff;
let frac = amort_first_period_frac(date_purchased, first_period, basis);
let first_amort = cost * frac * rate_d;
if period == 0.0 {
return Ok(round_half_away_from_zero(first_amort.min(cost - salvage)));
}
let mut remaining = cost - first_amort;
let mut n = 1.0;
loop {
if remaining <= salvage {
return Ok(0.0);
}
let this_amort = remaining * rate_d;
if n as i64 == period as i64 {
if remaining - this_amort < salvage {
return Ok(round_half_away_from_zero((remaining - salvage).max(0.0)));
}
return Ok(round_half_away_from_zero(this_amort));
}
remaining -= this_amort;
n += 1.0;
if n > 10_000.0 {
return Ok(0.0);
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn oddfprice(
settlement: f64,
maturity: f64,
issue: f64,
first_coupon: f64,
rate: f64,
yld: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> f64 {
oddfprice_from_yield(
settlement,
maturity,
issue,
first_coupon,
rate,
yld,
redemption,
frequency,
basis,
)
}
#[allow(clippy::too_many_arguments)]
fn oddfprice_from_yield(
settlement: f64,
maturity: f64,
issue: f64,
first_coupon: f64,
rate: f64,
yld: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> f64 {
let months = 12.0 / frequency;
let mut n = 1.0;
let mut d = first_coupon;
let mut guard = 0;
while d < maturity - 1e-9 && guard < 10_000 {
d = step_months(first_coupon, months, n);
n += 1.0;
guard += 1;
}
let prev_coupon = date_fn::edate(first_coupon, -months).unwrap_or(first_coupon);
let e = match basis as i64 {
1 => basis_days_between(prev_coupon, first_coupon, basis),
3 => 365.0 / frequency,
_ => 360.0 / frequency,
};
let dsc = basis_days_between(settlement, first_coupon, basis);
let schedule =
quasi_coupon_schedule(first_coupon, issue, settlement.max(first_coupon), frequency);
let mut dfc = 0.0;
let mut a = 0.0;
for w in schedule.windows(2) {
let (p_start, p_end) = (w[0], w[1]);
let seg = |lo: f64, hi: f64| -> f64 {
let s = p_start.max(lo);
let e = p_end.min(hi);
if e > s {
basis_days_between(s, e, basis)
} else {
0.0
}
};
dfc += seg(issue, first_coupon);
a += seg(issue, settlement);
}
let coupon = 100.0 * rate / frequency;
let term1 = redemption / (1.0 + yld / frequency).powf(n - 1.0 + dsc / e);
let term2 = coupon * (dfc / e) / (1.0 + yld / frequency).powf(dsc / e);
let mut term3 = 0.0;
let mut k = 2.0;
while k <= n {
term3 += coupon / (1.0 + yld / frequency).powf(k - 1.0 + dsc / e);
k += 1.0;
}
term1 + term2 + term3 - coupon * (a / e)
}
#[allow(clippy::too_many_arguments)]
pub fn oddfyield(
settlement: f64,
maturity: f64,
issue: f64,
first_coupon: f64,
rate: f64,
pr: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> Option<f64> {
let f = |y: f64| {
oddfprice_from_yield(
settlement,
maturity,
issue,
first_coupon,
rate,
y,
redemption,
frequency,
basis,
) - pr
};
bisection(f, -0.99, 10.0)
}
fn coupon_end_days(start: f64, end: f64, basis: f64) -> f64 {
if basis as i64 == 0 {
date_fn::days_30_360_coupon_end(start, end)
} else {
basis_days_between(start, end, basis)
}
}
fn oddlprice_e(last_interest: f64, _maturity: f64, frequency: f64, basis: f64) -> f64 {
let months = 12.0 / frequency;
let next_regular = date_fn::edate(last_interest, months).unwrap_or(last_interest);
coupon_end_days(last_interest, next_regular, basis)
}
#[allow(clippy::too_many_arguments)]
pub fn oddlprice(
settlement: f64,
maturity: f64,
last_interest: f64,
rate: f64,
yld: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> f64 {
let e = oddlprice_e(last_interest, maturity, frequency, basis);
let dcnl = coupon_end_days(last_interest, maturity, basis);
let dcsl = basis_days_between(last_interest, settlement, basis);
let dsc = basis_days_between(settlement, maturity, basis);
let coupon = 100.0 * rate / frequency;
let numerator = redemption + coupon * (dcnl / e);
numerator / (1.0 + (dsc / e) * (yld / frequency)) - coupon * (dcsl / e)
}
#[allow(clippy::too_many_arguments)]
pub fn oddlyield(
settlement: f64,
maturity: f64,
last_interest: f64,
rate: f64,
pr: f64,
redemption: f64,
frequency: f64,
basis: f64,
) -> f64 {
let e = oddlprice_e(last_interest, maturity, frequency, basis);
let dcnl = coupon_end_days(last_interest, maturity, basis);
let dcsl = basis_days_between(last_interest, settlement, basis);
let dsc = basis_days_between(settlement, maturity, basis);
let coupon = 100.0 * rate / frequency;
let numerator = redemption + coupon * (dcnl / e);
let denominator = pr + coupon * (dcsl / e);
(numerator / denominator - 1.0) * (frequency * e / dsc)
}
fn euro_rate(code: &str) -> Option<f64> {
match code.to_uppercase().as_str() {
"EUR" => Some(1.0),
"ATS" => Some(13.7603),
"BEF" | "LUF" => Some(40.3399),
"DEM" => Some(1.95583),
"ESP" => Some(166.386),
"FIM" => Some(5.94573),
"FRF" => Some(6.55957),
"IEP" => Some(0.787564),
"ITL" => Some(1936.27),
"NLG" => Some(2.20371),
"PTE" => Some(200.482),
"GRD" => Some(340.750),
"SIT" => Some(239.640),
"CYP" => Some(0.585274),
"MTL" => Some(0.429300),
"SKK" => Some(30.1260),
"EEK" => Some(15.6466),
"LVL" => Some(0.702804),
"LTL" => Some(3.45280),
_ => None,
}
}
fn euro_round_half_away(x: f64, decimals: i32) -> f64 {
let factor = 10f64.powi(decimals);
round_half_away_from_zero(x * factor) / factor
}
pub fn euroconvert(
number: f64,
source: &str,
target: &str,
full_precision: bool,
triangulation_precision: Option<f64>,
) -> Result<f64, String> {
let source_rate = euro_rate(source).ok_or("#VALUE!".to_string())?;
let target_rate = euro_rate(target).ok_or("#VALUE!".to_string())?;
let decimals_for = |code: &str| -> i32 {
match code.to_uppercase().as_str() {
"ITL" | "ESP" | "BEF" | "LUF" => 0,
_ => 2,
}
};
let result = if source.eq_ignore_ascii_case(target) {
number
} else if source.eq_ignore_ascii_case("EUR") {
number * target_rate
} else if target.eq_ignore_ascii_case("EUR") {
number / source_rate
} else {
let mut in_eur = number / source_rate;
if let Some(tp) = triangulation_precision {
if tp < 3.0 {
return Err("#NUM!".to_string());
}
in_eur = euro_round_half_away(in_eur, tp as i32);
}
in_eur * target_rate
};
if full_precision {
Ok(result)
} else {
Ok(euro_round_half_away(result, decimals_for(target)))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) {
assert!((a - b).abs() < 5e-3, "{a} != {b}");
}
#[test]
fn test_fuzz_pricemat_settlement_never_gets_feb_eom_bump() {
approx(
pricemat(
date_fn::edate(date_fn::ymd_to_serial(2015, 2, 28), 2.0).unwrap(),
date_fn::edate(date_fn::ymd_to_serial(2015, 2, 28), 24.0).unwrap(),
date_fn::ymd_to_serial(2015, 2, 28),
0.0812,
0.099,
0.0,
),
97.02941154961482,
);
approx(
pricemat(
date_fn::edate(date_fn::ymd_to_serial(2033, 9, 28), 5.0).unwrap(),
date_fn::edate(date_fn::ymd_to_serial(2033, 9, 28), 36.0).unwrap(),
date_fn::ymd_to_serial(2033, 9, 28),
0.0239,
0.0138,
0.0,
),
102.48507237805565,
);
}
#[test]
fn test_pmt_matches_docs_example() {
approx(pmt(0.08 / 12.0, 10.0, 10000.0, 0.0, 0.0), -1037.03);
}
#[test]
fn test_fv_matches_docs_example() {
approx(fv(0.06 / 12.0, 10.0, -200.0, -500.0, 1.0), 2581.40);
}
#[test]
fn test_pv_matches_docs_example() {
approx(pv(0.08 / 12.0, 240.0, 500.0, 0.0, 0.0), -59777.15);
}
#[test]
fn test_nper_is_inverse_of_fv() {
for pmt_type in [0.0, 1.0] {
let rate = 0.01;
let pmt_amt = -100.0;
let pv_amt = -1000.0;
let n = 24.0;
let fv_amt = fv(rate, n, pmt_amt, pv_amt, pmt_type);
approx(nper(rate, pmt_amt, pv_amt, fv_amt, pmt_type).unwrap(), n);
}
}
#[test]
fn test_rate_matches_docs_example() {
approx(rate(48.0, -200.0, 8000.0, 0.0, 0.0, 0.1).unwrap(), 0.007701);
}
#[test]
fn test_ipmt_ppmt_sum_to_pmt() {
let rate = 0.10 / 12.0;
let nper = 36.0;
let pv = 8000000.0;
let full_pmt = pmt(rate, nper, pv, 0.0, 0.0);
for per in 1..=36 {
let per = per as f64;
approx(
ipmt(rate, per, nper, pv, 0.0, 0.0) + ppmt(rate, per, nper, pv, 0.0, 0.0),
full_pmt,
);
}
}
#[test]
fn test_ispmt_matches_fuzzer_verified_excel_values() {
approx(ispmt(0.0749, 111.0, 153.0, 96163.49), -1977.1967767450978);
approx(ispmt(0.0303, 17.0, 38.0, 43643.33), -730.796075763158);
approx(ispmt(0.0497, 163.0, 193.0, 96345.37), -744.3054231606217);
}
#[test]
fn test_ipmt_type1_matches_fuzzer_verified_excel_values() {
approx(
ipmt(0.0085, 32.0, 150.0, 49910.41, 0.0, 1.0),
-371.35148593978215,
);
}
#[test]
fn test_ppmt_matches_fuzzer_verified_excel_values() {
approx(
ppmt(0.1715, 114.0, 125.0, 19772.69, 0.0, 1.0),
-433.191648049476,
);
approx(
ppmt(0.0752, 77.0, 178.0, 85932.73, 0.0, 1.0),
-3.6896416644154724,
);
approx(
ppmt(0.0288, 195.0, 284.0, 51703.71, 0.0, 1.0),
-112.4433992494153,
);
approx(
ppmt(0.0872, 18.0, 98.0, 93845.1, 0.0, 1.0),
-8.62330593780311,
);
let got = ppmt(0.0998, 248.0, 285.0, 67611.52, 0.0, 0.0);
let want = -181.64776461127138;
assert!((got - want).abs() < 0.25, "{got} != {want}");
}
#[test]
fn test_npv_matches_docs_example() {
approx(npv(0.10, &[-10000.0, 3000.0, 4200.0, 6800.0]), 1188.44);
}
#[test]
fn test_irr_matches_docs_example() {
approx(
irr(
&[-70000.0, 12000.0, 15000.0, 18000.0, 21000.0, 26000.0],
0.1,
)
.unwrap(),
0.0866,
);
}
#[test]
fn test_irr_recovers_via_zero_guess_fallback() {
let cash = [-20633.16, 7717.06, -18760.88, -8911.01, 3391.3, 16198.77];
approx(irr(&cash, 0.1).unwrap(), -0.19995872986748842);
}
#[test]
fn test_mirr_all_negative_cashflows_is_minus_one_not_num_error() {
let cash = [-5787.88, -814.95, -8609.23, -601.21, -12290.6, -16118.7];
approx(mirr(&cash, 0.0087, 0.0188).unwrap(), -1.0);
}
#[test]
fn test_sln_matches_docs_example() {
approx(sln(30000.0, 7500.0, 10.0), 2250.0);
}
#[test]
fn test_syd_matches_docs_example() {
approx(syd(30000.0, 7500.0, 10.0, 1.0), 4090.91);
}
#[test]
fn test_ddb_matches_docs_example() {
approx(ddb(2400.0, 300.0, 10.0, 1.0, 2.0), 480.0);
approx(ddb(2400.0, 300.0, 10.0, 2.0, 2.0), 384.0);
}
#[test]
fn test_effect_nominal_are_inverses() {
let e = effect(0.0525, 4.0);
approx(nominal(e, 4.0), 0.0525);
}
#[test]
fn test_dollarde_matches_docs_example() {
approx(dollarde(1.02, 16.0).unwrap(), 1.125);
}
#[test]
fn test_dollarfr_matches_docs_example() {
approx(dollarfr(1.125, 16.0).unwrap(), 1.02);
}
#[test]
fn test_fvschedule_matches_docs_example() {
approx(fvschedule(1.0, &[0.09, 0.11, 0.1]), 1.33089);
}
}