Skip to main content

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.
46use crate::*;
47
48/// Returns the periodic rate of an investment given the number of periods along with the present
49/// and future values.
50///
51/// See the [rate](./index.html) module page for the formulas.
52///
53/// Related functions:
54/// * To calculate a periodic rate and return a struct that shows the formula and optionally
55/// produces the the period-by-period values use [`rate_solution`].
56///
57/// # Arguments
58/// * `periods` - The number of periods such as quarters or periods. Often appears as `n` or `t`.
59/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `C`
60/// for cash flow or `P` for principal.
61/// * `future_value` - The final value of the investment.
62/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
63///
64/// If present_value and future_value are both zero then any rate will work so the function returns
65/// zero.
66///
67/// # Errors
68/// The call returns [`FinanceError`] if the present value is zero and the future value is nonzero or vice versa.
69/// It also returns an error if the number of periods is zero and the present value is not equal to the
70/// future value. In both cases this is because there's no periodic rate that could make that work.
71///
72/// # Examples
73/// ```
74/// use finance_solution::*;
75///
76/// // The interest will compound for 365 days.
77/// let periods = 365;
78///
79/// // The starting value is $10,000.
80/// let present_value = -10_000.00;
81///
82/// // The ending value is $11,000.
83/// let future_value = 11_000.00;
84///
85/// let continuous_compounding = false;
86///
87/// // Calculate the periodic rate needed.
88/// let rate = rate(periods, present_value, future_value, continuous_compounding).unwrap();
89/// dbg!(&rate);
90/// // The rate is 0.0261% per day.
91/// assert_rounded_6(0.000261, rate);
92/// ```
93/// # Errors
94/// Returns [`FinanceError::SameSignValues`], [`FinanceError::Unsolvable`], or
95/// [`FinanceError::NonFinite`] when inputs cannot determine a rate.
96///
97/// # Examples
98/// ```
99/// use finance_solution::{rate, FinanceError};
100///
101/// let r = rate(365, -10_000.0, 11_000.0, false).unwrap();
102/// assert!(r > 0.0);
103///
104/// match rate(12, 1_000.0, 1_100.0, false) {
105///     Err(FinanceError::SameSignValues { .. }) => {}
106///     other => panic!("expected SameSignValues, got {other:?}"),
107/// }
108/// ```
109pub fn rate<P, F, C>(
110    periods: u32,
111    present_value: P,
112    future_value: F,
113    compounding: C,
114) -> crate::FinanceResult<f64>
115where
116    P: Into<f64> + Copy,
117    F: Into<f64> + Copy,
118    C: Into<crate::Compounding>,
119{
120    rate_internal(
121        periods,
122        present_value.into(),
123        future_value.into(),
124        compounding.into().is_continuous(),
125    )
126}
127
128/// Returns the periodic rate of an investment given the number of periods along with the present
129/// and future values.
130///
131/// See the [rate](./index.html) module page for the formulas.
132///
133/// Related functions:
134/// * To calculate a periodic rate returning an f64 value instead of solution object, use [`rate`](./fn.rate.html).
135///
136/// # Arguments
137/// * `periods` - The number of periods such as quarters or periods. Often appears as `n` or `t`.
138/// * `present_value` - The starting value of the investment. May appear as `pv` in formulas, or `C`
139/// for cash flow or `P` for principal.
140/// * `future_value` - The final value of the investment.
141/// * `continuous_compounding` - True for continuous compounding, false for simple compounding.
142///
143/// If present_value and future_value are both zero then any rate will work so the function returns
144/// zero.
145///
146/// # Errors
147/// The call returns [`FinanceError`] if the present value is zero and the future value is nonzero or vice versa.
148/// It also returns an error if the number of periods is zero and the present value is not equal to the
149/// future value. In both cases this is because there's no periodic rate that could make that work.
150///
151/// # Examples
152/// Calculate a periodic rate and examine the period-by-period values.
153/// ```
154/// use finance_solution::*;
155/// // The interest will compound for ten periods.
156/// // The starting value is $10,000.
157/// // The ending value is $15,000.
158/// let periods = 10;
159/// let present_value = -10_000.00;
160/// let future_value = 15_000.00;
161/// let continuous_compounding = false;
162/// /// // Calculate the periodic rate and create a struct with a record of the
163/// // inputs, a description of the formula, and an option to calculate the
164/// // period-by-period values.
165/// let solution = rate_solution(periods, present_value, future_value, continuous_compounding).unwrap();
166/// dbg!(&solution);
167///
168/// let rate = solution.rate();
169/// dbg!(&rate);
170/// // The rate is 4.138% per period.
171/// assert_rounded_6(0.041380, rate);
172///
173/// // Examine the formulas.
174/// let formula = solution.formula();
175/// dbg!(&formula);
176/// assert_eq!("0.041380 = ((-15000.0000 / -10000.0000) ^ (1 / 10)) - 1", formula);
177/// let symbolic_formula = solution.symbolic_formula();
178/// dbg!(&symbolic_formula);
179/// assert_eq!("r = ((-fv / pv) ^ (1 / n)) - 1", symbolic_formula);
180///
181/// // Calculate the period-by-period values.
182/// let series = solution.series();
183/// dbg!(&series);
184/// ```
185pub fn rate_solution<P, F, C>(
186    periods: u32,
187    present_value: P,
188    future_value: F,
189    compounding: C,
190) -> crate::FinanceResult<TvmSolution>
191where
192    P: Into<f64> + Copy,
193    F: Into<f64> + Copy,
194    C: Into<crate::Compounding>,
195{
196    rate_solution_internal(
197        periods,
198        present_value.into(),
199        future_value.into(),
200        compounding.into().is_continuous(),
201    )
202}
203
204fn rate_internal(
205    periods: u32,
206    present_value: f64,
207    future_value: f64,
208    continuous_compounding: bool,
209) -> crate::FinanceResult<f64> {
210    crate::util::error::require_finite("present_value", present_value)?;
211    crate::util::error::require_finite("future_value", future_value)?;
212    if present_value + future_value == 0.0 {
213        return Ok(0.0);
214    }
215    if future_value == 0.0 {
216        return Ok(-1.0);
217    }
218    if (present_value < 0.0 && future_value < 0.0) || (present_value > 0.0 && future_value > 0.0) {
219        return Err(crate::FinanceError::SameSignValues {
220            present_value,
221            future_value,
222        });
223    }
224    if present_value == 0.0 && future_value != 0.0 {
225        return Err(crate::FinanceError::Unsolvable {
226            message: "present value is zero and future value is nonzero; cannot solve for rate",
227        });
228    }
229    if periods == 0 && present_value + future_value != 0.0 {
230        return Err(crate::FinanceError::Unsolvable {
231            message: "periods is zero and present + future value is nonzero; cannot solve for rate",
232        });
233    }
234    let rate = if continuous_compounding {
235        (-future_value / present_value).ln() / periods as f64
236    } else {
237        (-future_value / present_value).powf(1.0 / periods as f64) - 1.0
238    };
239    if rate.is_finite() {
240        Ok(rate)
241    } else {
242        Err(crate::FinanceError::NonFinite {
243            field: "rate",
244            value: rate,
245        })
246    }
247}
248
249pub(crate) fn rate_solution_internal(
250    periods: u32,
251    present_value: f64,
252    future_value: f64,
253    continuous_compounding: bool,
254) -> crate::FinanceResult<TvmSolution> {
255    if present_value == 0.0 && future_value == 0.0 {
256        let formula = "{special case}";
257        let symbolic_formula = "***";
258        let rate = 0.0;
259        return Ok(TvmSolution::new(
260            TvmVariable::Rate,
261            continuous_compounding,
262            rate,
263            periods,
264            present_value,
265            future_value,
266            formula,
267            symbolic_formula,
268        ));
269    }
270
271    let rate = rate_internal(periods, present_value, future_value, continuous_compounding)?;
272    let (formula, symbolic_formula) = if continuous_compounding {
273        let formula = format!(
274            "{:.6} = ln({:.4} / {:.4}) / {}",
275            rate, -future_value, present_value, periods
276        );
277        let symbolic_formula = "r = ln(-fv / pv) / t";
278        (formula, symbolic_formula)
279    } else {
280        let formula = format!(
281            "{:.6} = (({:.4} / {:.4}) ^ (1 / {})) - 1",
282            rate, -future_value, present_value, periods
283        );
284        let symbolic_formula = "r = ((-fv / pv) ^ (1 / n)) - 1";
285        (formula, symbolic_formula)
286    };
287    Ok(TvmSolution::new(
288        TvmVariable::Rate,
289        continuous_compounding,
290        rate,
291        periods,
292        present_value,
293        future_value,
294        &formula,
295        symbolic_formula,
296    ))
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_rate_edge() {
305        // Zero periods, values add up to zero.
306        assert_rounded_6(0.0, rate(0, 10_000.0, -10_000.0, false).unwrap());
307
308        // Nonzero periods, values add up to zero.
309        assert_rounded_6(0.0, rate(12, -10_000.0, 10_000.0, false).unwrap());
310    }
311
312    #[test]
313    fn test_rate_err_present_value_nan() {
314        // The present value is not a number.
315        assert!(rate(12, std::f64::NAN, 1_000.0, false).is_err());
316    }
317
318    #[test]
319    fn test_rate_err_present_value_inf() {
320        // The present value is infinite.
321        assert!(rate(12, std::f64::INFINITY, 1_000.0, false).is_err());
322    }
323
324    #[test]
325    fn test_rate_err_future_value_nan() {
326        // The future value is not a number.
327        assert!(rate(12, 1_000.0, std::f64::NAN, false).is_err());
328    }
329
330    #[test]
331    fn test_rate_err_future_value_inf() {
332        // The future value is infinite.
333        assert!(rate(12, 1_000.0, std::f64::NEG_INFINITY, false).is_err());
334    }
335
336    #[test]
337    fn test_rate_err_zero_periods() {
338        // Zero periods, values don't add up to zero.
339        assert!(rate(0, 10_000.0, 10_000.0, false).is_err());
340    }
341
342    /*
343    macro_rules! compare_to_excel {
344        ( $n:expr, $pv:expr, $fv:expr, $r_excel:expr, $r_manual_simple:expr, $r_manual_cont:expr ) => {
345            println!("$n = {}, $pv = {}, $fv = {}, $r_excel: {}, $r_manual_simple = {}, $r_manual_cont = {}", $n, $pv, $fv, $r_excel, $r_manual_simple, $r_manual_cont);
346            assert_approx_equal!($r_excel, $r_manual_simple);
347
348            let r_calc_simple = rate($n, $pv, $fv, false).unwrap();
349            println!("r_calc_simple = {}", r_calc_simple);
350            assert_approx_equal!($r_excel, r_calc_simple);
351
352            let r_calc_cont = rate($n, $pv, $fv, true).unwrap();
353            println!("r_calc_cont = {}", r_calc_cont);
354            assert_approx_equal!($r_manual_cont, r_calc_cont);
355
356            if is_approx_equal!(0.0, r_calc_simple) {
357                assert_approx_equal!(0.0, r_calc_cont);
358            } else {
359                let ratio = r_calc_cont / r_calc_simple;
360                println!("ratio = {}", ratio);
361                if $r_excel < 0.0 {
362                    assert!(ratio >= 1.0);
363                    assert!(ratio <= 2.0);
364                } else {
365                    assert!(ratio >= 0.0);
366                    assert!(ratio <= 1.0);
367                }
368            }
369        }
370    }
371    */
372
373    fn compare_to_excel(
374        test_case: usize,
375        n: u32,
376        pv: f64,
377        fv: f64,
378        r_excel: f64,
379        r_manual_simple: f64,
380        r_manual_cont: f64,
381    ) {
382        let display = false;
383
384        if display {
385            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)
386        }
387        assert_approx_equal!(r_excel, r_manual_simple);
388
389        let r_calc_simple = rate(n, pv, fv, false).unwrap();
390        if display {
391            println!("r_calc_simple = {}", r_calc_simple)
392        }
393        assert_approx_equal!(r_excel, r_calc_simple);
394
395        let r_calc_cont = rate(n, pv, fv, true).unwrap();
396        if display {
397            println!("r_calc_cont = {}", r_calc_cont);
398        }
399        assert_approx_equal!(r_manual_cont, r_calc_cont);
400
401        if is_approx_equal!(0.0, r_calc_simple) {
402            assert_approx_equal!(0.0, r_calc_cont);
403        } else {
404            let ratio = r_calc_cont / r_calc_simple;
405            if display {
406                println!("ratio = {}", ratio)
407            };
408            if r_excel < 0.0 {
409                assert!(ratio >= 1.0);
410                assert!(ratio <= 2.0);
411            } else {
412                assert!(ratio >= 0.0);
413                assert!(ratio <= 1.0);
414            }
415        }
416
417        // Solution with simple compounding.
418        let solution = rate_solution(n, pv, fv, false).unwrap();
419        if display {
420            dbg!(&solution);
421        }
422        solution.invariant();
423        assert!(solution.calculated_field().is_rate());
424        assert_eq!(false, solution.continuous_compounding());
425        assert_approx_equal!(r_excel, solution.rate());
426        assert_eq!(n, solution.periods());
427        assert_approx_equal!(n as f64, solution.fractional_periods());
428        assert_approx_equal!(pv, solution.present_value());
429        assert_approx_equal!(fv, solution.future_value());
430
431        // Solution with continuous compounding.
432        let solution = rate_solution(n, pv, fv, true).unwrap();
433        if display {
434            dbg!(&solution);
435        }
436        solution.invariant();
437        assert!(solution.calculated_field().is_rate());
438        assert!(solution.continuous_compounding());
439        assert_approx_equal!(r_manual_cont, solution.rate());
440        assert_eq!(n, solution.periods());
441        assert_approx_equal!(n as f64, solution.fractional_periods());
442        assert_approx_equal!(pv, solution.present_value());
443        assert_approx_equal!(fv, solution.future_value());
444    }
445
446    #[test]
447    fn test_rate_against_excel() {
448        compare_to_excel(
449            1,
450            90,
451            -0.1f64,
452            1f64,
453            0.0259143654700119f64,
454            0.0259143654700098f64,
455            0.025584278811045f64,
456        );
457        compare_to_excel(
458            2,
459            85,
460            1.05f64,
461            -1.5f64,
462            0.00420499208399443f64,
463            0.00420499208399305f64,
464            0.00419617581104391f64,
465        );
466        compare_to_excel(
467            3,
468            80,
469            -2.25f64,
470            2.25f64,
471            8.10490132135311E-16f64,
472            0f64,
473            0f64,
474        );
475        compare_to_excel(
476            4,
477            75,
478            4.3875f64,
479            -3.375f64,
480            -0.00349207865283533f64,
481            -0.00349207865410572f64,
482            -0.00349819019289988f64,
483        );
484        compare_to_excel(
485            5,
486            70,
487            -10.125f64,
488            5.0625f64,
489            -0.00985323817917932f64,
490            -0.00985323818144335f64,
491            -0.00990210257942779f64,
492        );
493        compare_to_excel(
494            6,
495            65,
496            0.759375f64,
497            -7.59375f64,
498            0.0360593046264088f64,
499            0.0360593046256343f64,
500            0.0354243860460622f64,
501        );
502        compare_to_excel(
503            7,
504            60,
505            -7.9734375f64,
506            11.390625f64,
507            0.00596228650143506f64,
508            0.00596228649269048f64,
509            0.00594458239897887f64,
510        );
511        compare_to_excel(
512            8,
513            55,
514            17.0859375f64,
515            -17.0859375f64,
516            4.31995919490311E-13f64,
517            0f64,
518            0f64,
519        );
520        compare_to_excel(
521            9,
522            50,
523            -33.317578125f64,
524            25.62890625f64,
525            -0.00523354233611077f64,
526            -0.00523354233613538f64,
527            -0.00524728528934982f64,
528        );
529        compare_to_excel(
530            10,
531            45,
532            76.88671875f64,
533            -38.443359375f64,
534            -0.0152852470655657f64,
535            -0.0152852470655688f64,
536            -0.0154032706791099f64,
537        );
538        compare_to_excel(
539            11,
540            40,
541            -5.76650390625f64,
542            57.6650390625f64,
543            0.0592537251772898f64,
544            0.0592537251772889f64,
545            0.0575646273248511f64,
546        );
547        compare_to_excel(
548            12,
549            35,
550            60.548291015625f64,
551            -86.49755859375f64,
552            0.010242814832087f64,
553            0.0102428148320715f64,
554            0.0101907126839638f64,
555        );
556        compare_to_excel(
557            13,
558            30,
559            -129.746337890625f64,
560            129.746337890625f64,
561            2.53808542775445E-15f64,
562            0f64,
563            0f64,
564        );
565        compare_to_excel(
566            14,
567            25,
568            253.005358886719f64,
569            -194.619506835937f64,
570            -0.0104396946981842f64,
571            -0.0104396947068867f64,
572            -0.0104945705786996f64,
573        );
574        compare_to_excel(
575            15,
576            20,
577            -583.858520507812f64,
578            291.929260253906f64,
579            -0.0340636710696604f64,
580            -0.0340636710751544f64,
581            -0.0346573590279973f64,
582        );
583        compare_to_excel(
584            16,
585            15,
586            43.7893890380859f64,
587            -437.893890380859f64,
588            0.165914401180033f64,
589            0.165914401179832f64,
590            0.15350567286627f64,
591        );
592        compare_to_excel(
593            17,
594            12,
595            -459.788584899902f64,
596            656.840835571289f64,
597            0.0301690469250706f64,
598            0.0301690469166949f64,
599            0.0297229119948944f64,
600        );
601        compare_to_excel(
602            18,
603            10,
604            985.261253356933f64,
605            -985.261253356933f64,
606            3.26110008113999E-16f64,
607            0f64,
608            0f64,
609        );
610        compare_to_excel(
611            19,
612            7,
613            -1921.25944404602f64,
614            1477.8918800354f64,
615            -0.0367869049970667f64,
616            -0.0367869049970667f64,
617            -0.0374806092096416f64,
618        );
619        compare_to_excel(
620            20,
621            5,
622            4433.6756401062f64,
623            -2216.8378200531f64,
624            -0.129449436703859f64,
625            -0.129449436703876f64,
626            -0.138629436111989f64,
627        );
628        compare_to_excel(
629            21,
630            4,
631            -332.525673007965f64,
632            3325.25673007965f64,
633            0.778279410038923f64,
634            0.778279410038923f64,
635            0.575646273248511f64,
636        );
637        compare_to_excel(
638            22,
639            3,
640            3491.51956658363f64,
641            -4987.88509511947f64,
642            0.126247880443697f64,
643            0.126247880443606f64,
644            0.118891647979577f64,
645        );
646        compare_to_excel(
647            23,
648            2,
649            -7481.82764267921f64,
650            7481.82764267921f64,
651            -9.19973496824152E-17f64,
652            0f64,
653            0f64,
654        );
655        compare_to_excel(
656            24,
657            1,
658            14589.5639032245f64,
659            -11222.7414640188f64,
660            -0.230769230769231f64,
661            -0.230769230769231f64,
662            -0.262364264467491f64,
663        );
664    }
665}