use crate::FloatLike;
pub fn pv<T: FloatLike>(rate: T, nper: T, pmt: T, fv: Option<T>, due: Option<bool>) -> T {
let fv: T = fv.unwrap_or(T::zero());
let due = due.unwrap_or(false);
let pv = if rate == T::zero() {
fv + (pmt * nper)
} else {
let nth_power = (T::one() + rate).powf(nper);
let fv_discounted = fv / nth_power;
let factor = (T::one() - (T::one() / nth_power)) / rate;
if due {
pmt * factor * (T::one() + rate) + fv_discounted
} else {
pmt * factor + fv_discounted
}
};
-pv
}
pub fn npv<T: FloatLike>(rate: T, cash_flows: &[T]) -> T {
let mut powf_acc = T::one();
let mut npv = T::zero();
for &cf in cash_flows.iter() {
npv += cf / (powf_acc);
powf_acc *= T::one() + rate;
}
npv
}
pub fn npv_differing_rates<T: FloatLike>(flow_table: &[(T, T)]) -> T {
flow_table
.iter()
.enumerate()
.map(|(t, &(cf, rate))| cf / (T::one() + rate).powf(T::from_usize(t)))
.sum()
}
pub fn xnpv<T: FloatLike>(rate: T, flow_table: &[(T, i32)]) -> T {
let init_date = flow_table.first().unwrap().1;
let one_plus_r = T::one() + rate;
let yr_length: T = T::from_u16(365);
flow_table
.iter()
.map(|&(cf, date)| {
let years = T::from_i32(date - init_date) / yr_length;
cf / (one_plus_r).powf(years)
})
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
extern crate std;
#[cfg(not(feature = "std"))]
use std::{assert, vec, vec::Vec};
#[test]
fn test_xnpv() {
let rate = 0.05;
let flows_table = vec![(-100.0, 0), (50.0, 365), (40.0, 730), (30.0, 1095), (20.0, 1460)];
let result = xnpv(rate, &flows_table);
let expected: f64 = 26.26940;
assert!(
(result - expected).abs() < 1e-5,
"Failed on case: {}. Expected: {}, Result: {}",
"5% discount rate, cash flows of -100, 50, 40, 30, 20",
expected,
result
);
}
#[test]
fn test_pv() {
struct TestCase {
rate: f64,
nper: f64,
pmt: f64,
fv: Option<f64>,
due: Option<bool>,
expected: f64,
description: &'static str,
}
impl TestCase {
fn new(
rate: f64,
nper: f64,
pmt: f64,
fv: Option<f64>,
due: Option<bool>,
expected: f64,
description: &'static str,
) -> TestCase {
TestCase {
rate,
nper,
pmt,
fv,
due,
expected,
description,
}
}
}
let cases = [
TestCase::new(
0.05,
10.0,
100.0,
None,
None,
-772.17349,
"Standard case with 5% rate, 10 periods, and $100 pmt",
),
TestCase::new(
0.05,
10.0,
100.0,
None,
Some(true),
-810.78217,
"Payment at the beg of period should result in higher present value",
),
TestCase::new(0.0, 10.0, -100.0, None, None, 1000.0, "Zero interest rate no growth"),
TestCase::new(
0.05,
10.0,
100.0,
Some(1000.0),
None,
-1386.08675,
"Bond with 5% rate, 10 periods, 10% coupon, and $1000 future value",
),
TestCase::new(
0.05,
10.0,
0.0,
Some(2000.0),
None,
-1227.82651,
"No cash flows, just a future pay out",
),
];
for case in &cases {
let calculated_pv = pv(case.rate, case.nper, case.pmt, case.fv, case.due);
assert!(
(calculated_pv - case.expected).abs() < 1e-5,
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_pv
);
}
}
#[test]
fn test_npv() {
struct TestCase {
rate: f64,
cash_flows: Vec<f64>,
expected: f64,
description: &'static str,
}
impl TestCase {
fn new(rate: f64, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
TestCase {
rate,
cash_flows,
expected,
description,
}
}
}
let cases = [
TestCase::new(
0.05,
vec![-100.0, 50.0, 40.0, 30.0, 20.0],
26.26940,
"Standard case with 5% rate and cash flows of -100, 50, 40, 30, 20",
),
TestCase::new(
0.05,
vec![100.0, 50.0, 40.0, 30.0, 20.0],
226.26940,
"All positive cash flows",
),
TestCase::new(
0.05,
vec![-100.0, 50.0, 40.0, 30.0, 20.0, 1000.0],
809.79557,
"Additional future cash flow should increase NPV",
),
];
for case in &cases {
let calculated_npv = npv(case.rate, &case.cash_flows);
assert!(
(calculated_npv - case.expected).abs() < 1e-5,
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_npv
);
}
}
#[test]
fn test_npv_differing_rates() {
struct TestCase {
flow_table: Vec<(f64, f64)>,
expected: f64,
description: &'static str,
}
impl TestCase {
fn new(rates: Vec<f64>, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
let flow_table = cash_flows.iter().zip(rates.iter()).map(|(&cf, &r)| (cf, r)).collect();
TestCase {
flow_table,
expected,
description,
}
}
}
let cases = [
TestCase::new(
vec![0.05, 0.06, 0.07, 0.08, 0.09],
vec![-100.0, 50.0, 40.0, 30.0, 20.0],
20.09083,
"Increasing rate and cash flows of -100, 50, 40, 30, 20",
),
TestCase::new(
vec![0.05, 0.06, 0.07, 0.08, 0.09],
vec![100.0, 50.0, 40.0, 30.0, 20.0],
220.09083,
"All positive cash flows",
),
TestCase::new(
vec![0.05, 0.06, 0.07, 0.08, 0.09, 0.1],
vec![-100.0, 50.0, 40.0, 30.0, 20.0, 1000.0],
641.01215,
"Additional future cash flow should increase NPV",
),
];
for case in &cases {
let calculated_npv = npv_differing_rates(&case.flow_table);
assert!(
(calculated_npv - case.expected).abs() < 1e-5,
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_npv
);
}
}
}