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}