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