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