rust_finprim/tvm/
pv.rs

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