use log::warn;
use super::tvm::*;
pub fn present_value<T, C>(
rate: f64,
periods: u32,
future_value: T,
compounding: C,
) -> crate::FinanceResult<f64>
where
T: Into<f64> + Copy,
C: Into<crate::Compounding>,
{
present_value_internal(
rate,
periods as f64,
future_value.into(),
compounding.into().is_continuous(),
)
}
pub fn present_value_solution<T, C>(
rate: f64,
periods: u32,
future_value: T,
compounding: C,
) -> crate::FinanceResult<TvmSolution>
where
T: Into<f64> + Copy,
C: Into<crate::Compounding>,
{
present_value_solution_internal(
rate,
periods as f64,
future_value.into(),
compounding.into().is_continuous(),
)
}
pub fn present_value_schedule<T>(rates: &[f64], future_value: T) -> crate::FinanceResult<f64>
where
T: Into<f64> + Copy,
{
let future_value = future_value.into();
crate::util::error::require_rates(rates)?;
crate::util::error::require_money("future_value", future_value)?;
if !future_value.is_normal() {
return Err(crate::FinanceError::ZeroValue {
field: "future_value",
});
}
let periods = rates.len();
let mut present_value = -future_value;
for i in (0..periods).rev() {
present_value /= 1.0 + rates[i];
}
if present_value.is_finite() {
Ok(present_value)
} else {
Err(crate::FinanceError::NonFinite {
field: "present_value",
value: present_value,
})
}
}
pub fn present_value_schedule_solution<T>(
rates: &[f64],
future_value: T,
) -> crate::FinanceResult<TvmScheduleSolution>
where
T: Into<f64> + Copy,
{
let future_value = future_value.into();
let present_value = present_value_schedule(rates, future_value)?;
Ok(TvmScheduleSolution::new(
TvmVariable::PresentValue,
rates,
present_value,
future_value,
))
}
pub(crate) fn present_value_internal(
rate: f64,
periods: f64,
future_value: f64,
continuous_compounding: bool,
) -> crate::FinanceResult<f64> {
crate::util::error::require_rate(rate)?;
crate::util::error::require_money("future_value", future_value)?;
if !future_value.is_normal() {
return Err(crate::FinanceError::ZeroValue {
field: "future_value",
});
}
if rate.abs() > 1.0 {
warn!(
"You provided a periodic rate ({}) greater than 1. Are you sure you expect a {}% return?",
rate,
rate * 100.0
);
}
let present_value = if continuous_compounding {
-future_value / std::f64::consts::E.powf(rate * periods)
} else {
-future_value / (1.0 + rate).powf(periods)
};
if present_value.is_finite() {
Ok(present_value)
} else {
Err(crate::FinanceError::NonFinite {
field: "present_value",
value: present_value,
})
}
}
pub(crate) fn present_value_solution_internal(
rate: f64,
periods: f64,
future_value: f64,
continuous_compounding: bool,
) -> crate::FinanceResult<TvmSolution> {
let present_value =
present_value_internal(rate, periods, future_value, continuous_compounding)?;
let rate_multiplier = 1.0 + rate;
let (formula, symbolic_formula) = if continuous_compounding {
let formula = format!(
"{:.4} = {:.4} / {:.6}^({:.6} * {})",
present_value,
-future_value,
std::f64::consts::E,
rate,
periods
);
let symbolic_formula = "pv = -fv / e^(rt)";
(formula, symbolic_formula)
} else {
let formula = format!(
"{:.4} = {:.4} / ({:.6} ^ {})",
present_value, -future_value, rate_multiplier, periods
);
let symbolic_formula = "pv = -fv / (1 + r)^n";
(formula, symbolic_formula)
};
Ok(TvmSolution::new_fractional_periods(
TvmVariable::PresentValue,
continuous_compounding,
rate,
periods,
present_value,
future_value,
&formula,
symbolic_formula,
))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::*;
#[test]
fn test_present_value_schedule() {
let rates = [0.04, 0.07, -0.12, -0.03, 0.11];
let future_value = 100_000.25;
let present_value = present_value_schedule(&rates, future_value).unwrap();
assert_rounded_4(-94843.2841, present_value);
let solution = present_value_schedule_solution(&rates, future_value).unwrap();
assert_rounded_4(100000.2500, solution.future_value());
assert_rounded_4(-94843.2841, solution.present_value());
let series = solution.series();
assert_eq!(6, series.len());
let period = &series[0];
assert_eq!(0, period.period());
assert_rounded_6(0.0, period.rate());
assert_rounded_4(-present_value, period.value());
let period = &series[1];
assert_eq!(1, period.period());
assert_rounded_6(0.04, period.rate());
assert_rounded_4(98_637.0154, period.value());
let period = &series[2];
assert_eq!(2, period.period());
assert_rounded_6(0.07, period.rate());
assert_rounded_4(105_541.6065, period.value());
let period = &series[3];
assert_eq!(3, period.period());
assert_rounded_6(-0.12, period.rate());
assert_rounded_4(92_876.6137, period.value());
let period = &series[4];
assert_eq!(4, period.period());
assert_rounded_6(-0.03, period.rate());
assert_rounded_4(90_090.3153, period.value());
let period = &series[5];
assert_eq!(5, period.period());
assert_rounded_6(0.11, period.rate());
assert_rounded_4(100_000.2500, period.value());
}
fn compare_to_excel(
test_case: usize,
r: f64,
n: u32,
fv: f64,
pv_excel: f64,
pv_manual_simple: f64,
pv_manual_cont: f64,
) {
let display = false;
if display {
println!("test_case = {}, r = {}, n = {}, fv = {}, pv_excel: {}, pv_manual_simple = {}, pv_manual_cont = {}", test_case, r, n, fv, pv_excel, pv_manual_simple, pv_manual_cont)
};
assert_approx_equal!(pv_excel, pv_manual_simple);
let pv_calc_simple = present_value(r, n, fv, false).unwrap();
if display {
println!("pv_calc_simple = {}", pv_calc_simple)
};
assert_approx_equal!(pv_excel, pv_calc_simple);
let pv_calc_cont = present_value(r, n, fv, true).unwrap();
if display {
println!("pv_calc_cont = {}", pv_calc_cont)
};
assert_approx_equal!(pv_manual_cont, pv_calc_cont);
let ratio = pv_calc_cont / pv_calc_simple;
if display {
println!("ratio = {}", ratio)
};
assert!(ratio >= 0.0);
assert!(ratio <= 1.0);
let solution = present_value_solution(r, n, fv, false).unwrap();
if display {
dbg!(&solution);
}
solution.invariant();
assert!(solution.calculated_field().is_present_value());
assert_eq!(false, solution.continuous_compounding());
assert_approx_equal!(r, solution.rate());
assert_eq!(n, solution.periods());
assert_approx_equal!(n as f64, solution.fractional_periods());
assert_approx_equal!(pv_excel, solution.present_value());
assert_approx_equal!(fv, solution.future_value());
let solution = present_value_solution(r, n, fv, true).unwrap();
if display {
dbg!(&solution);
}
solution.invariant();
assert!(solution.calculated_field().is_present_value());
assert!(solution.continuous_compounding());
assert_approx_equal!(r, solution.rate());
assert_eq!(n, solution.periods());
assert_approx_equal!(n as f64, solution.fractional_periods());
assert_approx_equal!(pv_manual_cont, solution.present_value());
assert_approx_equal!(fv, solution.future_value());
let rates = initialized_vector(n as usize, r);
let solution = present_value_schedule_solution(&rates, fv).unwrap();
if display {
dbg!(&solution);
}
solution.invariant();
assert!(solution.calculated_field().is_present_value());
assert_eq!(n, solution.periods());
assert_approx_equal!(pv_excel, solution.present_value());
assert_approx_equal!(fv, solution.future_value());
}
#[test]
fn test_present_value_against_excel() {
compare_to_excel(
1,
0.01f64,
90,
1f64,
-0.408391185151344f64,
-0.408391185151344f64,
-0.406569659740599f64,
);
compare_to_excel(
2,
-0.01f64,
85,
-1.5f64,
3.52451788132823f64,
3.52451788132823f64,
3.50947027788899f64,
);
compare_to_excel(3, 0f64, 80, 2.25f64, -2.25f64, -2.25f64, -2.25f64);
compare_to_excel(
4,
0.05f64,
75,
-3.375f64,
0.0869113201859512f64,
0.0869113201859512f64,
0.0793723922640307f64,
);
compare_to_excel(
5,
-0.05f64,
70,
5.0625f64,
-183.53236712846f64,
-183.53236712846f64,
-167.64697554088f64,
);
compare_to_excel(
6,
0.01f64,
65,
-7.59375f64,
3.97710447262579f64,
3.97710447262579f64,
3.96428511727897f64,
);
compare_to_excel(
7,
-0.01f64,
60,
11.390625f64,
-20.8178501685176f64,
-20.8178501685176f64,
-20.7550719606981f64,
);
compare_to_excel(
8,
0f64,
55,
-17.0859375f64,
17.0859375f64,
17.0859375f64,
17.0859375f64,
);
compare_to_excel(
9,
0.05f64,
50,
25.62890625f64,
-2.23493614322574f64,
-2.23493614322574f64,
-2.10374873426328f64,
);
compare_to_excel(
10,
-0.05f64,
45,
-38.443359375f64,
386.597546504632f64,
386.597546504632f64,
364.740438412197f64,
);
compare_to_excel(
11,
0.01f64,
40,
57.6650390625f64,
-38.7309044888379f64,
-38.7309044888379f64,
-38.6540316390219f64,
);
compare_to_excel(
12,
-0.01f64,
35,
-86.49755859375f64,
122.962317182378f64,
122.962317182378f64,
122.745878432934f64,
);
compare_to_excel(
13,
0f64,
30,
129.746337890625f64,
-129.746337890625f64,
-129.746337890625f64,
-129.746337890625f64,
);
compare_to_excel(
14,
0.05f64,
25,
-194.619506835937f64,
57.4716797951039f64,
57.4716797951039f64,
55.7594222710607f64,
);
compare_to_excel(
15,
-0.05f64,
20,
291.929260253906f64,
-814.33953749853f64,
-814.33953749853f64,
-793.546003343685f64,
);
compare_to_excel(
16,
0.01f64,
15,
-437.893890380859f64,
377.179672510109f64,
377.179672510109f64,
376.898764278606f64,
);
compare_to_excel(
17,
-0.01f64,
12,
656.840835571289f64,
-741.033445550103f64,
-741.033445550103f64,
-740.585974095395f64,
);
compare_to_excel(
18,
0f64,
10,
-985.261253356933f64,
985.261253356933f64,
985.261253356933f64,
985.261253356933f64,
);
compare_to_excel(
19,
0.05f64,
7,
1477.8918800354f64,
-1050.31016709206f64,
-1050.31016709206f64,
-1041.45280575294f64,
);
compare_to_excel(
20,
-0.05f64,
5,
-2216.8378200531f64,
2864.94240503709f64,
2864.94240503709f64,
2846.47610562283f64,
);
compare_to_excel(
21,
0.01f64,
4,
3325.25673007965f64,
-3195.50635796575f64,
-3195.50635796575f64,
-3194.87154873072f64,
);
compare_to_excel(
22,
-0.01f64,
3,
-4987.88509511947f64,
5140.56501667989f64,
5140.56501667989f64,
5139.78881110503f64,
);
compare_to_excel(
23,
0f64,
2,
7481.82764267921f64,
-7481.82764267921f64,
-7481.82764267921f64,
-7481.82764267921f64,
);
compare_to_excel(
24,
0.05f64,
1,
-11222.7414640188f64,
10688.3252038274f64,
10688.3252038274f64,
10675.4019041389f64,
);
compare_to_excel(
25,
-0.05f64,
0,
16834.1121960282f64,
-16834.1121960282f64,
-16834.1121960282f64,
-16834.1121960282f64,
);
}
}