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}