rust_finprim/tvm/
pv.rs

1use crate::{ONE, ZERO};
2use rust_decimal::prelude::*;
3use rust_decimal_macros::*;
4
5/// PV - Present Value
6///
7/// A general present value calculation, similar to the Excel `PV` function. Commonly used
8/// for bond pricing and annuity calculations.
9/// The `due` parameter expresses whether the annuity type is an ordinary annuity (false and the default) or an annuity due (true),
10/// Excel provides this parameter as `type` with 0 for ordinary annuity and 1 for annuity due.
11///
12/// The present value (PV) is the current value of a future sum of money or cash flow given a
13/// specified rate of return.
14///
15/// # Arguments
16/// * `rate` - The interest rate per period
17/// * `nper` - The number of compounding periods
18/// * `pmt` - The payment amount per period (negative for cash outflows)
19/// * `fv` (optional) - The future value
20/// * `due` (optional) - The timing of the payment (false = end of period, true = beginning of period), default is false
21/// (ordinary annuity)
22///
23/// At least one of `pmt` or `fv` should be non-zero.
24///
25/// # Returns
26/// The present value (PV)
27///
28/// # Example
29/// 10 Year bond with 3% YTM, $1000 future value, and 5% coupon rate (paid annually)
30/// * 5% interest rate
31/// * 10 compounding periods
32/// * $50 payment per period (5% of $1000)
33/// ```
34/// use rust_finprim::tvm::pv;
35/// use rust_decimal_macros::*;
36///
37/// let rate = dec!(0.05); let nper = dec!(10); let pmt = dec!(-50); let fv = dec!(1000);
38/// pv(rate, nper, pmt, Some(fv), None);
39/// ```
40pub fn pv(rate: Decimal, nper: Decimal, pmt: Decimal, fv: Option<Decimal>, due: Option<bool>) -> Decimal {
41    let fv: Decimal = fv.unwrap_or(ZERO);
42    let due = due.unwrap_or(false);
43
44    let mut pv = if rate == ZERO {
45        // Simplified formula when rate is zero
46        fv + (pmt * nper)
47    } else {
48        let nth_power = (ONE + rate).powd(-nper);
49        let fv_discounted = fv * nth_power;
50        let factor = (ONE - nth_power) / rate;
51
52        if due {
53            pmt * factor * (ONE + rate) + fv_discounted
54        } else {
55            pmt * factor + fv_discounted
56        }
57    };
58
59    // Present value negative since it represents a cash outflow
60    pv.set_sign_negative(true);
61    pv
62}
63
64/// NPV - Net Present Value
65///
66/// The net present value (NPV) is the difference between the present value of cash inflows and the present value of cash outflows over a period of time.
67/// NPV is used in capital budgeting to analyze the profitability of an investment or project.
68/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
69/// If the NPV is positive, the investment is considered profitable. If the NPV is negative, the investment is considered unprofitable.
70/// Similar to the Excel `NPV` function, with the main difference is that this implementation
71/// assumes the first cash flow is at time value 0 (initial investment).
72///
73/// # Arguments
74/// * `rate` - The discount rate per period
75/// * `cash_flows` - A slice of Decimal values representing the cash flows of the investment,
76/// note that the first cash flow is assumed to be at time value 0 (initial investment)
77///
78/// # Returns
79/// * The net present value (NPV)
80///
81/// # Example
82/// * 5% discount rate
83/// * Cash flows of $-100, $50, $40, $30, $20
84/// ```
85/// use rust_decimal_macros::*;
86/// use rust_finprim::tvm::npv;
87///
88/// let rate = dec!(0.05);
89/// let cash_flows = vec![dec!(-100), dec!(50), dec!(40), dec!(30), dec!(20)];
90/// npv(rate, &cash_flows);
91/// ```
92/// # Formula
93/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
94/// The formula is:
95/// $$NPV = \sum_{t=0}^{n} \frac{CF_t}{(1+r)^t}$$
96/// Where:
97/// * \\(CF_t\\) = cash flow at time \\(t\\)
98/// * \\(r\\) = discount rate
99pub fn npv(rate: Decimal, cash_flows: &[Decimal]) -> Decimal {
100    cash_flows
101        .iter()
102        .enumerate()
103        .map(|(t, &cf)| cf / (ONE + rate).powi(t as i64))
104        .sum()
105}
106
107/// NPV Differing Rates - Net Present Value with differing discount rates
108///
109/// The net present value (NPV) is the difference between the present value of cash inflows and the present value of cash outflows over a period of time.
110/// NPV is used in capital budgeting to analyze the profitability of an investment or project.
111/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
112/// If the NPV is positive, the investment is considered profitable. If the NPV is negative, the investment is considered unprofitable.
113/// This function allows for differing discount rates for each cash flow.
114///
115/// # Arguments
116/// * `flow_table` - A slice of tuples representing the cash flows and discount rates for each period `(cash_flow, discount_rate)`,
117/// note that the first cash flow is assumed to be at time value 0 (initial investment)
118///
119/// # Returns
120/// * The net present value (NPV)
121///
122/// # Example
123/// * Cash flows of $-100, $50, $40, $30, $20
124/// * Discount rates of 5%, 6%, 7%, 8%, 9%
125/// ```
126/// use rust_decimal_macros::*;
127/// use rust_finprim::tvm::npv_differing_rates;
128///
129/// let flow_table = vec![
130///     (dec!(-100), dec!(0.05)),
131///     (dec!(50), dec!(0.06)),
132///     (dec!(40), dec!(0.07)),
133///     (dec!(30), dec!(0.08)),
134///     (dec!(20), dec!(0.09)),
135/// ];
136/// npv_differing_rates(&flow_table);
137/// ```
138///
139/// # Formula
140/// The NPV is calculated by discounting all cash flows to the present value using a specified discount rate.
141/// $$NPV = \sum_{t=0}^{n} \frac{CF_t}{(1+r_t)^t}$$
142/// Where:
143/// * \\(CF_t\\) = cash flow at time \\(t\\)
144/// * \\(r_t\\) = discount rate at time \\(t\\)
145pub fn npv_differing_rates(flow_table: &[(Decimal, Decimal)]) -> Decimal {
146    flow_table
147        .iter()
148        .enumerate()
149        .map(|(t, &(cf, rate))| cf / (ONE + rate).powi(t as i64))
150        .sum()
151}
152
153/// XNPV - Net Present Value for irregular cash flows
154///
155/// The XNPV function calculates the net present value of a series of cash flows that are not necessarily periodic.
156///
157/// # Arguments
158/// * `rate` - The discount rate
159/// * `flow_table` - A slice of tuples representing the cash flows and dates for each period `(cash_flow, date)`
160/// where `date` represents the number of days from an arbitrary epoch. The first cash flow
161/// is assumed to be the initial investment date, the order of subsequent cash flows does
162/// not matter.
163///
164/// Most time libraries will provide a method yielding the number of days from an epoch. For example, in the `chrono` library
165/// you can use the `num_days_from_ce` method to get the number of days from the Common Era (CE) epoch, simply convert
166/// your date types to an integer representing the number of days from any epoch. Alternatively, you can calculate the
167/// time delta in days from an arbitrary epoch, such as the initial investment date.
168///
169/// Cash flows are discounted assuming a 365-day year.
170///
171/// # Returns
172/// * The net present value (NPV)
173///
174/// # Example
175/// * 5% discount rate
176/// * Cash flows of $-100, $50, $40, $30, $20
177/// * Dates of 0, 365, 420, 1360, 1460
178///
179/// ```
180/// use rust_decimal_macros::*;
181/// use rust_finprim::tvm::xnpv;
182///
183/// let rate = dec!(0.05);
184/// let flows_table = vec![
185///    (dec!(-100), 0),
186///    (dec!(50), 365),
187///    (dec!(40), 420),
188///    (dec!(30), 1360),
189///    (dec!(20), 1460),
190/// ];
191/// xnpv(rate, &flows_table);
192pub fn xnpv(rate: Decimal, flow_table: &[(Decimal, i32)]) -> Decimal {
193    // First date should be 0 (initial investment) and the rest should be difference from the initial date
194    let init_date = flow_table.first().unwrap().1;
195
196    flow_table
197        .iter()
198        .map(|&(cf, date)| {
199            let years = Decimal::from_i32(date - init_date).unwrap() / dec!(365);
200            cf / (ONE + rate).powd(years)
201        })
202        .sum()
203}
204
205#[cfg(test)]
206mod tests {
207    #[cfg(not(feature = "std"))]
208    extern crate std;
209    use super::*;
210    #[cfg(not(feature = "std"))]
211    use std::prelude::v1::*;
212    #[cfg(not(feature = "std"))]
213    use std::{assert, vec};
214
215    #[test]
216    fn test_xnpv() {
217        let rate = dec!(0.05);
218        let flows_table = vec![
219            (dec!(-100), 0),
220            (dec!(50), 365),
221            (dec!(40), 730),
222            (dec!(30), 1095),
223            (dec!(20), 1460),
224        ];
225
226        let result = xnpv(rate, &flows_table);
227        let expected = dec!(26.26940);
228        assert!(
229            (result - expected).abs() < dec!(1e-5),
230            "Failed on case: {}. Expected: {}, Result: {}",
231            "5% discount rate, cash flows of -100, 50, 40, 30, 20",
232            expected,
233            result
234        );
235    }
236
237    #[test]
238    fn test_pv() {
239        struct TestCase {
240            rate: Decimal,
241            nper: Decimal,
242            pmt: Decimal,
243            fv: Option<Decimal>,
244            due: Option<bool>,
245            expected: Decimal,
246            description: &'static str,
247        }
248        impl TestCase {
249            fn new(
250                rate: f64,
251                nper: f64,
252                pmt: f64,
253                fv: Option<f64>,
254                due: Option<bool>,
255                expected: f64,
256                description: &'static str,
257            ) -> TestCase {
258                TestCase {
259                    rate: Decimal::from_f64(rate).unwrap(),
260                    nper: Decimal::from_f64(nper).unwrap(),
261                    pmt: Decimal::from_f64(pmt).unwrap(),
262                    fv: fv.map(Decimal::from_f64).unwrap_or(None),
263                    due,
264                    expected: Decimal::from_f64(expected).unwrap(),
265                    description,
266                }
267            }
268        }
269
270        let cases = [
271            TestCase::new(
272                0.05,
273                10.0,
274                100.0,
275                None,
276                None,
277                -772.17349,
278                "Standard case with 5% rate, 10 periods, and $100 pmt",
279            ),
280            TestCase::new(
281                0.05,
282                10.0,
283                100.0,
284                None,
285                Some(true),
286                -810.78217,
287                "Payment at the beg of period should result in higher present value",
288            ),
289            TestCase::new(0.0, 10.0, -100.0, None, None, -1000.0, "Zero interest rate no growth"),
290            TestCase::new(
291                0.05,
292                10.0,
293                100.0,
294                Some(1000.0),
295                None,
296                -1386.08675,
297                "Bond with 5% rate, 10 periods, 10% coupon, and $1000 future value",
298            ),
299            TestCase::new(
300                0.05,
301                10.0,
302                0.0,
303                Some(2000.0),
304                None,
305                -1227.82651,
306                "No cash flows, just a future pay out",
307            ),
308        ];
309
310        for case in &cases {
311            let calculated_pv = pv(case.rate, case.nper, case.pmt, case.fv, case.due);
312            assert!(
313                (calculated_pv - case.expected).abs() < dec!(1e-5),
314                "Failed on case: {}. Expected {}, got {}",
315                case.description,
316                case.expected,
317                calculated_pv
318            );
319        }
320    }
321
322    #[test]
323    fn test_npv() {
324        struct TestCase {
325            rate: Decimal,
326            cash_flows: Vec<Decimal>,
327            expected: Decimal,
328            description: &'static str,
329        }
330        impl TestCase {
331            fn new(rate: f64, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
332                TestCase {
333                    rate: Decimal::from_f64(rate).unwrap(),
334                    cash_flows: cash_flows.iter().map(|&cf| Decimal::from_f64(cf).unwrap()).collect(),
335                    expected: Decimal::from_f64(expected).unwrap(),
336                    description,
337                }
338            }
339        }
340
341        let cases = [
342            TestCase::new(
343                0.05,
344                vec![-100.0, 50.0, 40.0, 30.0, 20.0],
345                26.26940,
346                "Standard case with 5% rate and cash flows of -100, 50, 40, 30, 20",
347            ),
348            TestCase::new(
349                0.05,
350                vec![100.0, 50.0, 40.0, 30.0, 20.0],
351                226.26940,
352                "All positive cash flows",
353            ),
354            TestCase::new(
355                0.05,
356                vec![-100.0, 50.0, 40.0, 30.0, 20.0, 1000.0],
357                809.79557,
358                "Additional future cash flow should increase NPV",
359            ),
360        ];
361
362        for case in &cases {
363            let calculated_npv = npv(case.rate, &case.cash_flows);
364            assert!(
365                (calculated_npv - case.expected).abs() < dec!(1e-5),
366                "Failed on case: {}. Expected {}, got {}",
367                case.description,
368                case.expected,
369                calculated_npv
370            );
371        }
372    }
373
374    #[test]
375    fn test_npv_differing_rates() {
376        struct TestCase {
377            flow_table: Vec<(Decimal, Decimal)>,
378            expected: Decimal,
379            description: &'static str,
380        }
381        impl TestCase {
382            fn new(rates: Vec<f64>, cash_flows: Vec<f64>, expected: f64, description: &'static str) -> TestCase {
383                let rates: Vec<Decimal> = rates.iter().map(|&r| Decimal::from_f64(r).unwrap()).collect();
384                let cash_flows: Vec<Decimal> = cash_flows.iter().map(|&cf| Decimal::from_f64(cf).unwrap()).collect();
385                let flow_table = cash_flows.iter().zip(rates.iter()).map(|(&cf, &r)| (cf, r)).collect();
386                TestCase {
387                    flow_table,
388                    expected: Decimal::from_f64(expected).unwrap(),
389                    description,
390                }
391            }
392        }
393
394        let cases = [
395            TestCase::new(
396                vec![0.05, 0.06, 0.07, 0.08, 0.09],
397                vec![-100.0, 50.0, 40.0, 30.0, 20.0],
398                20.09083,
399                "Increasing rate and cash flows of -100, 50, 40, 30, 20",
400            ),
401            TestCase::new(
402                vec![0.05, 0.06, 0.07, 0.08, 0.09],
403                vec![100.0, 50.0, 40.0, 30.0, 20.0],
404                220.09083,
405                "All positive cash flows",
406            ),
407            TestCase::new(
408                vec![0.05, 0.06, 0.07, 0.08, 0.09, 0.1],
409                vec![-100.0, 50.0, 40.0, 30.0, 20.0, 1000.0],
410                641.01215,
411                "Additional future cash flow should increase NPV",
412            ),
413        ];
414
415        for case in &cases {
416            let calculated_npv = npv_differing_rates(&case.flow_table);
417            assert!(
418                (calculated_npv - case.expected).abs() < dec!(1e-5),
419                "Failed on case: {}. Expected {}, got {}",
420                case.description,
421                case.expected,
422                calculated_npv
423            );
424        }
425    }
426}