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}