rust_finprim/tvm/pmt.rs
1use crate::{ONE, ZERO};
2use rust_decimal::prelude::*;
3
4/// PMT - Payment
5///
6/// General payment calculation, similar to the Excel `PMT` function. Commonly used for loan and mortgage calculations.
7/// The `due` parameter expresses whether the annuity type is an ordinary annuity (false and the default) or an annuity due (true),
8/// Excel provides this parameter as `type` with 0 for ordinary annuity and 1 for annuity due.
9///
10/// The payment (PMT) is the amount of money that is paid or received at each period in an annuity.
11///
12/// # Arguments
13/// * `rate` - The interest rate per period
14/// * `nper` - The number of compounding periods
15/// * `pv` - The present value of a series of cash flows or principal amount
16/// * `fv` (optional) - The future value
17/// * `due` (optional) - The timing of the payment (false = end of period, true = beginning of period), default is false
18/// (ordinary annuity)
19///
20/// At least one of `pv` or `fv` should be non-zero.
21///
22/// # Returns
23/// * The payment amount (PMT)
24///
25/// # Example
26/// * 5% interest rate
27/// * 10 compounding periods
28/// * $1000 present value
29/// * $100 future value
30/// ```
31/// use rust_decimal_macros::*;
32/// use rust_finprim::tvm::pmt;
33///
34/// let rate = dec!(0.05); let nper = dec!(10); let pv = dec!(1000); let fv = dec!(100);
35/// pmt(rate, nper, pv, Some(fv), None);
36/// ```
37///
38/// # Formula
39/// The payment amount (PMT) is calculated using the formula for the present value of an annuity.
40/// The formula is:
41/// $$PMT = \frac{r(PV (r+1)^n - FV)}{(r+1)^n -1}$$
42///
43/// Where:
44/// * \\(r\\) = interest rate per period
45/// * \\(PV\\) = present value of a series of cash flows or principal amount
46/// * \\(FV\\) = future value
47/// * \\(n\\) = number of compounding periods
48pub fn pmt(rate: Decimal, nper: Decimal, pv: Decimal, fv: Option<Decimal>, due: Option<bool>) -> Decimal {
49 let fv: Decimal = fv.unwrap_or(ZERO);
50 let due = due.unwrap_or(false);
51
52 if rate == ZERO {
53 // If the rate is zero, the nth_power should be 1 (since (1 + 0)^n = 1)
54 // The payment calculation when rate is zero is simplified
55 return -(pv + fv) / nper;
56 }
57
58 let nth_power = (ONE + rate).powd(nper);
59 let numerator = rate * (-pv * nth_power - fv);
60 let denominator = if due {
61 (ONE - nth_power) * (ONE + rate)
62 } else {
63 ONE - nth_power
64 };
65
66 -numerator / denominator
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 #[cfg(not(feature = "std"))]
73 extern crate std;
74 use rust_decimal_macros::*;
75 #[cfg(not(feature = "std"))]
76 use std::assert;
77 #[cfg(not(feature = "std"))]
78 use std::prelude::v1::*;
79
80 #[test]
81 fn test_pmt() {
82 struct TestCase {
83 rate: Decimal,
84 nper: Decimal,
85 pv: Decimal,
86 fv: Option<Decimal>,
87 due: Option<bool>,
88 expected: Decimal,
89 description: &'static str,
90 }
91
92 impl TestCase {
93 fn new(
94 rate: f64,
95 nper: f64,
96 pv: f64,
97 fv: Option<f64>,
98 due: Option<bool>,
99 expected: f64,
100 description: &'static str,
101 ) -> TestCase {
102 TestCase {
103 rate: Decimal::from_f64(rate).unwrap(),
104 nper: Decimal::from_f64(nper).unwrap(),
105 pv: Decimal::from_f64(pv).unwrap(),
106 fv: fv.map(Decimal::from_f64).unwrap_or(None),
107 due,
108 expected: Decimal::from_f64(expected).unwrap(),
109 description,
110 }
111 }
112 }
113
114 let cases = [
115 TestCase::new(
116 0.05,
117 10.0,
118 -1000.0,
119 Some(1000.0),
120 None,
121 50.0,
122 "5% coupon bond with 10 periods and $1000 present value",
123 ),
124 TestCase::new(
125 0.05,
126 10.0,
127 1000.0,
128 None,
129 None,
130 -129.50457,
131 "Paying off a $1000 loan with a 5% interest rate",
132 ),
133 TestCase::new(
134 0.0,
135 10.0,
136 1000.0,
137 Some(100.0),
138 None,
139 -110.0,
140 "Zero interest rate no growth",
141 ),
142 TestCase::new(
143 0.05,
144 10.0,
145 1000.0,
146 Some(100.0),
147 Some(true),
148 -130.90955,
149 "Payment at the beg of period should result in lower payment",
150 ),
151 TestCase::new(0.05, 10.0, 0.0, Some(1000.0), None, -79.50457, "No PV, just a FV"),
152 TestCase::new(
153 0.05,
154 10.0,
155 -1100.0,
156 Some(1000.0),
157 None,
158 62.95046,
159 "10yr bond trading at a premium, 5% YTM, what's my coupon payment?",
160 ),
161 ];
162
163 for case in &cases {
164 let calculated_pmt = pmt(case.rate, case.nper, case.pv, case.fv, case.due);
165 assert!(
166 (calculated_pmt - case.expected).abs() < dec!(1e-5),
167 "Failed on case: {}. Expected {}, got {}",
168 case.description,
169 case.expected,
170 calculated_pmt
171 );
172 }
173 }
174}