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}