finance_solution/tvm/
rate.rs

1//! **Periodic rate calculations.** Given an initial investment amount, a final amount, and a number
2//! of periods what does the rate per period need to be?
3//! 
4//! For most common usages, we recommend the [`rate_solution`](./fn.rate_solution.html) function to provide the best experience with debugging and additional features.
5//!
6// ! If you need to calculate the future value given a starting value, a number of periods, and one
7// ! or more rates use [`future_value`] or related functions.
8// !
9// ! If you need to calculate the present value given a future value, a number of periods, and one
10// ! or more rates use [`present_value`] or related functions.
11// !
12// ! If you need to calculate the number of periods given a fixed rate and a present and future value
13// ! use [`periods`] or related functions.
14//! # Formulas
15//!
16//! ## Simple Compounding
17//!
18//! With simple compound interest the rate is calculated with:
19//!
20//! > <img src="http://i.upmath.me/svg/rate%20%3D%20%5Csqrt%5Bperiods%5D%7B%5Cfrac%7Bfuture%5C_value%7D%7Bpresent%5C_value%7D%7D%20-%201" />
21//!
22//! Or using a few more common variable names:
23//!
24//! > <img src="http:i.upmath.me/svg/r%20%3D%20%5Csqrt%5Bn%5D%7B%5Cfrac%7Bfv%7D%7Bpv%7D%7D%20-%201" />
25//!
26//! `r` is the periodic rate, though this may appear as `i` for interest. `n` is often used for the
27//! number of periods, though it may be `t` for time if each period is assumed to be one year as in
28//! continuous compounding.
29//!
30//! Throughout this crate we use `pv` for present value and `fv` for future value. You may see these
31//! values called `P` for principal in some references.
32//!
33//! ## Continuous Compounding
34//!
35//! With continuous compounding the formula is:
36//!
37//! > <img src="http://i.upmath.me/svg/rate%20%3D%20%5Cfrac%7B%5Cln%5Cleft(%5Cfrac%7Bfuture%5C_value%7D%7Bpresent%5C_value%7D%5Cright)%7D%7Bperiods%7D" />
38//!
39//! or:
40//!
41//! > <img src="http://i.upmath.me/svg/r%20%3D%20%5Cfrac%7B%5Cln%5Cleft(%5Cfrac%7Bfv%7D%7Bpv%7D%5Cright)%7Dn" />
42//!
43//! With continuous compounding the period is assumed to be years and `t` (time) is often used as
44//! the variable name. Within this crate we stick with `n` for the number of periods so that all of
45//! the functions use the same variables.
46
47use crate::*;
48
49/// Returns the periodic rate of an investment given the number of periods along with the present
50/// and future values.
51///
52/// See the [rate](./index.html) module page for the formulas.
53///
54/// Related functions:
55/// * To calculate a periodic rate and return a struct that shows the formula and optionally
56/// produces the the period-by-period values use [`rate_solution`].
57///
58/// # Arguments
59/// * `periods` - The number of periods such as quarters or periods. Often appears as `n` or `t`.
60/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `C`
61/// for cash flow or `P` for principal.
62/// * `future_value` - The final value of the investment.
63/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
64///
65/// If present_value and future_value are both zero then any rate will work so the function returns
66/// zero.
67///
68/// # Panics
69/// The call will fail if the present value is zero and the future value is nonzero or vice versa.
70/// It will also fail if the number of periods is zero and the present value is not equal to the
71/// future value. In both cases this is because there's no periodic rate that could make that work.
72///
73/// # Examples
74/// ```
75/// use finance_solution::*;
76///
77/// // The interest will compound for 365 days.
78/// let periods = 365;
79///
80/// // The starting value is $10,000.
81/// let present_value = -10_000.00;
82///
83/// // The ending value is $11,000.
84/// let future_value = 11_000.00;
85///
86/// let continuous_compounding = false;
87///
88/// // Calculate the periodic rate needed.
89/// let rate = rate(periods, present_value, future_value, continuous_compounding);
90/// dbg!(&rate);
91/// // The rate is 0.0261% per day.
92/// assert_rounded_6(0.000261, rate);
93/// ```
94pub fn rate<P, F>(periods: u32, present_value: P, future_value: F, continuous_compounding: bool) -> f64
95    where
96        P: Into<f64> + Copy,
97        F: Into<f64> + Copy
98{
99    rate_internal(periods, present_value.into(), future_value.into(), continuous_compounding)
100}
101
102/// Returns the periodic rate of an investment given the number of periods along with the present
103/// and future values.
104///
105/// See the [rate](./index.html) module page for the formulas.
106///
107/// Related functions:
108/// * To calculate a periodic rate returning an f64 value instead of solution object, use [`rate`](./fn.rate.html).
109///
110/// # Arguments
111/// * `periods` - The number of periods such as quarters or periods. Often appears as `n` or `t`.
112/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `C`
113/// for cash flow or `P` for principal.
114/// * `future_value` - The final value of the investment.
115/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
116///
117/// If present_value and future_value are both zero then any rate will work so the function returns
118/// zero.
119///
120/// # Panics
121/// The call will fail if the present value is zero and the future value is nonzero or vice versa.
122/// It will also fail if the number of periods is zero and the present value is not equal to the
123/// future value. In both cases this is because there's no periodic rate that could make that work.
124///
125/// # Examples
126/// Calculate a periodic rate and examine the period-by-period values.
127/// ```
128/// use finance_solution::*;
129/// // The interest will compound for ten periods.
130/// // The starting value is $10,000.
131/// // The ending value is $15,000.
132/// let periods = 10;
133/// let present_value = -10_000.00;
134/// let future_value = 15_000.00;
135/// let continuous_compounding = false;
136/// /// // Calculate the periodic rate and create a struct with a record of the
137/// // inputs, a description of the formula, and an option to calculate the
138/// // period-by-period values.
139/// let solution = rate_solution(periods, present_value, future_value, continuous_compounding);
140/// dbg!(&solution);
141///
142/// let rate = solution.rate();
143/// dbg!(&rate);
144/// // The rate is 4.138% per period.
145/// assert_rounded_6(0.041380, rate);
146///
147/// // Examine the formulas.
148/// let formula = solution.formula();
149/// dbg!(&formula);
150/// assert_eq!("0.041380 = ((-15000.0000 / -10000.0000) ^ (1 / 10)) - 1", formula);
151/// let symbolic_formula = solution.symbolic_formula();
152/// dbg!(&symbolic_formula);
153/// assert_eq!("r = ((-fv / pv) ^ (1 / n)) - 1", symbolic_formula);
154///
155/// // Calculate the period-by-period values.
156/// let series = solution.series();
157/// dbg!(&series);
158/// ```
159pub fn rate_solution<P, F>(periods: u32, present_value: P, future_value: F, continuous_compounding: bool) -> TvmSolution
160    where
161        P: Into<f64> + Copy,
162        F: Into<f64> + Copy
163{
164    rate_solution_internal(periods, present_value.into(), future_value.into(), continuous_compounding)
165}
166
167fn rate_internal(periods: u32, present_value: f64, future_value: f64, continuous_compounding: bool) -> f64 {
168    if present_value + future_value == 0.0 {
169        // This is a special case where any rate will work.
170        return 0.0;
171    }
172    if future_value == 0.0 {
173        // This is a special case where the rate must be -100% because present value is nonzero.
174        return -1.0;
175    }
176    check_rate_parameters(periods, present_value, future_value);
177
178    let rate = if continuous_compounding {
179        // http://www.edmichaelreggie.com/TMVContent/APR.htm
180        (-future_value / present_value).ln() / periods as f64
181    } else {
182        (-future_value / present_value).powf(1.0 / periods as f64) - 1.0
183    };
184
185    if !rate.is_finite() {
186        dbg!(periods, present_value, future_value, continuous_compounding, rate);
187    }
188
189    assert!(rate.is_finite());
190    rate
191}
192
193pub (crate) fn rate_solution_internal(periods: u32, present_value: f64, future_value: f64, continuous_compounding: bool) -> TvmSolution {
194    if present_value == 0.0 && future_value == 0.0 {
195        // This is a special case where any rate will work.
196        let formula = "{special case}";
197        let symbolic_formula = "***";
198        let rate = 0.0;
199        return TvmSolution::new(TvmVariable::Rate, continuous_compounding, rate, periods, present_value, future_value, formula, symbolic_formula);
200    }
201
202    let rate = rate_internal(periods, present_value, future_value, continuous_compounding);
203    let (formula, symbolic_formula) = if continuous_compounding {
204        let formula = format!("{:.6} = ln({:.4} / {:.4}) / {}", rate, -future_value, present_value, periods);
205        let symbolic_formula = "r = ln(-fv / pv) / t";
206        (formula, symbolic_formula)
207    } else {
208        let formula = format!("{:.6} = (({:.4} / {:.4}) ^ (1 / {})) - 1", rate, -future_value, present_value, periods);
209        let symbolic_formula = "r = ((-fv / pv) ^ (1 / n)) - 1";
210        (formula, symbolic_formula)
211    };
212    TvmSolution::new(TvmVariable::Rate, continuous_compounding, rate, periods, present_value, future_value, &formula, symbolic_formula)
213}
214
215fn check_rate_parameters(periods: u32, present_value: f64, future_value: f64) {
216    assert!(present_value.is_finite(), "The present value must be finite (not NaN or infinity)");
217    assert!(future_value.is_finite(), "The future value must be finite (not NaN or infinity)");
218    assert!(!(present_value < 0.0 && future_value < 0.0), "The present value and future value are both negative. They must have opposite signs.");
219    assert!(!(present_value > 0.0 && future_value > 0.0), "The present value and future value are both positive. They must have opposite signs.");
220    assert!(!(present_value == 0.0 && future_value != 0.0), "The present value is zero and the future value is nonzero so there's no way to solve for rate.");
221    assert!(!(periods == 0 && present_value + future_value != 0.0), "The number of periods is zero and the present value plus the future value is nonzero so there's no way to solve for rate.");
222}
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    #[test]
229    fn test_rate_edge() {
230        // Zero periods, values add up to zero.
231        assert_rounded_6(0.0, rate(0, 10_000.0, -10_000.0, false));
232
233        // Nonzero periods, values add up to zero.
234        assert_rounded_6(0.0, rate(12, -10_000.0, 10_000.0, false));
235    }
236
237    #[should_panic]
238    #[test]
239    fn test_rate_err_present_value_nan() {
240        // The present value is not a number.
241        rate(12, std::f64::NAN, 1_000.0, false);
242    }
243
244    #[should_panic]
245    #[test]
246    fn test_rate_err_present_value_inf() {
247        // The present value is infinite.
248        rate(12, std::f64::INFINITY, 1_000.0, false);
249    }
250
251    #[should_panic]
252    #[test]
253    fn test_rate_err_future_value_nan() {
254        // The future value is not a number.
255        rate(12, 1_000.0, std::f64::NAN, false);
256    }
257
258    #[should_panic]
259    #[test]
260    fn test_rate_err_future_value_inf() {
261        // The future value is infinite.
262        rate(12, 1_000.0, std::f64::NEG_INFINITY, false);
263    }
264
265    #[should_panic]
266    #[test]
267    fn test_rate_err_zero_periods() {
268        // Zero periods, values don't add up to zero.
269        rate(0, 10_000.0, 10_000.0, false);
270    }
271
272    /*
273    macro_rules! compare_to_excel {
274        ( $n:expr, $pv:expr, $fv:expr, $r_excel:expr, $r_manual_simple:expr, $r_manual_cont:expr ) => {
275            println!("$n = {}, $pv = {}, $fv = {}, $r_excel: {}, $r_manual_simple = {}, $r_manual_cont = {}", $n, $pv, $fv, $r_excel, $r_manual_simple, $r_manual_cont);
276            assert_approx_equal!($r_excel, $r_manual_simple);
277
278            let r_calc_simple = rate($n, $pv, $fv, false);
279            println!("r_calc_simple = {}", r_calc_simple);
280            assert_approx_equal!($r_excel, r_calc_simple);
281
282            let r_calc_cont = rate($n, $pv, $fv, true);
283            println!("r_calc_cont = {}", r_calc_cont);
284            assert_approx_equal!($r_manual_cont, r_calc_cont);
285
286            if is_approx_equal!(0.0, r_calc_simple) {
287                assert_approx_equal!(0.0, r_calc_cont);
288            } else {
289                let ratio = r_calc_cont / r_calc_simple;
290                println!("ratio = {}", ratio);
291                if $r_excel < 0.0 {
292                    assert!(ratio >= 1.0);
293                    assert!(ratio <= 2.0);
294                } else {
295                    assert!(ratio >= 0.0);
296                    assert!(ratio <= 1.0);
297                }
298            }
299        }
300    }
301    */
302
303    fn compare_to_excel (test_case: usize, n: u32, pv: f64, fv: f64, r_excel: f64, r_manual_simple: f64, r_manual_cont: f64) {
304        let display = false;
305
306        if display { println!("test_case = {}, n = {}, pv = {}, fv = {}, r_excel: {}, r_manual_simple = {}, r_manual_cont = {}", test_case, n, pv, fv, r_excel, r_manual_simple, r_manual_cont) }
307        assert_approx_equal!(r_excel, r_manual_simple);
308
309        let r_calc_simple = rate(n, pv, fv,false);
310        if display { println!("r_calc_simple = {}", r_calc_simple) }
311        assert_approx_equal!(r_excel, r_calc_simple);
312
313        let r_calc_cont = rate(n, pv, fv, true);
314        if display { println!("r_calc_cont = {}", r_calc_cont); }
315        assert_approx_equal!(r_manual_cont, r_calc_cont);
316
317        if is_approx_equal!(0.0, r_calc_simple) {
318            assert_approx_equal!(0.0, r_calc_cont);
319        } else {
320            let ratio = r_calc_cont / r_calc_simple;
321            if display { println!("ratio = {}", ratio) };
322            if r_excel < 0.0 {
323                assert!(ratio >= 1.0);
324                assert!(ratio <= 2.0);
325            } else {
326                assert!(ratio >= 0.0);
327                assert!(ratio <= 1.0);
328            }
329        }
330
331        // Solution with simple compounding.
332        let solution = rate_solution(n, pv, fv, false);
333        if display { dbg!(&solution); }
334        solution.invariant();
335        assert!(solution.calculated_field().is_rate());
336        assert_eq!(false, solution.continuous_compounding());
337        assert_approx_equal!(r_excel, solution.rate());
338        assert_eq!(n, solution.periods());
339        assert_approx_equal!(n as f64, solution.fractional_periods());
340        assert_approx_equal!(pv, solution.present_value());
341        assert_approx_equal!(fv, solution.future_value());
342
343        // Solution with continuous compounding.
344        let solution = rate_solution(n, pv, fv, true);
345        if display { dbg!(&solution); }
346        solution.invariant();
347        assert!(solution.calculated_field().is_rate());
348        assert!(solution.continuous_compounding());
349        assert_approx_equal!(r_manual_cont, solution.rate());
350        assert_eq!(n, solution.periods());
351        assert_approx_equal!(n as f64, solution.fractional_periods());
352        assert_approx_equal!(pv, solution.present_value());
353        assert_approx_equal!(fv, solution.future_value());
354    }
355
356    #[test]
357    fn test_rate_against_excel() {
358        compare_to_excel(1, 90, -0.1f64, 1f64, 0.0259143654700119f64, 0.0259143654700098f64, 0.025584278811045f64);
359        compare_to_excel(2, 85, 1.05f64, -1.5f64, 0.00420499208399443f64, 0.00420499208399305f64, 0.00419617581104391f64);
360        compare_to_excel(3, 80, -2.25f64, 2.25f64, 8.10490132135311E-16f64, 0f64, 0f64);
361        compare_to_excel(4, 75, 4.3875f64, -3.375f64, -0.00349207865283533f64, -0.00349207865410572f64, -0.00349819019289988f64);
362        compare_to_excel(5, 70, -10.125f64, 5.0625f64, -0.00985323817917932f64, -0.00985323818144335f64, -0.00990210257942779f64);
363        compare_to_excel(6, 65, 0.759375f64, -7.59375f64, 0.0360593046264088f64, 0.0360593046256343f64, 0.0354243860460622f64);
364        compare_to_excel(7, 60, -7.9734375f64, 11.390625f64, 0.00596228650143506f64, 0.00596228649269048f64, 0.00594458239897887f64);
365        compare_to_excel(8, 55, 17.0859375f64, -17.0859375f64, 4.31995919490311E-13f64, 0f64, 0f64);
366        compare_to_excel(9, 50, -33.317578125f64, 25.62890625f64, -0.00523354233611077f64, -0.00523354233613538f64, -0.00524728528934982f64);
367        compare_to_excel(10, 45, 76.88671875f64, -38.443359375f64, -0.0152852470655657f64, -0.0152852470655688f64, -0.0154032706791099f64);
368        compare_to_excel(11, 40, -5.76650390625f64, 57.6650390625f64, 0.0592537251772898f64, 0.0592537251772889f64, 0.0575646273248511f64);
369        compare_to_excel(12, 35, 60.548291015625f64, -86.49755859375f64, 0.010242814832087f64, 0.0102428148320715f64, 0.0101907126839638f64);
370        compare_to_excel(13, 30, -129.746337890625f64, 129.746337890625f64, 2.53808542775445E-15f64, 0f64, 0f64);
371        compare_to_excel(14, 25, 253.005358886719f64, -194.619506835937f64, -0.0104396946981842f64, -0.0104396947068867f64, -0.0104945705786996f64);
372        compare_to_excel(15, 20, -583.858520507812f64, 291.929260253906f64, -0.0340636710696604f64, -0.0340636710751544f64, -0.0346573590279973f64);
373        compare_to_excel(16, 15, 43.7893890380859f64, -437.893890380859f64, 0.165914401180033f64, 0.165914401179832f64, 0.15350567286627f64);
374        compare_to_excel(17, 12, -459.788584899902f64, 656.840835571289f64, 0.0301690469250706f64, 0.0301690469166949f64, 0.0297229119948944f64);
375        compare_to_excel(18, 10, 985.261253356933f64, -985.261253356933f64, 3.26110008113999E-16f64, 0f64, 0f64);
376        compare_to_excel(19, 7, -1921.25944404602f64, 1477.8918800354f64, -0.0367869049970667f64, -0.0367869049970667f64, -0.0374806092096416f64);
377        compare_to_excel(20, 5, 4433.6756401062f64, -2216.8378200531f64, -0.129449436703859f64, -0.129449436703876f64, -0.138629436111989f64);
378        compare_to_excel(21, 4, -332.525673007965f64, 3325.25673007965f64, 0.778279410038923f64, 0.778279410038923f64, 0.575646273248511f64);
379        compare_to_excel(22, 3, 3491.51956658363f64, -4987.88509511947f64, 0.126247880443697f64, 0.126247880443606f64, 0.118891647979577f64);
380        compare_to_excel(23, 2, -7481.82764267921f64, 7481.82764267921f64, -9.19973496824152E-17f64, 0f64, 0f64);
381        compare_to_excel(24, 1, 14589.5639032245f64, -11222.7414640188f64, -0.230769230769231f64, -0.230769230769231f64, -0.262364264467491f64);
382    }
383}