use crate::{ONE, ZERO};
use rust_decimal::prelude::*;
use rust_decimal_macros::*;
pub fn pv(rate: Decimal, nper: Decimal, pmt: Decimal, fv: Option<Decimal>, due: Option<bool>) -> Decimal {
let fv: Decimal = fv.unwrap_or(ZERO);
let due = due.unwrap_or(false);
let mut pv = if rate == ZERO {
fv + (pmt * nper)
} else {
let nth_power = (ONE + rate).powd(-nper);
let fv_discounted = fv * nth_power;
let factor = (ONE - nth_power) / rate;
if due {
pmt * factor * (ONE + rate) + fv_discounted
} else {
pmt * factor + fv_discounted
}
};
pv.set_sign_negative(true);
pv
}
pub fn npv(rate: Decimal, cash_flows: &[Decimal]) -> Decimal {
cash_flows
.iter()
.enumerate()
.map(|(t, &cf)| cf / (ONE + rate).powi(t as i64))
.sum()
}
pub fn npv_differing_rates(flow_table: &[(Decimal, Decimal)]) -> Decimal {
flow_table
.iter()
.enumerate()
.map(|(t, &(cf, rate))| cf / (ONE + rate).powi(t as i64))
.sum()
}
pub fn xnpv(rate: Decimal, flow_table: &[(Decimal, i32)]) -> Decimal {
let init_date = flow_table.first().unwrap().1;
flow_table
.iter()
.map(|&(cf, date)| {
let years = Decimal::from_i32(date - init_date).unwrap() / dec!(365);
cf / (ONE + rate).powd(years)
})
.sum()
}
#[cfg(test)]
mod tests {
#[cfg(not(feature = "std"))]
extern crate std;
use super::*;
#[cfg(not(feature = "std"))]
use std::prelude::v1::*;
#[cfg(not(feature = "std"))]
use std::{assert, vec};
#[test]
fn test_xnpv() {
let rate = dec!(0.05);
let flows_table = vec![
(dec!(-100), 0),
(dec!(50), 365),
(dec!(40), 730),
(dec!(30), 1095),
(dec!(20), 1460),
];
let result = xnpv(rate, &flows_table);
let expected = dec!(26.26940);
assert!(
(result - expected).abs() < dec!(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: Decimal,
nper: Decimal,
pmt: Decimal,
fv: Option<Decimal>,
due: Option<bool>,
expected: Decimal,
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: Decimal::from_f64(rate).unwrap(),
nper: Decimal::from_f64(nper).unwrap(),
pmt: Decimal::from_f64(pmt).unwrap(),
fv: fv.map(Decimal::from_f64).unwrap_or(None),
due,
expected: Decimal::from_f64(expected).unwrap(),
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() < dec!(1e-5),
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_pv
);
}
}
#[test]
fn test_npv() {
struct TestCase {
rate: Decimal,
cash_flows: Vec<Decimal>,
expected: Decimal,
description: &'static str,
}
impl TestCase {
fn new(rate: f64, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
TestCase {
rate: Decimal::from_f64(rate).unwrap(),
cash_flows: cash_flows.iter().map(|&cf| Decimal::from_f64(cf).unwrap()).collect(),
expected: Decimal::from_f64(expected).unwrap(),
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() < dec!(1e-5),
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_npv
);
}
}
#[test]
fn test_npv_differing_rates() {
struct TestCase {
flow_table: Vec<(Decimal, Decimal)>,
expected: Decimal,
description: &'static str,
}
impl TestCase {
fn new(rates: Vec<f64>, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
let rates: Vec<Decimal> = rates.iter().map(|&r| Decimal::from_f64(r).unwrap()).collect();
let cash_flows: Vec<Decimal> = cash_flows.iter().map(|&cf| Decimal::from_f64(cf).unwrap()).collect();
let flow_table = cash_flows.iter().zip(rates.iter()).map(|(&cf, &r)| (cf, r)).collect();
TestCase {
flow_table,
expected: Decimal::from_f64(expected).unwrap(),
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() < dec!(1e-5),
"Failed on case: {}. Expected {}, got {}",
case.description,
case.expected,
calculated_npv
);
}
}
}